React useEffect的源码解读
前言
对源码的解读有利于搞清楚hooks到底做了什么,如果您觉得useeffect很“魔法”,这篇文章也许对您有些帮助。
本篇博客篇幅有限,只看useeffect,力求简单明了,带您到react hooks的深处看看
按图索骥找到hook相关源码(可以直接跳)
首先我们从github上得到react的源码,然后可以在packages中找到react文件夹,其中的index.js就是我们的入口。
代码很简单,就两行:
const react = require('./src/react'); module.exports = react.default || react;
所以接下来我们去看看 'react/src/react',代码有点多,我们简化一下:
import reactversion from 'shared/reactversion'; // ... import { useeffect, } from './reacthooks'; const react = { useeffect }; //... export default react;
很好,现在我们至少知道为什么hooks的引用方式是:
import {useeffect} from 'react'
接下来我们继续看看 'react/src/reacthooks'。
reacthooks文件(可以直接跳)
之前说好了只看useeffect,所以同样需要简化一下代码。
并且考虑到有人对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文件(可以直接跳)
看一下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'。
reactfiberhooks文件
几千行代码,头大。但是莫慌,咱们又不是来写react的,看看原理而已。
我们之前已经知道useeffect实际上是reactcurrentdispatcher.current.useeffect。
很明显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很像,最重要的是我们得两个函数串起来,看看他们到底实现了一个什么。
hook相关数据结构简图
这里我自己画了一张图,利于理解:
这张图的结构是一个组件某一时刻的结构。
图中黄色为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的开发者,如果在解读的过程中有什么疏漏和误解,还望诸位批评指正。
上一篇: 【Python3爬虫】突破反爬之应对前端反调试手段
下一篇: 【iOS】生成二维码
推荐阅读
-
Vue源码解读之Component组件注册的实现
-
jQuery选择器源码解读(四):tokenize方法的Expr.preFilter
-
React16源码解读:开篇带你搞懂几个面试考点
-
jQuery选择器源码解读(五):tokenize的解析过程
-
使用VScode 插件debugger for chrome 调试react源码的方法
-
Asp.Net Core Authorize你不知道的那些事(源码解读)
-
探究react-native 源码的图片缓存问题
-
hadoop2.6源码解读之DFSClient方法调用的RPC流程讲解
-
SpringMVC处理器适配器--源码解读前置知识HandlerMethodArgumentResolver与HandlerMethodReturnV接口的使用
-
源码解读 Golang 的 sync.Map 实现原理