3.2 实例挂载的基本思路

有了上面的基础,我们回头看初始化_init的代码,在代码中我们观察到initProxy后有一系列的函数调用,这些函数包括了创建组件关联,初始化事件处理,定义渲染函数,构建数据响应式系统等,最后还有一段代码,在el存在的情况下,实例会调用$mount进行实例挂载。

  1. Vue.prototype._init = function (options) {
  2. ···
  3. // 选项合并
  4. vm.$options = mergeOptions(
  5. resolveConstructorOptions(vm.constructor),
  6. options || {},
  7. vm
  8. );
  9. // 数据代理
  10. initProxy(vm);
  11. vm._self = vm;
  12. initLifecycle(vm);
  13. // 初始化事件处理
  14. initEvents(vm);
  15. // 定义渲染函数
  16. initRender(vm);
  17. // 构建响应式系统
  18. initState(vm);
  19. // 等等
  20. ···
  21. if (vm.$options.el) {
  22. vm.$mount(vm.$options.el);
  23. }
  24. }

以手写template模板为例,理清楚什么是挂载。我们会在选项中传递template为属性的模板字符串,如<div>{{message}}</div>,最终这个模板字符串通过中间过程将其转成真实的DOM节点,并挂载到选项中el代表的根节点上完成视图渲染。这个中间过程就是接下来要分析的挂载流程。

Vue挂载的流程是比较复杂的,接下来我将通过流程图,代码分析两种方式为大家展示挂载的真实过程。

3.2.1 流程图

3.2 实例挂载的基本思路 - 图1如果用一句话概括挂载的过程,可以描述为确认挂载节点,编译模板为render函数,渲染函数转换Virtual DOM,创建真实节点。

3.2.2 代码分析

接下来我们从代码的角度去剖析挂载的流程。挂载的代码较多,下面只提取骨架相关的部分代码。

  1. // 内部真正实现挂载的方法
  2. Vue.prototype.$mount = function (el, hydrating) {
  3. el = el && inBrowser ? query(el) : undefined;
  4. // 调用mountComponent方法挂载
  5. return mountComponent(this, el, hydrating)
  6. };
  7. // 缓存了原型上的 $mount 方法
  8. var mount = Vue.prototype.$mount;
  9. // 重新定义$mount,为包含编译器和不包含编译器的版本提供不同封装,最终调用的是缓存原型上的$mount方法
  10. Vue.prototype.$mount = function (el, hydrating) {
  11. // 获取挂载元素
  12. el = el && query(el);
  13. // 挂载元素不能为跟节点
  14. if (el === document.body || el === document.documentElement) {
  15. warn(
  16. "Do not mount Vue to <html> or <body> - mount to normal elements instead."
  17. );
  18. return this
  19. }
  20. var options = this.$options;
  21. // 需要编译 or 不需要编译
  22. // render选项不存在,代表是template模板的形式,此时需要进行模板的编译过程
  23. if (!options.render) {
  24. ···
  25. // 使用内部编译器编译模板
  26. }
  27. // 无论是template模板还是手写render函数最终调用缓存的$mount方法
  28. return mount.call(this, el, hydrating)
  29. }
  30. // mountComponent方法思路
  31. function mountComponent(vm, el, hydrating) {
  32. // 定义updateComponent方法,在watch回调时调用。
  33. updateComponent = function () {
  34. // render函数渲染成虚拟DOM, 虚拟DOM渲染成真实的DOM
  35. vm._update(vm._render(), hydrating);
  36. };
  37. // 实例化渲染watcher
  38. new Watcher(vm, updateComponent, noop, {})
  39. }

我们用语言描述挂载流程的基本思路。

  • 确定挂载的DOM元素,这个DOM需要保证不能为html,body这类跟节点。
  • 我们知道渲染有两种方式,一种是通过template模板字符串,另一种是手写render函数,前面提到template模板需要运行时进行编译,而后一个可以直接用render选项作为渲染函数。因此挂载阶段会有两条分支,template模板会先经过模板的解析,最终编译成render渲染函数参与实例挂载,而手写render函数可以绕过编译阶段,直接调用挂载的$mount方法。
  • 针对template而言,它会利用Vue内部的编译器进行模板的编译,字符串模板会转换为抽象的语法树,即AST树,并最终转化为一个类似function(){with(){}}的渲染函数,这是我们后面讨论的重点。
  • 无论是template模板还是手写render函数,最终都将进入mountComponent过程,这个阶段会实例化一个渲染watcher,具体watcher的内容,另外放章节讨论。我们先知道一个结论,渲染watcher的回调函数有两个执行时机,一个是在初始化时执行,另一个是当vm实例检测到数据发生变化时会再次执行回调函数。
  • 回调函数是执行updateComponent的过程,这个方法有两个阶段,一个是vm._render,另一个是vm._updatevm._render会执行前面生成的render渲染函数,并生成一个Virtual Dom tree,而vm._update会将这个Virtual Dom tree转化为真实的DOM节点。