众所周知react18已经发布了,我们可以在github上拉取到源码进行学习,顺便看看react的新特性是如何实现的。由于我之前没看过react的源码,所以这篇文章主要写一下hook的原理
本文主要讲解的是hook机制的实现,至于react的更新机制(为什么组件会更新)则会在后面再开一篇文章进行讲解
很多人会觉得看react的源码很难,看不下去,我以前也同感,但是我最近花了大概十来个小时看react18的hook源码,看下来之后总结了一点心得,希望可以帮助到想阅读源码但是感到困难的朋友。
react是用js实现的,只不过使用了一些设计模式、数据结构、一些开发技巧而已,
看到以下代码均可无脑跳过,当然也可以看一下帮助阅读理解,但是与主流程逻辑无关,跳过这些代码以后,hook三千行源码可以直接减个小一千行
if (__DEV__) {...}
react采用了分包的策略,导致不好定位逻辑的位置,这里我梳理了一些源码文件,感兴趣的朋友可以自己去阅读看看(我花了大概十个小时,一边摸鱼一边看的状态)
react/packages/react/index.js
react/packages/react/src/React.js
react/packages/react/src/ReactCurrentDispatcher.js
react/packages/react/src/ReactHooks.js
react/packages/react-reconciler/src/ReactFiberHooks.new.js
我们正常阅读的相关是从左到右,从上到下的,是要求内容是连贯的。但是看源码的时候,我们要先看最上层的代码,然后看到一个函数,就看这个函数的逻辑,如果函数里还用到了其他函数,那就再看里面函数的实现,等看完了,回到最上层,已经不记得前面看的是什么了
对于这个问题,我有三个建议
一年半前我入职虾皮的时候,也曾尝试过看react的源码,但是当时失败了,回想起来在虾皮的这些时间,我不断遇到和react机制有关的问题,比如js的事件循环,在react中直接操作dom等,虽然每次解决方案都很简单,但是背后的思考都增加了对react的理解,所以今天的我才能看懂react的源码
要想看到hook的源码,首先就要找到源码的位置,那么现在来找react18中hook的源码到底在哪里
以下的useXXX指代官方提供的hook如useEffect、useCallback等
react/packages/react/index.js
export {
...,
useXXX,
...,
} from './src/React';
react/packages/react/src/React.js
import {
...,
useXXX,
...
} from './ReactHooks';
export {
...,
useXXX,
...,
}
分析react/packages/react/index.js
以及react/packages/react/src/React.js
文件的导出导入关系,初步可以定位hook的实现位于react/packages/react/src/ReactHooks.js
文件中,那么我们直接来看ReactHooks.js文件里到底是怎么实现的hook
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cJbzQrPa-1654824221002)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/47aacb633c3f4d6892a8329b8dc29592~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image?)] 可恶哇,我本以为可以直接看到hook的实现,为何犹抱琵琶半遮面?那么这个resolveDispatcher()函数到底做了什么呢?
import ReactCurrentDispatcher from './ReactCurrentDispatcher';
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
if (__DEV__) {
...
}
// Will result in a null access error if accessed outside render phase. We
// intentionally don't throw our own error because this is in a hot path.
// Also helps ensure this is inlined.
return ((dispatcher: any): Dispatcher);
}
看样子所有hook真正的实现都藏在这个dispatcher里头了,好,那么我就来看看这个dispatcher到底是何方神圣。我打开了ReactCurrentDispatcher.js文件,然后呆住了
import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
};
export default ReactCurrentDispatcher;
ReactCurrentDispatcher.js文件里只有这么三两行代码,相信大家看到这里一定是崩溃的,毕竟根据依赖关系查了四五个文件,结果发现跟丢了,虽然找到了ReactCurrentDispatcher的实现,可是完全看不到hook实现的相关代码,更气人的是,用vscode搜索以后,发现相关的文件有30个,根本无法定位
那么怎么办呢?很简单,算法做不出来?暴力遍历之!看代码就要有锲而不舍的精神,查看全部跟ReactCurrentDispather有关的文件,发现一个文件react/packages/react-reconciler/src/ReactFiberHooks.new.js
非常可疑,实际上,这就是真正实现hook的地方
由于hook的实现略有些复杂,因此我就不根据源码阅读的顺序来讲解了,因为那样会提高理解的成本。我将直接讲述hook的原理。
react团队根据组件挂载、组件更新、组件重渲染这三种场景设计了三个Dispatcher: HooksDispatcherOnMount、HooksDispatcherOnUpdate、HooksDispatcherOnRerender
首先我们都知道,es6 module导出的是引用,因此ReactCurrentDispatcher的实现是可以被更改的。React每次更新组件之前,会根据上下文(其实就是链表中hook的状态)来判断这是一次组件挂载、组件更新、组件重渲染,然后会去取三种dispather中对应的那一个。虽然我们明面上调用的是useXXX(包括但不需要useEffect、useCallback、useState)这些hook,但是对于不同的更新模式,实际执行的逻辑是不一样的
PS 有部分hook在更新组件和重渲染组件时做的事情是一样的,比如useCallback
众所周知,react官方规定,开发者必须保证hook永远按相同的顺序运行,这是为什么呢?因为react内部是通过链表来记录每个hook的状态的,因此才要保证hook的顺序永远一致。换句话说,只要hook的顺序保持一致,那么无论是挂载组件的时候,还是组件更新的时候,还是组件重渲染的时候,按顺序从链表里拿出来的数据都能跟hook当前的状态一一对应
代码中记录hook状态的链表的变量是workInProgressHook
workInProgressHook的链表定义,baseQueue以及queue两个字段跟react的更新有关,暂不解读
export type Hook = {|
memoizedState: any, // 该hook上次运行的状态
baseState: any, // 该hook本次运行的状态
baseQueue: Update | null,
queue: any,
next: Hook | null,
|};
在第一次渲染组件的时候,react会记录下每个hook的执行顺序以及hook里的状态到workInProgressHook链表中。
所以对于后面每一次组件渲染中,react都会按照一成不变的顺序来执行hook,而hook中的逻辑和数据则由更新的模式而决定,在执行完全部的hook之后,返回ReactElement给调用方,这就是函数组件执行的过程。这过程中单个hook执行的伪代码如下
func = dispatcher.useXXX;
const res = func(...hookParams); // 计算hook结果 其中钩子的具体实现中(mountXXX、updateXXX、rerenderXXX)会去读取workInProgressHook中的数据
workInProgressHook = workInProgressHook.next;
return res;
本文讲述了在一次渲染中,react的函数组件中的hook是怎么运行的原理,至于函数组件如何更新,effect是如何产生,如何引起,我将会在下一篇文章详细讲述,感兴趣的朋友可以关注一下以免走丢~