Vue 组件 mount

9/17/2021 Vue


# $mount 实现

Vue 实例化的最后一步就是 Vue 实例的挂载,这里有一个判断:vm.$options.el 这个选项存在的时候 就会去执行 $mount 的操作。

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}
1
2
3

还有一种场景,那就是组件的挂载过程,组件的构造不用传递 el 属性,所以不会走到这里的逻辑。

const child = (vnode.componentInstance = createComponentInstanceForVnode(
  vnode,
  activeInstance
))
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
1
2
3
4
5

$mount 这个方法在 web 平台,针对不同的场景有不同的实现,

# runtime

runtime 版本的 Vue,这个方法的实现比较简单,内部调用了一个 mountComponent 的方法,并把 当前的 组件实例(this)要挂载的 dom 元素(el)传入了进去。参数 hydrating 是用来判断是否是服务端渲染的,这个做 ssr 的时候会用到。

Vue.prototype.$mount = function (el, hydrating): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
1
2
3
4

# runtime + compiler

而加了 编译器版本 的 Vue,这个方法的实现就相对复杂了一些。首先采用了一种面向切面编程(AOP)的思想,首先缓存了原始的函数实现,然后在重写的方法中加入针对这种场景的处理逻辑后,最终又去调用了原始的实现。

此外这里有几个判断逻辑需要关注下:

  • 首先是判断了要挂载的元素是否是 body 或者 html,vue 不允许这样做,因为最终渲染的结果会替换掉被挂载的元素,而不是插入到被挂载节点的内部

  • 然后就是,如果在实例化的时候,同时传入了 template 以及 render 函数,这两个的优先级 render > template

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el, hydrating): Component {
  el = el && query(el)

  // 不能挂载到 body 或者 html 元素上
  if (el === document.body || el === document.documentElement) return this

  const options = this.$options
  // 首先判断了组件的选项对象上是否定义了 render 方法
  if (!options.render) {
    let template = options.template
    // 如果没有 render 方法,就看下是否定义了 template 属性
    if (template) {
      // template 是字符串模板
      if (typeof template === 'string') {
        // 选择符匹配元素的 innerHTML 模板
        if (template.charAt(0) === '#') {
          // 则当做 id 选择器,获取元素
          template = idToTemplate(template)
        }
      } else if (template.nodeType) {
        // template 为 dom 元素匹配元素的 innerHTML 模板
        template = template.innerHTML
      } else {
        return this
      }
    } else if (el) {
      // 如果没有定义 template,就去拿 el 节点的HTML
      template = getOuterHTML(el)
    }

    // 如果得到了 template, 就转换成 render 函数
    if (template) {
      const { render, staticRenderFns } = compileToFunctions(
        template,
        {
          outputSourceRange: process.env.NODE_ENV !== 'production',
          shouldDecodeNewlines,
          shouldDecodeNewlinesForHref,
          delimiters: options.delimiters,
          comments: options.comments,
        },
        this
      )
      // 将 render 函数挂到了 options 上,接下来 mount 的流程中会用到它
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  // 最后调用原始的 mount 函数进行组件挂载
  return mount.call(this, el, hydrating)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

从这两种实现上可以看出,

  • compiler版本的 vue,模板的编译操作是在执行 $mount 方法时,通过 compileToFunctions 方法实现的,也就是所说的在运行时进行的编译

  • runtime版本的 vue,则是借助于像 webpack 这样的构建工具加上 vue-loader 等相关插件,在打包构建时将 template 编译成 render 函数的

具体的,在 vue-loader 的实现中,该部分的功能其实是借助的 vue 源码中的 vue-template-compiler 包进行处理的,这个包是维护在源码的 packages 目录下。他的版本是跟 vue 的版本一致的,这也是为什么官方要求 编译器的版本必须和基本的 vue 包保持同步 的原因,因为这样才能保证组件的 template 能够被编译成正确的 render 函数。要不然假如某一天 vue 支持了一个新的模板语法,而编译器还用的老版本,这就意味着新的语法无法被正确编译成 render 函数,从而导致错误

# template 的三种形式

  • 字符串模板
var vm = new Vue({
  el: '#app',
  template: '<div>模板字符串</div>',
})
1
2
3
4
  • dom 元素匹配元素的 innerHTML 模板
<div id="app">
  <div>test1</div>
  <span id="test"><div class="test2">test2</div></span>
</div>
<script>
  var vm = new Vue({
    el: '#app',
    template: document.querySelector('#test'),
  })
</script>
1
2
3
4
5
6
7
8
9
10
  • 选择符匹配元素的 innerHTML 模板
<div id="app">
  <div>test1</div>
  <script type="x-template" id="test">
    <p>test</p>
  </script>
</div>
<script>
  var vm = new Vue({
    el: '#app',
    template: '#test',
  })
</script>
1
2
3
4
5
6
7
8
9
10
11
12

# mountComponent 过程

两种版本的 vue,最终的 mount 逻辑中都是走到了 mountComponent 这个方法

export function mountComponent(vm, el, hydrating): Component {
  vm.$el = el
  // 1. 这里又校验了 render 方法是不是存在,不存在的话给默认值兜底,开发环境下给警告
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    // ...
  }

  // 2. beforeMount 钩子触发
  callHook(vm, 'beforeMount')

  let updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }

  // 3. 渲染 watcher
  new Watcher(
    vm,
    updateComponent,
    noop,
    {
      before() {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, 'beforeUpdate')
        }
      },
    },
    true /* isRenderWatcher */
  )
  hydrating = false

  if (vm.$vnode == null) {
    // 4. 挂载完成,添加标记并触发 mounted 钩子
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

这里主要分四步,

  • 对 render 方法做兜底处理
  • 调用 beforeMount 钩子
  • 创建 渲染 Watcher 实例
  • 组件挂载完成,添加挂载完成标记并触发 mounted 钩子

其中最重要的一步,就是在创建渲染 Watcher 实例这一步

# 渲染 Watcher

这部分代码单独拿出来看下

let updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

new Watcher(
  vm,
  updateComponent,
  noop,
  {
    before() {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    },
  },
  true /* isRenderWatcher */
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

先解释下,这里的渲染 Watcher 具体做了哪些事情,然后再去看实现细节。

  • 需要被渲染的组件都会经历 mount 过程,所以它们都会拥有一个渲染 Watcher
  • 渲染 Watcher 的第二个参数是 updateComponent 这个方法,这个方法调用的时机有两个:
    • 一个就是现在,即组件挂载的时候
    • 另外一个就是组件内部依赖的响应式数据发生改变的时候,这个场景下还会调用第四个参数中的 before 方法,触发 beforeUpdate 钩子
  • updateComponent 这个方法的内部,
    • vm._render 方法的作用就是调用组件的 render 方法用户手写的 render 或者 编译生成的 render ),调用 render 方法得到的结果就是组件的 VNode
    • vm_update 方法的作用就是将 render 生成的 VNode 渲染成真实的 DOM 元素,插入到页面中去,也就是常说的 Patch 过程

# Watcher

下面的代码只保留了关键部分,其他部分先省略,这里的参数再说明一下

  • vm 是当前组件实例
  • expOrFn 就是上面的 updateComponent
  • cb 这里是一个空函数,可以忽略
  • options 这里有一个 before 属性
  • isRenderWatcher 标记当前 watcher 是否是 渲染 Watcher,这里是 true
export default class Watcher {
  constructor (
    vm,
    expOrFn,
    cb,
    options,
    isRenderWatcher
  ) {
    this.vm = vm
    // 如果是 渲染Watcher
    if (isRenderWatcher) {
      vm._watcher = this
    }
    // 组件实例的 _watchers 属性,用于存储组件内的所有 watcher,包括渲染watcher 以及组件内定义的 watcher
    vm._watchers.push(this)
    // 将 before 函数挂载 当前 watcher 实例上
    if (options) {
      this.before = options.before
    }
    // 将 updateComponent 定义为当前的 watcher 实例的 getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    }
    // lazy 代表的是惰性求值,这里是false,所以会立即调用 watcher 实例的 get 方法
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * get方法会立即调用 this.getter
   */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 调用 this.getter,这里就是 updateComponent 方法
      value = this.getter.call(vm, vm)
    } catch (e) {
      // ...
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

下面看 Watcher 实例化过程,

  • 首先如果当前 watcher 是渲染 watcher 的话,会将当前 watcher 实例挂在 当前组件实例 vm._watcher 属性上
  • 并在当前组件实例的 _watchers 数组中,压入当前 watcher
  • 然后将 options.before 挂在了当前 watcher 的 before 属性上
  • expOrFn 如果是一个 function 的话,就挂在当前 watcher 的 getter 属性上
  • 最后就是判断如果当前 watcher 不是惰性求值的话,就通过执行 watcher 实例的 get 方法,求取 watcher 实例上 value 属性的值,这个时候就会触发上面的 getter

当前的 渲染 watcher 实例化 的场景中,get 方法会立即执行,也就意味着 updateComponent 这个方法会被立即执行。

let updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
1
2
3

上面已经提到,这个方法执行的实质就是:

  • vm._render 内部通过调用组件的 render 方法,生成 VNode
  • vm._update 通过调用组件实例的 vm._patch 方法,将 VNode 渲染成真实的 DOM,并插入到页面中,完成渲染。这也就是常说的 patch 过程

到这里,组件 mount 的大体流程就说完了,下一篇说一下 VNode 的生成过程

Last Updated: 10/21/2024, 4:15:17 PM