接上一篇 手写React Fiber渲染逻辑 一
本章内容介绍:用React fiber实现更新渲染逻辑,实现类组件、函数组件和Hooks
对React Fiber不太了解的,可以翻看上两篇文章
实现更新
调试用例
- 在index.html中新增两个按钮
<div id="root"></div>
<button id="reRender2">reRender2</button>
<button id="reRender3">reRender3</button>
复制代码
- 在index.js中为两个按钮添加事件
// -------------渲染更新----------
let reRender2 = document.getElementById('reRender2');
reRender2.addEventListener('click', () => {
let element2 = (
<div id="A1-new" style={style}>
A1-new
<div id="B1-new" style={style}>
B1-new
<div id="C1-new" style={style}>
C1-new
</div>
<div id="C2-new" style={style}>
C2-new
</div>
</div>
<div id="B2" style={style}>
B2
</div>
<div id="B3" style={style}>
B3
</div>
</div>
);
ReactDOM.render(element2, document.getElementById('root'));
});
let reRender3 = document.getElementById('reRender3');
reRender3.addEventListener('click', () => {
let element3 = (
<div id="A1-new2" style={style}>
A1-new2
<div id="B1-new2" style={style}>
B1-new2
<div id="C1-new2" style={style}>
C1-new2
</div>
<div id="C2-new2" style={style}>
C2-new2
</div>
</div>
<div id="B2" style={style}>
B2
</div>
</div>
);
ReactDOM.render(element3, document.getElementById('root'));
});
复制代码
- 点击两个按钮实现页面的第二和第三次更新渲染,效果如下:
alternate指针
老的fiber指当前页面已经渲染的节点,新fiber指将要渲染的节点
- 初次渲染完毕后,会给每个节点创建一个fiber对象,页面发生更新时,会创建一个新的fiber树
- 如图所示,currentRoot是我们页面当前显示的节点,当页面更新时,会创建一个新的根fiber
workInProgressRoot
- 每个新fiber节点都会有一个
alternate
指针,指向对应的老的fiber节点 - alternate指针的作用就是进行新老节点的对比,进行dom-diff
代码实现
- 在scheduler.js中添加两个全局变量
- 提交完成后,让
currentRoot
指向workInProgressRoot
,将workInProgressRoot
置为null
let currentRoot = null; //当前的根Fiber
let deletions = []; //要删除的fiber节点
// -----------commit阶段-----------
function commitRoot() {
.......
currentRoot = workInProgressRoot;
workInProgressRoot = null;
}
复制代码
- 更新时,
render
函数再次调用scheduleRoot
方法,如果currentRoot
有值就是更新,让rootFiber
的alternate
指针,指向currentRoot
export function scheduleRoot(rootFiber) {
if (currentRoot) {
// 更新
rootFiber.alternate = currentRoot;
workInProgressRoot = rootFiber;
} else {
// 第一次渲染
workInProgressRoot = rootFiber;
}
nextUnitOfWork = workInProgressRoot;
}
复制代码
修改reconcileChildren
方法
此处只做简单的diff,遍历一遍,如果新老节点类型相同就复用,不同就重新创建,没有实现react的diff算法
-
遍历新的虚拟dom,并找到对应的老的oldFiber,进行比较,然后生成新的fiber树,每个新fiber都有
alternate
指针,指向oldFiber
-
老的fiber节点与新的虚拟dom进行对比,如果类型相同,就复用老的fiber节点的
stateNode
,并将newFiber
的alternate
指向oldFiber
,将effectTag
标记为UPDATE
更新 -
如果类型不同,创建
newFiber
时将effectTag
标记为PLACEMENT
插入,不复用老节点的stateNode
,并将老节点oldFiber
加入deletions
中,提交时进行删除
/**
* 创建fiber 构建fiber树
* @param {*} currentFiber 当前fiber
* @param {*} newChildren 当前节点的子节点,虚拟dom数组
* @param {*} deletions 指向全局变量 deletions, 放置要删除的节点
*/
export function reconcileChildren(currentFiber, newChildren, deletions) {
let newChildIndex = 0; //新虚拟DOM数组中的索引
// 老的父fiber的第一个子fiber
let oldFiber = currentFiber.alternate && currentFiber.alternate.child;
let prevSibling;
while (newChildIndex < newChildren.length || oldFiber) {
const newChild = newChildren[newChildIndex];
// 两个节点是不是相同类型 span div...
const sameType = oldFiber && newChild && newChild.type === oldFiber.type;
let newFiber;
let tag;
if (newChild && newChild.type === ELEMENT_TEXT) {
tag = TAG_TEXT; //文本
} else if (newChild && typeof newChild.type === 'string') {
tag = TAG_HOST; //原生DOM组件
}
// 类型相同就更新,不同就重新创建插入
if (sameType) {
// 更新
newFiber = {
tag: oldFiber.tag, //原生DOM组件
type: oldFiber.type, //具体的元素类型
props: newChild.props, //新的属性对象
stateNode: oldFiber.stateNode, //复用老fiber的dom
return: currentFiber, //父Fiber
alternate: oldFiber, //上一个Fiber 指向旧树中的节点
effectTag: UPDATE, //更新节点
nextEffect: null,
};
} else {
// 新建
if (newChild) {
// 创建fiber
newFiber = {
tag, //原生DOM组件
type: newChild.type, //具体的元素类型
props: newChild.props, //新的属性对象
stateNode: null, //stateNode肯定是空的
return: currentFiber, //父Fiber
effectTag: PLACEMENT, //插入节点
};
}
if (oldFiber) {
oldFiber.effectTag = DELETION;
deletions.push(oldFiber);
}
}
// 构建fiber链表
if (newFiber) {
if (newChildIndex === 0) {
currentFiber.child = newFiber; //第一个子节点挂到父节点的child属性上
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber; //然后newFiber变成了上一个哥哥了
}
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
newChildIndex++;
}
}
复制代码
提交阶段
- 提交时,先清空
deletions
,将要删除的节点删除 - 然后根据节点的
effectTag
来进行插入和更新操作
function commitRoot() {
deletions.forEach(commitWork);
let currentFiber = workInProgressRoot.firstEffect;
while (currentFiber) {
commitWork(currentFiber);
currentFiber = currentFiber.nextEffect;
}
// 提交完成
deletions.length = 0;
currentRoot = workInProgressRoot;
workInProgressRoot = null;
}
function commitWork(currentFiber) {
if (!currentFiber) return;
let returnFiber = currentFiber.return;
const domReturn = returnFiber.stateNode;
if (currentFiber.effectTag === PLACEMENT && currentFiber.stateNode) {
//如果是新增DOM节点
domReturn.appendChild(currentFiber.stateNode);
} else if (currentFiber.effectTag === DELETION) {
// 删除
domReturn.removeChild(currentFiber.stateNode);
} else if (currentFiber.effectTag === UPDATE && currentFiber.stateNode) {
// 更新
if (currentFiber.type === ELEMENT_TEXT) {
if (currentFiber.alternate.props.text !== currentFiber.props.text)
// 更新文本节点
currentFiber.stateNode.textContent = currentFiber.props.text;
} else {
// 更新其他节点
updateDOM(currentFiber.stateNode, currentFiber.alternate.props, currentFiber.props);
}
} else {
}
}
currentFiber.effectTag = null;
}
复制代码
commit记录
双缓冲机制
- react中为了避免重复创建销毁fiber对象,造成不必要的内存开销,采用了双缓冲机制
- 页面更新一次之后,每个节点会有两个fiber对象,
newFiber
和newFiber.alternate
指向的oldFiber
- 当第三次渲染时,不会重新创建newFiber,而是会复用老的fiber对象,如图所示
双缓冲机制代码实现
- scheduleRoot方法修改
- 第一次更新之后的更新逻辑,不直接使用
rootFiber
,而是将currentRoot.alternate
给workInProgressRoot
使用,更新它的props
export function scheduleRoot(rootFiber) {
if (currentRoot && currentRoot.alternate) {
// 第一次更新之后的更新
// 双缓冲机制,复用之前的fiber对象
workInProgressRoot = currentRoot.alternate;
workInProgressRoot.alternate = currentRoot;
if (rootFiber) {
// 更新复用fiber节点的props
workInProgressRoot.props = rootFiber.props;
}
} else if (currentRoot) {
// 第一次更新
rootFiber.alternate = currentRoot;
workInProgressRoot = rootFiber;
} else {
// 第一次渲染
workInProgressRoot = rootFiber;
}
// 清除effect list
workInProgressRoot.firstEffect = workInProgressRoot.lastEffect = null;
nextUnitOfWork = workInProgressRoot;
}
复制代码
- reconcileChildren方法修改
- 节点类型相同时,如果老节点有
alternate
,就把老节点的alternate
,拿过来更新一下属性,作为newFiber
使用
if (sameType) {
// 更新
if (oldFiber.alternate) {
// 双缓冲机制,复用老的fiber
newFiber = oldFiber.alternate;
newFiber.props = newChild.props;
newFiber.alternate = oldFiber;
newFiber.effectTag = UPDATE;
} else {
newFiber = {
tag: oldFiber.tag, //原生DOM组件
type: oldFiber.type, //具体的元素类型
props: newChild.props, //新的属性对象
stateNode: oldFiber.stateNode, //复用老fiber的dom
return: currentFiber, //父Fiber
alternate: oldFiber, //上一个Fiber 指向旧树中的节点
effectTag: UPDATE, //更新节点
nextEffect: null,
};
}
}
复制代码
- 这样我们双缓冲机制的更新逻辑就实现了
commit记录
类组件渲染
测试类组件
import React from './react/react';
import ReactDOM from './react/react-dom';
class ClassCounter extends React.Component {
constructor(props) {
super(props);
this.state = { number: 0 };
}
onClick = () => {
this.setState(state => ({ number: state.number + 1 }));
};
render() {
return (
<div id="counter">
<span>{this.state.number}</span>
<button onClick={this.onClick}>加1</button>
</div>
);
}
}
ReactDOM.render(<ClassCounter />, document.getElementById('root'));
复制代码
UpdateQueue
- 更新队列,是个单链表结构
- 每次
setState
都会将要更新的数据放在队列中,到一定时机进行批量更新
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) {
let nextState = typeof currentUpdate.payload === 'function' ? currentUpdate.payload(state) : currentUpdate.payload;
state = { ...state, ...nextState };
currentUpdate = currentUpdate.nextUpdate;
}
this.firstUpdate = this.lastUpdate = null;
return state;
}
}
复制代码
类组件代码实现
- 在react.js文件中声明一个Componet类
- internalFiber指向Componet类对应的fiber节点,fiber节点有updateQueue
- 调用
setState
方法时,会先将要更新的payload放入更新队列,然后执行scheduleRoot
方法
class Component {
constructor(props) {
this.props = props;
}
setState(payload) {
this.internalFiber.updateQueue.enqueueUpdate(new Update(payload));
scheduleRoot();
}
}
// 函数组件标识
Component.prototype.isReactComponent = true;
const React = {
createElement,
Component,
};
复制代码
scheduler.js逻辑修改
- 当类组件setState时 传入的rootFiber为空,需要处理rootFiber为空的情况
// 暴露给外部, 当类组件setState时 传入的rootFiber为空
export function scheduleRoot(rootFiber) {
if (currentRoot && currentRoot.alternate) {
// 第一次之后的更新
// 双缓冲机制,复用之前的fiber对象
workInProgressRoot = currentRoot.alternate;
workInProgressRoot.alternate = currentRoot;
if (rootFiber) {
// 更新复用fiber节点的props
workInProgressRoot.props = rootFiber.props;
}
} else if (currentRoot) {
// 第一次更新
+ if (rootFiber) {
+ rootFiber.alternate = currentRoot;
+ workInProgressRoot = rootFiber;
+ } else {
+ workInProgressRoot = {
+ ...currentRoot,
+ alternate: currentRoot,
+ };
+ }
} else {
workInProgressRoot = rootFiber;
}
// 清除effect list
workInProgressRoot.firstEffect = workInProgressRoot.lastEffect = null;
nextUnitOfWork = workInProgressRoot;
}
复制代码
新增 updateClassComponent
处理类组件的更新
- 类组件的stateNode不是真实dom元素,是类组件的实例
- 每个类组件实例都有一个
internalFiber
指向类组件对应的fiber节点 - 给类组件实例的fiber节点创建一个updateQueue,用来处理类组件的更新逻辑
// 类组件
function updateClassComponent(currentFiber) {
if (!currentFiber.stateNode) {
currentFiber.stateNode = new currentFiber.type(currentFiber.props);
currentFiber.stateNode.internalFiber = currentFiber;
currentFiber.updateQueue = new UpdateQueue();
}
// 获取最新状态
currentFiber.stateNode.state = currentFiber.updateQueue.forceUpdate(currentFiber.stateNode.state);
// 重新渲染组件
const newChildren = [currentFiber.stateNode.render()];
reconcileChildren(currentFiber, newChildren, deletions);
}
复制代码
reconcileChildren
处理类组件的fiber创建
- 判断是否是类组件,给类组件添加对应tag
- 创建fiber时给每个fiber添加
updateQueue
属性,处理组件更新
export function reconcileChildren(currentFiber, newChildren, deletions) {
let newChildIndex = 0; //新虚拟DOM数组中的索引
// 老的父fiber的第一个子fiber
let oldFiber = currentFiber.alternate && currentFiber.alternate.child;
if (oldFiber) oldFiber.firstEffect = oldFiber.lastEffect = oldFiber.nextEffect = null;
let prevSibling;
while (newChildIndex < newChildren.length || oldFiber) {
const newChild = newChildren[newChildIndex];
// 两个节点是不是相同类型 span div...
const sameType = oldFiber && newChild && newChild.type === oldFiber.type;
let newFiber;
let tag;
+ // class 会被编译成函数,所以用function判断, 用 isReactComponent来判断是否是类组件
+ if (
+ newChild &&
+ typeof newChild.type === 'function' &&
+ newChild.type.prototype.isReactComponent
+ ) {
+ tag = TAG_CLASS;
+ } else if (newChild && typeof newChild.type === 'function') {
+ // 函数组件
+ tag = TAG_FUNCTION_COMPONENT;
} else if (newChild && newChild.type === ELEMENT_TEXT) {
tag = TAG_TEXT; //文本
} else if (newChild && typeof newChild.type === 'string') {
tag = TAG_HOST; //原生DOM组件
}
// 类型相同就更新,不同就重新创建插入
if (sameType) {
// 更新
if (oldFiber.alternate) {
// 双缓冲机制,复用老的fiber
newFiber = oldFiber.alternate;
newFiber.props = newChild.props;
newFiber.alternate = oldFiber;
newFiber.effectTag = UPDATE;
+ newFiber.updateQueue = oldFiber.updateQueue || new UpdateQueue();
} else {
newFiber = {
tag: oldFiber.tag, //原生DOM组件
type: oldFiber.type, //具体的元素类型
props: newChild.props, //新的属性对象
stateNode: oldFiber.stateNode, //复用老fiber的dom
return: currentFiber, //父Fiber
alternate: oldFiber, //上一个Fiber 指向旧树中的节点
effectTag: UPDATE, //更新节点
+ updateQueue: oldFiber.updateQueue || new UpdateQueue(),
};
}
} else {
// 新建
if (newChild) {
// 创建fiber
newFiber = {
tag, //原生DOM组件
type: newChild.type, //具体的元素类型
props: newChild.props, //新的属性对象
stateNode: null, //stateNode肯定是空的
return: currentFiber, //父Fiber
effectTag: PLACEMENT, //插入节点
+ updateQueue: new UpdateQueue(),
};
}
if (oldFiber) {
oldFiber.effectTag = DELETION;
deletions.push(oldFiber);
}
}
// 构建fiber链表
if (newFiber) {
if (newChildIndex === 0) {
currentFiber.child = newFiber; //第一个子节点挂到父节点的child属性上
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber; //然后newFiber变成了上一个哥哥了
}
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
newChildIndex++;
}
}
复制代码
类组件的提交逻辑
- 由于类组件没有真实dom(stateNode对应的是类的实例,不是真实dom),所以删除和更新时需要进行特殊处理
- 需要递归往下查找到有真实dom的子节点,然后进行删除或插入
function commitWork(currentFiber) {
if (!currentFiber) return;
let returnFiber = currentFiber.return;
// 类组件fiber 中的stateNode是类的实例,不是真实dom,需要往上查找到真实dom的节点
while (returnFiber.tag === TAG_CLASS) {
returnFiber = returnFiber.return;
}
const domReturn = returnFiber.stateNode;
if (currentFiber.effectTag === PLACEMENT && currentFiber.stateNode) {
// 类组件没有真实dom,需要往下查找
let nextFiber = currentFiber;
while (nextFiber.tag === TAG_CLASS) {
nextFiber = nextFiber.child;
}
//如果是新增DOM节点
domReturn.appendChild(nextFiber.stateNode);
} else if (currentFiber.effectTag === DELETION) {
// 删除
commitDeletion(currentFiber, domReturn);
} else if (currentFiber.effectTag === UPDATE && currentFiber.stateNode) {
// 更新
if (currentFiber.type === ELEMENT_TEXT) {
if (currentFiber.alternate.props.text !== currentFiber.props.text) {
currentFiber.stateNode.textContent = currentFiber.props.text;
} else {
updateDOM(currentFiber.stateNode, currentFiber.alternate.props, currentFiber.props);
}
} else {
}
}
currentFiber.effectTag = null;
}
// 删除类组件dom
function commitDeletion(currentFiber, domReturn) {
if (currentFiber.tag === TAG_CLASS) {
// 往下找
commitDeletion(currentFiber.child, domReturn);
} else {
domReturn.removeChild(currentFiber.stateNode);
}
}
复制代码
- 最终效果如下
commit记录
实现函数组件与Hooks
- 声明全局变量
workInProgressFiber
与hookIndex
let workInProgressFiber = null; // 正在工作中的fiber
let hookIndex = 0; // hook索引
复制代码
- 在scheduler.js中添加函数组件的update方法
updateFunctionComponent
- currentFiber.type 对应函数组件的函数名,调用
currentFiber.type(currentFiber.props)
相当于执行函数组件,返回一个虚拟节点 - 执行函数组件时,会调用函数中引用的react hooks方法
// 函数组件
function updateFunctionComponent(currentFiber) {
workInProgressFiber = currentFiber;
hookIndex = 0;
workInProgressFiber.hooks = [];
const newChildren = [currentFiber.type(currentFiber.props)];
reconcileChildren(currentFiber, newChildren, deletions);
}
复制代码
- 在scheduler.js中实现
useReducer
和useState
两个hooks useState
是通过useReducer
来实现的- 函数组件的
fiber
会有hooks
属性来存放用到的hooks,hooks包含两个属性state
状态和updateQueue
更新队列 - 第一次执行
useReducer
时会将初始状态存到fiber.hooks
中,函数组件更新时,会使用存储在fiber.hooks
中的状态 - dispatch方法可以修改 hooks中的state,并触发
scheduleRoot
进行重新渲染
// -------------------------------hooks--------------------------------------
export function useReducer(reducer, initialValue) {
let oldHook =
workInProgressFiber.alternate &&
workInProgressFiber.alternate.hooks &&
workInProgressFiber.alternate.hooks[hookIndex];
let newHook = oldHook;
if (oldHook) {
newHook.state = oldHook.updateQueue.forceUpdate(oldHook.state);
} else {
newHook = {
state: initialValue,
updateQueue: new UpdateQueue(),
};
}
const dispatch = action => {
console.log('action', action);
if (typeof action === 'function') {
action = action(newHook.state);
}
const newState = reducer ? reducer(newHook.state, action) : action;
newHook.updateQueue.enqueueUpdate(new Update(newState));
scheduleRoot();
};
// 将hook的数据放在 fiber的hooks中,并让hookIndex指针后移
workInProgressFiber.hooks[hookIndex++] = newHook;
return [newHook.state, dispatch];
}
export function useState(initState) {
return useReducer(null, initState);
}
复制代码
- 在react中引用
useReducer
useState
并导出
import { scheduleRoot, useReducer, useState } from './scheduler';
........
const React = {
createElement,
Component,
useReducer,
useState,
};
export default React;
复制代码
- 在index.js中引用hooks,用hooks实现一个计数器案例
import React from './react/react';
import ReactDOM from './react/react-dom';
// Hooks
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>
<h3 onClick={() => setNumberState(state => ({ number: state.number + 1 }))}>
useState Count: {numberState.number}
</h3>
<hr />
<h3 onClick={() => dispatch({ type: 'ADD' })}>useReducer Count: {countState.count}</h3>
</div>
);
}
ReactDOM.render(<FunctionCounter />, document.getElementById('root'));
复制代码
commit记录
代码奉上
有漏洞和不足之处,还请大家指正
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END