前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >React && VUE Virtual Dom的Diff算法统一之路 snabbdom.js解读

React && VUE Virtual Dom的Diff算法统一之路 snabbdom.js解读

作者头像
super.x
发布于 2019-04-12 07:50:44
发布于 2019-04-12 07:50:44
1.6K00
代码可运行
举报
运行总次数:0
代码可运行

VirtualDOM是react在组件化开发场景下,针对DOM重排重绘性能瓶颈作出的重要优化方案,而他最具价值的核心功能是如何识别并保存新旧节点数据结构之间差异的方法,也即是diff算法。毫无疑问的是diff算法的复杂度与效率是决定VirtualDOM能够带来性能提升效果的关键因素。因此,在VirtualDOM方案被提出之后,社区中不断涌现出对diff的改进算法,引用司徒正美的经典介绍:

最开始经典的深度优先遍历DFS算法,其复杂度为O(n^3),存在高昂的diff成本,然后是cito.js的横空出世,它对今后所有虚拟DOM的算法都有重大影响。它采用两端同时进行比较的算法,将diff速度拉高到几个层次。紧随其后的是kivi.js,在cito.js的基出提出两项优化方案,使用key实现移动追踪及基于key的编辑长度距离算法应用(算法复杂度 为O(n^2))。但这样的diff算法太过复杂了,于是后来者snabbdom将kivi.js进行简化,去掉编辑长度距离算法,调整两端比较算法。速度略有损失,但可读性大大提高。再之后,就是著名的vue2.0 把snabbdom整个库整合掉了。

因此目前VirtualDOM的主流diff算法趋向一致,在主要diff思路上,snabbdom与react的reconilation方式基本相同。

virtual dom中心思想

如果没有理解virtual dom的构建思想,那么你可以参考这篇精致文章Boiling React Down to a Few Lines in jQuery virtual dom优化开发的方式是:通过vnode,来实现无状态组件,结合单向数据流(undirectional data flow),进行UI更新,整体代码结构是:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var newVnode = render(vnode, state)
var oldVnode = patch(oldVnode, newVnode)
state.dispatch('change')
var newVnode = render(vnode, state)
var oldVnode = patch(oldVnode, newVnode)

virtual dom库选择

在众多virtual dom库中,我们选择snabbdom库,原因有很多:

1.snabbdom性能排名靠前,虽然这个benchmark的参考性不高 2。snabbdom示例丰富 3.snabbdom具有一定的生态圈,如motorcycle.js,cycle-snabbdom,cerebral 4.snabbdom实现的十分优雅,使用的是recursive方式调用patch,对比infernojs优化痕迹明显的代码,snabbdom更易读。 5.在阅读过程中发现,snabbdom的模块化,插件支持做得极佳

snabbdom的工作方式

我们来查看snabbdom基本使用方式。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// snabbdom在./snabbdom.js
var snabbdom = require('snabbdom')
// 初始化snabbdom,得到patch。随后,我们可以看到snabbdom设计的精妙之处
var patch = snabbdom.init([
  require('snabbdom/modules/class'),
  require('snabbdom/modules/props'),
  require('snabbdom/modules/style'),
  require('snabbdom/modules/eventlisteners')
])
// h是一个生成vnode的包装函数,factory模式?对生成vnode更精细的包装就是使用jsx
// 在工程里,我们通常使用webpack或者browserify对jsx编译
var h = require('snabbdom/h')
// 构造一个virtual dom,在实际中,我们通常希望一个无状态的vnode
// 并且我们通过state来创造vnode
// react使用具有render方法的对象来作为组件,这个组件可以接受props和state
// 在snabbdom里面,我们同样可以实现类似效果
// function component(state){return h(...)}
var vnode = 
  h(
    'div#container.two.classes', 
    {on: {click: someFn}}, 
    [ 
      h('span', {style: {fontWeight: 'bold'}}, 'This is bold'), 
      ' and this is just normal text', 
      h('a', {props: {href: '/foo'}}, 
      'I\'ll take you places!')
    ]
  )
// 得到初始的容器,注意container是一个dom element
var container = document.getElementById('container')
// 将vnode patch到container中
// patch函数会对第一个参数做处理,如果第一个参数不是vnode,那么就把它包装成vnode
// patch过后,vnode发生变化,代表了现在virtual dom的状态
patch(container, vnode)
// 创建一个新的vnode
var newVnode = 
  h(
    'div#container.two.classes', 
    {on: {click: anotherEventHandler}}, 
    [ 
      h('span', {style: {fontWeight: 'normal', fontStyle: 'italics'}},
      'This is now italics'), 
      ' and this is still just normal text', 
      h('a', {props: {href: '/bar'}}, 'I\'ll take you places!')
    ]
  )
// 将新的vnode patch到vnode上,现在newVnode代表vdom的状态
patch(vnode, newVnode)
vnode的定义
阅读vdom实现,首先弄清楚vnode的定义

vnode的定义在./vnode.js中 vnode具备的属性

1.tagName 可以是custom tag,可以是'div','span',etc,代表这个virtual dom的tag name 2.data, virtual dom数据,它们与dom element的prop、attr的语义类似。但是virtual dom包含的数据可以更灵活。 比如利用./modules/class.js插件,我们在data里面轻松toggle一个类名

h('p', {class: {'hide': hideIntro}})

  • children, 对应element的children,但是这是vdom的children。vdom的实现重点就在对children的patch上
  • text, 对应element.textContent,在children里定义一个string,那么我们会为这个string创建一个textNode
  • elm, 对dom element的引用
  • key,用于提示children patch过程,随后将详细说明

h参数

随后是h函数的包装

h的实现在./h.js

包装函数一共注意三点

  • 对svg的包装,创建svg需要namespace
  • 将vdom.text统一转化为string类型
  • 将vdom.children中的string element转化为textNode

与dom api的对接

实现在./htmldomapi.js

采用adapter模式,对dom api进行包装,然后将htmldomapi作为默认的浏览器接口 这种设计很机智。在扩展snabbdom的兼容性的时候,只需要改变snabbdom.init使用的浏览器接口,而不用改变patch等方法的实现

snabbdom的patch解析

snabbdom的核心内容实现在./snabbdom.js。snabbdom的核心实现不到三百行(233 sloc),非常简短。

在snabbdom里面实现了snabbdom的virtual dom diff算法与virtual dom lifecycle hook支持。 virtual dom diff vdom diff是virtual dom的核心算法,snabbdom的实现原理与react官方文档Reconciliation一致 总结起来有:

  • 对两个树结构进行完整的diff和patch,复杂度增长为O(n^3),几乎不可用
  • 对两个数结构进行启发式diff,将大大节省开销

一篇阅读量颇丰的文章React’s diff algorithm也说明的就是启发过程,可惜,没有实际的代码参照。现在,我们根据snabbdom代码来看启发规则的运用,结束后,你会明白virtual dom的实现有多简单。

首先来到snabbdom.js中init函数的return语句

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
return function(oldVnode, vnode) {
  var i, elm, parent;
  // insertedVnodeQueue存在于整个patch过程
  // 用于收集patch中新插入的vnode
  var insertedVnodeQueue = [];
  // 在进行patch之前,我们需要运行prepatch hook
  // cbs是init函数变量,即,这个return语句中函数的闭包
  // 这里,我们不理会lifecycle hook,而只关注vdom diff算法
  for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();

  // 如果oldVnode不是vnode(在第一次调用时,oldVnode是dom element)
  // 那么用emptyNodeAt函数来将其包装为vnode
  if (isUndef(oldVnode.sel)) {
    oldVnode = emptyNodeAt(oldVnode);
  }
  
  // sameVnode是上述“值不值得patch”的核心
  // sameVnode实现很简单,查看两个vnode的key与sel是否分别相同
  // ()=>{vnode1.key === vnode2.key && vnode1.sel === vnode2.
  // 比较语义不同的结构没有意义,比如diff一个'div'和'span'
  // 而应该移除div,根据span vnode插入新的span
  // diff两个key不相同的vnode同样没有意义
  // 指定key就是为了区分element
  // 对于不同key的element,不应该去根据newVnode来改变oldVnode的数据
  // 而应该移除不再oldVnode,添加newVnode
  if (sameVnode(oldVnode, vnode)) {
    // oldVnode与vnode的sel和key分别相同,那么这两个vnode值得去比较
    //patchVnode根据vnode来更新oldVnode
    patchVnode(oldVnode, vnode, insertedVnodeQueue);
  } else {
    //不值得去patch的,我们就暴力点
    // 移除oldVnode,根据newVnode创建elm,并添加至parent中
    elm = oldVnode.elm;
    parent = api.parentNode(elm);

    // createElm根据vnode创建element
    createElm(vnode, insertedVnodeQueue);

    if (parent !== null) {
      // 将新创建的element添加到parent中
      api.insertBefore(parent, vnode.elm, api.nextSibling(elm));
      // 同时移除oldVnode
      removeVnodes(parent, [oldVnode], 0, 0);
    }
  }

  // 结束以后,调用插入vnode的insert hook
  for (i = 0; i < insertedVnodeQueue.length; ++i) {
    insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]);
  }

  // 整个patch结束,调用cbs中的post hook
  for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
  return vnode;
};
```

    ###然后我们阅读patch的过程

```
function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
  var i, hook;
  // 如前,在patch之前,调用prepatch hook,但是这个是vnode在data里定义的prepatch hook,而不是全局定义的prepatch hook
  if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {
    i(oldVnode, vnode);
  }

  var elm = vnode.elm = oldVnode.elm, oldCh = oldVnode.children, ch = vnode.children;

  // 如果oldVnode和vnode引用相同,则没必要比较。在良好设计的vdom里,大部分时间我们都在执行这个返回语句。
  if (oldVnode === vnode) return;
  // 如果两次引用不同,那说明新的vnode创建了
  // 与之前一样,我们先看这两个vnode值不值得去patch
  if (!sameVnode(oldVnode, vnode)) {
    // 这四条语句是否与init返回函数里那四条相同?
    var parentElm = api.parentNode(oldVnode.elm);
    elm = createElm(vnode, insertedVnodeQueue);
    api.insertBefore(parentElm, elm, oldVnode.elm);
    removeVnodes(parentElm, [oldVnode], 0, 0);
    return;
  }
  // 这两个vnode值得去patch
  // 我们先patch vnode,patch的方法就是先调用全局的update hook
  // 然后调用vnode.data定义的update hook
  if (isDef(vnode.data)) {
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
    i = vnode.data.hook;
    if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
  }
  // patch两个vnode的text和children
  // 查看vnode.text定义
  // vdom中规定,具有text属性的vnode不应该具备children
  // 对于<p>foo:<b>123</b></p>的良好写法是
  // h('p', [ 'foo:', h('b', '123')]), 而非
  // h('p', 'foo:', [h('b', '123')])
  if (isUndef(vnode.text)) {
    // vnode不是text node,我们再查看他们是否有children
    if (isDef(oldCh) && isDef(ch)) {
      // 两个vnode都有children,那么就调用updateChildren
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
    } else if (isDef(ch)) {
      // 只有新的vnode有children,那么添加vnode的children
      if (isDef(oldVnode.text)) api.setTextContent(elm, '');
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
    } else if (isDef(oldCh)) {
      // 只有旧vnode有children,那么移除oldCh
      removeVnodes(elm, oldCh, 0, oldCh.length - 1);
    } else if (isDef(oldVnode.text)) {
      // 两者都没有children,并且oldVnode.text不为空,vnode.text未定义,则清空elm.textContent
      api.setTextContent(elm, '');
    }
  } else if (oldVnode.text !== vnode.text) {
    // vnode是一个text node,我们改变对应的elm.textContent
    // 在这里我们使用api.setText api
    api.setTextContent(elm, vnode.text);
  }
  if (isDef(hook) && isDef(i = hook.postpatch)) {
    i(oldVnode, vnode);
  }
}

patch的实现是否简单明了?甚至有觉得“啊?这就patch完了”的感觉。当然,我们还差最后一个,这个是重头戏——updateChildren。

最后阅读updateChildren*

updateChildren的代码较长且密集,但是算法十分简单 oldCh是一个包含oldVnode的children数组,newCh同理 我们先遍历两个数组(while语句),维护四个变量

  • 遍历oldCh的头索引 - oldStartIdx
  • 遍历oldCh的尾索引 - oldEndIdx
  • 遍历newCh的头索引 - newStartIdx
  • 遍历newCh的尾索引 - newEndIdx

当oldStartIdx > oldEndIdx或者newStartIdx > newOldStartIdx的时候停止遍历。

遍历过程中有五种比较 前四种比较

  • oldStartVnode和newStartVnode,两者elm相对位置不变,若值得(sameVnode)比较,这patch这两个vnode
  • oldEndVnode和newEndVnode,同上,elm相对位置不变,做相同patch检测
  • oldStartVnode和newEndVnode,如果oldStartVnode和newEndVnode值得比较,说明oldCh中的这- - oldStartVnode.elm向右移动了。那么执行api.insertBefore(parentElm,oldStartVnode.elm, api.nextSibling(oldEndVnode.elm))调整它的位置
  • oldEndVnode和newStartVnode,同上,但这是oldVnode.elm向左移,需要调整它的位置

最后一种比较

利用vnode.key,在ul>li*n的结构里,我们很有可能使用key来标志li的唯一性,那么我们就会来到最后一种情况。这个时候,我们先产生一个index-key表(createKeyToOldIdx),然后根据这个表来进行更改。

更改规则

如果newVnode.key不在表中,那么这个newVnode就是新的vnode,将其插入 如果newVnode.key在表中,那么对应的oldVnode存在,我们需要patch这两个vnode,并在patch之后,将这个oldVnode置为undefined(oldCh[idxInOld] = undefined),同时将oldVnode.elm位置变换到当前oldStartIdx之前,以免影响接下来的遍历

遍历结束后,检查四个变量,对移除剩余的oldCh或添加剩余的newCh patch总结 阅读完init函数return语句,patch,updateChildren,我们可以理解整个diff和patch的过程

有些函数createElm,removeVnodes并不重要

lifecycle hook

阅读完virtual dom diff算法实现后,我们可能会奇怪,关于style、class、attr的patch在哪里?这些实现都在modules,并通过lifecycle发挥作用 snabbdom的生命周期钩子函数定义在core doc - hook中。 再查看modules里的class会发现,class module通过两个hook钩子来对elm的class进行patch。这两个钩子是create和update。 回到init函数,这两个钩子在函数体开头注册

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
for (i = 0; i < hooks.length; ++i) {
  cbs[hooks[i]] = [];
  for (j = 0; j < modules.length; ++j) {
    if (modules[j][hooks[i]] !== undefined)
     cbs[hooks[i]].push(modules[j][hooks[i]]);
  }
}

create hook在createElm中调用。createElm是唯一添加vnode的方法,所以insertedVnodeQueue.push只发生在createElm中。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
关于Virtual DOM理解和Snabbdom源码浅析
Virtual DOM 本质上JS和DOM之间的一个映射缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)。
Vam的金豆之路
2021/12/01
1.1K0
关于Virtual DOM理解和Snabbdom源码浅析
3. 「snabbdom@3.5.1 源码分析」patch(如何打补丁?)
看到会返回一个patch函数。看到init内部有很多函数,这些函数大都都是用到api进行DOM操作,而api依赖入参domApi(如果放在外侧,domApi需要作为参数传递)。 这里实际上通过闭包私有化这些函数作为方法存在。
tinyant
2023/02/24
1.7K0
浅析 Snabbdom 中 vnode 和 diff 算法
目前前端使用最多的就是 vue 或 react 了,我们在学习这两个框架的过程中,总有一个绕不开的话题:vnode,也就是虚拟 DOM。什么是虚拟 DOM,引用一段 vue 官方的解释就是:
政采云前端团队
2022/03/29
7350
浅析 Snabbdom 中 vnode 和 diff 算法
Vue中的diff算法深度解析
模板tamplate经过parse,optimize,generate等一些列操作之后,把AST转为render function code进而生成虚拟VNode,模板编译阶段基本已经完成了,那么这一章,我们来探讨一下Vue中的一个算法策略--dom diff 首先来介绍下什么叫dom diff
yyds2026
2022/10/21
7970
面试官:了解过vue中的diff算法吗?说说看
diff 算法的在很多场景下都有应用,在 vue 中,作用于虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较
@超人
2021/02/26
7550
面试官:了解过vue中的diff算法吗?说说看
DIff算法看不懂就一起来锤我(带图)
面试官:"你了解虚拟DOM(Virtual DOM)跟Diff算法吗,请描述一下它们";
coder_koala
2021/09/18
7850
DIff算法看不懂就一起来锤我(带图)
Vue进阶 Diff算法详解
虚拟DOM就是把真实DOM树的结构和信息抽象出来,以对象的形式模拟树形结构,如下:
前端LeBron
2021/12/08
6120
Vue进阶 Diff算法详解
Vue中diff算法的理解
diff算法用来计算出Virtual DOM中改变的部分,然后针对该部分进行DOM操作,而不用重新渲染整个页面,渲染整个DOM结构的过程中开销是很大的,需要浏览器对DOM结构进行重绘与回流,而diff算法能够使得操作过程中只更新修改的那部分DOM结构而不更新整个DOM,这样能够最小化操作DOM结构,能够最大程度上减少浏览器重绘与回流的规模。
WindRunnerMax
2020/08/27
6950
大前端百科全书vue专题之虚拟dom+diff算法
因为使用了 Virtual DOM 的原因,Vue.js具有了跨平台的能力,例如:weex、小程序、web、h5、等
玖柒的小窝
2021/10/05
7260
大前端百科全书vue专题之虚拟dom+diff算法
理解DOM Diff算法
虚拟 DOM 出现的背景:在 jQuery 时代,可以自行控制 DOM 操作的时机,手动调整,但是当项目很大时,操作 DOM 的复杂度就会上来,DOM 操作会很耗费性能,操作 DOM 就还需要考虑优化 DOM 操作,提升性能。《高性能 JavaScript》这本书中说,把 DOM 和 JavaScript 各自想象成一个岛屿,它们之间用收费桥梁连接。操作 DOM 后需要经过跨流程通信和渲染线程触发的重新渲染(重绘或者重排),在开发中,应尽量减少操作 DOM。而虚拟 DOM 出现后,更新 DOM 交给框架处理。操作虚拟 DOM 可能并没有操作真实 DOM 快,但是它让开发人员不再把很多精力放在操作 DOM 上,而是专注于处理业务数据。本文以 Vue 原码中的 DOM diff 算法为例,介绍一下这个算法的实现原理。
多云转晴
2020/07/29
1.1K0
理解DOM Diff算法
Vue2剥丝抽茧-虚拟 dom 之增删
虚拟 dom 之移动优化 中介绍了虚拟 dom 的双端 diff 的算法,但是没有考虑当虚拟 dom 增加或者减少的情况,这篇文章介绍增删 dom 在各个场景下的的代码完善。
windliang
2022/08/20
2390
Vue2剥丝抽茧-虚拟 dom 之增删
Vue内部是如何渲染视图
vnode其实就是一个描述节点的对象,描述如何创建真实的DOM节点;vnode的作用就是新旧vnode进行对比,只更新发生变化的节点。
用户9700400
2022/11/21
9530
Vue内部是如何渲染视图
【揭秘Vue核心】为什么不建议在 v-for 指令中使用 index 作为 key,让你秒懂!
Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。
奋飛
2023/07/24
2920
【揭秘Vue核心】为什么不建议在 v-for 指令中使用 index 作为 key,让你秒懂!
Vue2剥丝抽茧-虚拟 dom 之更新
虚拟 dom 简介、虚拟 dom 之绑定事件 中我们将虚拟 dom 转换为了真实 dom 的结构,介绍了 dom 中 class 、style 、绑定事件的过程。
windliang
2022/08/20
3340
Vue2剥丝抽茧-虚拟 dom 之更新
那些年曾经没回答上来的vue面试题
Vue3.x 改用 Proxy 替代 Object.defineProperty。因为 Proxy 可以直接监听对象和数组的变化,并且有多达 13 种拦截方法。
bb_xiaxia1998
2022/09/26
5210
2023前端vue面试题及答案_2023-02-28
在 Vue2 中, 0bject.defineProperty 会改变原始数据,而 Proxy 是创建对象的虚拟表示,并提供 set 、get 和 deleteProperty 等处理器,这些处理器可在访问或修改原始对象上的属性时进行拦截,有以下特点∶
用户10358241
2023/02/28
1.8K0
6. 「vue@2.6.11 源码分析」组件渲染之虚拟DOM上界面
new Vue(..)之后总共有两个大的步骤,第一步是调用vm._init完成组件的各种准备(初始化)工作,然后是开始结合数据与模板实现页面的渲染。vue引入了虚拟DOM技术,这里页面渲染分为两步,将模板和数据(转为了render函数)转为虚拟DOM树,而后再将虚拟DOM树同步到界面上。上一小节已经分析过创建虚拟DOM树的过程,现在我们来看看虚拟DOM是如何同步到界面上的。
tinyant
2023/02/24
9580
6. 「vue@2.6.11 源码分析」组件渲染之虚拟DOM上界面
virtual DOM和diff算法(二)
根据昨天的学习,我们知道运用了virtual DOM和diff算法,可以提高程序性能,那么我们今天开始就要看看到底是怎么提高性能的。
用户3258338
2019/07/19
5120
Vue2剥丝抽茧-虚拟 dom 之移动
虚拟 dom 之更新 中我们假设了 dom 的结构没有发生变化,完成了 dom 属性和内容的更新。这篇文章,我们假设 dom 没有出现增减,只是发生了移动,看一下这种情况下的更新情况。
windliang
2022/08/20
3020
Vue2剥丝抽茧-虚拟 dom 之移动
字节前端一面常见vue面试题(必备)_2023-02-28
Vue 的编译过程就是将 template 转化为 render 函数的过程 分为以下三步
用户10377014
2023/02/28
6110
相关推荐
关于Virtual DOM理解和Snabbdom源码浅析
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文