开篇有奖
如果你最近一年进来面过试,很可能面临这些问题:
- react 16到底做了哪些更新;
- react hooks用过么,晓得其原理么;
第一个问题如果你提到了Fiber reconciler,fiber,链表,新的什么周期,可能在面试官眼里这仅仅是一个及格的答复。以下是我整顿的,自我感觉还良好的答复:
分三步:
- react作为一个ui库,将前端编程由传统的
命令式
编程转变为申明式
编程,即所谓的数据驱动视图,但如果简略粗犷的操作,比方讲生成的html间接采纳innerHtml替换,会带来重绘重排
之类的性能问题。为了尽量进步性能,React团队引入了虚构dom,即采纳js对象来形容dom树,通过比照前后两次的虚构对象,来找到最小的dom操作(vdom diff),以此进步性能。 - 下面提到的vDom diff,在react 16之前,这个过程咱们称之为
stack reconciler
,它是一个递归的过程,在树很深的时候,单次diff工夫过长会造成JS线程继续被占用,用户交互响应通畅,页面渲染会呈现显著的卡顿,这在古代前端是一个致命的问题。所以为了解决这种问题,react 团队对整个架构进行了调整,引入了fiber架构,将以前的stack reconciler替换为fiber reconciler
。采纳增量式渲染
。引入了工作优先级(expiration)
和requestIdleCallback
的循环调度算法,简略来说就是将以前的一根筋diff更新,首先拆分成两个阶段:reconciliation
与commit
;第一个reconciliation
阶段是可打断的,被拆分成一个个的小工作(fiber),在每一侦的渲染闲暇期做小工作diff。而后是commit阶段,这个阶段是不拆分且不能打断的,将diff节点的effectTag一口气更新到页面上。 - 因为reconciliation是能够被打断的,且存在工作优先级的问题,所以会导致commit前的一些生命周期函数屡次被执行, 如componentWillMount、componentWillReceiveProps 和 componetWillUpdate,但react官网已申明这些问题,并将其标记为unsafe,在React17中将会移除
- 因为每次唤起更新是从根节点(RootFiber)开始,为了更好的节点复用与性能优化。在react中始终存
workInprogressTree
(future vdom) 与oldTree
(current vdom)两个链表,两个链表互相援用。这无形中又解决了另一个问题,当workInprogressTree生成报错时,这时也不会导致页面渲染解体,而只是更新失败,页面依然还在。
以上就是我上半年面试本人一直总结迭代出的答案,心愿能对你有所启发。
接着来答复第二个问题,hooks实质是什么?
hooks 为什么呈现
当咱们在议论React这个UI库时,最先想到的是,数据驱动视图,简略来讲就是上面这个公式:
view = fn(state)
咱们开发的整个利用,都是很多组件组合而成,这些组件是纯正,不具备扩大的。因为React不能像一般类一样间接继承,从而达到性能扩大的目标。
呈现前的逻辑复用
在用react实现业务时,咱们复用一些组件逻辑去扩大另一个组件,最常见比方Connect,Form.create, Modal。这类组件通常是一个容器,容器外部封装了一些通用的性能(非视觉的占多数),容器外面的内容由被包装的组件本人定制,从而达到肯定水平的逻辑复用。
在hooks 呈现之前,解决这类需要最罕用的就两种模式:HOC高阶组件
和 Render Props
。
高阶组件相似于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 呈现前的函数式组件只是以模板函数存在,而后面两种计划,某种程度都是依赖类组件来实现。而提到了类,就不得不想到上面这些痛点:
- JS中的this是一个神仙级的存在, 是很多入门开发趟不过的坑;
- 生命周期的复杂性,很多时候咱们须要在多个生命周期同时编写同一个逻辑
- 写法臃肿,什么constructor,super,render
所以React团队回归view = fn(state)
的初心,心愿函数式组件也能领有状态治理的能力,让逻辑复用变得更简略,更纯正。
架构的更新
为什么在React 16前,函数式组件不能领有状态治理?其本质是因为16以前只有类组件在更新时存在实例,而16当前Fiber 架构的呈现,让每一个节点都领有对应的实例,也就领有了保留状态的能力,上面会详讲。
hooks 的实质
有可能,你听到过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.jsfunction 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
。
- currentlyRenderingFiber: 其实就是workInProgressTree, 即更新时链表以后正在遍历的fiber节点(源码正文:
The work-in-progress fiber. I've named it differently to distinguish it from the work-in-progress hook
); - queue: 指向hook.queue,保留以后hook操作相干的reducer 和 状态的对象,其来源于mountWorkInProgressHook这个函数,上面重点讲;
这个闭包将 fiber节点与action, action 与 state很好的串联起来了,举下面的例子就是:
- 当点击减少执行setAge, 执行后,新的state更新工作就贮存在fiber节点的hook.queue上,并触发更新;
- 当节点更新时,会遍历queue上的state工作链表,计算最终的state,并进行渲染;
ok,到这,闭包就讲完了。
第一个链表:hooks
在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
其实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 只是造成了状态待执行工作链表,真正失去最终状态,其实是在下一次更新(获取状态)时,即:
// 读取最新ageconst [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,与类组件到底到底是什么分割:
- useState: 状态的存储及更新,状态更新会触发组件更新,和类的state相似,只不过setState更新时是采纳Object.assign(oldstate, newstate); 而useState的set是间接代替式的
- useEffect: 相似于以前的componentDidMount 和 componentDidUpdate生命周期钩子(即render 执行后,再执行Effect, 所以当组件与子组件都有Effect时,
子组件的Effect先执行
), Update须要deps依赖来唤起; - useRefs: 用法相似于以前间接挂在类的this上,像this.selfCount 这种,用于变量的长期存储,而又不至于受函数更新,而被重定义;与useState的区别就是,refs的更新不会导致Rerender
- useMemo: 用法同以前的componentWillReceiveProps与getDerivedStateFromProps中,依据state和props计算出一个新的属性值:计算属性
- useCallback: 相似于类组件中constructor的bind,但比bind更弱小,防止回调函数每次render造成回调函数反复申明,进而造成不必要的diff;但须要留神deps,不然会掉进
闭包
的坑 - useReducer: 和redux中的Reducer相像,和useState一样,执行后能够唤起
Rerender
第一次写源码解析,出发点次要两点:
- 最近半年本人在react的确下了一些功夫,有一个输入也是为了本人当前更好的回顾;
- 网上太多的人用一个闭包来概括hooks,我感觉这是对技术的亵渎(集体意见);
文章中若有不详或不对之处,欢送斧正;
举荐浏览: 源码解析React Hook构建过程:没有设计就是最好的设计
首发链接:当咱们在用Hooks时,咱们到底在用什么?