通俗易懂的vue虚拟(Virtual )DOM和diff算法

最近在看一些底层方面的知识。因此想作个系列尝试去聊聊这些比较复杂又很重要的知识点。学习就比如是座大山,只有本身去爬山,才能看到不同的风景,体会更加深入。今天咱们就来聊聊Vue中比较重要的vue虚拟(Virtual )DOM和diff算法。vue

虚拟(Virtual )DOM

Virtual DOM 其实就是一棵以 JavaScript 对象(VNode 节点)做为基础的树,用对象属性来描述节点,至关于在js和真实dom中间加来一个缓存,利用dom diff算法避免没有必要的dom操做,从而提升性能。固然算法有时并非最优解,由于它须要兼容不少实际中可能发生的状况,好比后续会讲到两个节点的dom树移动。node

上几篇文章中讲vue的数据状态管理结合Virtual DOM更容易理解,在vue中通常都是经过修改元素的state,订阅者根据state的变化进行编译渲染,底层的实现能够简单理解为三个步骤:react

  • 一、用JavaScript对象结构表述dom树的结构,而后用这个树构建一个真正的dom树,插到浏览器的页面中。
  • 二、当状态改变了,也就是咱们的state作出修改,vue便会从新构造一棵树的对象树,而后用这个新构建出来的树和旧树进行对比(只进行同层对比),记录两棵树之间的差别。
  • 三、把2记录的差别在从新应用到步骤1所构建的真正的dom树,视图就更新了。

举例子:有一个 ul>li 列表,在template中的写法是:git

<ul id='list'>
  <li class='item1'>Item 1</li>
  <li class='item2'>Item 2</li>
  <li class='item3' style='font-size: 20px'>Item 3</li>
</ul>
复制代码

vue首先会将template进行编译,这其中包括parse、optimize、generate三个过程。github

parse会使用正则等方式解析template模版中的指令、class、style等数据,造成AST,因而咱们的ul> li 可能被解析成下面这样算法

// js模拟DOM结构
var element = {
  tagName: 'ul', // 节点标签名
  props: { // DOM的属性,用一个对象存储键值对
    class: 'item',
    id: 'list'
  },
  children: [ // 该节点的子节点
    {tagName: 'li', props: {class: 'item1'}, children: "Item 1"},
    {tagName: 'li', props: {class: 'item2'}, children: "Item 2"},
    {tagName: 'li', props: {class: 'item3', style: 'font-size: 20px'}, children: "Item 3"},
  ]
}
复制代码

optimize过程其实只是为了优化后文diff算法的,若是不加这个过程,那么每层的节点都须要作比对,即便没变的部分也得弄一遍,这也违背了Virtual DOM 最初本质,形成没必要要的资源计算和浪费。所以在编译的过程当中vue会主动标记static静态节点,我的理解为就是页面一些不变的或者不受state影响的节点。好比咱们的ul节点,不论li如何变化ul始终是不会变的,所以在这个编译的过程当中能够个ul打上一个标签。当后续update更新视图界面时,patch过程看到这个标签会直接跳过这些静态节点。数组

最后经过generate 将 AST 转化成 render function 字符串,获得结果是 render 的字符串以及 staticRenderFns 字符串。你们听起来可能很困惑,首先前两步你们应该都差很少知道了,当拿到一个AST时,vue内部有一个叫element ASTs的代码生成器,犹如名字同样generate函数拿到解析好的AST对象,递归AST树,为不一样的AST节点建立不一样的内部调用的方法,而后组合可执行的JavaScript字符串,等待后面的调用。最后可能会变成这个样子:浏览器

function _render() {
  with (this) { 
    return __h__(
      'ul', 
      {staticClass: "list"}, 
      [
        " ",
        __h__('li', {class: item}, [String((msg))]),
        " ",
        __h__('li', {class: item}, [String((msg))]),
        "",
        __h__('li', {class: item}, [String((msg))]),
        ""
      ]
    )
  };
}
复制代码

整个Virtual DOM生成的过程代码中可简化为以下,有兴趣的同窗能够去看具体对应的Vue源码,源码位置在src/compiler/index.js缓存

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 1.parse,模板字符串 转换成 抽象语法树(AST)
  const ast = parse(template.trim(), options)
  // 2.optimize,对 AST 进行静态节点标记
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  // 3.generate,抽象语法树(AST) 生成 render函数代码字符串
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
复制代码

diff算法以及key的做用

在最初的diff算法实际上是"不可用的",由于时间复杂度是O(n^3)。假设一个dom树有1000个节点,第一遍须要遍历tree1,第二遍遍历tree2,最后一遍就是排序组合成新树。所以这1000个节点须要计算1000^3 = 1亿次,这是很是庞大的计算,这种算法基本也不会用。bash

后面设计者们想出了一些方法,将时间复杂度由O(n^3)变成了O(n),那么这些设计者是若是实现的?这也就是diff算法的优点所在,也是日常咱们所理解到一些知识:

  • 一、只比较同一级,不跨级比较
  • 二、tag不相同,直接删掉重建,再也不深度比较
  • 三、tag和key,二者都相同,则认为是相同节点,不在深度比较

这就是一个简单的diff。经过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,因此时间复杂度只有 O(n)。

diff

以前在Virtual DOM中讲到当状态改变了,vue便会从新构造一棵树的对象树,而后用这个新构建出来的树和旧树进行对比。这个过程就是patch。比对得出「差别」,最终将这些「差别」更新到视图上。patch的过程也是vue及react的核心算法,理解起来比较困难。先看一些简单的图形了解diff是如何比较新旧VNode的差别的。

  • 场景1:更新删除移动

    diff移动
    移动的场景在diff中应该是最基础的。要达到这样的效果。咱们能够将b移动到同层的最后面或者把c移动到B前面再把D也移动到B前面,固然这是在引入了key的比对结果。若是没有key的话只会依次相互比较,将b ==> c、 c==> d、 d ==> b。而后在第三层中因为新建的c没有e、f所以会去新建e、f。为了让e、f获得复用,设key后,会从用key生成的对象oldKeyToIdx中查找匹配的节点。让算法知道不是删除节点而是移动节点,这就是有key和无key的做用。在数组中插入新节点也是一样的道理。

  • 场景2:删除新建

    diff删除新建
    咱们可能指望将C直接移动到B的后边,这是最优的操做。可是实际的diff操做是移除c在建立一个c插入到b的下面,这就是同层比较的结果。若是在一些必要时能够手工优化,例如在react的shouldComponentUpdate生命周期中就拦截了子组件的渲染进行优化。

在简单的理解了diff算法实际操做的过程。为了让你们更好的掌握,由于这块仍是比较复杂的。接下来将用伪代码的形式分析diff算法是如何进行深度优先遍历,记录差别, Vue的VDOM的diff算法借鉴的是snabbdom,不妨先从snabbdom Example入手

在vue中首先会对新旧两棵树进行深度优先的遍历,这样每一个节点都会有一个惟一的标记。在遍历的同时,每遍历一个节点就会把该节点和新的树进行对比,有差别的话就会记录到一个对象里。

/* 建立diff函数,接受新旧量两棵参数 */
function diff (oldTree, newTree) {
  var index = 0 //当前节点的标志
  var patches = {}  //用来记录每一个节点差别的对象
  dfsWalk(oldTree, newTree, index, patches) // 对两棵树进行深度优先遍历
  return patches //返回不一样的记录
}

function dfsWalk (oldNode, newNode, index, patches) {
  var currentPatch = []  // 定义一个数组将对比oldNode和newNode的不一样,记录下来
  if (newNode === null) {
    // 当执行从新排序时,真正的DOM节点将被删除,所以不须要在这里进行调整
  } else if (_.isString(oldNode) && _.isString(newNode)) {
    // 判断oldNode、newNode是不是字符串对象或者字符串值
    if (newNode !== oldNode) {
        //节点不一样直接放入到数组中
        currentPatch.push({ type: patch.TEXT, content: newNode })
    }
  } else if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
    // 节点是相同的,diff区分旧节点的props和子节点 
    
    // diff处理props
    var propsPatches = diffProps(oldNode, newNode)
    if (propsPatches) {
      currentPatch.push({ type: patch.PROPS, props: propsPatches })
    }
    
    // diff处理子节点,若是有‘ignore’这个标志的。diff就忽视这个子节点
    if (!isIgnoreChildren(newNode)) {
      diffChildren(
        oldNode.children,
        newNode.children,
        index,
        patches,
        currentPatch
      )
    }
  } else {
    // 节点不相同,用新节点直接替换旧节点
     currentPatch.push({ type: patch.REPLACE, node: newNode })
  }
}
 function isIgnoreChildren (node) {
  return (node.props && node.props.hasOwnProperty('ignore'))
}

/* 处理子节点diffChildren函数 */
function diffChildren (oldChildren, newChildren, index, patches, currentPatch) {
  var diffs = listDiff(oldChildren, newChildren, 'key')
  newChildren = diffs.children

  if (diffs.moves.length) {
    var reorderPatch = { type: patch.REORDER, moves: diffs.moves }
    currentPatch.push(reorderPatch)
  }

 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
  })
}
/* 处理子节点的props diffProps函数 */
function diffProps (oldNode, newNode) {
  var count = 0
  var oldProps = oldNode.props
  var newProps = newNode.props
  var key, value
  var propsPatches = {}
  // Find out different properties
  for (key in oldProps) {
    value = oldProps[key]
    if (newProps[key] !== value) {
      count++
      propsPatches[key] = newProps[key]
    }
  }
  // Find out new property
  for (key in newProps) {
    value = newProps[key]
    if (!oldProps.hasOwnProperty(key)) {
      count++
      propsPatches[key] = newProps[key]
    }
  }
  // If properties all are identical
  if (count === 0) {
    return null
  }
  return propsPatches
}
// 暴露diff函数
module.exports = diff

复制代码

感兴趣的话你也可查看简化版的diff。 完整简化版的diff算法

相关文章
相关标签/搜索