前言

对源码的解读有利于搞清楚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的开发者,如果在解读的过程中有什么疏漏和误解,还望诸位批评指正。

React useEffect的源码解读的更多相关文章

  1. React16源码解读:开篇带你搞懂几个面试考点

    引言 如今,主流的前端框架React,Vue和Angular在前端领域已成三足鼎立之势,基于前端技术栈的发展现状,大大小小的公司或多或少也会使用其中某一项或者多项技术栈,那么掌握并熟练使用其中至少一种 ...

  2. create-react-app源码解读之为什么不搞个山寨版的create-react-app呢?

    最近把 vue-cli@2.x 和 create-react-app 的源码都看了一遍.由于现在官方推荐使用 vue-cli@3.0 ,改动比较大,所以就不写关于 vue-cli 的了(据说是因为 v ...

  3. SDWebImage源码解读之SDWebImageDownloaderOperation

    第七篇 前言 本篇文章主要讲解下载操作的相关知识,SDWebImageDownloaderOperation的主要任务是把一张图片从服务器下载到内存中.下载数据并不难,如何对下载这一系列的任务进行设计 ...

  4. SDWebImage源码解读 之 NSData+ImageContentType

    第一篇 前言 从今天开始,我将开启一段源码解读的旅途了.在这里先暂时不透露具体解读的源码到底是哪些?因为也可能随着解读的进行会更改计划.但能够肯定的是,这一系列之中肯定会有Swift版本的代码. 说说 ...

  5. SDWebImage源码解读 之 UIImage+GIF

    第二篇 前言 本篇是和GIF相关的一个UIImage的分类.主要提供了三个方法: + (UIImage *)sd_animatedGIFNamed:(NSString *)name ----- 根据名 ...

  6. SDWebImage源码解读 之 SDWebImageCompat

    第三篇 前言 本篇主要解读SDWebImage的配置文件.正如compat的定义,该配置文件主要是兼容Apple的其他设备.也许我们真实的开发平台只有一个,但考虑各个平台的兼容性,对于框架有着很重要的 ...

  7. SDWebImage源码解读_之SDWebImageDecoder

    第四篇 前言 首先,我们要弄明白一个问题? 为什么要对UIImage进行解码呢?难道不能直接使用吗? 其实不解码也是可以使用的,假如说我们通过imageNamed:来加载image,系统默认会在主线程 ...

  8. SDWebImage源码解读之SDWebImageCache(上)

    第五篇 前言 本篇主要讲解图片缓存类的知识,虽然只涉及了图片方面的缓存的设计,但思想同样适用于别的方面的设计.在架构上来说,缓存算是存储设计的一部分.我们把各种不同的存储内容按照功能进行切割后,图片缓 ...

  9. SDWebImage源码解读之SDWebImageCache(下)

    第六篇 前言 我们在SDWebImageCache(上)中了解了这个缓存类大概的功能是什么?那么接下来就要看看这些功能是如何实现的? 再次强调,不管是图片的缓存还是其他各种不同形式的缓存,在原理上都极 ...

随机推荐

  1. 解决bootStrap selectpicker 下拉栏上方弹出

    最近项目中遇到了一个使用bootStrap selectpicker 进行下拉栏展示的时候出现在元素上方弹出展示的问题,可把我难受坏了,和测试互怼最终以失败告终(人家还是一个娇滴滴的小姑娘),在查了a ...

  2. uva 10253 Series-Parallel Networks (整数划分+多重集)

    UVa Online Judge 题意是计算给定数量的边通过串联并联两种方式,能组成多少种不同的网络.将它转化为一个树形结构,也就是求有多少不同构的树. 代码如下: #include <cstd ...

  3. [\s\S]*?懒惰模式特殊情形

    通常理解[\s\S]*?X (X代表任意指定字符) 表示匹配任何字符的懒惰模式,一旦遇到后面出现的X便停止匹配,但实际不是如此,会尽可能的把后面的内容也匹配进去.如: 表达式 <tr[\s\S] ...

  4. Android教程 -06 Activity的生命周期

    本篇播客我们重点介绍下如下知识点: 程序启动原理 Activity生命周期 Activity销毁与创建 视频建议采用超清模式观看, 欢迎点击订阅我的优酷 程序启动原理 当用户从主界面点击程序图标时,系 ...

  5. hdu 1026 Ignatius and the Princess I(bfs)

    Ignatius and the Princess I Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (J ...

  6. 2006年NOIP普及组复赛题解

    题目涉及算法: 明明的随机数:简单模拟: 开心的金明:01背包: Jam的计数法:模拟: 数列:二进制. 明明的随机数 题目链接:https://www.luogu.org/problem/P1059 ...

  7. .net Framework 源代码 · ScrollViewer

    本文是分析 .net Framework 源代码的系列,主要告诉大家微软做 ScrollViewer 的思路,分析很简单. 看完本文,可以学会如何写一个 ScrollViewer ,如何定义一个 IS ...

  8. 2019-8-24-win10-uwp-读取文本GBK错误

    title author date CreateTime categories win10 uwp 读取文本GBK错误 lindexi 2019-8-24 16:2:27 +0800 2018-2-1 ...

  9. linux scull 的设计

    编写驱动的第一步是定义驱动将要提供给用户程序的能力(机制).因为我们的"设备"是计算 机内存的一部分, 我们可自由做我们想做的事情. 它可以是一个顺序的或者随机存取的设 备, 一个 ...

  10. 2019-5-21-win10-uwp-商业游戏-1.1.5

    title author date CreateTime categories win10 uwp 商业游戏 1.1.5 lindexi 2019-05-21 11:38:20 +0800 2018- ...