Vue中的diff算法深度解析

模板tamplate经过parseoptimizegenerate等一些列操作之后,把AST转为render function code进而生成虚拟VNode,模板编译阶段基本已经完成了,那么这一章,我们来探讨一下Vue中的一个算法策略–dom diff 首先来介绍下什么叫dom diff

什么是虚拟dom

我们经过前面的章节学习已经知道,要知道渲染真实DOM的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染到真实dom上会引起整个dom树重绘重排,有没有可能我们只更新我们修改的那一小块dom而不要更新整个dom呢?

为了解决这个问题,我们的解决方案是–根据真实DOM生成一颗virtual DOM,当virtual DOM某个节点的数据改变后会生成一个新的Vnode,然后Vnode和oldVnode作对比,发现有不一样的地方就直接修改在真实的DOM上,然后使oldVnode的值为Vnode。这也就是我们所说的一个虚拟dom diff的过程

图示

在这里插入图片描述

传统的Diff算法所耗费的时间复杂度为O(n^3),那么这个O(n^3)是怎么算出来的?

  1. 传统diff算法时间复杂度为n(第一次Old与新的所有节点对比)----O(n)
  2. 传统diff算法时间复杂度为n(第二次Old树的所有节点与新的所有节点对比)----O(n^2)
  3. 新树的生成,节点可变编辑,时间复杂度为n(遍历当前树)----O(n^3)

第一次对比 (1:n)

在这里插入图片描述

第二次对比 (1:n)

在这里插入图片描述

第n次对比 (n:n)

在这里插入图片描述

到这里那么n个节点与n个节点暴力对比就对比完了,那么就开启第三轮可编辑树节点遍历,更改之后的树由vdom(old)vdom(new)

在这里插入图片描述

故而传统diff算法O(n^3)是这么算出来的,但是这不是我们今天研究的重点。

现代diff算法

现代diff算法策略说的是,同层级比较,广度优先

在这里插入图片描述

那么这里的话我们要深入源码了,在深入源码之前我们在心中应该形成这样一个概念,整个diff的流程是什么?我们再对比着源码解读

diff算法流程图

在这里插入图片描述

深入源码

我们在Vue初始化的时候调用lifecycleMixin函数的时候,会给Vue的原型上挂载_update方法

_update

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
   
    const vm: Component = this
    if (vm._isMounted) {
   
      //会调用声明周期中的beforeUpdate回调函数
      callHook(vm, 'beforeUpdate')
    }
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = vm
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    //若组件本身的vnode未生成,直接用传入的vnode生成dom
    if (!prevVnode) {
   
      // initial render
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      )
      // no need for the ref nodes after initial patch
      // this prevents keeping a detached DOM tree in memory (#5851)
      vm.$options._parentElm = vm.$options._refElm = null
    } else {
   
      //对新旧vnode进行diff
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    // update __vue__ reference
    if (prevEl) {
   
      prevEl.__vue__ = null
    }
    if (vm.$el) {
   
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
   
      vm.$parent.$el = vm.$el
    }

我们在这里可以看到vm.$el = vm.__patch__方法,追根溯源_patch_的定义:

Vue.prototype.__patch__ = inBrowser ? patch : noop

可见这里是一个浏览器环境的鉴别,如果在浏览器环境中,我们会执行patch,不在的话会执行noop,这是一个util里面的一个方法,用来跨平台的,我们这里暂时不考虑,接着我们去看patch的具体实现./patch文件,参考vue实战视频讲解:进入学习

import * as nodeOps from 'web/runtime/node-ops'
import {
    createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({
    nodeOps, modules })

createPatchFunction函数

/** * 创建patch方法 */
export function createPatchFunction (backend) {
   
  let i, j
  const cbs = {
   }

  const {
    modules, nodeOps } = backend

  for (i = 0; i < hooks.length; ++i) {
   
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
   
      if (isDef(modules[j][hooks[i]])) {
   
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }

  function emptyNodeAt (elm) {
   
    return new VNode(nodeOps.tagName(elm).toLowerCase(), {
   }, [], undefined, elm)
  }

  /**   * 创建一个回调方法, 用于删除节点   *    *    */
  function createRmCb (childElm, listeners) {
   
    function remove () {
   
      if (--remove.listeners === 0) {
   
        removeNode(childElm)
      }
    }
    remove.listeners = listeners
    return remove
  }

  function removeNode (el) {
   
    const parent = nodeOps.parentNode(el)
    // element may have already been removed due to v-html / v-text
    if (isDef(parent)) {
   
      nodeOps.removeChild(parent, el)
    }
  }

  /**   * 通过vnode的tag判断是否是原生dom标签或者组件标签   * 用于创建真实DOM节点时, 预先判断tag的合法性   */
  function isUnknownElement (vnode, inVPre) {
   
    return (
      !inVPre &&
      !vnode.ns &&
      !(
        config.ignoredElements.length &&
        config.ignoredElements.some(ignore => {
   
          return isRegExp(ignore)
            ? ignore.test(vnode.tag)
            : ignore === vnode.tag
        })
      ) &&
      config.isUnknownElement(vnode.tag)
    )
  }

  let creatingElmInVPre = 0

  // 创建一个节点
  function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
   
    // 节点已经被渲染, 需要使用一个克隆节点
    if (isDef(vnode.elm) && isDef(ownerArray)) {
   
      // This vnode was used in a previous render!
      // now it's used as a new node, overwriting its elm would cause
      // potential patch errors down the road when it's used as an insertion
      // reference node. Instead, we clone the node on-demand before creating
      // associated DOM element for it.
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    // 创建组件节点 详见本文件中的createComponent方法
    vnode.isRootInsert = !nested // for transition enter check
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
   
      return
    }

    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    /**     * 如果要创建的节点有tag属性, 这里做一下校验     * 如果该节点上面有v-pre指令, 直接给flag加1     * 如果没有v-pre需要调用isUnknownElement判断标签是否合法, 然后给出警告     */
    if (isDef(tag)) {
   
      if (process.env.NODE_ENV !== 'production') {
   
        if (data && data.pre) {
   
          creatingElmInVPre++
        }
        if (isUnknownElement(vnode, creatingElmInVPre)) {
   
          warn(
            'Unknown custom element: <' + tag + '> - did you ' +
            'register the component correctly? For recursive components, ' +
            'make sure to provide the "name" option.',
            vnode.context
          )
        }
      }

      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)

      /* istanbul ignore if */
      if (__WEEX__) {
   
        // in Weex, the default insertion order is parent-first.
        // List items can be optimized to use children-first insertion
        // with append="tree".
        const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
        if (!appendAsTree) {
   
          if (isDef(data)) {
   
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
        createChildren(vnode, children, insertedVnodeQueue)
        if (appendAsTree) {
   
          if (isDef(data)) {
   
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
      } else {
   
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
   
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }

      if (process.env.NODE_ENV !== 'production' && data && data.pre) {
   
        creatingElmInVPre--
      }
    } else if (isTrue(vnode.isComment)) {
   
      vnode.elm = nodeOps.createComment(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    } else {
   
      vnode.elm = nodeOps.createTextNode(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    }
  }
  /**   * 创建组件   * 如果组件实例已经存在, 只需要初始化组件并重新激活组件即可   */
  function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
   
    let i = vnode.data
    if (isDef(i)) {
   
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      if (isDef(i = i.hook) && isDef(i = i.init)) {
   
        i(vnode, false /* hydrating */, parentElm, refElm)
      }
      // after calling the init hook, if the vnode is a child component
      // it should've created a child instance and mounted it. the child
      // component also has set the placeholder vnode's elm.
      // in that case we can just return the element and be done.
      if (isDef(vnode.componentInstance)) {
   
        initComponent(vnode, insertedVnodeQueue)
        if (isTrue(isReactivated)) {
   
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }

  /**   * 初始化组件   * 主要的操作是已插入的vnode队列, 触发create钩子, 设置style的scope, 注册ref   */
  function initComponent (vnode, insertedVnodeQueue) {
   
    if (isDef(vnode.data.pendingInsert)) {
   
      insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
      vnode.data.pendingInsert = null
    }
    vnode.elm = vnode.componentInstance.$el
    if (isPatchable(vnode)) {
   
      invokeCreateHooks(vnode, insertedVnodeQueue)
      setScope(vnode)
    } else {
   
      // empty component root.
      // skip all element-related modules except for ref (#3455)
      registerRef(vnode)
      // make sure to invoke the insert hook
      insertedVnodeQueue.push(vnode)
    }
  }

  /**   * 激活组件   */
  function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
   
    let i
    // hack for #4339: a reactivated component with inner transition
    // does not trigger because the inner node's created hooks are not called
    // again. It's not ideal to involve module-specific logic in here but
    // there doesn't seem to be a better way to do it.
    let innerNode = vnode
    while (innerNode.componentInstance) {
   
      innerNode = innerNode.componentInstance._vnode
      if (isDef(i = innerNode.data) && isDef(i = i.transition)) {
   
        for (i = 0; i < cbs.activate.length; ++i) {
   
          cbs.activate[i](emptyNode, innerNode)
        }
        insertedVnodeQueue.push(innerNode)
        break
      }
    }
    // unlike a newly created component,
    // a reactivated keep-alive component doesn't insert itself
    insert(parentElm, vnode.elm, refElm)
  }

  /**   * 插入节点, 有父节点的插入到前面, 没有的插入到后面   */
  function insert (parent, elm, ref) {
   
    if (isDef(parent)) {
   
      if

你可能感兴趣的:(vue.js)