# React笔记

# React的理念

快速响应

  • 速度快
  • 响应自然

速度快:

由于React语法的灵活性,在编译时无法区分可能变化的部分,所以它为了速度快需要在运行时做更多的努力,例如:

  • 使用PureComponentReact.memo构建组件
  • 使用shouldComponentUpdate生命周期钩子
  • 渲染列表时使用key
  • 使用useCallbackuseMemo缓存函数和变量

响应自然:

将同步更新变为可中断的异步更新,在浏览器每一帧的时间中,预留一些时间给JS线程,React利用这部分时间更新组件,源码中预留的时间是5ms。当预留的时间不够用时,React将线程控制权交还给浏览器使其有时间渲染UI,React则等待下一帧时间到来继续被中断的工作。

# React15架构及缺点

# 架构

  • Reconciler(协调器):负责找出变化的组件
  • Renderer(渲染器):负责将变化的组件渲染到页面上

# 更新流程

当有发生更新时,Reconciler会做:

  • 调用函数组件、或class组件的render方法,将返回的JSX转化为虚拟DOM
  • 将虚拟DOM和上次更新时的虚拟DOM对比
  • 通过对比找出本次更新中变化的虚拟DOM
  • 通知Renderer将变化的虚拟DOM渲染到页面上

# React15架构的缺点:

Reconciler中,更新的方式是递归更新子组件,而对于递归更新,更新一旦开始,中途就无法中断,所以当层级很深时,递归更新时间就可能会超过16.6ms(主流浏览器刷新频率为60Hz,即每1000ms/60Hz,16.6ms刷新一次),这样用户交互就会卡顿。

而解决以上问题的关键就是用可中断的异步更新代替同步的更新,但是React15的架构却是不支持异步更新的。

因为在React15,ReconcilerRenderer是交替着工作,当前一轮更新的任务结束之后,第二轮再进行Renconciler,由于整个过程都是同步的,所以如果此时中途中断更新,就会使得后面的任务不执行,渲染也就停止了。

# React16架构

# 架构:

  • Scheduler(调度器):调度任务的优先级,高优先级的任务先进入Reconciler
  • Reconciler(协调器):负责找出变化的组件
  • Renderer(渲染器):负责将变化的组件渲染到页面上

相比于React15,新增了Scheduler

# Scheduler:

一个独立于React的库,实际上是一个功能完备的requestIdleCallbackpolyfill。

它产生的原因是:

  • 浏览器需要用是否有剩余时间作为任务中断的标准,所以我们需要一种机制,当浏览器有剩余时间的时候通知我们。
  • 部分浏览器已经实现了这个API,也就是requestIdleCallback,不过由于它有以下缺点而被React放弃使用了:
    • 浏览器兼容性问题
    • 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换tab后,之前tab注册的requestIdleCallback触发的频率会变得很低

所以React实现了一个功能更完备的requestIdleCallbackpolyfill:Scheduler。

作用:

  • 在空闲时间触发回调,以通知我们浏览器是否有剩余时间做任务中断
  • 提供了多种调度优先级供任务设置

# 更新流程

  • ReconcilerRenderer不再是交替工作
  • 当产生一个更新,先将更新内容交给SchedulerScheduler会判断有没有其它高优先级更新需要执行,若是没有的话则将更新的内容交给Reconcoler
  • Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记
  • 整个SchedulerReconciler的工作都在内存中进行。只有当所有组件都完成Reconciler的工作,才会统一交给Renderer
  • Renderer接收到通知,将变化的虚拟DOM渲染到页面上

# Fiber架构

# 代数效应

代数效应函数式编程中的一个概念,用于将副作用函数调用中分离。

代数效应在React中的应用:

  • useState
  • useRef
  • useReducer

代数效应与Generator:

异步可中断更新可以理解为:更新在执行过程中可能会被打断(浏览器时间分片用尽或有更高优任务插队),当可以继续执行时恢复之前执行的中间状态。

代数效应中try...handle的作用就是为了满足以上需求。

而此功能与浏览器原生的Generator很像,但它存在一些缺陷,因此不被使用:

  • 类似asyncGenerator也是传染性的,使用了Generator则上下文的其他函数也需要作出改变。这样心智负担比较重。
  • Generator执行的中间状态是上下文关联的。如果后面的计算需要依赖与前面的计算结果,就需要重新计算。

代数效应与Fiber:

Fiber:中文翻译为纤程,与进程(Process)、线程(Thread)、协程(Coroutine)同为程序执行过程。

可以理解为纤程是协程的一种实现,而在JS中协程的实现就是Generator

所以,我们可以将纤程(Fiber)、协程(Generator)理解为代数效应思想在JS中的体现。

React Fiber可以理解为:

React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态

其中每个任务更新单元为React Element对应的Fiber节点

# Fiber架构的工作原理

Fiber树是什么?它的构建和替换过程又是怎样的?

# 双缓存:

在内存中构建并直接替换的技术叫做双缓存

# 双缓存Fiber树:

在React中最多同时存在两颗Fiber树

  • 屏幕上显示内容对应着:current Fiber树
  • 正在内存中构建对应着:workInProgress Fiber树

Fiber树的节点:

  • current Fiber树中的节点:currentFiber
  • workInProgress Fiber树workInProgressFiber

两种节点之间的关系,靠alternate属性连接:

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

而在React应用的根节点就是通过current指针在不同的Fiber树rootFiber间切换来实现Fiber树的切换,这个过程伴随着DOM的更新。

FiberRootNode和rootFiber

在讲解切换更新流程时,需要明确几个概念:

如下的代码:

function App() {
  const [num, add] = useState(0);
  return (
    <p onClick={() => add(num + 1)}>{num}</p>
  )
}

ReactDOM.render(<App/>, document.getElementById('root'));

首次执行ReactDOM.render时会创建:

  • fiberRootNode(源码中为fiberRoot),它是整个应用的根节点;
  • rootFiber,它是<App />组件树的根节点。

在应用中我们可能会多次调用ReactDOM.render渲染不同的组件树,所以它们可能会有不同的rootFiber,但是fiberRootNode在整个应用中就只有一个。

它俩之间的关系,是靠fiberRootNode的一个名为current的属性来联系的:

fiberRootNode.current = rootFiber

mount和update阶段

而对应着生命周期来说,会分为mountupdate阶段。

mount时,会有三个阶段:

  1. 首屏渲染时,页面没有挂载任何的DOM,此时fiberRootNode.current = rootFiber,但是rootFiber并没有任何的子Fiber节点,所以此时currentFiber为空。
  1. 到了render阶段,根据组件的JSX在内存中依次构建了Fiber节点并连接成了Fiber树,这棵树也就是workInProgress Fiber树:
  1. 已构建完的workInProgress Fiber树commit阶段渲染到页面,此时fiberRootNodecurrent指针指向workInProgress Fiber树使其变为current Fiber 树

update阶段其实就像是我们前面说的了,当有节点触发状态改变:

  1. 会开启一次新的render阶段,构建一个新的workInProgress Fiber树
  1. workInProgress Fiber 树render阶段完成构建后进入commit阶段渲染到页面上。渲染完毕后,workInProgress Fiber 树变为current Fiber 树

# React源码文件结果

根目录
├── fixtures        # 包含一些给贡献者准备的小型 React 测试项目
├── packages        # 包含元数据(比如 package.json)和 React 仓库中所有 package 的源码(子目录 src)
├── scripts         # 各种工具链的脚本,比如git、jest、eslint等

# packages

具体来看看packages文件夹:

  • react文件夹:React的核心,包含所有全局 React API,例如React.createElement、React.Component
  • scheduler文件夹:Scheduler(调度器)的实现
  • share文件夹:源码中其他模块公用的方法全局变量

Renderer相关文件夹

- react-art
- react-dom                 # 注意这同时是DOM和SSR(服务端渲染)的入口
- react-native-renderer
- react-noop-renderer       # 用于debug fiber(后面会介绍fiber)
- react-test-renderer

试验性包的文件夹

- react-server        # 创建自定义SSR流
- react-client        # 创建自定义的流
- react-fetch         # 用于数据请求
- react-interactions  # 用于测试交互相关的内部特性,比如React的事件模型
- react-reconciler    # Reconciler的实现,你可以用他构建自己的Renderer

辅助包的文件夹

- react-is       # 用于测试组件是否是某类型
- react-client   # 创建自定义的流
- react-fetch    # 用于数据请求
- react-refresh  # “热重载”的React官方实现

比较重要的:

  • react-reconciler,他一边对接Scheduler,一边对接不同平台的Renderer,构成了整个 React16 的架构体系。

# 调试源码

说明:

  • 现在我们通过create-react-app创建的React项目中使用到的react源码不一定和 facebook/react 仓库中master分支的一样,因为给用户使用的必须是趋于稳定的一个版本。
  • 当我们在调试源码的时候可以选择仓库中的最新代码。

其实想要调试源码,大体来说只需要这么几个步骤:

  • facebook/react 仓库中clonemaster分支的最新源码

  • 在本地用最新的源码buildreactschedulerreact-dom三个包为dev环境可以使用的cjs包,可以使用指令:

    • yarn build react/index,react-dom/index,scheduler --type=NODE
      
    • 此时源码目录build/node_modules下会生成最新代码的包,为接下来的link做准备。

  • 使用create-react-app创建一个项目用于调试源码,然后将上述源码中的build包与此项目进行yarn link关联。

# JSX

# 为什么引入JSX?

React 认为组件才是王道,而组件是和模板紧密关联的,组件模板和组件逻辑分离让问题复杂化了。

所以就有了 JSX 这种语法,就是为了把 HTML 模板直接嵌入到 JS 代码里面,这样就做到了模板和组件关联,但是 JS 不支持这种包含 HTML 的语法,所以需要通过工具将 JSX 编译输出成 JS 代码才能使用。

例如页面已有的HTML代码为:

<body>
	<div id="app"></div>
</body>

我们想要在其中添加上一个简单的a标签:

<a href="http://lindaidai.wang">LinDaiDai</a>

如果想要将它用JS代码表示出来,你可能想到的会是用下面这种方式:

const app = document.getElementById('app');
const a = document.createElement('a');
a.setAttribute('href', 'https://lindaidai.wang');
a.innerHTML = 'LinDaiDai';
app.appendChild(a);

映射到React中,可能会是这样写:

React.createElement('a', {href: 'https://lindaidai.wang'}, 'LinDaiDai')
  • type:可以是一个string类型,也可以是一个组件

JSXjs的映射:

输入JSX

var a = <a href="http://lindaidai.wang">LinDaiDai</a>;

通过Babel编译输出JS

var a = React.createElement('a', {href: 'https://lindaidai.wang'}, 'LinDaiDai')

所以我们在每个JSX文件都必须显式的声明引入React

import React from 'react';

JSX返回的是一个ReactElement对象,不是组件实例

而在JS,也就是React.createElement它返回的是对组件的引用也就是组件实例

// A ReactElement
const myComponent = <MyComponent />

// render
const myComponentInstance = React.createElement(MyComponent, mountNode);
myComponentInstance.doSomething();

另外,需要注意的是,JSX并不只会被编译为React.createEelement这种形式。

当使用@babel/plugin-transform-react-jsx插件的时候,可以显式的告诉Babel编译时需要把JSX编译成一种函数调用的方式。

比如在preact这个类React库中,JSX会被编译为一个名为h的函数调用。

// 编译前
<span>LinDaiDai</span>
// 编译后
h("span", null, "LinDaiDai");

# 源码中的Reac.createElement

源码中的位置为:https://github.com/facebook/react/blob/master/packages/react/src/ReactElement.js

最终是会返回一个带有$$typeof: REACT_ELEMENT_TYPE标记的对象,这个对象就表示这其为React Element

全局有一个函数用于检测是不是React Element

export function isValidElement(object) {
  return (
    typeof object === 'object' &&
    object !== null &&
    object.$$typeof === REACT_ELEMENT_TYPE
  );
}

React中,所有JSX在运行时的返回结果(即React.createElement()的返回值)都是React Element

# JSX和React Component的关系

React中,我们常使用ClassComponentFunctionComponent构建组件。

对于两种组件来说,我们如果将其在控制台中打印出来,会发现打印对象的type都是它们自身。

而如果想要区分是哪种组件的话,可以通过ClassComponent实例原型上的isReactComponent变量判断是否是ClassComponent

ClassComponent.prototype.isReactComponent = {};

但是不能通过引用类型来区分,因为:

AppClass instanceof Function === true;
AppFunc instanceof Function === true;

# JSX和Fiber节点

从上面的内容我们可以发现,JSX是一种描述当前组件内容的数据结构,他不包含组件schedulereconcilerender所需的相关信息。

比如如下信息就不包括在JSX中:

  • 组件在更新中的优先级
  • 组件的state
  • 组件被打上的用于Renderer标记

这些内容都包含在Fiber节点中。

所以,在组件mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点

  • React元素:创建开销极小的普通对象
  • 能够直接使用HTML标签这样的写法:const element = <div className="foo" />是因为JSX
  • 将一个元素渲染为DOMReactDOM.render(element, container)
  • JSX是一种描述当前组件内容的数据结构
  • Fiber节点包含组件在更新中的优先级,组件的state,组件被打上用于Renderer的标记
阅读全文