Vue2源码学习笔记之平台化包装

在 core 文件夹下面的代码是 Vue 的核心代码,它并不涉及任何与平台相关的内容。 Vue 的平台目前主要有两个, 分别是 Web 平台和 Weex 平台。 Web 平台是和浏览器相关,它里面的代码运行在浏览器中。而 Weex 平台的代

目录
  1. 运行时版本
    1. 小结
  2. 带编译版本
  3. 总结
  4. 参考资料

在 core 文件夹下面的代码是 Vue 的核心代码,它并不涉及任何与平台相关的内容。

Vue 的平台目前主要有两个, 分别是 Web 平台和 Weex 平台。 Web 平台是和浏览器相关,它里面的代码运行在浏览器中。而 Weex 平台的代码却是和移动端相关的,这里不作介绍。

下面主要还是学习 Web 平台相关的代码。平台化包装也有两个版本,分别是 Runtime (运行时)版本和 Runtime + Compiler (完整)版本。其中,运行时版本是完整版本的组成部分,完整版本还包含了编译器。

运行时版本#

运行时的版本比较简单,因为没有涉及到编译器,它的编译入口是platforms/webpack/entry-runtime.js,下面查看它的代码。

import Vue from "./runtime/index";
 
export default Vue;

这个文件只是单独的引用了platforms/web/runtime/index.js文件中的Vue,然后再把它导出。

现在打开这个引用的文件,查看它的代码。先看前面的部分代码。

/* @flow */
import Vue from "core/index";
import config from "core/config";
import { extend, noop } from "shared/util";
import { mountComponent } from "core/instance/lifecycle";
import { devtools, inBrowser } from "core/util/index";
 
import {
  query,
  mustUseProp,
  isReservedTag,
  isReservedAttr,
  getTagNamespace,
  isUnknownElement,
} from "web/util/index";
 
import { patch } from "./patch";
import platformDirectives from "./directives/index";
import platformComponents from "./components/index";
 
// install platform specific utils
// ! 设置 Web 平台配置选项
Vue.config.mustUseProp = mustUseProp;
Vue.config.isReservedTag = isReservedTag;
Vue.config.isReservedAttr = isReservedAttr;
Vue.config.getTagNamespace = getTagNamespace;
Vue.config.isUnknownElement = isUnknownElement;
 
// ...
export default Vue;

先不管导入的模块部分。看上面代码的最后几行,这里再次设置了 Vue 的构造函数的config属性。

还记得我们在核心代码 core 中也设置config属性,不过那里的设置的都是些初始值或者空值。

现在配置的值,都是和 Web 平台相关的。给构造函数Vueconfig设置了五个属性,分别是:mustUsePropisReservedTagisReservedAttrgetTagNamespaceisUnknownElement,先不去管这些属性。

继续查看下面的代码。

// ! 安装 Web 平台运行时的指令 v-model v-show 和组件 <transition/> <transition-group/>
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)s

使用extend方法,给Vue选项中的directivescomponents扩展内容。这些是和平台相关的组件和指令。

那么到底扩展了什么内容呢?查看runtime/directives/index.js文件的代码。

import model from "./model";
import show from "./show";
 
export default {
  model,
  show,
};

这里给指令集directives扩展了两个新指令modelshow,即v-modelv-show,这两个指令应该都非常熟悉吧,这里就不多介绍了。

然后再查看runtime/components/index.js文件的代码。

import Transition from "./transition";
import TransitionGroup from "./transition-group";
 
export default {
  Transition,
  TransitionGroup,
};

这里给组件数组components扩展了两个新组件TransitionTransitionGroup,这两个组件是设置过渡动画的组件。

接下来继续看runtime/index.js上的代码。

// install platform patch functional
// ! 安装 Web 平台的 __patch__ 方法
Vue.prototype.__patch__ = inBrowser ? patch : noop;
 
// public mount method
// ! 安装 Web 平台的挂载方法 $mount (这里是 runtime 版本)
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating);
};

给 Vue 构造函数的原型对象上添加了两个方法__patch__$mount

__patch__是把虚拟 DOM 转换成真实 DOM 的补丁方法。这里还判断了是否是浏览器环境,如果不是则为noopnoop是一个空函数。

$mount是组件的挂载方法,不过因为这里的是 Runtime 版本,在完整版本中还会对这个方法进行扩展,增加编译器相关的代码。

再看最后的代码。

if (inBrowser) {
  setTimeout(() => {
    if (config.devtools) {
      /* ... */
    }
    // ...
  });
}
export default Vue;

在浏览器环境中安装vue-devtool的全局钩子,用于在浏览器中进行开发调试。

最后是导出 Vue 的构造函数Vue,这样运行时版本的 Vue 的平台化包装(扩展)就完成了。

小结#

Runtime 版本的入口文件主要给 Vue 的构造函数添加了 Web 平台专用的属性和方法。

  • Vue.config配置了五个 Web 平台专用属性,分别是:mustUsePropisReservedTagisReservedAttrgetTagNamespaceisUnknownElement
  • Vue.optionsfilters属性添加两个指令v-modelv-show
  • Vue.optionscomponents属性添加两个过渡动画组件TransitionTransitionGroup
  • Vue的原型对象上增加两个方法__patch__$mount
  • 安装vue-devtool的全局钩子

带编译版本#

下面再查看完整版本编译器的入口文件platforms/webpack/entry-runtime-with-compiler.js,对比运行时版本的入口文件,主要是增加了编译器相关的代码。

打开这个文件,先看前面的部分代码。

import config from "core/config";
import { warn, cached } from "core/util/index";
import { mark, measure } from "core/util/perf";
 
import Vue from "./runtime/index"; // ! 导入 runtime 版本的 Vue
import { query } from "./util/index";
import { compileToFunctions } from "./compiler/index";
import {
  shouldDecodeNewlines,
  shouldDecodeNewlinesForHref,
} from "./util/compat";
 
// ! 通过 id 获取元素的 innerHTML
const idToTemplate = cached((id) => {
  const el = query(id);
  return el && el.innerHTML;
});

首先还是引入模块文件,其中包括引入 Runtime 版本的 Vue 的构造函数Vue

然后定义一个通过 id 获取元素的innerHTML的方法idToTemplate,在下面重写$mount方法中会用到。

继续往下看代码。

const mount = Vue.prototype.$mount; // ! 缓存 runtime 版本的 $mount 方法
 
// ! 重写 $mount 方法 with compiler
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // ...
};

首先缓存了运行时版本的$mount方法,然后又重写了这个方法,主要是加入了编译器相关的代码。

先不管重写的$mount方法,继续查看最后的代码。

/**
 * Get outerHTML of elements, taking care
 * of SVG elements in IE as well.
 */
function getOuterHTML(el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML;
  } else {
    const container = document.createElement("div");
    container.appendChild(el.cloneNode(true));
    return container.innerHTML;
  }
}
 
Vue.compile = compileToFunctions; // ! 新增方法 compile
 
export default Vue;

定义了一个getOuterHTML方法,这个方法在上面重写$mount方法中会用到。

然后给Vue新增了一个静态方法compile,赋值为compileToFunctions方法,从方法的名字可以看出这应该是一个编译的方法,把模板字符串编译成渲染函数。

但是,这个方法从哪里来的呢?是从platforms/web/compiler/index.js文件中引入进来的,暂时先不管它。

最后还是导出 Vue 的构造函数Vue,这样包含编译器的 Vue 的平台化包装(扩展)也完成了。

总结,这个带编译器的入口文件代码结构其实也很简单,就是重写运行时版本中 Vue 构造函数的原型对象的$mount方法,在重写的过程中添加编译相关的代码。

总结#

到了这一步,Vue 的构造函数的扩展就已经初步完成。

现在再次回顾下 Vue 的构造函数Vue最初的样子。

function Vue(options) {
  if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) {
    warn("Vue is a constructor and should be called with the `new` keyword");
  }
  this._init(options);
}

构造函数非常简单,如果把警告的代码再去掉,代码会更少,内容就只有一行

function Vue(options) {
  this._init(options);
}

现在,就是这么一个简单的构造函数,在经过一系列的扩展和包装后,就成为了一个非常复杂的前端框架。

实在是有点不可思议,这也说明 Vue 的结构设计非常清晰明了!

回顾下它是怎么扩展的:

  • core/instance/index.js:扩展构造函数的原型对象的属性和方法
  • core/index.js:扩展构造函数静态的属性和方法
  • platforms/webpack/entry-runtime.js:扩展 Web 平台专用的属性和方法
  • platforms/webpack/entry-runtime-with-compiler.js:重写运行时的$mount方法,添加编译相关的代码。

参考资料#