说明
-
首先这篇文章是读
vue.js
源代码的梳理性文章,文章分块梳理,记录着自己的一些理解及大致过程;更重要的一点是希望在vue.js 3.0
发布前深入的了解其原理。 -
如果你从未看过或者接触过
vue.js
源代码,建议你参考以下列出的vue.js
解析的相关文章,因为这些文章更细致的讲解了这个工程,本文只是以一些demo
演示某一功能点或API
实现,力求简要梳理过程。- 逐行级别的源码分析 - 强烈推荐
- Vue.js 源码分析
- Vue.js 源码解析
-
如果搞清楚了工程目录及入口,建议直接去看代码,这样比较高效 ( 遇到难以理解对应着回来看看别人的讲解,加以理解即可 )
-
文章所涉及到的代码,基本都是缩减版,具体还请参阅 vue.js - 2.5.17。
-
如有任何疏漏和错误之处欢迎指正、交流。
初始化前
调用关系
JavaScript
本身是一种直译式脚本语言,在找到入口后,主要需要理清其调用关系? 找出 Vue 构造函数的在哪定义了?按照这个逻辑,跟着程序一步一步走即可。
首先src/platforms/web/entry-runtime-with-compiler.js
这个文件最开始,引入一些方法与配置,并导入了 Vue
进而程序去执行 ./runtime/index
文件
import config from 'core/config';
import { warn, cached } from 'core/util/index';
import { mark, measure } from 'core/util/perf';
import Vue from './runtime/index';
import { query } from './util/index';
import { compileToFunctions } from './compiler/index';
import {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref
} from './util/compat';
以下代码省略, 将在分析初始化时展开...
复制代码
接着 src/platforms/web/runtime/index.js
这个文件也是引入一些方法与配置,并导入了 Vue
, 程序继续走到 core/index
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, isChrome } 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';
以下代码省略, 将在分析初始化时展开...
复制代码
来到核心代码 src/core/index.js
该文件仍然也是从外部文件导入了 Vue
, 程序来到 ./instance/index
import Vue from './instance/index';
import { initGlobalAPI } from './global-api/index';
import { isServerRendering } from 'core/util/env';
import { FunctionalRenderContext } from 'core/vdom/create-functional-component';
以下代码省略, 将在分析初始化时展开...
复制代码
最后 src/core/instance/index.js
import { initMixin } from './init';
...
/**
* Vue构造函数
*
* @param {*} options 选项参数
*/
function Vue(options) {
if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
warn('Vue是一个构造函数,应该用“new”关键字调用');
}
this._init(options);
}
export default Vue;
以下代码省略, 将在分析初始化时展开...
复制代码
综上:
- ①
src/core/instance/index.js
( 定义Vue
构造函数 ) => - ②
src/core/index.js
( 在 Vue 构造函数上添加全局的 API ) => - ③
web/runtime/index.js
( 安装特定于平台的 utils & 运行时指令和组件 & 定义公用的挂载方法 & 配置 devtools 全局钩子 ) => - ④
web/entry-runtime-with-compiler.js
( 重写 mount 函数增加编译模板的能力 )
初始化前做的事情
根据上述调用关系一步一步走,首先看到最初定义 Vue 构造函数的文件到底做了哪些事情
初始化前 - 定义 Vue 构造函数并执行五个函数
import { initMixin } from './init';
import { stateMixin } from './state';
import { renderMixin } from './render';
import { eventsMixin } from './events';
import { lifecycleMixin } from './lifecycle';
import { warn } from '../util/index';
function Vue(options) {
if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
warn('Vue是一个构造函数,应该用“new”关键字调用');
}
this._init(options);
}
initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);
export default Vue;
复制代码
initMixin
该方法就做了一件事,在 Vue.prototype
添加 _init
方法。
export function initMixin(Vue: Class ) {
Vue.prototype._init = function(options?: Object) {
// 代码省略,在初始化会细致分析
};
}
复制代码
stateMixin
import {
set,
del,
observe,
defineReactive,
toggleObserving
} from '../observer/index';
...
export function stateMixin(Vue: Class ) {
// 在使用object.defineproperty时,flow在直接声明定义对象方面存在一些问题,因此我们必须在这里以程序的方式构建对象。
const dataDef = {};
dataDef.get = function() {
return this._data;
};
const propsDef = {};
propsDef.get = function() {
return this._props;
};
// 在非生产环境下 设置 $data $props 为只读属性
if (process.env.NODE_ENV !== 'production') {
dataDef.set = function(newData: Object) {
warn('避免替换实例根$data。 而是使用嵌套数据属性。', this);
};
propsDef.set = function() {
warn(`$props 是只读的。`, this);
};
}
// 在Vue原型上定义两个属性,并分别代理了 _data _props 的实例属性
Object.defineProperty(Vue.prototype, '$data', dataDef);
Object.defineProperty(Vue.prototype, '$props', propsDef);
// 在 vue 原型上添加 实例方法 / 数据相关: $set/$delete/$watch
Vue.prototype.$set = set; // 向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新
Vue.prototype.$delete = del; // 删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。
Vue.prototype.$watch = function( // 观察 Vue 实例变化的一个表达式或计算属性函数。回调函数得到的参数为新值和旧值。
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
// 代码省略,在初始化会细致分析
};
...
}
复制代码
eventsMixin
在 Vue.prototype
添加实例方法 / 事件相关:$on
/$once
/$off
/$emit
export function eventsMixin(Vue: Class ) {
// 作用:监听当前实例上的自定义事件。事件可以由vm.$emit触发。回调函数会接收所有传入事件触发函数的额外参数。
Vue.prototype.$on = function(
event: string | Array,
fn: Function
): Component {
// ...
};
// 作用:监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器
Vue.prototype.$once = function(event: string, fn: Function): Component {
// ...
};
// 作用:移除自定义事件监听器。
Vue.prototype.$off = function(
event?: string | Array,
fn?: Function
): Component {
// ...
};
// 作用:触发当前实例上的事件。附加参数都会传给监听器回调。
Vue.prototype.$emit = function(event: string): Component {
// ...
};
}
复制代码
lifecycleMixin
在 Vue.prototype
添加实例方法 / 生命周期相关:_update
/$forceUpdate
/$destroy
export function lifecycleMixin(Vue: Class ) {
Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) {
// ...
}
// 作用:迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。
Vue.prototype.$forceUpdate = function() {
// ...
}
// 作用:完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及事件监听器。
Vue.prototype.$destroy = function() {
// ...
}
复制代码
initRender
在 Vue.prototype
添加实例方法:$nextTick
/_render
/_o
/_n
等。
export function installRenderHelpers(target: any) {
target._o = markOnce;
target._n = toNumber;
target._s = toString;
target._l = renderList;
target._t = renderSlot;
target._q = looseEqual;
target._i = looseIndexOf;
target._m = renderStatic;
target._f = resolveFilter;
target._k = checkKeyCodes;
target._b = bindObjectProps;
target._v = createTextVNode;
target._e = createEmptyVNode;
target._u = resolveScopedSlots;
target._g = bindObjectListeners;
}
复制代码
import {
warn,
nextTick,
emptyObject,
handleError,
defineReactive
} from '../util/index';
import { installRenderHelpers } from './render-helpers/index';
export function renderMixin(Vue: Class ) {
installRenderHelpers(Vue.prototype); // 安装运行时方便助手
Vue.prototype.$nextTick = function(fn: Function) {
return nextTick(fn, this);
};
Vue.prototype._render = function(): VNode {
// ...
};
}
复制代码
断点调试
综上所述该文件主要做了两件事:定义 Vue
构造函数、包装 Vue.prototype
。
初始化前 - 在 Vue 构造函数上添加全局的 API
import Vue from './instance/index';
import { initGlobalAPI } from './global-api/index';
import { isServerRendering } from 'core/util/env';
import { FunctionalRenderContext } from 'core/vdom/create-functional-component';
initGlobalAPI(Vue); // 在 Vue 构造函数上添加全局的API
// 在 Vue.prototype 上添加 $isServer 只读属性,该属性代理了 isServerRendering 方法
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
});
// 在 Vue.prototype 上添加 $ssrContext 只读属性,该属性代理了 $vnode.ssrContext
Object.defineProperty(Vue.prototype, '$ssrContext', {
get() {
return this.$vnode && this.$vnode.ssrContext;
}
});
// 为 ssr 运行时助手安装公开 FunctionalRenderContext
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
});
Vue.version = '__VERSION__'; // 在 Vue 上添加静态属性 version
export default Vue;
复制代码
initGlobalAPI
初始化全局 API
/* @flow */
import config from '../config';
import { initUse } from './use';
import { initMixin } from './mixin';
import { initExtend } from './extend';
import { initAssetRegisters } from './assets';
import { set, del } from '../observer/index';
import { ASSET_TYPES } from 'shared/constants';
import builtInComponents from '../components/index';
import {
warn,
extend,
nextTick,
mergeOptions,
defineReactive
} from '../util/index';
// 全局API以静态属性和方法的形式被添加到 Vue 构造函数
export function initGlobalAPI(Vue: GlobalAPI) {
const configDef = {};
configDef.get = () => config;
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn('不要替换 Vue.config 对象,请设置单独的字段代替。');
};
}
Object.defineProperty(Vue, 'config', configDef); // 在 Vue 上添加 config 只读属性,该属性代理了 config
// 暴露 util 的方法。注意:这些不被认为是公共API的一部分——除非您意识到了风险,否则请避免依赖它们。
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
};
// 在 Vue 上添加 set/delete/nextTick/options 属性
Vue.set = set;
Vue.delete = del;
Vue.nextTick = nextTick;
Vue.options = Object.create(null);
// 在 Vue.options 添加 components, directives, filters 属性
// ASSET_TYPES = [ 'component', 'directive', 'filter' ]
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null);
});
// 这用于标识“基本”构造函数,以便在Weex的多实例场景中扩展所有纯对象组件。
Vue.options._base = Vue;
// 将 builtInComponents 的属性混入到 Vue.options.components 中
extend(Vue.options.components, builtInComponents); // extend() 将属性混合到目标对象中
/*
包装之后 Vue.options 结果如下:
Vue.options = {
components: {
KeepAlive
},
directives: Object.create(null),
filters: Object.create(null),
_base: Vue
}
*/
// 在 Vue 构造函数上添加 use 静态方法,全局API Vue.use
initUse(Vue);
// 在 Vue 构造函数上添加 mixins 静态方法,全局API Vue.mixins
initMixin(Vue);
// 在 Vue 构造函数上添加 Vue.cid 静态属性 extend 静态方法,全局API Vue.extend
initExtend(Vue);
// 在 Vue 构造函数上添加 三个 静态方法,分别用来全局注册组件,指令和过滤器
initAssetRegisters(Vue);
}
复制代码
接下来就其中细节部分分别展开讨论
来自 ../components/index
的 builtInComponents
实际只是导出了包含内置组件(keep-alive
)属性的对象
import KeepAlive from './keep-alive';
export default {
KeepAlive
};
复制代码
keep-alive
内容如下:
export default {
name: 'keep-alive',
abstract: true, // 是否是抽象组件
props: {
// ...
},
created() {
// ...
},
destroyed() {
// ...
},
mounted() {
// ...
},
render() {
// ...
}
};
复制代码
initUse
export function initUse(Vue: GlobalAPI) {
// 作用:安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。
// 如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。
Vue.use = function(plugin: Function | Object) {
// ...
};
}
复制代码
initMixin
export function initMixin(Vue: GlobalAPI) {
// 作用:全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。
// 插件作者可以使用混入,向组件注入自定义的行为。不推荐在应用代码中使用。
Vue.mixin = function(mixin: Object) {
// ...
};
}
复制代码
initExtend
export function initExtend(Vue: GlobalAPI) {
// 每个实例构造函数,包括Vue,都有一个惟一的cid。这使我们能够为原型继承创建包装的“子构造函数”并缓存它们。
Vue.cid = 0
let cid = 1
// 作用:使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
Vue.extend = function (extendOptions: Object): Function {
// ...
}
复制代码
initAssetRegisters
export function initAssetRegisters(Vue: GlobalAPI) {
// 创建 asset 注册方法
// ASSET_TYPES = [ 'component', 'directive', 'filter' ]
ASSET_TYPES.forEach(type => {
Vue[type] = function(
id: string,
definition: Function | Object
): Function | Object | void {
// ...
}
// Vue.component( id, [definition] ) 注册或获取全局组件。注册还会自动使用给定的id设置组件的名称
// Vue.directive( id, [definition] ) 注册或获取全局指令。
// Vue.filter( id, [definition] ) 注册或获取全局过滤器。
}
复制代码
断点调试
综上所述该文件主要做了一件事:包装 Vue
构造函数。
初始化前 - 安装特定于平台的 utils & 运行时指令和组件 & 定义公用的挂载方法 & 配置 devtools 全局钩子
/* @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, isChrome } 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';
/********* 安装特定于平台的utils **********/
Vue.config.mustUseProp = mustUseProp; // 检查属性是否必须使用属性绑定,例如,值与平台相关。
Vue.config.isReservedTag = isReservedTag; // 检查是否是保留标签,以便不能将其注册为组件。这是平台相关的,可能会被覆盖。
Vue.config.isReservedAttr = isReservedAttr; // 检查是否是保留属性,使其不能用作组件 prop。这是平台相关的,可能会被覆盖。
Vue.config.getTagNamespace = getTagNamespace; // 获取元素的名称空间
Vue.config.isUnknownElement = isUnknownElement; // 检查标记是否为未知元素。平台相关的。
/********* 安装特定于平台的utils **********/
/********* 安装平台运行时指令和组件 **********/
extend(Vue.options.directives, platformDirectives);
extend(Vue.options.components, platformComponents);
/*
对 Vue.options.directives/components 合并包装之后:
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: Object.create(null),
_base: Vue
}
*/
/********* 安装平台运行时指令和组件 **********/
Vue.prototype.__patch__ = inBrowser ? patch : noop; // 安装平台补丁功能
/**
* 公用的挂载方法
*
* @param {String | Element} el 挂载元素
* @param {Boolean} hydrating 用于 Virtual DOM 的补丁算法
* @returns {Function} 真正的挂载组件的方法
*/
Vue.prototype.$mount = function(
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating);
};
/************** 配置 devtools 全局钩子函数 与 开发提示 **************/
if (inBrowser) {
setTimeout(() => {
if (config.devtools) {
if (devtools) {
devtools.emit('init', Vue);
} else if (
process.env.NODE_ENV !== 'production' &&
process.env.NODE_ENV !== 'test' &&
isChrome
) {
console[console.info ? 'info' : 'log'](
'下载Vue Devtools扩展以获得更好的开发体验:\n' +
'https://github.com/vuejs/vue-devtools'
);
}
}
if (
process.env.NODE_ENV !== 'production' &&
process.env.NODE_ENV !== 'test' &&
config.productionTip !== false &&
typeof console !== 'undefined'
) {
console[console.info ? 'info' : 'log'](
`您正在以开发模式运行Vue。\n` +
`在部署生产时,请确保打开生产模式。\n` +
`详情请浏览 https://vuejs.org/guide/deployment.html`
);
}
}, 0);
}
/************** 配置 devtools 全局钩子函数 与 开发提示 **************/
export default Vue;
复制代码
platformDirectives
import model from './model';
import show from './show';
export default {
model,
show
};
复制代码
-
model
实现:const directive = { inserted (el, binding, vnode, oldVnode) { // ... } componentUpdated (el, binding, vnode) { // ... } }; export default directive; 复制代码
-
show
实现:export default { bind(el: any, { value }: VNodeDirective, vnode: VNodeWithData) { // ... }, update(el: any, { value, oldValue }: VNodeDirective, vnode: VNodeWithData) { // ... }, unbind( el: any, binding: VNodeDirective, vnode: VNodeWithData, oldVnode: VNodeWithData, isDestroy: boolean ) { // ... } }; 复制代码
platformComponents
import Transition from './transition';
import TransitionGroup from './transition-group';
export default {
Transition,
TransitionGroup
};
复制代码
-
Transition
实现:export const transitionProps = { name: String, appear: Boolean, css: Boolean, mode: String, type: String, enterClass: String, leaveClass: String, enterToClass: String, leaveToClass: String, enterActiveClass: String, leaveActiveClass: String, appearClass: String, appearActiveClass: String, appearToClass: String, duration: [Number, String, Object] }; export default { name: 'transition', props: transitionProps, abstract: true, render(h: Function) { // ... } }; 复制代码
-
TransitionGroup
实现:const props = extend( { tag: String, moveClass: String }, transitionProps ); export default { props, beforeMount() { // ... }, render(h: Function) { // ... }, updated() { // ... }, methods: { hasMove(el: any, moveClass: string): boolean { // ... } } }; 复制代码
断点调试
综上所述该文件主要对 Vue.config
进行扩展、 对 Vue.options.directives/components
进行合并包装、添加公用的挂载方法 $mount
、配置 devtools
全局钩子函数。
初始化前 - 重写 $mount
函数,给运行时版的 $mount
函数增加编译模板的能力
import config from 'core/config';
import { warn, cached } from 'core/util/index';
import { mark, measure } from 'core/util/perf';
import Vue from './runtime/index';
import { query } from './util/index';
import { compileToFunctions } from './compiler/index';
import {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref
} from './util/compat';
const mount = Vue.prototype.$mount; // 缓存运行时版的 $mount 函数
// 重写 $mount 函数,给运行时版的 $mount 函数增加编译模板的能力
Vue.prototype.$mount = function(
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el); // 处理 挂载点
// 过滤 body html
if (el === document.body || el === document.documentElement /*html*/) {
process.env.NODE_ENV !== 'production' &&
warn(
`Do not mount Vue to or - mount to normal elements instead.`
);
return this;
}
/*************** 解析模板/el并转换为render函数 ***************/
const options = this.$options;
if (!options.render) {
let template = options.template; // 获取合适的内容作为模板(template)
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
// 把该字符串作为 css 选择符去选中对应的元素,并把该元素的 innerHTML 作为模板
template = idToTemplate(template);
if (process.env.NODE_ENV !== 'production' && !template) {
warn(`模板元素未找到或为空: ${options.template}`, this);
}
}
} else if (template.nodeType) {
// 元素节点
template = template.innerHTML;
} else {
if (process.env.NODE_ENV !== 'production') {
warn('无效的模板选项:' + template, this);
}
return this;
}
} else if (el) {
template = getOuterHTML(el); // el 选项指定的挂载点将被作为组件模板
}
if (template) {
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile');
}
/*************** 将模板(template)字符串编译为渲染函数 ***************/
const { render, staticRenderFns } = compileToFunctions(
template,
{
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
},
this
);
options.render = render;
options.staticRenderFns = staticRenderFns;
/*************** 将模板(template)字符串编译为渲染函数 ***************/
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end');
measure(`vue ${this._name} compile`, 'compile', 'compile end');
}
}
}
/*************** 解析模板/el并转换为render函数 ***************/
return mount.call(this, el, hydrating);
};
/**
* 获取元素的outerHTML,并在IE中处理SVG元素。
*/
function getOuterHTML(el: Element): string {
// IE9-11 中 SVG 标签元素是没有 innerHTML 和 outerHTML 这两个属性
if (el.outerHTML) {
return el.outerHTML;
} else {
const container = document.createElement('div');
container.appendChild(el.cloneNode(true)); // 返回调用该方法的节点的一个副本(是否深度克隆)
return container.innerHTML;
}
}
/**
* 根据 ID 获取或替换 HTML 元素的内容
*/
const idToTemplate = cached(id => {
const el = query(id);
return el && el.innerHTML;
});
Vue.compile = compileToFunctions;
export default Vue;
复制代码
总结: 跟着程序执行过程看下来,整个初始化的过程就是对 Vue 构造函数的包装与丰富。
本部分内容旨在梳理初始化的全过程,对其中全局 API 及方法实现并未细化。
承接上文 - 「试着读读Vue源代码」工程目录及本地运行(断点调试)
承接下文 - 「试着读读 Vue 源代码」new Vue()发生了什么 ❓