若是你最近一年出去面过试,极可能面临这些问题:html
第一个问题若是你提到了Fiber reconciler,fiber,链表,新的什么周期,可能在面试官眼里这仅仅是一个及格的回答。如下是我整理的,自我感受还良好的回答:前端
分三步:react
命令式
编程转变为声明式
编程,即所谓的数据驱动视图,但若是简单粗暴的操做,好比讲生成的html直接采用innerHtml替换,会带来重绘重排
之类的性能问题。为了尽可能提升性能,React团队引入了虚拟dom,即采用js对象来描述dom树,经过对比先后两次的虚拟对象,来找到最小的dom操做(vdom diff),以此提升性能。stack reconciler
,它是一个递归的过程,在树很深的时候,单次diff时间过长会形成JS线程持续被占用,用户交互响应迟滞,页面渲染会出现明显的卡顿,这在现代前端是一个致命的问题。因此为了解决这种问题,react 团队对整个架构进行了调整,引入了fiber架构,将之前的stack reconciler替换为fiber reconciler
。采用增量式渲染
。引入了任务优先级(expiration)
和requestIdleCallback
的循环调度算法,简单来讲就是将之前的一根筋diff更新,首先拆分红两个阶段:reconciliation
与commit
;第一个reconciliation
阶段是可打断的,被拆分红一个个的小任务(fiber),在每一侦的渲染空闲期作小任务diff。而后是commit阶段,这个阶段是不拆分且不能打断的,将diff节点的effectTag一口气更新到页面上。workInprogressTree
(future vdom) 与 oldTree
(current vdom)两个链表,两个链表相互引用。这无形中又解决了另外一个问题,当workInprogressTree生成报错时,这时也不会致使页面渲染崩溃,而只是更新失败,页面仍然还在。以上就是我上半年面试本身不断总结迭代出的答案,但愿能对你有所启发。git
接着来回答第二个问题,hooks本质是什么?github
当咱们在谈论React这个UI库时,最早想到的是,数据驱动视图,简单来说就是下面这个公式:面试
view = fn(state)
咱们开发的整个应用,都是不少组件组合而成,这些组件是纯粹,不具有扩展的。由于React不能像普通类同样直接继承,从而达到功能扩展的目的。算法
在用react实现业务时,咱们复用一些组件逻辑去扩展另外一个组件,最多见好比Connect,Form.create, Modal。这类组件一般是一个容器,容器内部封装了一些通用的功能(非视觉的占多数),容器里面的内容由被包装的组件本身定制,从而达到必定程度的逻辑复用。编程
在hooks 出现以前,解决这类需求最经常使用的就两种模式:HOC高阶组件
和 Render Props
。redux
高阶组件相似于JS中的高阶函数,即输入一个函数,返回一个新的
函数, 好比React-Redux中的Connect:性能优化
class Home extends React.Component { // UI } export default Connect()(Home);
高阶组件因为每次都会返回一个新的组件,对于react来讲,这是不利于diff和状态复用的,因此高阶组件的包装不能在render 方法中进行,而只能像上面那样在组件声明时包裹,这样也就不利于动态传参。而Render Props模式的出现就完美解决了这个问题,其原理就是将要包裹的组件做为props属性传入,而后容器组件调用这个属性,并向其传参, 最多见的用props.children
来作这个属性。举个🌰:
class Home extends React.Component { // UI } <Route path = "/home" render= {(props) => <Home {...props} } />
更多关于render 与 Hoc,能够参见之前写的一片弱文:React进阶,写中后台也能写出花
上面提到的高阶组件和RenderProps, 看似解决了逻辑复用的问题,但面对复杂需求时,即一个组件须要使用多个复用包裹时,两种方案都会让咱们的代码陷入常见的嵌套地狱
, 好比:
class Home extends React.Component { // UI } export default Connect()(Form.create()(Home));
除了嵌套地狱的写法让人困惑,但更致命的深度会直接影响react组件更新时的diff性能。
Hooks 出现前的函数式组件只是以模板函数存在,而前面两种方案,某种程度都是依赖类组件来完成。而提到了类,就不得不想到下面这些痛点:
因此React团队回归view = fn(state)
的初心,但愿函数式组件也能拥有状态管理的能力,让逻辑复用变得更简单,更纯粹。
为何在React 16前,函数式组件不能拥有状态管理?其本质是由于16之前只有类组件在更新时存在实例,而16之后Fiber 架构的出现,让每个节点都拥有对应的实例,也就拥有了保存状态的能力,下面会详讲。
有可能,你听到过Hooks的本质就是闭包
。可是,若是满分100的话,这个说法最多只能得60分。
哪满分答案是什么呢?闭包 + 两级链表
。
下面就来一一分解, 下面都以useState来举例剖析。
JS 中闭包是难点,也是必考点,归纳的讲就是:
闭包是指有权访问另外一个函数做用域中变量或方法
的函数,建立闭包的方式就是在一个函数内建立闭包函数,经过闭包函数访问这个函数的局部变量, 利用闭包能够突破做用链域的特性,将函数内部的变量和方法
传递到外部。
export default function Hooks() { const [count, setCount] = useState(0); const [age, setAge] = useState(18); const self = useRef(0); const onClick = useCallback(() => { setAge(19); setAge(20); setAge(21); }, []); console.log('self', self.current); return ( <div> <h2>年龄: {age} <a onClick={onClick}>增长</a></h2> <h3>轮次: {count} <a onClick={() => setCount(count => count + 1)}>增长</a></h3> </div> ); }
以上面的示例来说,闭包就是setAge这个函数,何以见得呢,看组件挂载阶段hook执行的源码:
// packages/react-reconciler/src/ReactFiberHooks.js function mountReducer(reducer, initialArg, init) { const hook = mountWorkInProgressHook(); let initialState; if (init !== undefined) { initialState = init(initialArg); } else { initialState = initialArg; } hook.memoizedState = hook.baseState = initialState; const queue = (hook.queue = { last: null, dispatch: null, lastRenderedReducer: reducer, lastRenderedState: initialState, }); // 重点 const dispatch = (queue.dispatch = (dispatchAction.bind( null, currentlyRenderingFiber, queue, ))); return [hook.memoizedState, dispatch]; }
因此这个函数就是mountReducer,而产生的闭包就是dispatch函数(对应上面的setAge),被闭包引用的变量就是currentlyRenderingFiber
与 queue
。
The work-in-progress fiber. I've named it differently to distinguish it from the work-in-progress hook
);这个闭包将 fiber节点与action, action 与 state很好的串联起来了,举上面的例子就是:
ok,到这,闭包就讲完了。
在ReactFiberHooks文件开头声明currentHook变量的源码有这样一段注释。
/* Hooks are stored as a linked list on the fiber's memoizedState field. hooks 以链表的形式存储在fiber节点的memoizedState属性上 The current hook list is the list that belongs to the current fiber. 当前的hook链表就是当前正在遍历的fiber节点上的 The work-in-progress hook list is a new list that will be added to the work-in-progress fiber. work-in-progress hook 就是即将被添加到正在遍历fiber节点的hooks新链表 */ let currentHook: Hook | null = null; let nextCurrentHook: Hook | null = null;
从上面的源码注释能够看出hooks链表与fiber链表是极其类似的;也得知hooks 链表是保存在fiber节点的memoizedState属性的, 而赋值是在renderWithHooks函数具体实现的;
export function renderWithHooks( current: Fiber | null, workInProgress: Fiber, Component: any, props: any, refOrContext: any, nextRenderExpirationTime: ExpirationTime, ): any { renderExpirationTime = nextRenderExpirationTime; currentlyRenderingFiber = workInProgress; // 获取当前节点的hooks 链表; nextCurrentHook = current !== null ? current.memoizedState : null; // ...省略一万行 }
有可能代码贴了这么多,你还没反应过来这个hooks 链表具体指什么?
其实就是指一个组件包含的hooks, 好比上面示例中的:
const [count, setCount] = useState(0); const [age, setAge] = useState(18); const self = useRef(0); const onClick = useCallback(() => { setAge(19); setAge(20); setAge(21); }, []);
造成的链表就是下面这样的:
因此在下一次更新时,再次执行hook,就会去获取当前运行节点的hooks链表;
const hook = updateWorkInProgressHook(); // updateWorkInProgressHook 就是一个纯链表的操做:指向下一个 hook节点
到这 hooks 链表是什么,应该就明白了;这时你可能会更明白,为何hooks不能在循环,判断语句中调用,而只能在函数最外层使用,由于挂载或则更新时,这个队列须要是一致的,才能保证hooks的结果正确。
其实state 链表不是hooks独有的,类操做的setState也存在,正是因为这个链表存在,因此有一个经(sa)典(bi)React 面试题:
setState为何默认是异步,何时是同步?
结合实例来看,当点击增长会执行三次setAge
const onClick = useCallback(() => { setAge(19); setAge(20); setAge(21); }, []);
第一次执行完dispatch后,会造成一个状态待执行任务链表:
若是仔细观察,会发现这个链表仍是一个环
(会在updateReducer后断开), 这一块设计至关有意思,我如今也还没搞明白为何须要环,值得细品,而创建这个链表的逻辑就在dispatchAction函数中。
function dispatchAction(fiber, queue, action) { // 只贴了相关代码 const update = { expirationTime, suspenseConfig, action, eagerReducer: null, eagerState: null, next: null, }; // Append the update to the end of the list. const last = queue.last; if (last === null) { // This is the first update. Create a circular list. update.next = update; } else { const first = last.next; if (first !== null) { // Still circular. update.next = first; } last.next = update; } queue.last = update; // 触发更新 scheduleWork(fiber, expirationTime); }
上面已经说了,执行setAge 只是造成了状态待执行任务链表,真正获得最终状态,实际上是在下一次更新(获取状态)时,即:
// 读取最新age const [age, setAge] = useState(18);
而获取最新状态的相关代码逻辑存在于updateReducer中:
function updateReducer(reducer, initialArg,init?) { const hook = updateWorkInProgressHook(); const queue = hook.queue; // ...隐藏一百行 // 找出第一个未被执行的任务; let first; // baseUpdate 只有在updateReducer执行一次后才会有值 if (baseUpdate !== null) { // 在baseUpdate有值后,会有一次解环的操做; if (last !== null) { last.next = null; } first = baseUpdate.next; } else { first = last !== null ? last.next : null; } if (first !== null) { let newState = baseState; let newBaseState = null; let newBaseUpdate = null; let prevUpdate = baseUpdate; let update = first; let didSkip = false; // do while 遍历待执行任务的状态链表 do { const updateExpirationTime = update.expirationTime; if (updateExpirationTime < renderExpirationTime) { // 优先级不足,先标记,后面再更新 } else { markRenderEventTimeAndConfig( updateExpirationTime, update.suspenseConfig, ); // Process this update. if (update.eagerReducer === reducer) { // 简单的说就是状态已经计算过,那就直接用 newState = update.eagerState; } else { const action = update.action; newState = reducer(newState, action); } } prevUpdate = update; update = update.next; // 终止条件是指针为空 或 环已遍历完 } while (update !== null && update !== first); // ...省略100行 return [newState, dispatch]; } }
最后来看,状态更新的逻辑彷佛是最绕的。但若是看过setState,这一块可能就比较容易。至此,第二个链表state就理清楚了。
读到这里,你就应该明白hooks 究竟是怎么实现的:
闭包加两级链表
虽然我这里只站在useState这个hooks作了剖析,但其余hooks的实现基本相似。
另外分享一下在我眼中的hooks,与类组件到底究竟是什么联系:
子组件的Effect先执行
), Update须要deps依赖来唤起;闭包
的坑Rerender
第一次写源码解析,出发点主要两点:
文章中如有不详或不对之处,欢迎斧正;