react源码学习1(理念)
Posted lin-fighting
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了react源码学习1(理念)相关的知识,希望对你有一定的参考价值。
大纲
1 react的理念
2 react新老架构
3 Fiber架构
理念
react的官网介绍是构建快速响应的大型web应用。
重点是快速响应
制约快速响应主要有两种方式:
1 当遇到大量计算的时候,设备性能不足页面掉帧,导致卡帧(cpu瓶颈)
2 网络响应慢 (io瓶颈)
1 cpu瓶颈
浏览器的的主要刷新频率是60hz,也就是一帧大概16ms
浏览器每一帧完成的动作有:执行js脚本->样式布局->样式绘制
而js线程与gui渲染线程是互斥的,就是执行js脚本不会执行布局绘制,执行布局绘制不会执行js脚本。
而我们有些操作比如同时渲染一个3000长度的数组,此时js脚本的执行时间大大超过16ms,导致gui线程无法工作,会给用户卡的感觉。
react16之前无法解决这个问题,而react16之后架构改变,使用fiber结合requestIdleCallback去执行js脚本,将长任务比如渲染3000长度的数组,分为几个短任务,在每一帧中预留一些时间给js线程,当时间一过,脚本还没执行完毕,react将线程控制权还给浏览器,等待下一帧空闲时间再执行该脚本,浏览器就有时间执行gui线程,就会减少掉帧的出现。
这种将长任务拆分到每一帧中,称为时间切片。
解决cpu的瓶颈在于时间切片,也就是将同步的更新变为可中断的异步更新。
Io瓶颈
IO瓶颈取决于网络问题,前端无法控制,但可以控制交互效果,如react提供的Suspense(一个用于显示加载中界面的组件)以及配套的hooks—useDeferredValue
在react源码内部支持这些特性同样需要讲同步更新变为可中断的异步更新。
总结:为了快速响应主要就是解决cpu瓶颈与io瓶颈,而解决的关键就是将同步更新变为可中断的异步更新。
react架构
react15-16重构了整个架构。
因为react15的架构无法满足于快速响应的原理。
https://editor.csdn.net/md/?articleId=119855711
之前就学过b站简单实现react也了解了一点点,
react15的架构分为两层,
Reconciler(协调器) 负责找出需要变化的组件
Renderer(渲染器) 负责将变化渲染到组件上。
协调器
在类组件中,可以通过this.setState,this.forceUpdate,ReactDom.render等api来触发更新,每当有更新变化的时候,reconcilers就会做如下事情:
1 调用render方法,将jsx转为虚拟dom
2 通过diff算法比较新老虚拟dom
3 通过比对找出需要变化的虚拟dom
4 通知Renderer将变化渲染到组件上。
渲染器Renderer
不同的平台有不同的渲染器,前端比较熟悉的是ReactDOM
在每次更新的时候Renderer接受reconciler的通知将变化的组件渲染到当前宿主环境上。
react15架构的缺点:
在reconciler中,渲染组件会调用mountComponent方法,更改会调用upDateComponent方法,这两个方法都是会递归更新子组件。递归遍历,一旦开始无法中断,层级深的时候,时间远远超过16ms,就会导致卡顿。
而且,reconclier与renderer是交替进行的,什么意思呢?就是比如一个列表需要更换,reconciler发现第一个需要改,就立马交给renderer改,改完之后第二个才会进行reconciler中,这样的交替即使支持中断更新也会导致渲染不完整问题。
而无法将同步的更新变为可中断的异步更新,基于这一点,react决定重写整个架构。
新的react架构
react16的架构分为三层
1 Scheduler(调度器) 调度任务的优先级,高优先级的进入recondiler
2 Reconciler(协调器)负责找出组件的变化,通知Render
3 Renderer(渲染器),负责将变化的组件渲染到组件上。
相比react15,react16新增了Scheduler调度器。
我们以浏览器每帧剩余时间来作为任务中断的标准,所以需要浏览器有空闲的时间通知我们,之前实现的简单react就有用
requestIdleCallback,这个api部分浏览器已经实现,他会在每一帧空闲的时候调用,两个参数可以获取每一帧的剩余时间和够不够时间执行js脚本,但是react并没有采用这个api,第一个是浏览器兼容性问题,第二个是触发频率不稳定,容易受影响。
为此,react实现了功能更为强大的requestIdleCallback的profill,也就是Scheduler调度器,除了空闲时调用回调函数,还有很多调度优先级共任务设置。
react16的Reconclier
react16的reconciler在每次工作的时候会调用类似于requestIdleCallback的参数询问是否还有时间执行,
** @noinline */
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
若没时间,就中断,等下一个空闲时间再执行,
那这样就会导致渲染dom不完全,react怎么解决的呢?
react16的Rconclier与Renderer的工作方式不再是交替运行,当Schedule将任务交给Reconciler的时候,Reconclier会为需要更改的fiber打上增删改的标记,整个Schedule与Reconciler的工作是在内存里完成的,而且这部分可以中断。只有所有组件都完成Reconciler的工作的时候,才会交给Renderer去渲染。
Renderer
Renderder根据Reconclier打上的标记,做出相对应的操作。
借用学习文章的一个图
总结
快速响应的关键问题就是cpu瓶颈和io瓶颈,而解决问题的关键就是将同步的更新变为可中断的异步更新。react15的架构是Reconciler和Renderer交替工作,而两者都是使用深度遍历,这就导致其不可中断,满足不了可中断的异步更新的需求,故react重写了整个架构。
react16的架构分为Schedule,Reconclier,Renderer三个,因为是根据浏览器每一帧的空闲时间来做决定,所以react实现了类似requeistIDleCallback方法的功能甚至更强大,可以优先调度,就是Schedule,他会在每一帧的空闲时间告诉react有没有时间,可不可以执行。
而Reconclier与Renderer也不再是交替进行,而是Reconciler每次执行都会通过Sechedule确定档期浏览器的这一帧够不够时间,不够等下一帧再执行。而且Reconciler会为所有需要更改的fiber打上标记。Scheudle与Reconclier是在内存中完成的,可以中断,只有当所有组件的Reconclier完成时,Renderer才会开始工作(解决了如果中断dom渲染不完全的问题),而Renderer就会通过Reconclier的标记将需要更新的组件渲染到ui上
Fiber
react16的Reconclier采用了fiber架构,什么是fiebr架构呢?
Fiber架构的代数效应
将副作用从函数逻辑中抽离,使函数关注点保持纯粹
在react中最明显的就是hooks,像useState我们不需要去知道state在component中如何被保存,react已经帮助我们实现好了,我们要做的只是使用它们完成业务逻辑。
代数效应与generator,
react15-16的Reconclier主要的改变就是从同步更新变为可中断的异步更新,浏览器的generator就是类似的实现,generator在每次有时间的时候就调用下next完成一步,没有时间就返回线权,等下次有时间再执行next。但是generator也有一些缺陷,
如图,浏览器有空闲时间就执行一个doExpensiveWork,如果只考虑单一优先级任务的中断与继续,generator很好的实现了这个功能,但如果想要实现高优先级任务插队完成,比如上图已经完成了AB,此时B接收到一个高优先级的任务,那么因为generator执行的中间状态是上下文关联的,再计算y时无法复用之前计算好的x,需要重新计算,如果使用全局变量,又会引入新的复杂度。
代数效应与fiber
fiber就是代数效应在js的实现过程。
react fiber是react内部的一套状态更新机制,支持任务优先级,可中断,并可恢复。具体到每个节点比如文本节点,元素节点都是fiber节点。
Fiber架构
起源:
react15使用的是reconclier进行递归遍历更新创建虚拟dom,遍历过程是不可中断的,由于无法满足需求,全新的fiber架构应运而生,代替虚拟dom。
含义:
fiber可以从三个方面来理解:
1 架构:
react 15 的reconclier采用递归的方式进行,数据保存在数组调用栈中,所以reconclier也称为stack reconclier,而react16的reconciler 基于fiber节点实现的,所以也被称为Fiber Reconciler
2 静态结构
作为静态的数据结构,一个fiber节点对应一个react element,react fiber保留着每个节点的信息,比如组建类型,dom的信息等等。
3 动态的工作单元
fiber作为动态的工作单元,自身也保存了比如本次更新的动作删除还是编辑,与其他节点的联系(通过return nextsibling child)三个指针构成树。
Fiber的节点属性定义:
分为四种:
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 作为静态数据结构的属性,比如对应着组件类型,dom的类型
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
// 该fiber对应的dom
this.stateNode = null;
// 用于连接其他Fiber节点形成Fiber树
//指向父节点,为何是return,因为儿子执行完毕下一个执行完毕的就是父亲。
this.return = null;
//永远指向大儿子
this.child = null;
//指向自己的兄弟
this.sibling = null;
this.index = 0;
this.ref = null;
// 作为动态的工作单元的属性
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
//变更类型,形成一个单链表
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 指向该fiber在另一次更新时对应的fiber
//指向更新前的节点,新老对比。
this.alternate = null;
}
作为架构来说:之前实现简单react有写过,每个fiber节点都有三个指针,return,child, sibling三个节点构成fiber树。
为何return不叫parent,这与fiber的实现顺序有关系,fiber树的实现逻辑是1 从大儿子开始,没有儿子就兄弟,没有兄弟就自己父亲。
2 怎么算节点完成了呢?(也就是可以执行completewrok去收集依赖),只要自己的儿子完成即可。
这与执行completework的顺序有关系。第一个最先执行的就是第一个最先完成的,就是没有儿子的大儿子,如果大儿子没有兄弟,那么父亲也就算完成了,也需要执行compltetwork,所以叫做return。因为下一个需要执行的就是父亲了。
Fiber架构的工作原理
- 双缓存
我们使用canvas绘画的时候每一帧绘制前都会调用ctx.clearRect清除上一帧的画面,当当前画布计算量较大的时候就会导致白屏出现,这时候我们可以在内存中预先绘制当前帧动画,当清除后立马换上,从而减少白瓶的出现,这种在内存中构建并直接替换的技术成为双缓存 - react的双缓存
reacat的双缓存体现在两颗fiber树当中,一颗current fiber树,一颗workInProgress fiber树。当前屏幕显示的fiber树称为currentFiber树,而在内存中构建的fiber树称为workInProgress fiber树。两颗树的fiber节点通过一个指针,alternate链接。
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
- react通过current指针在不同fiber树的rootFiber根结点间进行切换来完成,current fiber树的切换,即当workInProgress树构建完交给Renderer的时候,current指针指向workInProgress,此时workInProgress fiber树就变成current Fiber树
每次更新都会创建一个新的workInProgress 树,通过current 与 workInProgress的切换,完成dom的更新。 - mount的操作
在首次渲染的时候,如渲染< div> 1 < /div>
首次执行ReactDOM.render的时候会创建两个fiber,一个是整个应用的根结点(fiberRoot),一个是当前组件树的根节点,rootFiber。
区分这两点很重要
整个应用只有一个fiberRoot,但是更新的时候不同的组件树会拥有不同的rootFiber。fiberRoot的current会指向当前渲染的fiber树,该属就称为current fiber树。
fiberRoot.current = rootFiber
- 由于是首次渲染,current fiber树还没有任何的子节点,在render(不是renderer,而是reconclier的render阶段,用来收集依赖,effect list,来看看哪些需要改变)阶段,根据组件返回的jsx在内存中构建fiber节点并且通过指针将其构成一颗fiber树,该树就称为workInProgress fiber树。在构建workInProgress树的时候会通过每个fiber节点的alternate指针,尝试服用current fiber上的一些已有的属性。
- 构建完commit阶段
workInprogress fiber树在构建完会在commit阶段渲染到ui上,commit阶段是不可以停止的,此时RootFiber的current指向了workInProgress fiber树,此时workInProgress fiber树就变层了current fiber树
未完待续。。。
仅供学习笔记使用,学习文章地址:
https://react.iamkasong.com/preparation/idea.html#cpu%E7%9A%84%E7%93%B6%E9%A2%88
以上是关于react源码学习1(理念)的主要内容,如果未能解决你的问题,请参考以下文章