基于react17
react17新增哪些内容?
react17基本概念
解决问题:react变慢
**办法:**使用异步可中断
实现
fiber:一套数据结构既能对应真实dom又能作为分隔单元,这就是Fiber
scheduler: 调度器。异步执行机制,js实现一套时间片运行的机制,类似requestIdleCallback
lane: 细粒度的管理各个任务的优先级,优先级高的优先执行。各个Fiber工作单元比较优先级,相同优先级的任务可以一起更新。
上层实现
batchedUpdates 批量异步更新 Suspense
副作用
会对当前的渲染造成二次影响的操作。文件读取、接口请求等
从render开始看react架构
const state = reconcile(update); //render(update);
const UI = commit(state); //渲染层
- mount/update jsx
- render阶段
- Scheduler(调度器): 排序优先级,让优先级高的任务先进行reconcile
- Reconciler 调节器 (协调层): 为每一个react元素创建 Fiber 数据结构并为 Fiber 节点打标记flag/effectTag属性,存储当前 Fiber 节点要进行的 DOM 操作 ,然后在render 结束后, fiber 会被保存到 fiberRoot 中
- commit阶段
Renderer(渲染层):先获取到render 的结果, 根据 fiber 中的 effectTag 属性进行相应的 DOM 操作,此阶段是不可以被打断的
jsx
React.createElement(返回virtual-dom对象)的语法糖
fiber双缓存
fiber中存储dom树、updateQueue、updte。其中dom树包含:virtual element、next、sibling、return(父节点)
双缓存是指内存中有两棵fiber树。一棵current fiber当前页面的fiber树,一棵是workInProgress fiber,在update的时候会diff current fiber生成workInProgress fiber,包含updateQueue、副作用有关的信息,updateQueue是单链表由多个update组成。在render阶段结束后作为current fiber渲染到页面。
两棵树之间使用alternate指针互相指向。可以确保diff、reconciler等阶段在内存中完成不阻塞页面渲染
scheduler
调度器,timeQueue taskQueue 异步可中断的触发任务。将高优先级的发送给conciler
lane模型
使用二进制最小细粒度的标记优先级 其中1出现次数越多优先级越高
reconciler 协调器
协调器,根据update,使用diff算法以 current fiber为模板打出flags(增删改) 和 effectFlags得出workInProgress Fiber
render阶段遍历Fiber树类似dfs的过程,‘捕获’阶段发生在beginWork函数中,该函数做的主要工作是创建Fiber节点,计算state和diff算法,‘冒泡’阶段发生在completeWork中,该函数主要是做一些收尾工作,例如处理节点的props、和形成一条effectList链表,该链表是被标记了更新的节点形成的链表(摒弃递归是为了做中断)
注意: fiberRoot是整个项目的根节点,只存在一个,rootFiber是应用的根节点,可能存在多个,例如多个ReactDOM.render(<App />, document.getElementById("root"));
创建多个应用节点
rederer 渲染
commit操作前面的effectList(firstEffect、nextEffect)
commit主要操作在commitRoot函数中,遍历effectList,分别使用以下三个函数处理节点。commitBeforeMutationEffects(执行dom操作前: getSnapShotBeforeUpdte)、commitMutationEffects(执行dom操作)、commitLayoutEffects(执行dom操作后:componentDidMount、useEffect、componentDidUpdate),他们主要做的事情如下,后面会详细讲解,现在在大脑里有一个结构就行
concurrent mode 异步并发
它是一类功能的合集(如fiber、schduler、lane、suspense),其目的是为了提高应用的响应速度,使应用cpu密集型的更新不在那么卡顿,其核心是实现了一套异步可中断、带优先级的更新。
给js操作一个时间片,如果超出时间仍然没有执行完毕,进入callbackQueue。暂停该js的执行,等下一帧继续执行,把执行权交回给浏览器去绘制。
fiber数据结构
fiberNode
//dom节点信息 tag key
//树连接 child return sibling
//数据信息 prop state
//lane优先级 调度器调度以及reconciler触发顺序
//effectList 需要做的处理 firstEffect nextEffect lastEffect
//alternate 双缓存机制
//ReactFiber.old.js
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
//作为静态的数据结构 保存节点的信息
this.tag = tag;//对应组件的类型
this.key = key;//key属性
this.elementType = null;//元素类型
this.type = null;//func或者class
this.stateNode = null;//真实dom节点
//作为fiber数架构 连接成fiber树
this.return = null;//指向父节点
this.child = null;//指向child
this.sibling = null;//指向兄弟节点
this.index = 0;
this.ref = null;
//用作为工作单元 来计算state
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
//effect相关
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
//优先级相关的属性
this.lanes = NoLanes;
this.childLanes = NoLanes;
//current和workInProgress的指针
this.alternate = null;
}
一次update触发流程
update&updateQueue
scheduler
render
commmit
jsx揭秘
jsx是react.createElement的语法糖
在mount/update阶段,jsx会被babel转换成react.createElement(type/FuncCom/ClassCom,{...props},[...children]);createElement生成vNode即virtual dom
setState 创建update
export function createUpdate(eventTime: number, lane: Lane): Update<*> {//创建update
const update: Update<*> = {
eventTime,
lane, //此update的优先级
tag: UpdateState, //更新的类型,例如UpdateState、ReplaceState
payload: null, //第一个参数 {data:12}
callback: null, //第二个参数
next: null, //链表,例如同时触发多个setState时会形成多个Update,用next连接
};
return update;
}//将update加入到updateQueue 一个链表中
//ReactUpdateQueue.old.js
export function initializeUpdateQueue<State>(fiber: Fiber): void {
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
},
effects: null,
};
fiber.updateQueue = queue;
}
// 处理updateque中的update,优先级低的跳过,放入fiber.lanes中等待下轮处理
// effect记录中间产生的副作用
//-------------------------
// 进入schedule流程
react的diff算法
实现传统diff O(n^3)到O(n)的跃迁
diff策略
1.跨域层级的改动很少可以忽略不计。所以只比较同层级dom树的变动
2.同type的节点复用
3.同层级的一组子节点通过唯一的key值区分,移动位置复用
placeChild函数
//ReactChildFiber.old.js
function placeChild(newFiber, lastPlacedIndex, newIndex) {
newFiber.index = newIndex;
if (!shouldTrackSideEffects) {
return lastPlacedIndex;
}
var current = newFiber.alternate;
if (current !== null) {
var oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
//oldIndex小于lastPlacedIndex的位置 则将节点插入到最后
newFiber.flags = Placement;
return lastPlacedIndex;
} else {
return oldIndex;//不需要移动 lastPlacedIndex = oldIndex;
}
} else {
//新增插入
newFiber.flags = Placement;
return lastPlacedIndex;
}
}
function reconcilerChildernArray(
returnFiber:Fiber,
currentFirstChild:Fiber | null,
newChildren:Arrry<*>,//用于比较的jsx数组
lanes:Lanes
){
}
多节点集合情况
function reconcileChildrenArray(
returnFiber: Fiber,//父fiber节点
currentFirstChild: Fiber | null,//childs中第一个节点
newChildren: Array<*>,//新节点数组 也就是jsx数组
lanes: Lanes,//lane相关 第12章介绍
): Fiber | null {
let resultingFirstChild: Fiber | null = null;//diff之后返回的第一个节点
let previousNewFiber: Fiber | null = null;//新节点中上次对比过的节点
let oldFiber = currentFirstChild;//正在对比的oldFiber
let lastPlacedIndex = 0;//上次可复用的节点位置 或者oldFiber的位置
let newIdx = 0;//新节点中对比到了的位置
let nextOldFiber = null;//正在对比的oldFiber
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {//第一次遍历
if (oldFiber.index > newIdx) {//nextOldFiber赋值
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
//单节点比较 key、tag&type key不同则标记删除当前节点 type不同则标记删除当前节点和后面的children
//更新节点,如果key不同则 return newFiber=null跳出本次diff,另外处理
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;//当curfiber或newfiber为null时结束树diff,单独处理剩余部分
}
if (shouldTrackSideEffects) {
//检查shouldTrackSideEffects,检查是否是mount阶段
if (oldFiber && newFiber.alternate === null) {
deleteChild(returnFiber, oldFiber);//如果是mount将oldFiber删除
}
}
//上面已经确认可以复用,此处placeIndx为当前的index
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//标记节点插入
//新节点链起来:sibling
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
//老节点循环
oldFiber = nextOldFiber;
}
//新节点已经遍历完成
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
//将oldFiber中没遍历完的节点标记为DELETION
return resultingFirstChild;
}
//老树已遍历结束则剩余新树都是插入操作
if (oldFiber === null) {
//处理新树剩余部分
for (; newIdx < newChildren.length; newIdx++) {//第2次遍历
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//插入新增节点
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
//oldFiber已经被多次 oldFiber=oldfiber.siblin 将剩下的oldFiber加入map中
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
//
for (; newIdx < newChildren.length; newIdx++) {//第三次循环 处理节点移动
const newFiber = updateFromMap(//查找符合的节点即:key不同但type相同的index
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
existingChildren.delete(//删除找到的节点
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//标记为插入的逻辑
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
if (shouldTrackSideEffects) {
//删除existingChildren中剩下的节点
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
useState hooks手写
import React from "react";
import ReactDOM from "react-dom";
let workInProgressHook;//当前工作中的hook
let isMount = true;//是否时mount时
const fiber = {//fiber节点
memoizedState: null,//hook链表
stateNode: App//dom
};
const Dispatcher = (() => {//Dispatcher对象
function mountWorkInProgressHook() {//mount时调用
const hook = {//构建hook
queue: {//更新队列
pending: null//未执行的update队列
},
memoizedState: null,//当前state
next: null//下一个hook
};
if (!fiber.memoizedState) {
fiber.memoizedState = hook;//第一个hook的话直接赋值给fiber.memoizedState
} else {
workInProgressHook.next = hook;//不是第一个的话就加在上一个hook的后面,形成链表
}
workInProgressHook = hook;//记录当前工作的hook
return workInProgressHook;
}
function updateWorkInProgressHook() {//update时调用
let curHook = workInProgressHook;
workInProgressHook = workInProgressHook.next;//下一个hook
return curHook;
}
function useState(initialState) {
let hook;
if (isMount) {
hook = mountWorkInProgressHook();
hook.memoizedState = initialState;//初始状态
} else {
hook = updateWorkInProgressHook();
}
let baseState = hook.memoizedState;//初始状态
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next;//第一个update
do {
const action = firstUpdate.action;
baseState = action(baseState);
firstUpdate = firstUpdate.next;//循环update链表
} while (firstUpdate !== hook.queue.pending);//通过update的action计算state
hook.queue.pending = null;//重置update链表
}
hook.memoizedState = baseState;//赋值新的state
return [baseState, dispatchAction.bind(null, hook.queue)];//useState的返回
}
return {
useState
};
})();
function dispatchAction(queue, action) {//触发更新
const update = {//构建update
action,
next: null
};
if (queue.pending === null) {
update.next = update;//update的环状链表
} else {
update.next = queue.pending.next;//新的update的next指向前一个update
queue.pending.next = update;//前一个update的next指向新的update
}
queue.pending = update;//更新queue.pending
isMount = false;//标志mount结束
workInProgressHook = fiber.memoizedState;//更新workInProgressHook
schedule();//调度更新
}
function App() {
let [count, setCount] = Dispatcher.useState(1);
let [age, setAge] = Dispatcher.useState(10);
return (
<>
<p>Clicked {count} times</p>
<button onClick={() => setCount(() => count + 1)}> Add count</button>
<p>Age is {age}</p>
<button onClick={() => setAge(() => age + 1)}> Add age</button>
</>
);
}
function schedule() {
ReactDOM.render(<App />, document.querySelector("#root"));
}
schedule();
未完待续......