在 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 平台相关的。给构造函数Vue的config设置了五个属性,分别是:mustUseProp、isReservedTag、isReservedAttr、getTagNamespace和isUnknownElement,先不去管这些属性。
继续查看下面的代码。
// ! 安装 Web 平台运行时的指令 v-model v-show 和组件 <transition/> <transition-group/>
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)s使用extend方法,给Vue选项中的directives和components扩展内容。这些是和平台相关的组件和指令。
那么到底扩展了什么内容呢?查看runtime/directives/index.js文件的代码。
import model from "./model";
import show from "./show";
export default {
model,
show,
};这里给指令集directives扩展了两个新指令model和show,即v-model和v-show,这两个指令应该都非常熟悉吧,这里就不多介绍了。
然后再查看runtime/components/index.js文件的代码。
import Transition from "./transition";
import TransitionGroup from "./transition-group";
export default {
Transition,
TransitionGroup,
};这里给组件数组components扩展了两个新组件Transition和TransitionGroup,这两个组件是设置过渡动画的组件。
接下来继续看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 的补丁方法。这里还判断了是否是浏览器环境,如果不是则为noop,noop是一个空函数。
$mount是组件的挂载方法,不过因为这里的是 Runtime 版本,在完整版本中还会对这个方法进行扩展,增加编译器相关的代码。
再看最后的代码。
if (inBrowser) {
setTimeout(() => {
if (config.devtools) {
/* ... */
}
// ...
});
}
export default Vue;在浏览器环境中安装vue-devtool的全局钩子,用于在浏览器中进行开发调试。
最后是导出 Vue 的构造函数Vue,这样运行时版本的 Vue 的平台化包装(扩展)就完成了。
小结#
Runtime 版本的入口文件主要给 Vue 的构造函数添加了 Web 平台专用的属性和方法。
- 在
Vue.config配置了五个 Web 平台专用属性,分别是:mustUseProp、isReservedTag、isReservedAttr、getTagNamespace和isUnknownElement。 - 在
Vue.options的filters属性添加两个指令v-model和v-show。 - 在
Vue.options的components属性添加两个过渡动画组件Transition和TransitionGroup。 - 在
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方法,添加编译相关的代码。