vue源码阅读二:虚拟 DOM 是如何生成的?(上)

921 阅读2分钟

前言

我们看源码,我觉得最好带着问题去看源码,这样我们会专注于一个点去看源码,不会被源码的一些其他功能,把我们带离最初想去的地方。本章主要的目的是,弄明白 vue 是如何生成虚拟 DOM 的。

从入口开始

我们从入口文件一步一步慢慢的分析。先看入口文件。

  • 入口文件:web/entry-runtime-with-compiler.js

import Vue from './runtime/index'
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element, // 根元素,可以是字符串或者是 DOM 元素
  hydrating?: boolean // 服务端渲染相关,服务端渲染时为 true
): Component {
  ...
  const options = this.$options
  // 没有手写 render 方法时,获取 template(template 会被编译成 render 方法)
  if (!options.render) {
    // 先获取模板
    let template = options.template
    ...
    if (template) {
      ...
      // render 方法会生成 vnode, template => render 方法 => vnode
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      // 将 render 方法添加到 this.$options 上
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  // 调用保存的 mount 函数,此时已获得由模板编译过来的 render 函数
  return mount.call(this, el, hydrating)
}
  • runtime 文件:./runtime/index

import Vue from 'core/index'
import { mountComponent } from 'core/instance/lifecycle'
...
// 在原型上,添加 $mount 方法,这个方法会返回 mountComponent 方法。
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
...
  • core 文件: core/index

import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
...
initGlobalAPI(Vue) // 初始化全局 API,如 nextTick
...
  • instance 文件:./instance/index

// vue 的构造函数,代码模块化,方便维护
...
function Vue(options) {
  ... 
  this._init(options)
}

// 在 vue prototype 上添加方法

initMixin(Vue) // _init 方法等
stateMixin(Vue) // 数据相关,如 $watch 方法等
eventsMixin(Vue) // 事件相关,如 $emit 方法等
lifecycleMixin(Vue) // _update 方法等
renderMixin(Vue) // _render 方法等

new Vue 时发生什么

由上面的代码,我们可以看到,当我们new Vue() 时,会触发一系列的初始化,然后调用 _init() 方法。

生成虚拟 DOM

  • _init()

既然 new Vue() 时,调用的是 _init() 方法 ,我们就先看看 _init() 方法主要做了什么事情。它是在 initMixin() 函数执行时,添加到原型上的方法。在 init 方法的最后,我们看到如下代码。它调用了vm.$mount方法。

Vue.prototype._init = function (options?: Object) {
  ...
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}
  • $mount 方法

在上面的分析中,我们知道 Vue 中有两个$mount 方法。定义如下:

// 第一个 $mount 方法
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
// 第二个 $mount 方法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element, // 根元素,可以是字符串或者是 DOM 元素
  hydrating?: boolean // 服务端渲染相关,服务端渲染时为 true
): Component {
  ...
  if (!options.render) {
    ...
    if (template) {
      ...
      // render 方法会生成 vnode, template => render 方法 => vnode
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      // 将 render 方法添加到 this.$options 上
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  // 调用保存的 mount 函数,此时已获得由模板编译过来的 render 函数
  return mount.call(this, el, hydrating)
}

我们可以看到:

  • 第一个 $mount 方法:是给第二个 $mount 方法调用用的,它会返回mountComponent方法。

  • 第二个 $mount 方法:将 template 编译成 render 方法,保存 render 方法到 $options 上。最后调用第一个 $mount 方法。

接下来,我们看下mountComponent方法。

  • mountComponent 方法

mountComponent 方法的主要代码如下:

// mountComponent
export function mountComponent(
  vm: Component, // 组件实例
  el: ?Element, // 挂载的元素
  hydrating?: boolean // 服务端渲染相关
): Component {
  ...
  updateComponent = () => {
    // vm._render(),生成 vnode,在 instance/render.js 中
    // vm._update(),更新 dom
    vm._update(vm._render(), hydrating)
  }
  // watcher 会调用 updateComponent,先生成 vnode ,然后调用 update 更新 dom;
  new Watcher(vm, updateComponent, noop, {
    before() { ... }
  }, true /* isRenderWatcher */)
  return vm
}

// watcher
export default class Watcher {
  constructor(
    vm: Component, // 组件实例
    expOrFn: string | Function, 
    cb: Function, // 当监听的数据变化时,会触发该回调
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    ...
    // expOrFn 是 `updateComponent` 方法
    this.getter = expOrFn
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  get() {
    ...
    try {
      // 相当于执行 updateComponent()
      value = this.getter.call(vm, vm)
    } catch (e) {
    ... 
  }
}
  • mountComponent 方法的作用是实例化一个 Watcher,同时也创建了一个 updateComponent方法。
  • Watcher :作用是监听数据的变化,实例化时会执行 updateComponent方法。它会执行下面这行代码。
vm._update(vm._render(), hydrating)
  • vm._render()

这个才是正主。前期的准备工作已做完,下面到了生成虚拟DOM的时候了。

Vue.prototype._render = function (): VNode {
  ...
  // render 方法是由模板编译过来的
  const { render, _parentVnode } = vm.$options
  // render 生成 vnode 
  vnode = render.call(vm._renderProxy, vm.$createElement)
  ...
}

可以看到,vue 会调用由 template 编译过来的 render 方法生成 虚拟 DOM。需要注意的是:

  • 当使用编译而来的 render 方法时,执行的是下面的 createElement 生成虚拟 DOM
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  • 当使用手写的 render 方法时,执行的是下面的 createElement 生成虚拟 DOM
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

然后 vue 具体是如何生成虚拟 DOM 的呢,且听下回分解。

总结

  • new Vue() 时,会合并相关的配置项、执行一系列的初始化、以及调用 _init()
  • _init() 方法中,会执行 vm.$mount(vm.$options.el)
  • $mount 方法有两个,第一个返回mountComponent方法。第二个 $mount 方法是将模板编译成 render 方法,然后调用第一个 mount 方法。
  • mountComponent 方法中,实例化 Watcher,在此过程中,会执行 updateComponent 方法,相当于调用 vm._update(vm._render(), hydrating)
  • 在执行 vm._render() 时,会调用由模板编译而来的 render 方法,最后生成了 虚拟 DOM。

参考

Vue原理解析(四):你知道被大家聊烂了的虚拟Dom是怎么生成的吗?
Vue 实例挂载的实现