资讯专栏INFORMATION COLUMN

学习Virtual Dom笔记

DobbyKim / 503人阅读

摘要:通过深度优先遍历两棵树,每层节点进行对比,记录下每个节点的差异。所以可以对那棵树也进行深度优先遍历,遍历的时候从步骤二生成的对象中找出当前遍历的节点差异,然后进行操作。

实现虚拟(Virtual) Dom

把一个div元素的属性打印出来,如下:

可以看到仅仅是第一层,真正DOM的元素是非常庞大的,这也是DOM加载慢的原因。
相对于DOM对象,原生的JavaScript对象处理起来更快,而且更简单。DOM树上的结构、属性信息都可以用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写法是:

  • Item 1
  • Item 2
  • Item 3

DOM树的信息可以用JavaScript对象表示出来,则说明可以用JavaScript对象去表示树结构来构建一棵真正的DOM树。

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

Virtual DOM算法,可以归纳为以下几个步骤:

用JavaScript对象结构表示DOM树的结构,然后用这个树构建一个真正的DOM树,插到文档当中

当状态变更的时候,重新构建一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树的差异

2所记录的差异应用到步骤1所构建的的真正的DOM树上,视图就更新了

Virtual DOM本质就是在JS和DOM之间做了一个缓存,JS操作Virtual DOM,最后再应用到真正的DOM上。

难点-算法实现

步骤一:用JS对象模拟虚拟DOM

JavaScript来表示一个DOM节点,则需要记录它的节点类型、属性、子节点:
element.js

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结构可以表示为:

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构建真正的

    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节点,然后设置这个节点的属性,最后递归地把自己的子节点也构建起来。所以需要:

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

    上面的ulRoot是真正的DOM节点,把它塞进文档中,这样body里面就有了真正的

      的DOM结构:

      • Item 1
      • Item 2
      • Item 3

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

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

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

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

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

      // 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,那么:

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

      同理ppatches[1]ulpatches[3],以此类推

      b.差异类型

      DOM操作会有的差异:

      替换掉原来的节点,例如把上面的div换成了section

      移动、删除、新增子节点,例如上面的div的子节点,把pul顺序互换

      修改了节点的属性

      对于文本节点,文本内容可能会改变。例如修改上面的文本节点2内容为Virtual DOM2

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

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

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

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

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

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

      如果修改文本节点,如上面的文本节点2,记录如下:

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

      c.列表对比算法

      上面如果把div中的子节点重新排序,看如puldiv的顺序换成了divpul。按照同层进行顺序对比的话,它们都会被替换掉,这样DOM开销非常大。而实际上只需要通过节点移动就可以的了。
      假设现在可以英文字母唯一得标志每一个子节点:
      旧的节点顺序:
      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)))
      获取某个父节点的子节点的操作,就可以记录如下:

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

      由于tagName是可以重复的,所以不能用这个来进行对比。需要给子节点加上一盒唯一标识key,列表对比的时候,使用key进行对比,这样就能复用旧的DOM树上的节点。
      通过深度优先遍历两棵树,每层节点进行对比,记录下每个节点的差异。完整的diff算法访问:https://github.com/livoras/si...

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

      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 操作:

      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代码访问:https://github.com/livoras/si...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/108884.html

相关文章

  • vue 源码学习笔记三 vue中如何生成虚拟DOM

    摘要:调用了方法,参数是拿到后,判断类型是否为,如果有多个,则是模板上有多个根节点,触发告警。 vm._render 生成虚拟dom 我们知道在挂载过程中, $mount 会调用 vm._update和vm._render 方法,vm._updata是负责把VNode渲染成真正的DOM,vm._render方法是用来把实例渲染成VNode,这里的_render是实例的私有方法,和前面我们说...

    VioletJack 评论0 收藏0
  • Vue2.5笔记:Vue中的模版

    摘要:模版语法中的模版是基于的模版语法,所有的模版都是合法的,所以能被遵循规范的浏览器和解析器解析。表达式模版中不仅仅可以进行简单的数据绑定操作,我们还可以在模版中进行简单的表达式。我们也简单的叙述了模版编译的整个流程。 我们在上一篇说到如何把 Vue 实例中的数据显示到视图中,就会需要用到我们的模版,我们只是简单的使用了一些,模版其实还有很多其他的特性。今天我们就来看看模版的其他特性。 模...

    shevy 评论0 收藏0
  • 2017-08-23 前端日报

    摘要:前端日报精选免费的计算机编程类中文书籍英文技术文档看不懂看印记中文就够了的内部工作原理美团点评点餐前后端分离实践让你的动画坐上时光机中文译有多棒简书译别再使用图片轮播了掘金译如何在中使用掘金个让增长成亿美元公司的独特方法众成翻 2017-08-23 前端日报 精选 FPB 2.0:免费的计算机编程类中文书籍 2.0英文技术文档看不懂?看印记中文就够了!Virtual DOM 的内部工作...

    lordharrd 评论0 收藏0
  • 当我们谈论Virtual DOM时,我们在说什么——etch源码解读

    摘要:接下来我们深入函数,看看它干了什么。在我们写的代码里,我们会手动将元素挂载到树上。到这里,我们已经完成了元素挂载的全过程,接下来我们看一看更新的时候会发生什么。这部分应该是负责的,我们要在组件的方法中调用。 etch简介 首先我们有必要介绍一下etch。 etch是atom团队下的开源项目,是一套非常简洁然而功能十分完善的virtualDOM机制。我在偶然的情况下接触到了这个开源项...

    RayKr 评论0 收藏0
  • Vue.js 源码学习笔记

    摘要:实际上,我在看代码的过程中顺手提交了这个,作者眼明手快,当天就进行了修复,现在最新的代码里已经不是这个样子了而且状态机标识由字符串换成了数字常量,解析更准确的同时执行效率也会更高。 最近饶有兴致的又把最新版 Vue.js 的源码学习了一下,觉得真心不错,个人觉得 Vue.js 的代码非常之优雅而且精辟,作者本身可能无 (bu) 意 (xie) 提及这些。那么,就让我来吧:) 程序结构梳...

    darkbaby123 评论0 收藏0

发表评论

0条评论

DobbyKim

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<