weixin_39664585
weixin_39664585
2021-01-09 00:43

深度剖析:如何实现一个 Virtual DOM 算法

作者:戴嘉华

转载请注明出处并保留原文链接( https://github.com/livoras/blog/issues/13 )和作者信息。

目录:

  • 1 前言
  • 2 对前端应用状态管理思考
  • 3 Virtual DOM 算法
  • 4 算法实现
  • 4.1 步骤一:用JS对象模拟DOM树
  • 4.2 步骤二:比较两棵虚拟DOM树的差异
  • 4.3 步骤三:把差异应用到真正的DOM树上
  • 5 结语
  • 6 References

1 前言

本文会在教你怎么用 300~400 行代码实现一个基本的 Virtual DOM 算法,并且尝试尽量把 Virtual DOM 的算法思路阐述清楚。希望在阅读本文后,能让你深入理解 Virtual DOM 算法,给你现有前端的编程提供一些新的思考。

本文所实现的完整代码存放在 Github

2 对前端应用状态管理的思考

假如现在你需要写一个像下面一样的表格的应用程序,这个表格可以根据不同的字段进行升序或者降序的展示。

sort-table

这个应用程序看起来很简单,你可以想出好几种不同的方式来写。最容易想到的可能是,在你的 JavaScript 代码里面存储这样的数据:

 javascript
var sortKey = "new" // 排序的字段,新增(new)、取消(cancel)、净关注(gain)、累积(cumulate)人数
var sortType = 1 // 升序还是逆序
var data = [{...}, {...}, {..}, ..] // 表格数据

用三个字段分别存储当前排序的字段、排序方向、还有表格数据;然后给表格头部加点击事件:当用户点击特定的字段的时候,根据上面几个字段存储的内容来对内容进行排序,然后用 JS 或者 jQuery 操作 DOM,更新页面的排序状态(表头的那几个箭头表示当前排序状态,也需要更新)和表格内容。

这样做会导致的后果就是,随着应用程序越来越复杂,需要在JS里面维护的字段也越来越多,需要监听事件和在事件回调用更新页面的DOM操作也越来越多,应用程序会变得非常难维护。后来人们使用了 MVC、MVP 的架构模式,希望能从代码组织方式来降低维护这种复杂应用程序的难度。但是 MVC 架构没办法减少你所维护的状态,也没有降低状态更新你需要对页面的更新操作(前端来说就是DOM操作),你需要操作的DOM还是需要操作,只是换了个地方。

既然状态改变了要操作相应的DOM元素,为什么不做一个东西可以让视图和状态进行绑定,状态变更了视图自动变更,就不用手动更新页面了。这就是后来人们想出了 MVVM 模式,只要在模版中声明视图组件是和什么状态进行绑定的,双向绑定引擎就会在状态更新的时候自动更新视图(关于MV*模式的内容,可以看这篇介绍)。

MVVM 可以很好的降低我们维护状态 -> 视图的复杂程度(大大减少代码中的视图更新逻辑)。但是这不是唯一的办法,还有一个非常直观的方法,可以大大降低视图更新的操作:一旦状态发生了变化,就用模版引擎重新渲染整个视图,然后用新的视图更换掉旧的视图。就像上面的表格,当用户点击的时候,还是在JS里面更新状态,但是页面更新就不用手动操作 DOM 了,直接把整个表格用模版引擎重新渲染一遍,然后设置一下innerHTML就完事了。

听到这样的做法,经验丰富的你一定第一时间意识这样的做法会导致很多的问题。最大的问题就是这样做会很慢,因为即使一个小小的状态变更都要重新构造整棵 DOM,性价比太低;而且这样做的话,inputtextarea的会失去原有的焦点。最后的结论会是:对于局部的小视图的更新,没有问题(Backbone就是这么干的);但是对于大型视图,如全局应用状态变更的时候,需要更新页面较多局部视图的时候,这样的做法不可取。

但是这里要明白和记住这种做法,因为后面你会发现,其实 Virtual DOM 就是这么做的,只是加了一些特别的步骤来避免了整棵 DOM 树变更

另外一点需要注意的就是,上面提供的几种方法,其实都在解决同一个问题:维护状态,更新视图。在一般的应用当中,如果能够很好方案来应对这个问题,那么就几乎降低了大部分复杂性。

3 Virtual DOM算法

DOM是很慢的。如果我们把一个简单的div元素的属性都打印出来,你会看到:

dom-attr

而这仅仅是第一层。真正的 DOM 元素非常庞大,这是因为标准就是这么设计的。而且操作它们的时候你要小心翼翼,轻微的触碰可能就会导致页面重排,这可是杀死性能的罪魁祸首。

相对于 DOM 对象,原生的 JavaScript 对象处理起来更快,而且更简单。DOM 树上的结构、属性信息我们都可以很容易地用 JavaScript 对象表示出来:

 javascript
var element = {
  tagName: 'ul', // 节点标签名
  props: { // DOM的属性,用一个对象存储键值对
    id: 'list'
  },
  children: [ // 该节点的子节点
    {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
  ]
}

上面对应的HTML写法是:

 html
<ul id="list">
  <li class="item">Item 1</li>
  <li class="item">Item 2</li>
  <li class="item">Item 3</li>
</ul>

既然原来 DOM 树的信息都可以用 JavaScript 对象来表示,反过来,你就可以根据这个用 JavaScript 对象表示的树结构来构建一棵真正的DOM树。

之前的章节所说的,状态变更->重新渲染整个视图的方式可以稍微修改一下:用 JavaScript 对象表示 DOM 信息和结构,当状态变更的时候,重新渲染这个 JavaScript 的对象结构。当然这样做其实没什么卵用,因为真正的页面其实没有改变。

但是可以用新渲染的对象树去和旧的树进行对比,记录这两棵树差异。记录下来的不同就是我们需要对页面真正的 DOM 操作,然后把它们应用在真正的 DOM 树上,页面就变更了。这样就可以做到:视图的结构确实是整个全新渲染了,但是最后操作DOM的时候确实只变更有不同的地方。

这就是所谓的 Virtual DOM 算法。包括几个步骤: 1. 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中 2. 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异 3. 把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了

Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)。

4 算法实现

4.1 步骤一:用JS对象模拟DOM树

用 JavaScript 来表示一个 DOM 节点是很简单的事情,你只需要记录它的节点类型、属性,还有子节点:

element.js

 javascript
function Element (tagName, props, children) {
  this.tagName = tagName
  this.props = props
  this.children = children
}

module.exports = function (tagName, props, children) {
  return new Element(tagName, props, children)
}

例如上面的 DOM 结构就可以简单的表示:

 javascript
var el = require('./element')

var ul = el('ul', {id: 'list'}, [
  el('li', {class: 'item'}, ['Item 1']),
  el('li', {class: 'item'}, ['Item 2']),
  el('li', {class: 'item'}, ['Item 3'])
])

现在ul只是一个 JavaScript 对象表示的 DOM 结构,页面上并没有这个结构。我们可以根据这个ul构建真正的<ul>

 javascript
Element.prototype.render = function () {
  var el = document.createElement(this.tagName) // 根据tagName构建
  var props = this.props

  for (var propName in props) { // 设置节点的DOM属性
    var propValue = props[propName]
    el.setAttribute(propName, propValue)
  }

  var children = this.children || []

  children.forEach(function (child) {
    var childEl = (child instanceof Element)
      ? child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点
      : document.createTextNode(child) // 如果字符串,只构建文本节点
    el.appendChild(childEl)
  })

  return el
}

render方法会根据tagName构建一个真正的DOM节点,然后设置这个节点的属性,最后递归地把自己的子节点也构建起来。所以只需要:

 javascript
var ulRoot = ul.render()
document.body.appendChild(ulRoot)

上面的ulRoot是真正的DOM节点,把它塞入文档中,这样body里面就有了真正的<ul>的DOM结构:

 html
<ul id="list">
  <li class="item">Item 1</li>
  <li class="item">Item 2</li>
  <li class="item">Item 3</li>
</ul>

完整代码可见 element.js

4.2 步骤二:比较两棵虚拟DOM树的差异

正如你所预料的,比较两棵DOM树的差异是 Virtual DOM 算法最核心的部分,这也是所谓的 Virtual DOM 的 diff 算法。两个树的完全的 diff 算法是一个时间复杂度为 O(n^3) 的问题。但是在前端当中,你很少会跨越层级地移动DOM元素。所以 Virtual DOM 只会对同一个层级的元素进行对比:

compare-in-level

上面的div只会和同一层级的div对比,第二层级的只会跟第二层级对比。这样算法复杂度就可以达到 O(n)。

4.2.1 深度优先遍历,记录差异

在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每个节点都会有一个唯一的标记:

dfs-walk

在深度优先遍历的时候,每遍历到一个节点就把该节点和新的的树进行对比。如果有差异的话就记录到一个对象里面。

 javascript

// diff 函数,对比两棵树
function diff (oldTree, newTree) {
  var index = 0 // 当前节点的标志
  var patches = {} // 用来记录每个节点差异的对象
  dfsWalk(oldTree, newTree, index, patches)
  return patches
}

// 对两棵树进行深度优先遍历
function dfsWalk (oldNode, newNode, index, patches) {
  // 对比oldNode和newNode的不同,记录下来
  patches[index] = [...]

  diffChildren(oldNode.children, newNode.children, index, patches)
}

// 遍历子节点
function diffChildren (oldChildren, newChildren, index, patches) {
  var leftNode = null
  var currentNodeIndex = index
  oldChildren.forEach(function (child, i) {
    var newChild = newChildren[i]
    currentNodeIndex = (leftNode && leftNode.count) // 计算节点的标识
      ? currentNodeIndex + leftNode.count + 1
      : currentNodeIndex + 1
    dfsWalk(child, newChild, currentNodeIndex, patches) // 深度遍历子节点
    leftNode = child
  })
}

例如,上面的div和新的div有差异,当前的标记是0,那么:

 javascript
patches[0] = [{difference}, {difference}, ...] // 用数组存储新旧节点的不同

同理ppatches[1]ulpatches[3],类推。

4.2.2 差异类型

上面说的节点的差异指的是什么呢?对 DOM 操作可能会: 1. 替换掉原来的节点,例如把上面的div换成了section 2. 移动、删除、新增子节点,例如上面div的子节点,把pul顺序互换 3. 修改了节点的属性 4. 对于文本节点,文本内容可能会改变。例如修改上面的文本节点2内容为Virtual DOM 2

所以我们定义了几种差异类型:

 javascript
var REPLACE = 0
var REORDER = 1
var PROPS = 2
var TEXT = 3

对于节点替换,很简单。判断新旧节点的tagName和是不是一样的,如果不一样的说明需要替换掉。如div换成section,就记录下:

 javascript
patches[0] = [{
  type: REPALCE,
  node: newNode // el('section', props, children)
}]

如果给div新增了属性idcontainer,就记录下:

 javascript
patches[0] = [{
  type: REPALCE,
  node: newNode // el('section', props, children)
}, {
  type: PROPS,
  props: {
    id: "container"
  }
}]

如果是文本节点,如上面的文本节点2,就记录下:

 javascript
patches[2] = [{
  type: TEXT,
  content: "Virtual DOM2"
}]

那如果把我div的子节点重新排序呢?例如p, ul, div的顺序换成了div, p, ul。这个该怎么对比?如果按照同层级进行顺序对比的话,它们都会被替换掉。如pdivtagName不同,p会被div所替代。最终,三个节点都会被替换,这样DOM开销就非常大。而实际上是不需要替换节点,而只需要经过节点移动就可以达到,我们只需知道怎么进行移动。

这牵涉到两个列表的对比算法,需要另外起一个小节来讨论。

4.2.3 列表对比算法

假设现在可以英文字母唯一地标识每一个子节点:

旧的节点顺序:


a b c d e f g h i

现在对节点进行了删除、插入、移动的操作。新增j节点,删除e节点,移动h节点:

新的节点顺序:


a b c h d f g i j

现在知道了新旧的顺序,求最小的插入、删除操作(移动可以看成是删除和插入操作的结合)。这个问题抽象出来其实是字符串的最小编辑距离问题(Edition Distance),最常见的解决算法是 Levenshtein Distance,通过动态规划求解,时间复杂度为 O(M * N)。但是我们并不需要真的达到最小的操作,我们只需要优化一些比较常见的移动情况,牺牲一定DOM操作,让算法时间复杂度达到线性的(O(max(M, N))。具体算法细节比较多,这里不累述,有兴趣可以参考代码

我们能够获取到某个父节点的子节点的操作,就可以记录下来:

 javascript
patches[0] = [{
  type: REORDER,
  moves: [{remove or insert}, {remove or insert}, ...]
}]

但是要注意的是,因为tagName是可重复的,不能用这个来进行对比。所以需要给子节点加上唯一标识key,列表对比的时候,使用key进行对比,这样才能复用老的 DOM 树上的节点。

这样,我们就可以通过深度优先遍历两棵树,每层的节点进行对比,记录下每个节点的差异了。完整 diff 算法代码可见 diff.js

4.3 步骤三:把差异应用到真正的DOM树上

因为步骤一所构建的 JavaScript 对象树和render出来真正的DOM树的信息、结构是一样的。所以我们可以对那棵DOM树也进行深度优先的遍历,遍历的时候从步骤二生成的patches对象中找出当前遍历的节点差异,然后进行 DOM 操作。

 javascript
function patch (node, patches) {
  var walker = {index: 0}
  dfsWalk(node, walker, patches)
}

function dfsWalk (node, walker, patches) {
  var currentPatches = patches[walker.index] // 从patches拿出当前节点的差异

  var len = node.childNodes
    ? node.childNodes.length
    : 0
  for (var i = 0; i < len; i++) { // 深度遍历子节点
    var child = node.childNodes[i]
    walker.index++
    dfsWalk(child, walker, patches)
  }

  if (currentPatches) {
    applyPatches(node, currentPatches) // 对当前节点进行DOM操作
  }
}

applyPatches,根据不同类型的差异对当前节点进行 DOM 操作:

 javascript
function applyPatches (node, currentPatches) {
  currentPatches.forEach(function (currentPatch) {
    switch (currentPatch.type) {
      case REPLACE:
        node.parentNode.replaceChild(currentPatch.node.render(), node)
        break
      case REORDER:
        reorderChildren(node, currentPatch.moves)
        break
      case PROPS:
        setProps(node, currentPatch.props)
        break
      case TEXT:
        node.textContent = currentPatch.content
        break
      default:
        throw new Error('Unknown patch type ' + currentPatch.type)
    }
  })
}

完整代码可见 patch.js

5 结语

Virtual DOM 算法主要是实现上面步骤的三个函数:elementdiffpatch。然后就可以实际的进行使用:

 javascript
// 1. 构建虚拟DOM
var tree = el('div', {'id': 'container'}, [
    el('h1', {style: 'color: blue'}, ['simple virtal dom']),
    el('p', ['Hello, virtual-dom']),
    el('ul', [el('li')])
])

// 2. 通过虚拟DOM构建真正的DOM
var root = tree.render()
document.body.appendChild(root)

// 3. 生成新的虚拟DOM
var newTree = el('div', {'id': 'container'}, [
    el('h1', {style: 'color: red'}, ['simple virtal dom']),
    el('p', ['Hello, virtual-dom']),
    el('ul', [el('li'), el('li')])
])

// 4. 比较两棵虚拟DOM树的不同
var patches = diff(tree, newTree)

// 5. 在真正的DOM元素上应用变更
patch(root, patches)

当然这是非常粗糙的实践,实际中还需要处理事件监听等;生成虚拟 DOM 的时候也可以加入 JSX 语法。这些事情都做了的话,就可以构造一个简单的ReactJS了。

本文所实现的完整代码存放在 Github,仅供学习。

6 References

https://github.com/Matt-Esch/virtual-dom/blob/master/vtree/diff.js

该提问来源于开源项目:livoras/blog

  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 复制链接分享
  • 邀请回答

100条回答

  • weixin_39700625 weixin_39700625 3月前

    点赞 评论 复制链接分享
  • weixin_39768444 weixin_39768444 3月前

    666

    点赞 评论 复制链接分享
  • weixin_39716044 weixin_39716044 3月前

    很是形象

    点赞 评论 复制链接分享
  • weixin_39742471 weixin_39742471 3月前

    学习

    点赞 评论 复制链接分享
  • weixin_39929602 weixin_39929602 3月前

    虚拟dom并不是操作真正的DOM,只是用js对象描述的DOM的结构

    点赞 评论 复制链接分享
  • weixin_39765057 weixin_39765057 3月前

    如果我们把一个简单的div元素的属性都打印出来,你会看到......而这仅仅是第一层。for...in不是会遍历到原型链中的属性吗?为什么说仅仅是第一层?

    点赞 评论 复制链接分享
  • weixin_39875031 weixin_39875031 3月前

    这也叫差异算法? 从头比到脚,得出了一个需要多做n步到patch,呵

    点赞 评论 复制链接分享
  • weixin_39664585 weixin_39664585 3月前

    所以你想表达的是?

    点赞 评论 复制链接分享
  • weixin_39746382 weixin_39746382 3月前

    受益匪浅,多谢博主!!

    点赞 评论 复制链接分享
  • weixin_39776787 weixin_39776787 3月前

    Mark,感谢大大热心分享~

    点赞 评论 复制链接分享
  • weixin_39621075 weixin_39621075 3月前

    非常棒!

    点赞 评论 复制链接分享
  • weixin_39528366 weixin_39528366 3月前

    function diffChildren (oldChildren, newChildren, index, patches) { var leftNode = null var currentNodeIndex = index oldChildren.forEach(function (child, i) { var newChild = newChildren[i] currentNodeIndex = (leftNode && leftNode.count) // 计算节点的标识 ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1 dfsWalk(child, newChild, currentNodeIndex, patches) // 深度遍历子节点 leftNode = child }) } 如果有leftNode时, currentNodeIndex = currentNodeIndex + leftNode.count + 1 ?? 这样算不太对吧,应该currentNodeIndex = currentNodeIndex + leftNode.count吧 ?

    点赞 评论 复制链接分享
  • weixin_39588542 weixin_39588542 3月前

    太赞了! 问个新手问题:“两个树的完全的 diff 算法是一个时间复杂度为 O(n^3) ”这个是怎么来的?有没有形象点的图形化步骤介绍?

    点赞 评论 复制链接分享
  • weixin_39614546 weixin_39614546 3月前

    学习了,顺便拿来练手自己写写理解一下。

    点赞 评论 复制链接分享
  • weixin_40009207 weixin_40009207 3月前

    赞!既涨了姿势,又看见了各位思想碰撞的火花!

    点赞 评论 复制链接分享
  • weixin_39544333 weixin_39544333 3月前

    想问代码里面计算virtual element的key的代码

     javascript
      this.key = props
        ? props.key
        : void 666
    

    这段代码有什么含义?如何计算唯一的key?

    点赞 评论 复制链接分享
  • weixin_39980596 weixin_39980596 3月前

    zan

    点赞 评论 复制链接分享
  • weixin_39567222 weixin_39567222 3月前

    真大神也

    点赞 评论 复制链接分享
  • weixin_39989862 weixin_39989862 3月前

    不错, 思路很清晰

    点赞 评论 复制链接分享
  • weixin_39609483 weixin_39609483 3月前

    666

    点赞 评论 复制链接分享
  • weixin_39639514 weixin_39639514 3月前

    真赞

    点赞 评论 复制链接分享
  • weixin_39802969 weixin_39802969 3月前

    找了好久终于找到你,分析的很透彻,赞赞赞

    点赞 评论 复制链接分享
  • weixin_39900180 weixin_39900180 3月前

    学习了。赞!

    点赞 评论 复制链接分享
  • weixin_39961943 weixin_39961943 3月前

    mark

    点赞 评论 复制链接分享
  • weixin_39996750 weixin_39996750 3月前

    学习,赞

    点赞 评论 复制链接分享
  • weixin_39830205 weixin_39830205 3月前

    学习了

    点赞 评论 复制链接分享
  • weixin_39600291 weixin_39600291 3月前

    请问react是如何实现,在一个事件循环中批处理虚拟dom的刷新的

    点赞 评论 复制链接分享
  • weixin_39598308 weixin_39598308 3月前

    膜拜大神

    点赞 评论 复制链接分享
  • weixin_39968820 weixin_39968820 3月前

    请问楼主为什么不在操作 virtual dom 时给 element 加个dirty flag?然后渲染时判断 dirty 就重新渲染,而是又要 diff 又要 patch 多两次遍历?

    点赞 评论 复制链接分享
  • weixin_39732825 weixin_39732825 3月前

    list diff算法应该不是react原版,这个diff结果只有,缺少效率可想而知

    实现上有bug,用增 + 删实现的,忘记了,用例:

    
    var key = 'k'
    var oldList = [1, 2, 3, 7, 4].map(function(item, i) {return {k: item, v: item}})
    var newList = [1, 4, 5, 3, 7, 6].map(function(item, i) {return {k: item, v: item}})
    // 结果[1, 4, 5, 3, 7, 6, 4]
    

    list diff策略是更新效率的关键,没有这么简单,需要仔细揣摩react具体实现(几年打磨的东西不是几行就能等价模拟的),最简单的list diff至少要有密切相关,改完看用不用移):

    
    var move = function(list, from, to) {
        var item = list.splice(from, 1);
        if (from > to)
            list.splice(to, 0, item[0]);
        else
            list.splice(to - 1, 0, item[0]);
    };
    var diff = function(oldList, newList) {
        var changes = [];
        // 镜像,模拟位置
        var _oldList = oldList.slice();
        // 遍历旧的,找出 删
        oldList.forEach(function(item, i) {
            if (newList.indexOf(item) === -1) {
                changes.push({
                    type: 'remove',
                    index: i
                });
                _oldList.splice(i, 1);
            }
        });
    
        // 遍历新的,找出 增/移
        newList.forEach(function(item, i) {
            var index = _oldList.indexOf(item);
            if (index === -1) {
                // 增
                changes.push({
                    type: 'insert',
                    index: i,
                    item: item
                });
                _oldList.splice(i, 0, item);
            }
            else {
                // 位置是否一样
                if (index === i) {
                    // 检查要不要改
                    changes.push({
                        type: 'check',
                        index: i
                    });
                }
                else {
                    // 移
                    var step = {
                        type: 'move',
                        from: index,
                        to: i
                    };
                    changes.push(step);
                    move(_oldList, step.from, step.to);
                }
            }
        });
    
        return changes;
    };
    
    // test
    var showSteps = function(changes) {
        changes.forEach(function(change) {
            switch (change.type) {
                case 'insert':
                    console.log('insert ' + change.item + ' before ' + oldList[change.index]);
                    oldList.splice(change.index, 0, change.item);
                    break;
                case 'remove':
                    console.log('remove ' + oldList[change.index]);
                    oldList.splice(change.index, 1);
                    break;
                case 'check':
                    console.log('check ' + oldList[change.index] + ' for update');
                    break;
                case 'move':
                    console.log('move ' + oldList[change.from] + ' to ' + oldList[change.to]);
                    move(oldList, change.from, change.to);
                    break;
                default:
                    cosole.error('not here');
                    break;
            }
        })
        if (JSON.stringify(oldList) === JSON.stringify(newList)) console.info(oldList);
        else console.error('diff出错');
    }
    var oldList = [1, 2, 3, 4, 7];
    var newList = [1, 4, 5, 7, 3, 6];
    var changes = diff(oldList, newList);
    showSteps(changes);
    
    // 结果
    remove 2
    check 1 for update
    move 4 to 3
    insert 5 before 3
    check 3 for update
    check 7 for update
    insert 6 before undefined
    

    另外,这里只向后看一位,很弱(错位的时候后面全都是增 + 删,实际上应该是):

    
    // if remove current simulateItem make item in right place
    // then just remove it
    var nextItemKey = getItemKey(simulateList[j + 1], key)
    if (nextItemKey === itemKey) {
      remove(i)
      removeSimulate(j)
      j++ // after removing, current j is right, just jump to next one
    } else {
      // else insert item
      insert(i, item)
    }
    

    当然,感谢博主提供的整个过程分析

    点赞 评论 复制链接分享
  • weixin_39802020 weixin_39802020 3月前

    厉害了!

    点赞 评论 复制链接分享
  • weixin_39609620 weixin_39609620 3月前

    感谢博主分享,收获很大

    点赞 评论 复制链接分享
  • weixin_39779467 weixin_39779467 3月前

    void 666 === undefined 任意数字都行

    点赞 评论 复制链接分享
  • weixin_39801465 weixin_39801465 3月前

    手动点赞

    点赞 评论 复制链接分享
  • weixin_39981093 weixin_39981093 3月前

    赞,写的不错,学习了

    点赞 评论 复制链接分享
  • weixin_39858124 weixin_39858124 3月前

    图片不能打开了啊~~

    点赞 评论 复制链接分享
  • weixin_39789979 weixin_39789979 3月前

    这篇文章我服,尤其是那个list列表对比算法

    点赞 评论 复制链接分享
  • weixin_39905624 weixin_39905624 3月前

    谢谢分享,有时间来实现一遍。

    点赞 评论 复制链接分享
  • weixin_39866881 weixin_39866881 3月前

    五体投地

    点赞 评论 复制链接分享
  • weixin_39866881 weixin_39866881 3月前

    我和作者居然是一届的。。看看现在的自己,惭愧啊,万分惭愧。。

    点赞 评论 复制链接分享
  • weixin_39612297 weixin_39612297 3月前

    niubility :)

    点赞 评论 复制链接分享
  • weixin_39978282 weixin_39978282 3月前

    上次看了这篇文章,直到今天才阅读源码,学到很多。

    点赞 评论 复制链接分享
  • weixin_39737233 weixin_39737233 3月前

    这确实是一个好文,不过有一个问题

    MVVM 可以很好的降低我们维护状态 -> 视图的复杂程度(大大减少代码中的视图更新逻辑)。但是这不是唯一的办法,还有一个非常直观的方法,可以大大降低视图更新的操作:一旦状态发生了变化,就用模版引擎重新渲染整个视图,然后用新的视图更换掉旧的视图。就像上面的表格,当用户点击的时候,还是在JS里面更新状态,但是页面更新就不用手动操作 DOM 了,直接把整个表格用模版引擎重新渲染一遍,然后设置一下innerHTML就完事了

    这个并不是mvvm的做法,这个有点像是 jquery + template 的实现,mvvm在初始化页面的时候会去分析出一个对应关系,这个步骤也可以叫做依赖收集,也就是说一旦有数据更改,它会去找到对应依赖这个数据的元素,然后进行局部修改。

    点赞 评论 复制链接分享
  • weixin_39664585 weixin_39664585 3月前

    请注意这里的转折:

    但是这不是唯一的办法,还有一个非常直观的方法,可以大大降低视图更新的操作...

    点赞 评论 复制链接分享
  • weixin_39595271 weixin_39595271 3月前

    难道你认为设置相同的innerHTML内容不会导致浏览器重绘?看起来dom操作好像少了,但是与Virtual DOM的思路是相悖的

    点赞 评论 复制链接分享
  • weixin_39737233 weixin_39737233 3月前

    不好意思,没注意到哈。

    点赞 评论 复制链接分享
  • weixin_39737233 weixin_39737233 3月前

    -front 虚拟DOM到最后不也是要用一些元素的DOM api么?比如 innerHTML , 为什么说是相悖的呢?多多指教。

    难道你认为设置相同的innerHTML内容不会导致浏览器重绘

    这句话不太理解 mvvm 只改我依赖的元素呀,其他不变呢。只是它初始化的时候比较慢而已,它要去生成一个 vmodel

    点赞 评论 复制链接分享
  • weixin_39624705 weixin_39624705 3月前

    好文章啊,收藏先。

    点赞 评论 复制链接分享
  • weixin_39647412 weixin_39647412 3月前

    收藏

    点赞 评论 复制链接分享
  • weixin_39827304 weixin_39827304 3月前

    列表对比算法那里,看了代码感觉不是很理解,能否指点一下?为什么要创建中间的simulateList?

    点赞 评论 复制链接分享
  • weixin_39620943 weixin_39620943 3月前

    很赞!

    点赞 评论 复制链接分享
  • weixin_39532699 weixin_39532699 3月前

    有一个地方不太明白额。虚拟DOM最终不也是操作了原生的DOM了吗,这跟直接使用DOM API进行更新的区别在哪儿,同样都操作原生DOM,为什么虚拟DOM比较快?

    点赞 评论 复制链接分享
  • weixin_39908948 weixin_39908948 3月前

    赞赞

    点赞 评论 复制链接分享
  • weixin_39557813 weixin_39557813 3月前

    👍

    点赞 评论 复制链接分享
  • weixin_39979080 weixin_39979080 3月前

    没错,同样都是操作了 DOM ,但是你要明白,使用虚拟 DOM 的 DOM 操作步骤是很小的(但并不一定是最小的),而如果你想达到同样小的操作步骤,你用手动操作 DOM 的方法来写就很麻烦了。虚拟 DOM 的目的并不是“更快的 DOM 操作”,而是优化视图更新逻辑。

    点赞 评论 复制链接分享
  • weixin_39664585 weixin_39664585 3月前

    不错 :+1: 很好的资料

    点赞 评论 复制链接分享
  • weixin_39519554 weixin_39519554 3月前

    赞 ,写的很好

    点赞 评论 复制链接分享
  • weixin_39761822 weixin_39761822 3月前

    妥妥的收藏了

    点赞 评论 复制链接分享
  • weixin_39913807 weixin_39913807 3月前

    代码分析很到位,可以作为下一个学习的点 :+1:

    点赞 评论 复制链接分享
  • weixin_39727934 weixin_39727934 3月前

    en

    点赞 评论 复制链接分享
  • weixin_39720181 weixin_39720181 3月前

    nice!

    点赞 评论 复制链接分享
  • weixin_39884877 weixin_39884877 3月前

    最近在学习 mithril.js ,发现这个框架挺适合我的,支持IE6+,虚拟Dom,同构程序。

    点赞 评论 复制链接分享
  • weixin_39664585 weixin_39664585 3月前

    听说过这个库,有空也研究下

    点赞 评论 复制链接分享
  • weixin_39884877 weixin_39884877 3月前

    本来打算在你这个库的基础上开发一个框架,但是项目太紧了,没时间来写,因此打算用其它的库。我本来用 riot.js 开发过几个项目,挺好用的;但是这边公司要支持IE6+,因此发现只有mithril.js呢,但 mithril.js 比 riot.js 坑多一点。

    点赞 评论 复制链接分享
  • weixin_39620037 weixin_39620037 3月前

    我最近也在使用riotjs, 话说还有ie6需要支持,汗。。。太坑了吧

    点赞 评论 复制链接分享
  • weixin_39620037 weixin_39620037 3月前

    刚刚看了看mithril.js发现它的view完全使用js方式来做,用一个m()方法来生成dom元素。感觉对前端开发人员不友好。。。

    点赞 评论 复制链接分享
  • weixin_39884877 weixin_39884877 3月前

    mithril 跟 react 的方式是一样的,都是写 jsx 方式,她类似的有个 msx ,在js里面写 html ; 没办法,公司要支持 IE6+ ,所以只能考虑用它。在学习成本上面他要高于 riot.js 的,有些api也不是很容易让人理解。 riot.js 确实很好用,但只支持IE9+。

    点赞 评论 复制链接分享
  • weixin_39577289 weixin_39577289 3月前

    赞,相当好,谢谢。

    点赞 评论 复制链接分享
  • weixin_39878991 weixin_39878991 3月前

    // 2. 通过虚拟DOM构建真正的DOM var root = tree.render() document.body.appendChild(root)

    如上,什么场景下会先先新建一个dom加到document上去,虚拟dom做修改后再更新root。 我理解的场景: 已经有一个dom了,先获取该dom,copy其结构,然后做修改,再统一更新root,减少repaint。

    所以楼主代码流程里的场景是什么,请指教...

    点赞 评论 复制链接分享
  • weixin_39620037 weixin_39620037 3月前

    传统的服务端开发人员会直接使用后端语言渲染好页面给浏览器。就像你说的dom已经存在了,js只需要更新dom。

    但是现在很多的前端页面,还有一些SPA应用,开始的时候页面上面几乎没有什么dom元素,都是生成dom元素之后再添加进去的。

    不过,确实像你说的,更新dom元素的场景应该会比较多 :smiley:

    点赞 评论 复制链接分享
  • weixin_39664585 weixin_39664585 3月前

    已经说得很好了。

    比较适合组件化的前端页面。假如你有一个可复用的组件,那么这个组件的渲染构造、状态维护就是由前端控制的。对于高度组件化的前端页面,可能整个页面都是由组件拼接而成,那么这时候整个DOM的构造几乎都是由前端JS控制的了。

    可以举一个例子,例如时间选择的组件,整个组件不能由服务器一步渲染完成,它的构造必然是在JS中进行,这时候就需要在JS中先构造DOM鸟。

    ReactJS就是一个很好的例子,把页面完全进行了组件化。

    点赞 评论 复制链接分享
  • weixin_39837139 weixin_39837139 3月前

    学习

    点赞 评论 复制链接分享
  • weixin_39676034 weixin_39676034 3月前

    学习

    点赞 评论 复制链接分享
  • weixin_39839410 weixin_39839410 3月前

    很好的思想!

    点赞 评论 复制链接分享
  • weixin_39989949 weixin_39989949 3月前

    盖楼,膜拜楼主,学习了

    点赞 评论 复制链接分享
  • weixin_39621044 weixin_39621044 3月前

    赞,学习了

    点赞 评论 复制链接分享
  • weixin_39713833 weixin_39713833 3月前

    点赞 评论 复制链接分享
  • weixin_39961943 weixin_39961943 3月前

    :+1:

    点赞 评论 复制链接分享
  • weixin_39631667 weixin_39631667 3月前

    :+1:

    点赞 评论 复制链接分享
  • weixin_39677027 weixin_39677027 3月前

    不错,学习了。

    点赞 评论 复制链接分享
  • weixin_39620037 weixin_39620037 3月前

    写得很不错!!!

    点赞 评论 复制链接分享
  • weixin_39804329 weixin_39804329 3月前

    有没有JSX语法的实现的资料

    点赞 评论 复制链接分享
  • weixin_39890431 weixin_39890431 3月前

    分析的好透彻。。

    点赞 评论 复制链接分享
  • weixin_39985472 weixin_39985472 3月前

    这个赞

    点赞 评论 复制链接分享
  • weixin_39664585 weixin_39664585 3月前

    最近在写,到时候也写篇博客整理一下JSX实现相关的。

    点赞 评论 复制链接分享
  • weixin_39759600 weixin_39759600 3月前

    zan !

    点赞 评论 复制链接分享
  • weixin_39568653 weixin_39568653 3月前

    赞!

    点赞 评论 复制链接分享
  • weixin_39625172 weixin_39625172 3月前

    赞赞赞...

    点赞 评论 复制链接分享
  • weixin_39610631 weixin_39610631 3月前

    问下文章里的图是用什么工具画的?

    点赞 评论 复制链接分享
  • weixin_39664585 weixin_39664585 3月前

    keynote

    点赞 评论 复制链接分享
  • weixin_39684454 weixin_39684454 3月前

    简单的reactjs,不错

    点赞 评论 复制链接分享
  • weixin_39905816 weixin_39905816 3月前

    DOM是很慢的,待考究,DOM的CURD是很慢的可能更合适。

    点赞 评论 复制链接分享
  • weixin_39864571 weixin_39864571 3月前

    判断两棵树的某个节点是否为同一个节点是最关键的

    点赞 评论 复制链接分享
  • weixin_39664585 weixin_39664585 3月前

    -WinDow 如果没有key的情况下,无法判断,所以如果两个节点tagName不一样会整棵子树替换掉;如果有key的话,关键点就是怎么用list-diff使得对比的时候两个节点是同一个节点了

    点赞 评论 复制链接分享
  • weixin_39884877 weixin_39884877 3月前

    请问一下,simple-virtual-dom 支持IE6+吗?

    点赞 评论 复制链接分享
  • weixin_39980347 weixin_39980347 3月前

    赞!

    点赞 评论 复制链接分享
  • weixin_39713578 weixin_39713578 3月前

    好文

    点赞 评论 复制链接分享
  • weixin_39518530 weixin_39518530 3月前

    赞!关于算法复杂度如何从O(n^3)到O(n),以及从O(MN)到O(max(M, N)),在React的文档里也有介绍,可以参考http://facebook.github.io/react/docs/reconciliation.html

    点赞 评论 复制链接分享
  • weixin_39846289 weixin_39846289 3月前

    不错

    点赞 评论 复制链接分享
  • weixin_39727105 weixin_39727105 3月前

    这个确实需要膜拜

    点赞 评论 复制链接分享

为你推荐