# React笔记
# React的理念
快速响应:
- 速度快
- 响应自然
速度快:
由于React
语法的灵活性,在编译时无法区分可能变化的部分,所以它为了速度快需要在运行时做更多的努力,例如:
- 使用
PureComponent
或React.memo
构建组件 - 使用
shouldComponentUpdate
生命周期钩子 - 渲染列表时使用
key
- 使用
useCallback
和useMemo
缓存函数和变量
响应自然:
将同步更新变为可中断的异步更新,在浏览器每一帧的时间中,预留一些时间给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,Reconciler
和Renderer
是交替着工作,当前一轮更新的任务结束之后,第二轮再进行Renconciler
,由于整个过程都是同步的,所以如果此时中途中断更新,就会使得后面的任务不执行,渲染也就停止了。
# React16架构
# 架构:
Scheduler
(调度器):调度任务的优先级,高优先级的任务先进入Reconciler
Reconciler
(协调器):负责找出变化的组件Renderer
(渲染器):负责将变化的组件渲染到页面上
相比于React15,新增了Scheduler
。
# Scheduler:
一个独立于React的库,实际上是一个功能完备的requestIdleCallback
polyfill。
它产生的原因是:
- 浏览器需要用是否有剩余时间作为任务中断的标准,所以我们需要一种机制,当浏览器有剩余时间的时候通知我们。
- 部分浏览器已经实现了这个API,也就是
requestIdleCallback
,不过由于它有以下缺点而被React
放弃使用了:- 浏览器兼容性问题
- 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换tab后,之前tab注册的
requestIdleCallback
触发的频率会变得很低
所以React实现了一个功能更完备的requestIdleCallback
polyfill:Scheduler。
作用:
- 在空闲时间触发回调,以通知我们浏览器是否有剩余时间做任务中断
- 提供了多种调度优先级供任务设置
# 更新流程
- Reconciler与Renderer不再是交替工作
- 当产生一个更新,先将更新内容交给Scheduler,Scheduler会判断有没有其它高优先级更新需要执行,若是没有的话则将更新的内容交给Reconcoler
- 当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记
- 整个Scheduler与Reconciler的工作都在内存中进行。只有当所有组件都完成Reconciler的工作,才会统一交给Renderer
- Renderer接收到通知,将变化的虚拟DOM渲染到页面上
# Fiber架构
# 代数效应
代数效应
是函数式编程
中的一个概念,用于将副作用
从函数
调用中分离。
代数效应在React中的应用:
useState
useRef
useReducer
代数效应与Generator:
异步可中断更新
可以理解为:更新
在执行过程中可能会被打断(浏览器时间分片用尽或有更高优任务插队),当可以继续执行时恢复之前执行的中间状态。
代数效应中try...handle
的作用就是为了满足以上需求。
而此功能与浏览器原生的Generator
很像,但它存在一些缺陷,因此不被使用:
- 类似
async
,Generator
也是传染性
的,使用了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阶段
而对应着生命周期来说,会分为mount
和update
阶段。
在mount
时,会有三个阶段:
- 首屏渲染时,页面没有挂载任何的
DOM
,此时fiberRootNode.current = rootFiber
,但是rootFiber
并没有任何的子Fiber节点
,所以此时currentFiber
为空。
![](https://react.iamkasong.com/img/rootfiber.png)
- 到了
render阶段
,根据组件的JSX
在内存中依次构建了Fiber节点
并连接成了Fiber树
,这棵树也就是workInProgress Fiber树
:
![](https://react.iamkasong.com/img/workInProgressFiber.png)
- 已构建完的
workInProgress Fiber树
在commit阶段
渲染到页面,此时fiberRootNode
的current
指针指向workInProgress Fiber树
使其变为current Fiber 树
:
![](https://react.iamkasong.com/img/wipTreeFinish.png)
update
阶段其实就像是我们前面说的了,当有节点触发状态改变:
- 会开启一次新的
render
阶段,构建一个新的workInProgress Fiber树
:
![](https://react.iamkasong.com/img/wipTreeUpdate.png)
workInProgress Fiber 树
在render阶段
完成构建后进入commit阶段
渲染到页面上。渲染完毕后,workInProgress Fiber 树
变为current Fiber 树
:
![](https://react.iamkasong.com/img/currentTreeUpdate.png)
# 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 仓库中
clone
下master
分支的最新源码在本地用最新的源码
build
出react
、scheduler
、react-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
类型,也可以是一个组件
JSX
到js
的映射:
输入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
中,我们常使用ClassComponent
与FunctionComponent
构建组件。
对于两种组件来说,我们如果将其在控制台中打印出来,会发现打印对象的type
都是它们自身。
而如果想要区分是哪种组件的话,可以通过ClassComponent
实例原型上的isReactComponent
变量判断是否是ClassComponent
:
ClassComponent.prototype.isReactComponent = {};
但是不能通过引用类型来区分,因为:
AppClass instanceof Function === true;
AppFunc instanceof Function === true;
# JSX和Fiber节点
从上面的内容我们可以发现,JSX
是一种描述当前组件内容的数据结构,他不包含组件schedule、reconcile、render所需的相关信息。
比如如下信息就不包括在JSX
中:
- 组件在更新中的
优先级
- 组件的
state
- 组件被打上的用于Renderer的
标记
这些内容都包含在Fiber节点
中。
所以,在组件mount
时,Reconciler
根据JSX
描述的组件内容生成组件对应的Fiber节点
。
React
元素:创建开销极小的普通对象- 能够直接使用HTML标签这样的写法:
const element = <div className="foo" />
是因为JSX
- 将一个元素渲染为
DOM
:ReactDOM.render(element, container)
JSX
是一种描述当前组件内容的数据结构Fiber
节点包含组件在更新中的优先级,组件的state
,组件被打上用于Renderer
的标记