实现虚拟dom
- jsx其实是一种特殊语法,在webpack打包的时候 babel编译会编译成js jsx=>createElement方法
- 虚拟dom就是一个js对象,以js对象的方式描述界面上dom的样子
- React.createElement就是生成虚拟dom的方法
createElement方法
- 创建了一个React对象,里面有个方法叫做createElement
const React = { createElement }
- 参数: 元素的类型,属性,儿子们
- 返回一个对象 就是react元素,即虚拟dom。 包含type,props(children也是props的一部分)
- 这样虚拟dom就构建完成
实现初次渲染
-
定义元素类型
-
render是要把一个元素渲染到一个容器内部
-
每一个fiber对象包含
- tag 标识此元素类型
- stateNode 一般情况下如果这个元素是一个原生节点的话,stateNode指向真实dom元素
- props.children 是一个数组 里面放的是react元素,虚拟dom。后面会根据里面的每一个react元素创建对应的fiber
-
从根节点开始渲染和调度, 分为两个阶段
- render阶段
- 此阶段有两个任务:1.根据虚拟dom生成fiber树; 2.收集effectlist
- 对比新旧虚拟dom,逆行增量更新或创建
- 成果是effect list 知道哪些节点删除了 哪些节点增加了
- 这个阶段可能比较花时间,可以对任务进行拆分,拆分的维度是虚拟dom,此阶段可以暂停
- commit阶段
- 进行dom的更新创建阶段,此阶段不能暂停,要一气呵成
- render阶段
render阶段
- performUnitOfWork
- 对当前fiber节点调用beginWork方法构建fiber树
- 返回下一个fiber节点:有儿子返回儿子;
- 如果没有儿子,则当前节点完成 调用completeUnitOfWork
- 没有儿子 找弟弟
- 如果没有弟弟 先通过return返回父节点 让父亲完成(调用completeUnitOfWork)。再通过sibling找弟弟 (也就是找叔叔)
- 直到找到根节点fiber为止
- beginWork
- 1.先处理自己,创建真实的dom元素,挂在当前节点fiber的stateNode属性上
- 2.创建子fiber 拿出当前节点的children属性(react元素), 遍历 依次创建子元素的fiber,并通过retrun和父节点产生关联;通过sibling和遍历的这些兄弟节点产生关联,形成小fiber树
- beginWork 全部执行完成 fiber树就创建完成了
- completeUnitOfWork
- 收集有副作用的fiber 然后组成effectlist单向链表
- 先找到当前节点fiber的父fiber(currentFiber.return)
- 每个fiber有两个属性 firstEffect指向第一个有副作用的子fiber ; lastEffect指向最后一个有副作用的子fiber; 中间的用nextEffect做成一个单链表
commit阶段
- 循环effectlist单向链表,将dom依次进行挂载
实现元素的更新
- 第一次渲染结束之后,将当前渲染成功的根fiber(workInProgressRoot)赋值给currentRoot(currentRoot = workInProgressRoot);然后清空workInProgressRoot(workInProgressRoot = null)
- workInProgressRoot 渲染的时候有 渲染结束就没了
- currentRoot是一直都有的,代表当前页面上看到的状态
更新的原理
- 在系统中会有两棵fiber树:currentRoot 当前根fiber workInProgressRoot 正在工作的根fiber
- 每个节点都是fiber节点,workInProgressRoot的根fiber和每一个子fiber都有一个指针指向对应的老节点,通过alternate属性。
- 每次更新的时候都是
拿到新的虚拟dom跟老的fiber节点进行比较
,原因是在上次更新的时候,将虚拟dom节点转成了fiber节点,转完之后虚拟dom节点就没有了,只保留了fiber节点,每次重新渲染得到的都是虚拟dom节点,要把虚拟dom节点跟上一次的fiber节点进行对比,能更新则更新,不能则创建 - 通过currentRoot有没有值来判断是初次渲染还是后面的更新渲染
- 要想实现更新,还需要增加一个新的数组,因为删除的节点并不放在effectlist里,需要单独记录
- 在commitRoot方法中,在执行effectlist之前先把该删除的元素删除掉,把deletions清空
- 在commitWork方法中,增加删除节点和更新文本节点的逻辑
dom-diff 比较
-
在 reconcileChildren方法中
-
在初次渲染时,reconcileChildren方法完成的是:根据当前节点的chidlren属性,也就是新的虚拟dom数组创建子fiber树
-
通过当前节点的alternate属性找到对应的老fiber,从而找到老fiber的儿子fiber => oldFiber
-
while循环中加入 || oldFiber 因为新老节点 可能是删除或者增加操作 保证都能循环到
-
通过老的fiber节点和新的虚拟dom进行比较来生成新的fiber节点,根据不同的情况标记不同的effectTag同时将需要删除的节点加入到deletions数组当中
-
if (oldFiber.alternate) { 说明至少已经更新了一次,可以复用fiber对象了
双缓冲机制
- 每次更新需要生成一个新的fiber树,fiber节点通过alternate指针指向老节点。不停的更新就不停的创建fiber树,每个fiber树的每个节点都是新的,更新的越来越多,性能会越来越差(因为对象创建的越来越多)
- 所以出现了一个优化方案叫做 双缓冲
- 第一次渲染创建一棵fiber树,赋值给currentRoot
- 第二次渲染再创建一棵fiber树,并通过alternate进行关联,赋值给currentRoot
- 到第三次渲染时,是将第一次渲染出来的fiber树拿过来作为这一次的workInProgressRoot,让他的alternate指向第二次的渲染出来的fiber树
- 后续就一直复用上上一次渲染的那棵树,这样就永远只有两棵树在来回复用
if (currentRoot && currentRoot.alternate) { // 表示第二次更新以及之后的更新
} else if (currentRoot) {// 表示第一次更新
实现类组件
Component类
updateQueue.js
- 更新队列,每次调用satState的时候会创建一个更新,放到更新队列里去
- 更新队列实际上是一个单链表
export class Update {
constructor(payload) {
this.payload = payload;
}
}
// 单向链表
export class UpdateQueue {
constructor() {
this.firstUpdate = null;
this.lastUpdate = null;
}
enqueueUpdate(update) { // 加入更新队列
if (this.lastUpdate === null) {
this.firstUpdate = this.lastUpdate = update;
} else {
this.lastUpdate.nextUpdate = update;
this.lastUpdate = update;
}
}
forceUpdate(state) { // 更新
let currentUpdate = this.firstUpdate;
while (currentUpdate) {
state = typeof currentUpdate.payload == 'function' ? currentUpdate.payload(state) : currentUpdate.payload;
currentUpdate = currentUpdate.nextUpdate;
}
this.firstUpdate = this.lastUpdate = null;
return state;
}
}
class Component {
constructor(props) {
this.props = props;
this.updateQueue = new UpdateQueue();
}
setState(payload) {
// updateQueue是放在类组件对应的fiber节点上 internalFiber
this.internalFiber.updateQueue.enqueueUpdate(new Update(payload));
scheduleRoot(); // 从根节点开始调度
}
}
Component.prototype.isReactComponent = true;
let React = {
createElement,
Component
}
- beginWork
-
类组件的stateNode是组件的实例
-
currentFiber.type 是类本身(如 class Counter extends React.Component中的Counter)
-
双向指针:
currentFiber.stateNode.internalFiber = currentFiber;
类组件的实例的internalFiber属性指向fibercurrentFiber.stateNode = new currentFiber.type(currentFiber.props);
fiber的stateNode指向组件的实例
-
初始化更新队列
-
给组件实例的state赋值
-
老fiber的儿子和新虚拟dom进行对比
-
- 类组件本身也会创建一个fiber节点, stateNode是类的实例,并不是dom元素,所以实例里的div要挂到这个实例fiber对应的父fiber对应的真实节点上去。所以commit也要做出更改
- commitWork中:需要处理非dom节点的情况
- 如果不是dom节点fiber,通过return向上查找,知道找到真实dom节点fiber为止
- 新增节点时,通过children向下查找真实dom节点
- 删除也需要判断
函数组件的实现
- beginWork
- 没有实例,所以处理起来比较简单
- 将执行之后返回的虚拟dom 来对比创建fiber树即可
- 在reconcileChildren中加入函数组件类型 其他逻辑不变
实现hooks
- useState是一个语法糖,是基于useReducer实现的
- hooks的使用
import React from './react';
import ReactDOM from './react-dom';
function reducer(state, action) {
switch (action.type) {
case 'ADD':
return { count: state.count + 1 };
default:
return state;
}
}
function FunctionCounter() {
const [numberState, setNumberState] = React.useState({ number: 0 });
const [countState, dispatch] = React.useReducer(reducer, { count: 0 });
return (
<div>
<h1 onClick={() => setNumberState(state => ({ number: state.number + 1 }))}>
Count: {numberState.number}
</h1 >
<hr />
<h1 onClick={() => dispatch({ type: 'ADD' })}>
Count: {countState.count}
</h1 >
</div>
)
}
ReactDOM.render(
<FunctionCounter />,
document.getElementById('root')
);
- 为了实现hooks,首先需要加两个相关的变量
let workInProgressFiber = null;
正在工作中的fiberlet hookIndex = 0;
hook索引- 因为一个函数里面可能会有多个hooks,需要使用索引来指向每一个hook
- 实现useReducer
- 当执行useReducer的时候,首先需要知道是在哪一个函数组件中执行的useReducer
- 然后要知道当前的hook是第几个hook
- workInProgressFiber是在真正执行函数组件的时候创建的
- 在执行currentFiber.type(currentFiber.props)之前, 现将下面三个变量赋值
- 所以在执行函数组件之前:
workInProgressFiber = currentFiber;
当前的function component对应的fiberhookIndex = 0;
workInProgressFiber.hooks = [];
循环链表
- 环状链表
- queue.pending永远指向最后一个更新
- queue.pending.next 永远指向第一个更新
- 更新的顺序是不变的
- 循环链表的构建和遍历
-
初次渲染流程图
-
更新流程图
-
currentlyRenderingFiber 当前正在工作的fiber
-
workInProgressHook 函数组件中当前正在工作的hook 执行到哪一个hook workInProgressHook就指的是谁
-
所有的hook通过next连接成一个单向链表