Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
yangrenmu opened this issue Nov 25, 2019 · 0 comments
Open

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

yangrenmu opened this issue Nov 25, 2019 · 0 comments
Labels

Comments

@yangrenmu
Copy link
Owner

前言

我们看源码,我觉得最好带着问题去看源码,这样我们会专注于一个点去看源码,不会被源码的一些其他功能,把我们带离最初想去的地方。本章主要的目的是,弄明白 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 实例挂载的实现

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant