1.Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler
2.Reconciler(协调器)—— 负责找出变化的组件
3.Renderer(渲染器)—— 负责将变化的组件渲染到页面上
React是用javaScript构建快速响应的大型web应用的首选方式,何为快速响应?
当遇到大量操作计算或者设备性能产生的页面掉帧导致卡顿,发发送网络请求后,由于需要等待数据返回才能进一步操作导致不能快速响应,这两类场景可以概括为CPU的瓶颈和IO的瓶颈,react如何解决?
在浏览器每一帧的时间中,预留一些时间给JS线程,React利用这部分时间更新组件(可以看到,在源码 (opens new window)中,预留的初始时间是5ms) ƒ 这种将长任务分拆到每一帧中,像蚂蚁搬家一样一次执行一小段任务的操作,被称为时间切片(time slice)
IO的瓶颈如何解决
为此,React实现了Suspense功能及配套的hook——useDeferredValue
而在源码内部,为了支持这些特性,同样需要将同步的更新变为可中断的异步更新
当首屏渲染时,通过prepareFreshStack,初始化FiberRoot的finishedWork属性,同时通过root.current创建新的workInProgress,然后判断workInProgress !== null,while循环调用performUnitOfWork,performUnitOfWork当中开始进行beginWork,beginWork执行完毕,将新属性同步到老属性上面,nitOfWork.memoizedProps = unitOfWork.pendingProps,beginWork的作用就是通过当前的Fiber创建子fiber,建立fiber链,根节点调用updateHostRoot,此时current和workInProgress一定会同时存在,调用reconcileChildren函数,传入current和workInProgess,,此时current !== null,直接走更新逻辑,通过reconcileSingleElement直接进行单节点的dom diff,因为首次child为null,调用createFiberFromElement通过jsx创建fiber节点,然后通过created.return = returnFiber,建立父子关系,返回created,执行完毕,返回workInProgress.child,进行深度优先遍历,接下来因为一般render函数的第一个参数为函数组件,此时函数组件并没有alternate属性,所以current为空,设置didReceiveUpdate为false,在mount时FunctionComponent是按indeterminate处理的,调用mountIndeterminateComponent,取pendingProps,调用RenderWithHooks,判断current是否为null,调用HooksDispatcherOnMount或者HooksDispatcherOnUpdate,直接调用component函数,获取jsx对象,返回jsx对象,然后调用reconcileChildren对象,传入null,走初次渲染逻辑,初次渲染走插入逻辑,也就是将flags设置为2,走Placement逻辑,接着通过beingWork返回的的next,判断是否为Null,不为Null赋值workInProgress,循环performUnitOfWork逻辑,当next为null时,递的mount流程结束,调用completeUnitOfWork,完成第一个fiber节点,通过return和sibling,往上走到root。
首次渲染调用completeWork时,alternate为null,那倒newProps,也就是workInProgress的pendingProps,判断workInProgress.tag,如果为HostComponent,通过判断current和workInProgress.stateNode区分是更新还是初始化,初始化逻辑调用createInstance创建实例,由于是深度优先遍历,当workInProgress进行归阶段时,也就意味着其子树的dom节点已创建,所以只需将子树中离instance最近的dom节点追加到instance上即可,调用finalizeInitialChildren,初始化instance,也就是dom的属性,然后通过return和sibling向上初始化,完成之后,获取root.current.alternate,也就是workInProgress,设置为root.finishedWork,调用commitRoot,通过Fibr上的flags副作用,进行节点操作。
核心实现在于commitBeforeMutationEffects,class组件会在其中执行getSnapshotBeforeUpdate,因为只实现了functionComponent,所以可以忽略,commitMutationEffects,mutation阶段,需要进行操作的HostComponent组件,会在这个阶段进行dom操作,在commitMutationEffects执行完毕之后,root.current = finishedWork,此时改变rootFiberNode的指针,指向最新的workInProgress,最后一步执行commitLayoutEffects,LayoutEffects阶段,在其中执行useLayoutEffect的create函数,这就是他和useEffect最大的区别,useLayoutEffect执行的时间是在dom操作完成后,此时下一帧还没有开始渲染,此时如果做一些动画就非常适合,而如果把执行动画的操作放到useEffect中,因为他是被Scheduler模块调度,被postMessage注册到宏任务里面的,等到他执行时下一帧已经渲染出来,dom操作后的效果已经体现在了页面上了,如果此时动画的起点还是前一帧的话页面就会出现闪烁的情况。
React分为两种模式,render和createRoot两种入口,分为legacy和concurrent两种
legacy模式(同步)
render调用legacyRenderSubtreeIntoContainer,最后createRootImpl会调用到createFiberRoot创建fiberRootNode,然后调用createHostRootFiber创建rootFiber,其中fiberRootNode是整个项目的的根节点,rootFiber是当前应用挂在的节点,也就是ReactDOM.render调用后的根节点
concurrent模式(异步)
createRoot调用createRootImpl创建fiberRootNode和rootNode,在createRootImpl中调用listenToAllSupportedEvents初始化事件注册
创建完Fiber节点后,调用ReactDOMRoot.prototype.render执行updateContainer,然后scheduleUpdateOnFiber异步调度performConcurrentWorkOnRoot进入render阶段和commit阶段
不同点
在函数scheduleUpdateOnFiber中根据不同优先级进入不同分支,legacy模式进入performSyncWorkOnRoot,concurrent模式会异步调度performConcurrentWorkOnRoot
1.react根据双缓冲机制维护了两个fiber树
current Fiber树:用于渲染页面
workinProgress Fiber树:用于在内存构建中,方便在构建完成后直接替换current Fiber树
2.Fiber双缓存
首次渲染时: render阶段会根据jsx对象生成新的Fiber节点,然后这些Fiber节点会被标记成带有‘Placement’的副作用,说明他们是新增节点,需要被插入到真实节点中,在commitWork阶段就会操作成真实节点,将它们插入到dom树中。
页面触发更新时 render阶段会根据最新的jsx生成的虚拟dom和current Fiber树进行对比,比较之后生成workinProgress Fiber(workinProgress Fiber树的alternate指向Current Fiber树的对应节点,这些Fiber会带有各种副作用,比如‘Deletion’、‘Update’、'Placement’等)这一对比过程就是diff算法
当workinProgress Fiber树构建完成,workInprogress 则成为了curent Fiber渲染到页面上
diff ⽐较的是什么? ⽐较的是 current fiber 和 vdom,⽐较之后⽣成 workInprogress Fiber
BaseFiberRootProperties定义了fiberRoot大部分属性
type BaseFiberRootProperties = {
// 类型
tag: RootTag,
// root节点,也就是ReactDOM.render(<App />, document.getElementById('root'))的第二个参数
containerInfo: any,
// 老的Fiber节点,用来复用
current: {
alternate:FiberNode,
child:{
//当前Fiber的替身
alternate:null
child:null
childLanes:0
deletions: null
elementType:"div"
flags:0
index:0
// key用于标记元素是否可复用
key:null
lanes:0
// 记录上一次的更新状态
memoizedProps:{ children: '渲染内容' }
memoizedState:null
mode:0
// 最新的状态,update依赖于memoizedProps,也用于判断是否更新
pendingProps:{ children: '渲染内容' }
// 父级Fiber节点
return: FiberNode { tag: 3, pendingProps: null, key: null, mode: 0,
// 兄弟节点
sibling:null
// 真实的DOM节点
stateNode:div
subtreeFlags:0
// 当前的Fiber类型
tag:5
// 真实Dom的节点类型
type:"div"
updateQueue:null
}
},
//任务有三种,优先级有高低:
//(1)没有提交的任务
//(2)没有提交的被挂起的任务
//(3)没有提交的可能被挂起的任务
//当前更新对应的过期时间
finishedExpirationTime: ExpirationTime,
// 已经完成任务的FiberRoot对象,如果你只有一个Root,那么该对象就是这个Root对应的Fiber或null
// 在commit(提交)阶段只会处理该值对应的任务
finishedWork: Fiber | null,
// 在任务被挂起的时候,通过setTimeout设置的响应内容,
// 并且清理之前挂起的任务 还没触发的timeout
timeoutHandle: TimeoutHandle | NoTimeout,
// 顶层context对象,只有主动调用renderSubtreeIntoContainer才会生效
context: Object | null,
pendingContext: Object | null,
// 用来判断 第一次渲染 是否需要融合
hydrate: boolean,
firstBatch: Batch | null,
callbackNode: *,
// 跟root有关联的回调函数的时间
callbackExpirationTime: ExpirationTime,
// 存在root中,最旧的挂起时间
// 不确定是否挂起的状态(所有任务一开始均是该状态)
firstPendingTime: ExpirationTime,
// 存在root中,最新的挂起时间
// 不确定是否挂起的状态(所有任务一开始均是该状态)
lastPendingTime: ExpirationTime,
// 挂起的组件通知root再次渲染的时间
// 通过一个promise被reslove并且可以重新尝试的优先级
pingTime: ExpirationTime,
// 更新队列
UpdateQueue:{
// 前一次更新计算得出的状态,它是第一个被跳过的update之前的那些update计算得出的state。会以它为基础计算本次的state
baseState: State
// 存储着本次更新的update队列,是实际的updateQueue。shared的意思是current节点与workInProgress节点共享一条更新队列。
shared: {
pending: Update | null
}
// 前一次更新时updateQueue中第一个被跳过的update对象
firstBaseUpdate: Update | null
// lastBaseUpdate相当于,前一次更新中,updateQueue中以第一个被跳过的update为起点一直到的最后一个update的形成的链表,截取最后一个而获得的update
lastBaseUpdate: Update | null
}
};
链表是另一种形式的链表存储结构,模拟源码enqueueUpdate方法
它的特点是最后一个节点的指针区域指向头节点,整个链表形成一个环,永远指向最后一个更新
// pedding.next指向第一个第一个更新,更新顺序是不变的,此为环状列表
function dispatchAction(queue,action){
const update = {action,next:null};
const pedding = queue.pedding;
if(pedding == null){
update.next = update;
}else{
update.next = pedding.next;
pedding.next = update;
}
queue.pedding = update;
}
//队列
let queue = {padding:null};
dispatchAction(queue,'action1')
dispatchAction(queue,'action2')
dispatchAction(queue,'action3')
// pedding: { action: 'action3', next: { action: 'action1', next: [Object] } }
const peddingQueue = queue.pedding;
// 源码中的遍历环形链表
while(peddingQueue){
let first = peddingQueue.pedding;
let update = first;
do{
console.log(update) // action1 action2 action3
update = update.next;
}while(update !== first){}
}
- 新版本 React18 是异步模式,React17版本是也是异步,但是在setTimeout中是同步
- React17 使用React.render (legacy同步模式),使用unstable_batchedUpdates可以解决在promise和setTimeout中不受React控制的问题,React18 使用 React.createRoot(concurrent异步模式)
-
React 在执行 setState 的时候会把更新的内容放入队列
-
在事件执行结束后会计算 state 的数据,然后执行回调
-
最后根据最新的 state 计算虚拟 DOM 更新真实 DOM
-
优点
1.为保持内部一致性,如果改为同步更新的方式,尽管 setState 变成了同步,但是 props 不是
2.为后续的架构升级启用并发更新,React 会在 setState 时,根据它们的数据来源分配不用的优先级,这些数据来源有:事件回调句柄,动画效果等,再根据优先级并发处理,提升渲染性能
3.setState 设计为异步,可以显著提升性能(非合成事件和钩子函数当中是同步的,例如 Promise 中就是同步),使用 batchedUpdates 可以已经批量更新
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 批量更新所以是 0
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 批量更新所以是 0
setTimeout(() => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // React18不用unstable_batchedUpdates也会异步批量所以是 1,react17版本会是同步2
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // React18不用unstable_batchedUpdates也会异步批量所以是 1,react17版本会是同步3
});
1.只对同级元素进行比较
2.不同的类型对应不同的元素
3.可以通过key来标识同一个节点
DomDiff 的过程其实就是老的 Fiber 树 和 新的 jsx 对比生成新的 Fiber 树 的过程
单节点
1.新旧节点 type 和 key 都不一样,标记为删除
2.如果对比后发现新老节点一样的,那么会复用老节点,复用老节点的 DOM 元素和 Fiber 对象 再看属性有无变更 ,如果有变化,则会把此 Fiber 节点标准为更新
3.如果 key 相同,但是 type 不同,则不再进行后续对比了, 直接把老的节点全部删除
多节点
1.如果新的节点有多个的话 我们经过二轮遍历 第一轮处理更新的情况 属性和类型 type 的更新 更新或者说保持 不变的频率会比较高 第二轮处理新增 删除 移动 的情况
<ul>
<li key="A">A</li>
<li key="B">B</li>
<li key="C">C</li>
<li key="D">D</li>
<li key="E">E</li>
<li key="F">F</li>
</ul>
/*************/
<ul>
<li key="A">A-NEW</li>
<li key="C">C-NEW</li>
<li key="E">E-NEW</li>
<li key="B">B-NEW</li>
<li key="G">G-NEW</li>
</ul>
// 如果第一轮遍历的时候,发现key不一样,则立刻跳出第一轮循环
// key不一样,说明可能有位置变化,更新A
//第二轮循环,新建map={"B":"B","C":"C","D":"D","E":"E","F":"F"},可以复用的节点标记为更新,从map中删除,然后map={"D":"D","F":"F"},还没有被复用的fiber节点,等新的jsx数组遍历完之后,把map中的所有节点标记为删除,再更新,然后移动,记录第一轮的lastPlaceIndex,最小的oldIndex移动,最后插入新元素。
- 捕获事件是先注册先执行,冒泡事件是先注册后执行
- React17之前事件会冒泡到 document 上执行,所以导致和浏览器表现不一致(17 之后没问题了,因为挂到 root节点 上了)
-
新版本在createRoot时,会调用createImpl,在root节点listenToAllSupportedEvents直接初始化事件系统
-
事件的原则不管是捕获阶段还是冒泡阶段,都是先注册,先执行
// result:事件是先注册先执行
// 父元素React事件捕获
// 子元素React事件捕获
// 父元素原生事件捕获
// 子元素原生事件捕获
// 子元素原生事件冒泡
// 父元素原生事件冒泡
// 子元素React事件冒泡
// 父元素原生事件冒泡
// element.addEventListener(event, function, useCapture) useCapture === true ? '捕获' : '冒泡',默认冒泡
// e.preventDefault() 阻止事件默认行为
// onClickCapture 捕获 onClick 冒泡
// React16由于会冒泡到docuemnt上执行,所以会导致最后show为false
componentDidMount(){
this.setState({
show:false
})
}
handleClick = (event)=>{
// event.nativeEvent.stopProgation(); // 不再向上冒泡了,但是本元素剩下的函数还会执行,也就是React16的话,依然会执行
// event.nativeEvent.stopImmediateProgation(); // 阻止监听同一事件的其他事件监听器被调用,阻止后续事件代理到docuemnt上,可以解决React16合成事件的问题
this.setState({
show:true
})
}
<button onClick={this.handleClick}></button>
{this.state.show && <a>显示</a>}