手搓一个mini-react
createElement
creactElement()函数,作用就是把我们的真实dom转化为react.element(虚拟dom)。而jsx本质上就是调用了creatELement()将真实dom转化为不标准的虚拟dom节点。标准的虚拟dom还有其他属性比如key等等,这里只用到最关键的两属性,type和props,还有props中children属性
这里引申出来的知识点:jsx背后的工作机制就是利用React.creactELement()把html标签转化为js对象,也就是虚拟dom。且在react世界中由babel翻译jsx代码。
节点的children属性一般是个数组,所以我们的elements都是以tree的形式存在
在这里有一个小细节,就是当我们的孩子是文本内容时,也把它渲染成虚拟dom。我们给文本内容也赋予一个type:TEXT_ELEMENT,这一步虽然有点多余但是统一了节点的属性代码可读性强
ok,到这我们就可以用我们自己创建的creatElement函数去模拟jsx语法糖啦
render
render函数讲述了一件事情:如何把我们的elment挂载到我们的container上的?
我们来看render函数进行了以下操作
1、根据当前的element生成dom
2、赋予dom节点属性
3、遍历孩子,递归进行render
4、把当前dom节点挂载到父节点上
Concurrent Mode
上面的render函数存在痛点,我们是根据递归去一层一层的渲染dom节点并连接起来生成dom树的,渲染的过程无法中断,只能一步到位。所以在渲染dom树的过程中,当我们的element很大,我们render的执行时间就很长,也就是js线程执行时间会很长。
在这个过程中主线程全程被挂起,而当浏览器出现优先级高的事件时,比如输入input值,主线程无法及时渲染页面,必须等到render结束js线程空闲之后才会去响应当前事件,可能就会出现页面卡顿的问题。怎么解决??
问题所在就是我们去递归渲染节点,这是一个同步渲染的过程
我们这里利用requestIdleCallback这个浏览器API,作用就是询问浏览器,如果浏览器空闲则执行回调函数。
在render中先询问浏览器有空吗,浏览器有空的话就分配给render一个时间片去干自己事情。这时render就能在规定时间内去渲染节点,时间到了就再发起询问。把同步渲染变成异步渲染。怎么实现??
首先我们引入unitofwork工作单元这个概念,window.requestIdleCallback这个api作用就是向浏览器发起询问,浏览器有空便执行回调。所以我们就在回调函数中不断得去处理我们的工作单元,执行完当前的unitofwork就得马不停蹄地寻找nextuniofwork,关键就是怎么寻找到nextuniofwork也就是下一个工作单元呢,这些unitofwork到底是什么?
Fiber
我们为每一个elment生成一个fiber,通过fiber tree 把这些工作单元串起来。沿着fiber tree 去寻找nextuniofwork
fiber大法好,什么是fiber呢?fiber是一种数据结构,是一个对象拥有child,parent,sibling这些属性。
为什么得是fiber?
这里讲的就是fiber存在的三角关系使得我们很容易地找到下一个工作单元,这是element做不到的
寻找下一个工作单元的过程:先找当前fiber.child,没有?那就找fiber.sibling,还没有?那就找fiber的“uncle”,也就是fiber.parent.sibling,还还没有?那就继续往上找,一直找到root,就说明我们遍历了一圈还是没有nextunitofwork,也就是说所有的unitofwork都处理完了
ok。我们来看看如何启动这个渲染机制的?
首先我们的根节点肯定就作为我们首个unitofwork啦,我们把render函数重写,用来初始化unitofwork。unitofwork一旦生成,通过requestIdleCallback(workloop)入口,我们就能开始fiber tree的构建以及dom的渲染
be like:
接着我们看看performunitofwork干了啥
第一步,生成dom节点并构建dom树
这里我们埋下一个伏笔,这里会有什么问题?下面揭晓答案
creatDom()很熟悉把,就是之前render函数copy过来的,生成dom节点的逻辑相同
第二步,为子节点创建fiber
构建fiber tree逻辑就是,每个fiber只有一个child,所以在遍历孩子的时候第一个孩子作为当前fiber.child,之后的孩子都必须处理成为兄弟才能构成我们的fiber tree,所以就引入prevSibling这个变量,这里可能有点小饶,自己画个图理解一下
第三步,返回下一个待处理的工作单元nextunitofwork
还记得那个寻找的优先级吗?child->sibling->uncle
到此为止,我们就能实现异步渲染dom节点构建dom树了
Render and Commit Phases
but
因为performunitofwork函数是在workloop中执行的,所以就有可能浏览器被打断。每当生成一个dom节点就挂载到dom树,如果在遍历fiber tree的阶段浏览器有高优先级的事件触发了,渲染会被中断,用户就会看到不完整的UI界面
如何解决?我们把渲染阶段和提交阶段分开,当渲染出完整的fiber tree后我们再去渲染真实dom节点,也就是构建dom树。
首先设置当前的fiber树的root,我愿称之为work in progress root
当nextunitofwork为空,也就是说fiber树渲染完成,我们就从渲染阶段进入我们的提交阶段。
commitRoot()作为入口函数,commitWork()处理每一个fiber,我们通过递归去构建我们的dom树
Reconciliation
but
我们现在每次调用render都会生成全新的fiber tree然后再构建dom树,这里只需要不断追加dom节点就可以构建dom树了,但是楞头新增dom挺废性能的,如果能重复利用就好了
我们想一想,就是我们能不能利用上一个提交了的fiber tree呢?通过对比新旧fiber对当前的fiber进行微调,如何微调?我们对比完新旧fiber就给当前的fiber打上标签tag,当前dom是需要新增或者当前dom失效了需要删除亦或者当前dom只需要更新,最后在commit阶段根据tag去操作dom节点。
回忆一下,这里像不像常常提到的diff的过程
首先设置currentRoot为上一次commit的fiber tree,并通过alternate属性将新旧fiber tree连接起来
在performunitofwork里我们不再楞头新增fiber了,取而代之的是一个 reconcileChildren函数用于生成当前child对应的fiber
oldFiber就是当前fiber的上一个版本。
这里进行对比新旧fiber
我们经常说的key在diff算法中的作用就是体现在这里,key可以快速定位到发生改变的孩子fiber,从而加快整个diff对比的速度。不过我们在这并没有实现key
引用文档对key的说明
通过对比之后当前fiber说明对应的dom节点只需要更新,不需要销毁或新增
当前fiber表示需要新增dom
当前fiber表示需要删除dom
每次render都维护一个deletions数组,用于删除dom
我们在渲染阶段结束后,渲染构建dom树之前进行删除dom节点操作
然后进入commit提交阶段,根据fiber上的tag进行不同的dom操作
追加dom节点
删除dom节点
更新dom节点
我们需要对比新旧fiber的props属性进行增删改
对于props中的事件需要特殊处理
到此,我们就分离了fiber tree的render阶段和fiber tree渲染成dom树的commit阶段
Function Components
在react中我们有函数式组件,我们是否也可以用createElement去生成react.element对象进而渲染到页面呢,目前是不能的
原因两点:
1、函数式组件对应的fiber没有dom节点
2、函数式组件的孩子是函数返回值
我们在performunitofwork中处理当前fiber时做一下判断
非函数式组件,直接走正常流程,生成dom节点,将孩子也变成fiber
函数式组件,没有dom就不设置了,通过调用函数本身拿到孩子,只处理孩子
到这里,函数式组件的fiber tree能成功构建出来,也就是render阶段完成
那么在commit阶段要考虑到一个点就是函数式组件没有自己的dom节点,所以在进行dom操作时要特殊处理
添加节点时,因为当前孩子的父亲是个函数组件没有dom,所以就再往上找,直到找到具有dom的fiber,然后挂载上去
需要删除节点时,删掉函数式组件,就是把函数式组件的孩子删了就可以
回忆一下,我们在写函数式组件或者类组件时return的时候最外层必须是个div或者fragement包裹,这或许就是原因吧。
Hooks
官方栗子:
在react的hooks中,useState和useEffect是”万恶之源“,很多hook都是基于这俩创建出来的。我们这里实现一下useState吧
首先理解一下hooks的作用是啥,函数式组件是无状态组件,我们需要hooks来帮助我们保存函数组件的状态,处理一些副作用函数。
而一个函数组件可以调用多次或多个hook,所以我们会在函数组件里维护一个hooks队列,而hooks数组维护的是每个hook维护的一个对象包括状态,依赖等,并由hookIndex去定位
首先我们会对比新旧fiber.hooks[hookIndex],如果调用了setState函数发生了状态改变,我们重新render组件,所以我们oldHokk.state就是最新的数据,故如果状态发生了变化就直接复用。将当前hook加入当前函数组件的hooks队列中,hookIndex累加
因为我们在当前函数组件中可以多次调用setState,所以我们在当前的hook中定义一个queue队列维护setState(callback)中所有的回调函数。这里setState触发页面重新渲染的原理就是我们会重置nextunitofwork以及deletions,触发渲染的条件,然后执行performuniofwork()重新开始一系列渲染操作
be like
执行setState会重新渲染组件,然后呢我们就能拿到oldHook.queue,再执行acitons相应的操作。actions为空数组说明此次组件的重新渲染不是由我挑起的
End
到这里我们就成功手搓了一个mini-react
转载自:https://juejin.cn/post/7208112485765103674