Vue 中我们是通过 $mount 实例方法去挂载 vm 的,而 $mount 方法在多个文件中都有定义。

  1. new Vue({
  2. router,
  3. store,
  4. render: (h) => h(App),
  5. }).$mount('#app');

src/platforms/web/entry-runtime-with-compiler.js

携带编译器的版本,重写了 $mount 方法,主要是将 template 转换为 render 函数。

src/platforms/web/runtime/index.js

核心方法,真正的挂载函数,在编译器处理完 template 之后,就会调用该文件的 $mount

src/platforms/weex/runtime/index.js

weex 平台下的挂载函数,我们本次不需要关心。

因为 $mount 这个方法的实现是和平台、构建方式都相关的。接下来我们重点分析带 compiler 版本的 $mount 实现(src/platforms/web/entry-runtime-with-compiler.js),因为抛开 webpack vue-loader,我们在纯前端浏览器环境分析 Vue 的工作原理,有助于我们对原理理解的深入。

compiler 版本的 $mount 实现非常有意思。也因为代码量太多,就不一一列举了。我们只需要观看部分重点代码即可。

vm.$mount 深入剖析


src/platforms/web/entry-runtime-with-compiler.js

  1. 在定义 $mount 之前,还定义了一个 idToTemplate 函数,主要是为了避免 template 传入了 dom id ,所以将通过 #id 获取到的 dom 内容进行返回

    1. const idToTemplate = cached((id) => {
    2. const el = query(id);
    3. return el && el.innerHTML;
    4. });
  2. 缓存 vue 原型 prototype 上的 $mount 函数,主要是在 src/platforms/web/runtime/index.js 中定义了。然后重写当前 $mount 挂载函数。主要作用是在当前 $mount 函数内进行了一系列处理后,在调用回原来原型上的 $mount 挂载函数 ```typescript const mount = Vue.prototype.$mount Vue.prototype.$mount = function (el?: string | Element, hydrating?: boolean): Component { // … return mount.call(this, el, hydrating); };

  1. 3. 将用户传入的_ el_ 进行转换为_ dom_ 元素。_query_ 函数判断逻辑如下,如果 el 是字符串,则通过_ querySelector _获取对应的 _dom_,否则通过_ createElement _创建一个空的 _div_ 元素返回
  2. ```typescript
  3. el = el && query(el)
  1. $mount 中判断了传入 el 属性不能是 html、body 对象。主要是因为挂载的时候会将整个 el 对象替换成新处理的 dom 对象。如果你传入了 html、body 对象的话,那么恭喜你,原来它们上面的 title、meta、link、script 标签通通都没了。所以 Vue 避免了这样的操作,在必要的时候返回报错了

    1. if (el === document.body || el === document.documentElement) {
    2. process.env.NODE_ENV !== 'production' && warn(`Do not mount Vue to <html> or <body> - mount to normal elements instead.`);
    3. return this;
    4. }
  2. 判断是否有传入 rende_r 函数,有则直接跳过 _template 转换 render 步骤。直接调用原有的 $mount 挂载函数

    1. if (!options.render) {
    2. // ...
    3. }
    4. return mount.call(this, el, hydrating);
  3. 在没有传入 render 但是有传入 template 的情况下进行了三种判断,主要的作用就是避免 template 传入了 dom 或者是 dom id,而不是 template 模板语法。所以会将其进行转换为 template 模板语法

    1. if (template) {
    2. if (typeof template === 'string') {
    3. if (template.charAt(0) === '#') {
    4. template = idToTemplate(template);
    5. /* istanbul ignore if */
    6. if (process.env.NODE_ENV !== 'production' && !template) {
    7. warn(`Template element not found or is empty: ${options.template}`, this);
    8. }
    9. }
    10. } else if (template.nodeType) {
    11. template = template.innerHTML;
    12. } else {
    13. if (process.env.NODE_ENV !== 'production') {
    14. warn('invalid template option:' + template, this);
    15. }
    16. return this;
    17. }
    18. }
  4. 在没有传入 render、也没有传入 template 但是传入了 el 的情况下,则将该 dom 元素的内容赋值给 template。此处的 el 已经是 dom 元素了,上面的代码进行了转换

    1. else if (el) {
    2. template = getOuterHTML(el);
    3. }
  5. 此处是该文件的核心部分,通过调用 compileToFunctions 函数将 template 转换为 render 函数。compileToFunctionssrc/platforms/web/compiler/index.js 中有定义

    1. if (template) {
    2. const { render } = compileToFunctions(...);
    3. options.render = render;
    4. }

总结一下,当前重写的 $mount 主要作用是 ,如果没有定义 render 方法,则会把 el 转换为 template 字符串,然后在将 template 字符串转换成 render 函数。

这里我们要牢记,在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法,那么这个过程是 Vue 的一个 “在线编译” 的过程,它是调用 compileToFunctions 方法实现的,编译过程我们之后会介绍。

最后,调用原先原型上的 $mount 方法挂载。

src/platforms/web/runtime/index.js

上面我们介绍了 src/platforms/web/entry-runtime-with-compiler.js 中重写的 $mount 函数,而在本文件也有定义一个 $mount 函数,之所以这么设计完全是为了复用,因为它是可以被 runtime only 版本的 Vue 直接使用的。而其他版本则可以在其基础上更容易实现其他扩展功能。

  1. $mount 方法支持传入 2 个参数。
    • 第一个是 el,它表示挂载的元素,可以是字符串,也可以是 DOM 对象,如果是字符串在浏览器环境下会调用 query 方法转换成 DOM 对象的。
    • 第二个参数是和服务端渲染相关,在浏览器环境下我们不需要传第二个参数。$mount 方法实际上会去调用 mountComponent 方法,这个方法定义在 src/core/instance/lifecycle.js 文件中
  1. Vue.prototype.$mount = function (el?: string | Element, hydrating?: boolean): Component {
  2. el = el && inBrowser ? query(el) : undefined;
  3. return mountComponent(this, el, hydrating);
  4. };

mountComponent

  1. Vue 实例添加静态属性 $el

    1. vm.$el = el;
  2. mountComponent 主要是在运行时版本调用的,通过 render 函数进行挂载操作,它是不会帮你将 template 或者 el 转为 render 函数的。所以当你没有传入 render 时,进行了判断是否有传入 template 或者 el ,有传入或者没有传入,则都会触发对应的警告

    1. if (!vm.$options.render) {
    2. if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') || vm.$options.el || el) {
    3. warn(
    4. 'You are using the runtime-only build of Vue where the template ' +
    5. 'compiler is not available. Either pre-compile the templates into ' +
    6. 'render functions, or use the compiler-included build.',
    7. vm
    8. );
    9. } else {
    10. warn('Failed to mount component: template or render function not defined.', vm);
    11. }
    12. }
  3. 调用生命周期钩子函数 beforeMount

    1. callHook(vm, 'beforeMount');
  4. 定义 updateComponent 函数,主要作用是通过 vm._render 函数生成虚拟 Node,再调用 vm._update 更新 DOM

    1. updateComponent = () => {
    2. vm._update(vm._render(), hydrating);
    3. };
  5. 实例化一个 Render Watcher,在这里起到两个作用,一个是初始化的时候会执行回调函数 updateComponent,另一个是当 vm 实例中的监测的数据发生变化的时候再次执行回调函数 updateComponent,并且调用生命周期钩子函数 beforeUpdate

    1. new Watcher(
    2. vm,
    3. updateComponent,
    4. noop,
    5. {
    6. before() {
    7. if (vm._isMounted && !vm._isDestroyed) {
    8. callHook(vm, 'beforeUpdate');
    9. }
    10. },
    11. },
    12. true /* isRenderWatcher */
    13. );
  6. 最后判断当前为根节点的时候设置 vm._isMounted true, 表示这个实例已经挂载了,同时执行 mounted 钩子函数。 这里注意 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例

    1. if (vm.$vnode == null) {
    2. vm._isMounted = true;
    3. callHook(vm, 'mounted');
    4. }

mountComponent 方法的逻辑也是非常清晰的,它会完成整个渲染工作,接下来我们要重点分析其中的细节,也就是最核心的 2 个方法:vm._rendervm._update。

vm._render 深入剖析


Vue _render 方法是实例的一个私有方法,它用来把实例渲染成一个虚拟 Node。它的定义在 src/core/instance/render.js 文件 renderMixin 函数中。

_render

  1. 我们主要关心核心部分代码即可,其他多余代码就不一 一列举了,_render 代码中的核心部分是 render 函数的调用,call 的第一个参数是上下文对象,在这里可以先不用关心,第二个参数vm.$createElement 才是实际的参数 ,这里的 render 来源于 vm.$options 也就是用户在 new Vue 时候传入的 render

一般很少会自己写 render 函数代码,我们都是在 .vue 单文件组件进行编码,然后 import xxx.vue 文件时, webpackvue-loader 会自动把 xxx.vue 文件编译成 render 函数。

  1. Vue.prototype._render = function (): VNode {
  2. const vm: Component = this;
  3. const { render, _parentVnode } = vm.$options;
  4. vnode = render.call(vm._renderProxy, vm.$createElement);
  5. return vnode;
  6. };
  1. 通过例子可以更好的了解 render 函数
  • 带编译器的写法
    1. <div id="app">
    2. {{ message }}
    3. </div>
  1. new Vue({
  2. el: '#app',
  3. data: {
  4. message: 'Hello Vue!',
  5. },
  6. });
  • 不带编译器,我们自己手写 render 函数实现
    1. <div id="app"></div>
  1. new Vue({
  2. el: '#app',
  3. render(h) {
  4. return h('div', this.message);
  5. },
  6. data: {
  7. message: 'Hello Vue!',
  8. },
  9. });
  1. 再回到 _render 函数中的 render 方法的调用,可以看到上方不带编译器代码中的 h 函数,其实就是 vm.$createElement。所以接下来可以重点分析一下 $createElement,因为它是 _vm_rende_r 的核心
    1. vnode = render.call(vm._renderProxy, vm.$createElement);

$createElement

$createElement 也是定义在 src/core/instance/render.js 文件,但是是在 initRender 函数中。

  1. 这边也是主要列举关键部分代码,其他多余的代码就不一一列举了。可以看到除了 vm.$createElement 方法,还有一个 vm._c 函数,它是被模板编译成的 render 函数使用,而 vm.$createElement 是用户手写 render 函数使用的, 这两个函数支持的参数相同,并且内部都调用了 createElement 方法

    1. export function initRender(vm: Component) {
    2. vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false);
    3. vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true);
    4. }

createElement

Vue 利用 createElement 函数创建 VNode,它定义在 src/core/vdom/create-element.js 中。

  1. createElement 函数实际上是对 _createElement 函数的封装,它允许传入的参数更加灵活,在处理这些参数后,调用真正创建 VNode 的函数 _createElementcreateElement 定义了 6 个参数,我们只要关心重要的几个即可:
    • content 代表 vm 的实例
    • tag 代表需要创建的标签类型
    • data 代表 Vnode 的数据
    • children 代表子节点 VNode
      1. export function createElement(context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean): VNode | Array<VNode> {
      2. if (Array.isArray(data) || isPrimitive(data)) {
      3. normalizationType = children;
      4. children = data;
      5. data = undefined;
      6. }
      7. if (isTrue(alwaysNormalize)) {
      8. normalizationType = ALWAYS_NORMALIZE;
      9. }
      10. return _createElement(context, tag, data, children, normalizationType);
      11. }

render_ 的实现过程到此为止了。createElement 的实现方式其实跟 _Snabbdom 虚拟 DOM 库中的 h 函数很类似。如果感兴趣可以先去了解一下其的用法以及原理。核心功能就是通过 h 函数创建元素,返回 VNode 。而 _update 则是更相似于 Snabbdom 中的 patch

vm._update 深入剖析


Vue 的 _update 是实例的一个私有方法,它被调用的时机有 2 个,一个是首次渲染,一个是数据更新的时候。由于我们这一章节只分析首次挂载过程,数据更新部分会在之后分析响应式原理的时候涉及。_update 方法的作用是把 VNode 渲染成真实的 DOM,它的定义在 src/core/instance/lifecycle.js lifecycleMixin 函数中。

_update

  1. 这边只列举核心部分的代码,其他代码就不一 一列举了。在下方代码中进行了判断当前是否为首次渲染或者是更新阶段。

update_ 的核心就是调用 _vm.patch_ 方法,这个方法实际上在不同的平台,比如 web weex 上 的定义是不一样的,因此在 web 平台中它的定义在 src/platforms/web/runtime/index.js

  1. Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  2. if (!prevVnode) {
  3. // initial render
  4. vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
  5. } else {
  6. // updates
  7. vm.$el = vm.__patch__(prevVnode, vnode);
  8. }
  9. };

patch

  1. 可以看到,甚至在 web 平台上,是否是服务端渲染也会对这个方法产生影响。因为在服务端渲染中,没有真实的浏览器 DOM 环境,所以不需要把 VNode 最终转换成 DOM,因此是一个空函数,而在浏览器端渲染中,它指向了 patch 方法,它的定义在 src/platforms/web/runtime/patch.js
    1. Vue.prototype.__patch__ = inBrowser ? patch : noop;

patch

  1. 该方法的定义是调用 createPatchFunction 方法的返回值,这里传入了一个对象,包含 nodeOps 参数和 modules 参数。其中,nodeOps 封装了一系列 DOM 操作的方法,modules 定义了一些模块的钩子函数的实现,我们这里先不详细介绍,来看一下 createPatchFunction 的实现,它定义在 src/core/vdom/patch.js ```typescript 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 });

  1. <a name="eL3e8"></a>
  2. #### createPatchFunction
  3. _createPatchFunction _内部定义了一系列的辅助方法,最终返回了一个 _patch_ 方法,这个方法就赋值给了 _vm._update _函数里调用的 _vm.__patch__ _。
  4. 在介绍_ patch _的方法实现之前,我们可以思考一下为何 _Vue.js _源码绕了这么一大圈,把相关代码分散到各个目录。因为前面介绍过,_patch _是平台相关的,在 _Web_ 和_ Weex _环境,它们把虚拟 _DOM_ 映射到 "平台 _DOM _" 的方法是不同的,并且对 "_DOM"_ 包括的属性模块创建和更新也不尽相同。
  5. 因此每个平台都有各自的 _nodeOps_ 和 _modules_,它们的代码需要托管在 _src/platforms _这个大目录下。
  6. 而不同平台的_ patch_ 的主要逻辑部分是相同的,所以这部分公共的部分托管在 _core_ 这个大目录下。差异化部分只需要通过参数来区别,这里用到了一个函数柯里化的技巧,通过 _createPatchFunction_ 把差异化参数提前固化,这样不用每次调用 _patch _的时候都传递 _nodeOps _和 _modules _了,这种编程技巧也非常值得学习。
  7. ```typescript
  8. export function createPatchFunction(backend) {
  9. // ...
  10. return function patch(oldVnode, vnode, hydrating, removeOnly) {
  11. // ...
  12. patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
  13. };
  14. }

createPatchFunction 返回的 patch 函数中,核心部分是 patchVnode 更新虚拟 DOM 和 真实 DOM。这边就到此为止,不深入讲解了。