对源码的解读有利于搞清楚Hooks到底作了什么,若是您以为useEffect很“魔法”,这篇文章也许对您有些帮助。react
本篇博客篇幅有限,只看useEffect,力求简单明了,带您到React Hooks的深处看看数组
首先咱们从Github上获得react的源码,而后能够在packages中找到react文件夹,其中的index.js就是咱们的入口。数据结构
代码很简单,就两行:函数
const React = require('./src/React'); module.exports = React.default || React;
因此接下来咱们去看看 'react/src/React',代码有点多,咱们简化一下:ui
import ReactVersion from 'shared/ReactVersion'; // ... import { useEffect, } from './ReactHooks'; const React = { useEffect }; //... export default React;
很好,如今咱们至少知道为何Hooks的引用方式是:spa
import {useEffect} from 'react'设计
接下来咱们继续看看 'react/src/ReactHooks'。code
以前说好了只看useEffect,因此一样须要简化一下代码。component
而且考虑到有人对TypeScript语法不熟悉,还去掉了TypeScript语法,以后的简化代码也会如此。对象
如今咱们看下简化后的代码:
import invariant from 'shared/invariant'; import ReactCurrentDispatcher from './ReactCurrentDispatcher'; function resolveDispatcher() { const dispatcher = ReactCurrentDispatcher.current; // React版本不对或者Hook使用有误什么的就报错 // ... return dispatcher; } export function useEffect(create,inputs) { const dispatcher = resolveDispatcher(); return dispatcher.useEffect(create, inputs); }
这里能够看到,咱们的useEffect其实是ReactCurrentDispatcher.current.useEffect。
看一下ReactCurrentDispatcher文件,这里没有简化:
import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks'; const ReactCurrentDispatcher = { current: (null: null | Dispatcher), }; export default ReactCurrentDispatcher;
发现他的current的类型是null或者Dispatcher,因此这里咱们很简单就能猜到,这个东西的源码在 'react-reconciler/src/ReactFiberHooks'。
几千行代码,头大。可是莫慌,我们又不是来写react的,看看原理而已。
咱们以前已经知道useState其实是ReactCurrentDispatcher.current.useState。
很明显ReactCurrentDispatcher.current不论是什么东西单独列出来,咱们只须要知道谁赋值给他就好了。
精简代码,去掉用__DEV__区分的开发代码以后,咱们发现整个文件给ReactCurrentDispatcher.current赋值的没几个。
而惟一一个与异常判断无关的是renderWithHooks函数中的这一块代码:
export function renderWithHooks( current, workInProgress, Component, props, secondArg, nextRenderExpirationTime ){ ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; let children = Component(props, secondArg); return children; }
咱们不知道这段代码是干吗的,可是他确定是渲染组件时用的。
而这里很显然ReactCurrentDispatcher.current的值就只能是HooksDispatcherOnMount和HooksDispatcherOnUpdate。
很明显这两个一个用于加载时,一个用于更新时。
而后咱们们搜一下相关代码:
const HooksDispatcherOnMount = { useEffect: mountEffect }; const HooksDispatcherOnUpdate = { useEffect: updateEffect };
也就是说,组件加载时,useEffect会调用mountEffect,组件更新时会调用updateEffect。
让咱们继续看看这两个函数:
function mountEffect(create, deps) { return mountEffectImpl( UpdateEffect | PassiveEffect, UnmountPassive | MountPassive, create, deps, ); } function updateEffect(create, deps) { return updateEffectImpl( UpdateEffect | PassiveEffect, UnmountPassive | MountPassive, create, deps, ); }
这里的UpdateEffect和PassiveEffect是二进制常数,用位运算的方式操做。
先不用知道具体意义,知道是个常量便可。
接下来咱们看看具体的mountEffectImpl:
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps){ const hook = mountWorkInProgressHook(); // useEffect不传依赖,那么就为null const nextDeps = deps === undefined ? null : deps; currentlyRenderingFiber.effectTag |= fiberEffectTag; // 链表尾部hook对象的memoizedState为pushEffect的返回值 hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps); }
咱们看到第一行代码调用mountWorkInProgressHook新建了一个hook对象,让咱们看看mountWorkInProgressHook:
function mountWorkInProgressHook() { const hook = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null, }; if (workInProgressHook === null) { // This is the first hook in the list currentlyRenderingFiber.memoizedState = workInProgressHook = hook; } else { // Append to the end of the list workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; }
很明显这里有个链表结构workInProgressHook,若是workInProgressHook链表为null就将新建的hook对象赋值给它,若是不为null,那么就加在链表尾部。
这里有必要讲解一下:
Hooks做为一个链表存储在fiber的memoizedState中。
currentHook 是当前fiber的链表。
workInProgressHook 是即将被加入到 work-in-progress fiber的链表。
而后咱们再看看pushEffect:
function pushEffect(tag, create, destroy, deps) { // 新建一个effect,很明显又是个链表结构 const effect = { tag, create, destroy, deps, // Circular next: null, }; // 从currentlyRenderingFiber.updateQueue获取组件更新队列 let componentUpdateQueue= currentlyRenderingFiber.updateQueue; // 判断组件更新队列是否为空,每次在调用renderWithHooks都会将这个componentUpdateQueue置为null // 这样的话每次update这个组件时,就会建立一个新的effect链表 if (componentUpdateQueue === null) { // 为空就建立一个组件更新队列 componentUpdateQueue = createFunctionComponentUpdateQueue(); // 并赋值给currentlyRenderingFiber.updateQueue currentlyRenderingFiber.updateQueue = componentUpdateQueue; // 组件更新队列最新的effect为咱们新建的effect componentUpdateQueue.lastEffect = effect.next = effect; } else { // 若是组件更新队列已经存在,获取它最新的Effect const lastEffect = componentUpdateQueue.lastEffect; if (lastEffect === null) { // 若是最新的Effect为null,那么组件更新队列最新的Effect为咱们新建的effect componentUpdateQueue.lastEffect = effect.next = effect; } else { // 不然将咱们的effect加入到链表结构中最末尾,而后他的next为链表结构的第一个effect // 这里的effect链表是个闭环 const firstEffect = lastEffect.next; lastEffect.next = effect; effect.next = firstEffect; componentUpdateQueue.lastEffect = effect; } } return effect; }
咱们再看看更新时调用的updateEffectImpl:
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps) { // 这里 updateWorkInProgressHook // workInProgressHook = workInProgressHook.next; // currentHook = currentHook.next; const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; let destroy = undefined; if (currentHook !== null) { const prevEffect = currentHook.memoizedState; destroy = prevEffect.destroy; if (nextDeps !== null) { const prevDeps = prevEffect.deps; // 对比两个依赖数组的各个值之间是否有变更,若是没变更,那么就设置标志位为NoHookEffect if (areHookInputsEqual(nextDeps, prevDeps)) { pushEffect(NoHookEffect, create, destroy, nextDeps); return; } } } currentlyRenderingFiber.effectTag |= fiberEffectTag; hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps); }
咱们能够看到updateEffectImpl和mountEffectImpl很像,最重要的是咱们得两个函数串起来,看看他们到底实现了一个什么。
这里我本身画了一张图,利于理解:
这张图的结构是一个组件某一时刻的结构。
图中黄色为Fiber节点,绿色为Hook节点,蓝色为Effect节点。
Fiber节点,其实就是咱们的虚DOM节点,react会生成一个Fiber节点树,每一个组件在Fiber树上都有对应的Fiber节点。
其中currentlyRenderingFiber表示咱们正在进行渲染的节点,它来自于workInProgress,current表示已经渲染的节点。
组件加载时,会执行各个useEffect,而后就会创建一个Hook链表,而workInProgress的memoizedState字段就指向了Hook链表的尾部Hook节点。
而构建每一个Hook节点时,会同时构造一个Effect节点,一样,Hook节点的memoizedState字段就指向了对应的Effect节点。
而每一个Effect节点又会链接起来造成一个链表,而后workInProgress的updateQueue字段指向了Effect链表的尾部Effect节点。
组件更新时,会依次对比currentHook指向的Effect的依赖数组与新的依赖数组的不一样,若是同样,就设置Effect节点的effectTag为NoHookEffect。
可是不管依赖数组中的值是否改变,都会新构造一个Effect节点,做为Hook节点的memoizedState字段的值。
而后在准备渲染时,会去直接找到Fiber节点的updateQueue的lastEffect,也就是直接指向Effect链表的尾部Effect节点。
由于effect链表是闭环的,这里经过lastEffect的next找到第一个Effect。
而后循环遍历effect链表,当effectTag为NoHookEffect则不作操做,不然会去先执行effect的destroy操做,而后再执行create操做。
对,你没看错,总结起来就是每次更新后,只要依赖项改变,那么就会执行useEffect的卸载函数,再执行第一个参数create函数。
这一部分代码比较远:
function commitHookEffectList( unmountTag, mountTag, finishedWork, ) { const updateQueue = finishedWork.updateQueue; let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { const firstEffect = lastEffect.next; let effect = firstEffect; do { if ((effect.tag & unmountTag) !== NoHookEffect) { // Unmount const destroy = effect.destroy; effect.destroy = undefined; if (destroy !== undefined) { destroy(); } } if ((effect.tag & mountTag) !== NoHookEffect) { // Mount const create = effect.create; effect.destroy = create(); } effect = effect.next; } while (effect !== firstEffect); } }
这里的位运算你们可能有点看不懂,由于NoHookEffect的值是0,因此只要effect.tag被设置为NoHookEffect,那么
effect.tag & unmountTag
就必然为NoHookEffect。
咱们还记得,咱们以前的玩法,依赖数组各个值不变时,就设置Effect节点的effectTag为NoHookEffect。
此时是绝对不会执行先destroy Effect节点,再执行Effect函数create的操做。
而若是effect.tag的值不为NoHookEffect,那也得须要effect.tag与unmountTag至少有一个位相同才能执行destroy。
让咱们看看以前不管是mountEffectImpl仍是updateEffectImpl都默认传的是:UnmountPassive | MountPassive,也就是说effect.tag为UnmountPassive | MountPassive。
而很明显这个设计的目的在于,当mountTag为MountPassive时执行create函数,而unmountTag为UnmountPassive时建立执行destroy函数。
而只有下面这个地方会作这个Passive操做:
export function commitPassiveHookEffects(finishedWork: Fiber): void { if ((finishedWork.effectTag & Passive) !== NoEffect) { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: case Chunk: { commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork); commitHookEffectList(NoHookEffect, MountPassive, finishedWork); break; } default: break; } } }
这里的意思很明显,先遍历一遍effect链表,每一个依赖项变了的hook都destroy一下,而后再遍历一遍effect链表,每一个依赖项变了的,都执行create函数一下。
也就是说每次都会按照useEffect的调用顺序,先执行全部useEffect的卸载函数,再执行全部useEffect的create函数。
而commitPassiveHookEffects又是只有flushPassiveEffects这个函数最终能调用到。
而每次 React 在检测到数据变化时,flushPassiveEffects就会执行。
不管是props仍是state的变化都会如此。
因此若是您真的有须要去模拟一个像以前的componentDidMount和componentWillUnmount的生命周期,那么最好用上一个单独的Effect:
useEffect(()=>{ // 加载时的逻辑 return ()=>{ // 卸载时的逻辑 } },[])
这里用[]做为依赖数组,是由于这样依赖就不会变更,也就是只在加载时执行一次加载逻辑,卸载时执行一次卸载逻辑。
不加依赖数组时,那么每次渲染都会执行一次加载和卸载。
但愿个人这篇文章让您有一些收获。
这里主要想说一下本身在源码阅读中的感想。
读这部分的源码,其实更像是在盲人摸象。
为了追求效率,须要避开一些比较复杂的东西,好比咱们提到的Fiber节点树,又好比其实useEffect替换effect和具体执行effect并非同步的。
不然解读不少东西要花不少时间,怕是真的要写一个系列了。
源码阅读略显艰涩,还好关键的地方都是有注释的,在阅读的过程当中也能收获很多东西。
我并非react的开发者,若是在解读的过程当中有什么疏漏和误解,还望诸位批评指正。