React是一个基于vdom的前端框架。componentrender生成vdom,然后renderer渲染vdom。当状态更新时,组件将重新渲染以生成新的vdom。在浏览器平台下,为了减少dom的创建,React会对两次render的结果进行diff,尽可能复用dom以提高性能。diff算法是前端框架中比较复杂的部分,代码也比较多,不过今天我们就不上代码了,只看图就明白了。首先我们看一下React的纤程架构:纤程架构React通过jsx描述页面结构:functionProfile(){return
{[user.firstName,user.lastName].join("")}
}用babel等编译后会变成render函数:import{jsxas_jsx}from“react/jsx-runtime”;从“react/jsx-runtime”导入{jsxsas_jsxs};constprofile=_jsxs("div",{children:[_jsx("img",{src:"avatar.png",className:"profile",}),_jsx("h3",{children:[user.firstName,user.lastName].join(""),}),],});render函数执行的结果是vdom,它是ReactElement的一个实例:16之前,React直接递归渲染vdom,setState会触发重新渲染,对比渲染后的新旧vdom,对差异进行dom操作。16之后,为了优化性能,先把vdom转成fiber,也就是从tree转成链表,然后render。整体渲染过程分为两个阶段:渲染阶段:从vdom转fiber,将需要dom操作的节点用effectTag标记。Commit阶段:对标有effectTag的fiber节点进行dom操作,执行所有effect副作用函数。从vdom转换为fiber的过程称为reconcile。这个过程可以被调度程序中断和执行。diff算法工作在reconcile阶段:第一次渲染不需要diff,直接从vdom转fiber。再次渲染时,会生成一个新的vdom。此时与之前的fiber进行比较,决定如何生成新的fiber,将可复用的节点标记为modified,将剩余的旧节点标记为deleted。新节点标记添加。接下来,让我们仔细看看React的diff算法:React'sdiffalgorithm。在谈diff算法的实现之前,我们首先要明白为什么要做diff。我们可以不做吗?当然每次渲染都可以直接把vdom转成fiber,不需要和之前的对比。这是可行的。其实SSR的时候不需要做diff,因为组件会渲染成字符串,第二次渲染也会生成字符串。这个时候我们是不是需要和之前的字符串进行比较,哪些字符串可以被复用呢?不,SSR中没有diff,vdom每次都渲染一个新字符串。那为什么要在浏览器中做diff呢?因为创建dom的性能成本非常高,如果不复用dom,前端框架的性能就太差了。diff算法的目的是比较两次渲染结果,找出可重用的部分,然后删除其余部分,并添加新的。那么如何实现React的diff算法呢?比如父节点下有A、B、C、D四个子节点,渲染出来的vdom是这样的:reconcile之后,就会变成这样一个fiber结构:那么再渲染的话,A和C都渲染了,B,E的vdom,这时候怎么处理呢?再次渲染vdom时,也会进行vdom到fiber的reconcile阶段,但要尽量复用之前的节点。那怎么复用呢?一个一个比较不好吗?先把之前的fiber节点放到一个map中,key就是节点的key:然后每个新的vdom都到这个map中去寻找有没有可复用的,如果找到就移过来,更新后的effectTag会被标记:这样遍历vdom节点后,map中还剩下一些,不可重复使用,然后删除它们,并标记删除的effectTag;如果vdom中还有一些可复用的节点没有找到,直接创建,并标记为新的effectTag。这样就实现了更新时的reconcile,也就是上面的diff算法。其实核心就是找到可重用的节点,删除剩下的旧节点,添加新节点。但有时可以简化。比如上次渲染是A、B、C、D,这次渲染也是A、B、C、D。那么直接对比顺序就可以了,不需要先建图再搜索。因此,React的diff算法分为两次遍历:第一轮遍历,将vdom和oldfiber逐一比较,如果可以重用则处理下一个节点,否则结束遍历。如果新的vdom都处理完了,就把剩下的旧的fiber节点删掉。如果还有未处理的vdom,则进行第二次遍历:第二轮遍历,将剩下的oldfibers放入map中,遍历剩下的vdoms,从map中搜索,找到则移动。第二轮遍历后,删除剩余的oldfiber,添加剩余的vdom。这样就完成了新的纤维结构的创建,也就是调和的过程。比如上面的例子,第一轮遍历是这样的:将新的vdom和旧的fiber逐一比较,发现A可以被复用,然后创建一个新的fiber节点,并标记为update。C不可重用,所以结束第一轮遍历,进入第二轮遍历。将剩下的旧fiber节点放入map中,再遍历新的vdom节点。如果您可以从地图上找到它们,则它们可以重复使用。将它们移动过来并用更新标记它们。遍历完成后,删除剩余的旧fiber节点,添加剩余的新vdom。这样就完成了更新时的协调过程。总结一下,react是一个基于vdom的前端框架。组件渲染生成vdom,渲染器将vdom渲染成dom。浏览器下使用react-dom的renderer会先将vdom转成fiber,在dom中找到需要更新的部分,标记上增删改查的effectTag。这个过程称为协调,可以被调度程序中断和执行。reconcile结束后,根据effectTag一次性更新dom,称为commit。这就是react基于fiber的渲染流程,分为两个阶段:render(reconcile+schedule)和commit。渲染一次并生成fibers后,渲染后的vdom应该与之前的fibers进行比较,然后再决定如何生成新的fibers。目标是尽可能多地重用现有的光纤节点。这称为差异算法。React的diff算法分为两个阶段:第一阶段一个一个比较,如果可以复用则进行下一个,如果不能复用则结束。第二阶段,将剩余的旧fiber放入map中,遍历剩余的vdoms,逐一查找map中是否有可复用的节点。最后,删除剩余的旧纤程,并添加剩余的新vdom。这样就完成了更新时的协调过程。其实diff算法的核心就是复用节点,无论是通过一对一比较还是map搜索,都是找到可复用的节点并进行移动。然后其余的应该删除或添加。一旦了解了如何查找可重用节点,您就了解了diff算法的核心。