首先這篇文章是讀 vue.js
源代碼的梳理性文章,文章分塊梳理,記錄着本身的一些理解及大體過程;更重要的一點是但願在 vue.js 3.0
發佈前深刻的瞭解其原理。css
若是你從未看過或者接觸過 vue.js
源代碼,建議你參考如下列出的 vue.js
解析的相關文章,由於這些文章更細緻的講解了這個工程,本文只是以一些 demo
演示某一功能點或 API
實現,力求簡要梳理過程。html
若是搞清楚了工程目錄及入口,建議直接去看代碼,這樣比較高效 ( 遇到難以理解對應着回來看看別人的講解,加以理解便可 )vue
文章所涉及到的代碼,基本都是縮減版,具體還請參閱 vue.js - 2.5.17。node
JavaScript
自己是一種直譯式腳本語言,在找到入口後,主要須要理清其調用關係? 找出 Vue 構造函數的在哪定義了?按照這個邏輯,跟着程序一步一步走便可。github
首先src/platforms/web/entry-runtime-with-compiler.js
web
這個文件最開始,引入一些方法與配置,並導入了 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
api
這個文件也是引入一些方法與配置,並導入了 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';
如下代碼省略, 將在分析初始化時展開...
複製代碼
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 構造函數的文件到底作了哪些事情
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;
複製代碼
該方法就作了一件事,在 Vue.prototype
添加 _init
方法。
export function initMixin(Vue: Class<Component>) {
Vue.prototype._init = function(options?: Object) {
// 代碼省略,在初始化會細緻分析
};
}
複製代碼
import {
set,
del,
observe,
defineReactive,
toggleObserving
} from '../observer/index';
...
export function stateMixin(Vue: Class<Component>) {
// 在使用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 {
// 代碼省略,在初始化會細緻分析
};
...
}
複製代碼
在 Vue.prototype
添加實例方法 / 事件相關:$on
/$once
/$off
/$emit
export function eventsMixin(Vue: Class<Component>) {
// 做用:監聽當前實例上的自定義事件。事件能夠由vm.$emit觸發。回調函數會接收全部傳入事件觸發函數的額外參數。
Vue.prototype.$on = function( event: string | Array<string>, fn: Function ): Component {
// ...
};
// 做用:監聽一個自定義事件,可是隻觸發一次,在第一次觸發以後移除監聽器
Vue.prototype.$once = function(event: string, fn: Function): Component {
// ...
};
// 做用:移除自定義事件監聽器。
Vue.prototype.$off = function( event?: string | Array<string>, fn?: Function ): Component {
// ...
};
// 做用:觸發當前實例上的事件。附加參數都會傳給監聽器回調。
Vue.prototype.$emit = function(event: string): Component {
// ...
};
}
複製代碼
在 Vue.prototype
添加實例方法 / 生命週期相關:_update
/$forceUpdate
/$destroy
export function lifecycleMixin(Vue: Class<Component>) {
Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) {
// ...
}
// 做用:迫使 Vue 實例從新渲染。注意它僅僅影響實例自己和插入插槽內容的子組件,而不是全部子組件。
Vue.prototype.$forceUpdate = function() {
// ...
}
// 做用:徹底銷燬一個實例。清理它與其它實例的鏈接,解綁它的所有指令及事件監聽器。
Vue.prototype.$destroy = function() {
// ...
}
複製代碼
在 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<Component>) {
installRenderHelpers(Vue.prototype); // 安裝運行時方便助手
Vue.prototype.$nextTick = function(fn: Function) {
return nextTick(fn, this);
};
Vue.prototype._render = function(): VNode {
// ...
};
}
複製代碼
斷點調試
綜上所述該文件主要作了兩件事:定義 Vue
構造函數、包裝 Vue.prototype
。
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;
複製代碼
初始化全局 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() {
// ...
}
};
複製代碼
export function initUse(Vue: GlobalAPI) {
// 做用:安裝 Vue.js 插件。若是插件是一個對象,必須提供 install 方法。
// 若是插件是一個函數,它會被做爲 install 方法。install 方法調用時,會將 Vue 做爲參數傳入。
Vue.use = function(plugin: Function | Object) {
// ...
};
}
複製代碼
export function initMixin(Vue: GlobalAPI) {
// 做用:全局註冊一個混入,影響註冊以後全部建立的每一個 Vue 實例。
// 插件做者可使用混入,向組件注入自定義的行爲。不推薦在應用代碼中使用。
Vue.mixin = function(mixin: Object) {
// ...
};
}
複製代碼
export function initExtend(Vue: GlobalAPI) {
// 每一個實例構造函數,包括Vue,都有一個唯一的cid。這使咱們可以爲原型繼承建立包裝的「子構造函數」並緩存它們。
Vue.cid = 0
let cid = 1
// 做用:使用基礎 Vue 構造器,建立一個「子類」。參數是一個包含組件選項的對象。
Vue.extend = function (extendOptions: Object): Function {
// ...
}
複製代碼
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
構造函數。
/* @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;
複製代碼
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
) {
// ...
}
};
複製代碼
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) {
// ...
}
};
複製代碼
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 <html> or <body> - 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源代碼」工程目錄及本地運行(斷點調試)