React18 源码阅读 (一) -- 深入浅出hook

众所周知react18已经发布了,我们可以在github上拉取到源码进行学习,顺便看看react的新特性是如何实现的。由于我之前没看过react的源码,所以这篇文章主要写一下hook的原理

本文主要讲解的是hook机制的实现,至于react的更新机制(为什么组件会更新)则会在后面再开一篇文章进行讲解

阅读源码的小tips

很多人会觉得看react的源码很难,看不下去,我以前也同感,但是我最近花了大概十来个小时看react18的hook源码,看下来之后总结了一点心得,希望可以帮助到想阅读源码但是感到困难的朋友。

react是用js实现的,只不过使用了一些设计模式、数据结构、一些开发技巧而已,

跳过一切__DEV__相关代码

看到以下代码均可无脑跳过,当然也可以看一下帮助阅读理解,但是与主流程逻辑无关,跳过这些代码以后,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源码中涉及了很多设计模式、数据结构、js的语言特性,如果不了解这些的话,不建议阅读react源码
  • 做笔记 记录下每个函数的实现,真正核心的实现就涉及几个函数,做笔记能帮助我们记录下每个函数的实现,从而避免栈式阅读
  • 多思考 看到一个函数的实现,一个类型声明的时候,要思考为何这样设计,是想用什么数据结构,什么设计模式?当理解了代码的意图以后,代码就会很好理解

一年半前我入职虾皮的时候,也曾尝试过看react的源码,但是当时失败了,回想起来在虾皮的这些时间,我不断遇到和react机制有关的问题,比如js的事件循环,在react中直接操作dom等,虽然每次解决方案都很简单,但是背后的思考都增加了对react的理解,所以今天的我才能看懂react的源码

React18 hook源码定位

要想看到hook的源码,首先就要找到源码的位置,那么现在来找react18中hook的源码到底在哪里

定位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

ReactHooks.js

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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的地方

React18 hook运行原理

由于hook的实现略有些复杂,因此我就不根据源码阅读的顺序来讲解了,因为那样会提高理解的成本。我将直接讲述hook的原理。

dispatcher

react团队根据组件挂载、组件更新、组件重渲染这三种场景设计了三个Dispatcher: HooksDispatcherOnMount、HooksDispatcherOnUpdate、HooksDispatcherOnRerender

首先我们都知道,es6 module导出的是引用,因此ReactCurrentDispatcher的实现是可以被更改的。React每次更新组件之前,会根据上下文(其实就是链表中hook的状态)来判断这是一次组件挂载、组件更新、组件重渲染,然后会去取三种dispather中对应的那一个。虽然我们明面上调用的是useXXX(包括但不需要useEffect、useCallback、useState)这些hook,但是对于不同的更新模式,实际执行的逻辑是不一样的

  • 挂载组件的时候,useXXX实际执行的逻辑是mountXXX函数(包括但不限于mountEffect、mountCallback)
  • 更新组件的时候,useXXX实际执行的是updateXXX函数
  • 重渲染组件的时候,useXXX实际执行的rerenderXXX函数

PS 有部分hook在更新组件和重渲染组件时做的事情是一样的,比如useCallback

何以记录hook状态

众所周知,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是如何产生,如何引起,我将会在下一篇文章详细讲述,感兴趣的朋友可以关注一下以免走丢~

你可能感兴趣的:(react.js,javascript,前端)