Vue Diff 算法详解

前言

本文比较长,主要篇幅都是在介绍 diff 算法,在了解 diff 算法之前,有必要了解一下 Vue 为什么需要 diff 算法,以及 Vue 在什么时刻会使用到 diff 算法,我们先来看看这两个问题。

Vue 为什么需要 diff 算法?

diff 算法是 虚拟 DOM 的必然产物,通过对新旧虚拟 DOM 的比对,将变化的地方更新到真实 DOM 上,可以尽可能减少不必要的 DOM 操作,提升性能。

前文 介绍响应式原理说到,一个组件对应一个 Watcher 实例,当状态被修改时,相关的 Watcher 实例会被通知更新,此时对应的组件都会重新 render 并生成新的 vnode,一个组件有大有小,总会有不需要更新的地方,若整个组件都进行 DOM 更新,对性能影响将是极大的,因此需要使用 diff 算法,通过新旧 vnode 比对,从而找到实际需要更新的结点,再进行更新,这个过程称为 patch

在 Vue 内部,组件的挂载、更新、移除都是经过这个 patch 的阶段,介绍 patch 之前先来了解一下 vue 会在什么时刻执行 patch。

执行 patch 的时刻

Vue.prototype.$mount

执行 new Vue 时传入 el 参数,内部会调用 $mount,或直接通过 new Vue().$mount('#app') 挂载组件,Vue.prototype.$mount() 实际上是调用了 mountComponent

// core/stance/init.js
Vue.prototype._init = function (options) {
  // ...
  const vm = this;
  if (vm.$options.el) {
    vm.$mount(vm.$options.el);
  }
}

// platforms/web/runtime/index.js
Vue.prototype.$mount = function (el, hydrating) {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
复制代码

mountComponent

从函数名字就知道这个函数是用于挂载组件的,执行这个函数时做了以下几件事

  1. 调用生命周期函数 beforeMount
  2. 定义一个函数 updateComponent,这个函数内部会执行 vm._update,而 vm._update 的参数是 vm._render() 执行后的结果,即生成新的 vnode
  3. 创建一个 Watcher 实例,并将updateComponent 作为参数,在组件挂载和更新时会调用这个函数

下面是 mountComponentWatcher 涉及到的主要代码

function mountComponent (vm, el) {
  callHook(vm, 'beforeMount')
  
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
}

// src/core/observer/watcher.js
export default class Watcher {
  constructor(vm, expOrFn, cb, options, isRenderWatcher) {
    this.getter = expOrFn  // updateComponent 作为 expOrFn 参数保存在 getter 属性
    this.value = this.lazy ? undefined : this.get() // 挂载时会调用一次
  }
  get () {
    // ...
    value = this.getter.call(vm, vm)
    // ...
  }
  // 通知更新,会调用 getter
  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this) // 异步队列更新的方式,最后也会调用到 run,此处不再赘述
    }
  }
  run () {
    // ...
    const value = this.get()
  }
}
复制代码

vm._update

现在我们知道组件挂载和更新都会调用 _update 方法,它接收重新 render 生成的 vnode 作为参数,在内部可以获取到当前的 vnode (preVnode),然后分下面两种情况进行处理

  • 没有 preVnode:当前没有旧 vnode,是挂载阶段,调用 __patch__ 传入 DOM 元素 $el 和新的 vnode
  • 有 preVnode:代表当前是更新阶段,调用 __patch__ 传入 preVnode 和新的 vnode

两种情况都调用同一个函数,只是传入的参数不一样,因此在 __patch__ 内部还需要判断分情况处理

Vue.prototype._update = function (vnode, hydrating) {
  var vm = this;
  var prevEl = vm.$el;
  var prevVnode = vm._vnode;
  if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode);
  }
}
复制代码

vm.__patch__

vm.__patch__ 也是原型对象上的方法,定义如下

在浏览器才需要 patch,服务端渲染没有 DOM 操作和挂载的概念,不需要 patch

// src/platforms/web/runtime/index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop
复制代码

继续查找,patch 函数是调用 createPatchFunction 返回的

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'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules })
复制代码

createPatchFunction 接收两个参数,这里大概了解一下分别是什么

  • nodeOps 是web平台下一些 DOM API 的封装,在 patch 阶段,自然少不了要操作DOM
  • modules 是一些核心的模块,如 attr, class, style, event, directive, ref 等,这些模块对外暴露 create, update, remove, destory 等API,在 patch 阶段会被调用

createPatchFunction 代码很多,声明了很多函数,在 patch 的各个阶段方便调用,可见 patch 逻辑是很复杂的,但是这里我们只关注 diff 算法的实现,其他就不过多介绍了,下面来看 patch 函数的定义

export function createPatchFunction (backend) {
  return function patch (oldVnode, vnode, hydrating, removeOnly) { }
}
复制代码

patch 接收 oldVnodevnode,然后根据两个参数的值分不同的情况进行处理,终于要探究 patch 的实现原理了,接下来就来逐步分析代码,了解 vue 如何实现 diff 算法。

patch 阶段工作原理

patch 方法的代码很多,但基本上只是条件判断,再执行不同的操作,diff 算法的核心代码主要是当新旧 vnode 是对应相同元素时,继续比较它们的子结点及如何实现高效的对比,因此这里只需了解一下大概的流程即可,下面是 patch 方法执行的流程图

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享