首先這篇文章是讀 vue.js
源代碼的梳理性文章,文章分塊梳理,記錄着本身的一些理解及大體過程;更重要的一點是但願在 vue.js 3.0
發佈前深刻的瞭解其原理。javascript
若是你從未看過或者接觸過 vue.js
源代碼,建議你參考如下列出的 vue.js
解析的相關文章,由於這些文章更細緻的講解了這個工程,本文只是以一些 demo
演示某一功能點或 API
實現,力求簡要梳理過程。html
若是搞清楚了工程目錄及入口,建議直接去看代碼,這樣比較高效 ( 遇到難以理解對應着回來看看別人的講解,加以理解便可 )vue
文章所涉及到的代碼,基本都是縮減版,具體還請參閱 vue.js - 2.5.17。java
/** * Vue構造函數 * * @param {*} options 選項參數 */
function Vue(options) {
if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
warn('Vue是一個構造函數,應該用「new」關鍵字調用');
}
this._init(options);
}
複製代碼
咱們知道 new Vue()
將執行 Vue
構造函數, 進而執行 _init()
, 那 _init
方法從何處而來?答案是Vue
在初始化時添加了該方法,若是你對初始化還不是很清楚,建議你參考上文對初始化過程的梳理性文章:「試着讀讀 Vue 源代碼」初始化先後作了哪些事情❓。git
_init()
import config from '../config';
import { initProxy } from './proxy';
import { initState } from './state';
import { initRender } from './render';
import { initEvents } from './events';
import { mark, measure } from '../util/perf';
import { initLifecycle, callHook } from './lifecycle';
import { initProvide, initInjections } from './inject';
import { extend, mergeOptions, formatComponentName } from '../util/index';
let uid = 0;
export function initMixin(Vue: Class<Component>) {
Vue.prototype._init = function(options?: Object) {
const vm: Component = this; // 當前 Vue 實例
vm._uid = uid++; // 當前 Vue 實例惟一標識
/**************************** 非生產環境下進行性能監控 --- start ****************************/
let startTag, endTag;
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`;
endTag = `vue-perf-end:${vm._uid}`;
mark(startTag);
}
vm._isVue = true; // 一個標誌,避免該對象被響應系統觀測
/****************** 對 Vue 提供的 props、data、methods等選項進行合併處理 ******************/
// _isComponent 內部選項:在 Vue 建立組件的時候纔會生成
if (options && options._isComponent) {
initInternalComponent(vm, options); // 優化內部組件實例化,由於動態選項合併不是常慢,並且沒有一個內部組件選項須要特殊處理。
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor), // parentVal
options || {}, // childVal
vm
);
}
// 設置渲染函數的做用域代理,其目的是提供更好的提示信息(如:在模板內訪問實例不存在的屬性,則會在非生產環境下提供準確的報錯信息)
if (process.env.NODE_ENV !== 'production') {
initProxy(vm);
} else {
vm._renderProxy = vm;
}
vm._self = vm; // 暴露真實的實例自己
/**************************** 執行相關初始化程序及調用初期生命週期函數 ****************************/
initLifecycle(vm); // 初始化生命週期
initEvents(vm); // 初始化事件
initRender(vm); // 初始化渲染
callHook(vm, 'beforeCreate'); // 調用生命週期鉤子函數 -- beforeCreate
initInjections(vm); // resolve injections before data/props
initState(vm); // 初始化 initProps、initMethods、initData、initComputed、initWatch
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created'); // 此時尚未任何掛載的操做,因此在 created 中是不能訪問DOM的,即不能訪問 $el
/**************************** 非生產環境下進行性能監控 --- end ****************************/
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false);
mark(endTag);
measure(`vue ${vm._name} init`, startTag, endTag);
}
/**************************** 根據掛載點,調用掛載函數 ****************************/
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
}
複製代碼
_init
方法所作的事情可大概梳理出如下要點:
注:性能監控:利用
Web Performance API
容許網頁訪問某些函數來測量網頁和Web
應用程序的性能; 這裏是Vue - mark、measure
具體代碼實現,就不過多贅述了; 接下來着重看被監控的幾個步驟主要作了什麼?github
若是就單單看代碼,可能就不太直觀且不易理解;不如直接用 Demo 代入斷點調試看看每一步是如何作的,那將會使你對代碼的運行有更直觀的理解與認識。數組
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>vue.js DEMO</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app">
<p>計算屬性:{{messageTo}}</p>
<p>數據屬性:{{ message }}</p>
<button @click="update">更新</button>
<item v-for="item in list" :msg="item" :key="item" @rm="remove(item)" />
</div>
<script> new Vue({ el: '#app', components: { item: { props: ['msg'], template: `<div style="margin-top: 20px;">{{ msg }} <button @click="$emit('rm')">x</button></div>`, created() { console.log('---componentA - 組件生命週期鉤子執行 created---'); } } }, mixins: [ { created() { console.log('---created - mixins---'); }, methods: { remove(item) { console.log('響應移除:', item); } } } ], data: { message: 'hello vue.js', list: ['hello,', 'the updated', 'vue.js'], obj: { a: 1, b: { c: 2, d: 3 } } }, computed: { messageTo() { return `${this.message} !;`; } }, watch: { message(val, oldVal) { console.log(val, oldVal, 'message - 改變了'); } }, methods: { update() { this.message = `${this.list.join(' ')} ---- ${Math.random()}`; } } }); </script>
</body>
</html>
複製代碼
根據上述 demo
斷點進入 Vue
構造函數 options
參數以下斷點圖所:app
根據上述 Demo
咱們着重分析執行代碼即 mergeOptions
函數,根據代碼可知該函數是對咱們傳入的options
作了一層處理,而後賦值給實例屬性$options
。dom
resolveConstructorOptions
, 該函數主要判斷構造函數是否存在父類,若存在父類須要對 vm.constructor.options
進行處理返回,若不存在直接返回vm.constructor.options
; 根據上述Demo
直接返回 vm.constructor.options
。
注:在上文初始化過程對 vm.constructor.options
進行處理,其結果爲:
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: Object.create(null),
_base: Vue
};
複製代碼
// _isComponent 內部選項:在 Vue 建立組件的時候纔會生成
if (options && options._isComponent) {
initInternalComponent(vm, options); // 優化內部組件實例化,由於動態選項合併不是常慢,並且沒有一個內部組件選項須要特殊處理。
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor), // parentVal
options || {}, // childVal
vm
);
}
複製代碼
根據上述分析,程序進入 mergeOptions
函數內部,下面斷點圖展現了該函數的入參:
mergeOptions
將兩個 option
對象合併到一個新的 options
,用於實例化和繼承的核心實用程序中。
export function mergeOptions( parent: Object, child: Object, vm?: Component ): Object {
// 校驗組件的名字是否符合要求:
// 限定組件的名字由普通的字符和中橫線(-)組成,且必須以字母開頭。
// 檢測是不是內置的標籤(如:slot) || 檢測是不是保留標籤(html、svg等)。
if (process.env.NODE_ENV !== 'production') {
checkComponents(child);
}
// 若是 child 是一個函數的話,去其靜態屬性 options 重寫 child;
if (typeof child === 'function') {
child = child.options;
}
/************************ 規範化處理 ************************/
normalizeProps(child, vm);
normalizeInject(child, vm);
normalizeDirectives(child);
/************************ extends/mixins 遞歸處理合並 ************************/
const extendsFrom = child.extends;
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, vm);
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm);
}
}
/************************ 合併階段 ************************/
const options = {};
let key;
for (key in parent) {
mergeField(key);
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
function mergeField(key) {
const strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
return options;
}
複製代碼
normalizeProps(child, vm);
normalizeInject(child, vm);
normalizeDirectives(child);
複製代碼
上述代碼主要對 Vue 選項進行規範化處理,咱們知道 Vue 的選項支持多種寫法,但最終都須要化爲統一格式,進行處理。 下面所列出的是各類寫法與規範化以後的對比; 上述代碼實現就不過多論述了,可直接根據上述導航到代碼段去看便可。
Props:
props: ['size', 'myMessage']
props: { height: Number }
props: { height: { type: Number, default: 0 } }
props: { size: { type: null }, myMessage: { type: null } }
props: { height: { type: Number } }
props: { height: { type: Number, default: 0 } }
Inject:
inject: ['foo']
,inject: { bar: 'foo' }
inject: { foo: { from: 'foo' } }
inject: { bar: { from: 'foo' } }
Directives:
directives: { foo: function() { console.log('自定義指令: v-foo') }
directives: { foo: { bind: function() { console.log('v-foo'), update: function() { console.log('v-foo') } } }
代碼到執行到這裏,將開始真正的合併了,最終返回合併以後的options
。
const options = {};
let key;
for (key in parent) {
mergeField(key);
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
function mergeField(key) {
const strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
return options;
複製代碼
這裏特別說明一下,Vue
爲每個選項合併都提供了選項合併的策略函數,strats
變量存放着這些函數。這裏就不分別對每一個策略函數進行展開論述了。
const defaultStrat = function(parentVal: any, childVal: any): any {
return childVal === undefined ? parentVal : childVal;
};
export function mergeDataOrFn( parentVal: any, childVal: any, vm?: Component ): ?Function {
// ...
}
// optionMergeStrategies: Object.create(null),
const strats = config.optionMergeStrategies;
// el / propsData 合併策略函數
if (process.env.NODE_ENV !== 'production') {
strats.el = strats.propsData = function(parent, child, vm, key) {
// ...
};
}
// data 合併策略函數
strats.data = function( parentVal: any, childVal: any, vm?: Component ): ?Function {
// ...
};
// watch 合併策略函數
strats.watch = function( parentVal: ?Object, childVal: ?Object, vm?: Component, key: string ): ?Object {
// ...
};
// props、methods、inject、computed 合併策略函數
strats.props = strats.methods = strats.inject = strats.computed = function( parentVal: ?Object, childVal: ?Object, vm?: Component, key: string ): ?Object {
// ...
};
// provide 合併策略函數
strats.provide = mergeDataOrFn;
複製代碼
根據上述分析, mergeOptions
函數將返回規範化,且合併以後options
,下面斷點圖展現了合併以後的options
:
initLifecycle(vm); // 初始化生命週期
initEvents(vm); // 初始化事件
initRender(vm); // 初始化渲染
callHook(vm, 'beforeCreate'); // 調用生命週期鉤子函數 -- beforeCreate
initInjections(vm); // resolve injections before data/props
initState(vm); // 初始化 initProps、initMethods、initData、initComputed、initWatch
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created'); // 此時尚未任何掛載的操做,因此在 created 中是不能訪問DOM的,即不能訪問 $el
複製代碼
initLifecycle
$children
屬性裏$parent
爲父實例export function initLifecycle(vm: Component) {
const options = vm.$options;
/** * abstract - 是不是抽象組件 * 抽象組件: 它自身不會渲染一個 DOM 元素,也不會出如今父組件鏈中。(如 keep-alive transition ) */
let parent = options.parent;
if (parent && !options.abstract) {
// 循環查找第一個非抽象的父組件
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent;
}
parent.$children.push(vm);
}
vm.$parent = parent;
vm.$root = parent ? parent.$root : vm;
vm.$children = [];
vm.$refs = {};
vm._watcher = null;
vm._inactive = null;
vm._directInactive = false;
vm._isMounted = false;
vm._isDestroyed = false;
vm._isBeingDestroyed = false;
}
複製代碼
initEvents
export function initEvents(vm: Component) {
// 在當前實例添加 `_events` `_hasHookEvent` 屬性
vm._events = Object.create(null);
vm._hasHookEvent = false; // 用於判斷是否存在生命週期鉤子的事件偵聽器
const listeners = vm.$options._parentListeners; // 初始化父附加事件
if (listeners) {
updateComponentListeners(vm, listeners);
}
}
複製代碼
initRender
export function initRender(vm: Component) {
vm._vnode = null; // the root of the child tree
vm._staticTrees = null; // v-once cached trees
/*************************** 解析並處理 slot **************************/
const options = vm.$options;
const parentVnode = (vm.$vnode = options._parentVnode); // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context;
vm.$slots = resolveSlots(options._renderChildren, renderContext);
vm.$scopedSlots = emptyObject;
/*************************** 包裝 createElement() **************************/
// render: (createElement: () => VNode) => VNode createElement
// 將createElement fn綁定到這個實例,以便在其中得到適當的呈現上下文。
// args順序:標籤、數據、子元素、normalizationType、alwaysNormalize內部版本由模板編譯的呈現函數使用
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false);
// 規範化老是應用於公共版本,用於用戶編寫的呈現函數。
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true);
/*************************** 在實例添加 $attrs/$listeners **************************/
// $attrs和$listeners 用於更容易的臨時建立。它們須要是反應性的,以便使用它們的 HOC 老是被更新
const parentData = parentVnode && parentVnode.data;
if (process.env.NODE_ENV !== 'production') {
// 定義響應式的屬性
defineReactive(
vm,
'$attrs',
(parentData && parentData.attrs) || emptyObject,
() => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm);
},
true
);
defineReactive(
vm,
'$listeners',
options._parentListeners || emptyObject,
() => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm);
},
true
);
} else {
defineReactive(
vm,
'$attrs',
(parentData && parentData.attrs) || emptyObject,
null,
true
);
defineReactive(
vm,
'$listeners',
options._parentListeners || emptyObject,
null,
true
);
}
/*************************** 在實例添加 $attrs/$listeners **************************/
}
複製代碼
callHook
export function callHook(vm: Component, hook: string) {
pushTarget(); // 爲了不在某些生命週期鉤子中使用 props 數據致使收集冗餘的依賴 #7573
const handlers = vm.$options[hook];
if (handlers) {
// 在合併選項處理時:生命週期鉤子選項會被合併處理成一個數組
for (let i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm);
} catch (e) {
// 捕獲生命週期函數執行過程當中可能拋出的異常
handleError(e, vm, `${hook} hook`);
}
}
}
// 判斷是否存在生命週期鉤子的事件偵聽器,在 initEvents 中初始化,若存在觸發響應鉤子函數
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook);
}
popTarget();
}
複製代碼
這裏額外提一下: 可使用 hook: 加 生命週期鉤子名稱 的方式來監聽組件相應的生命週期
<child @hook:beforeCreate="handleChildBeforeCreate" @hook:created="handleChildCreated" @hook:mounted="handleChildMounted" @hook:生命週期鉤子名稱 />
複製代碼
initInjections
export function initInjections(vm: Component) {
const result = resolveInject(vm.$options.inject, vm); // 做用:尋找父代組件提供的數據
if (result) {
// provide 和 inject 綁定並非可響應的。
// 這是刻意爲之的。然而,若是你傳入了一個可監聽的對象,那麼其對象的屬性仍是可響應的。
toggleObserving(false); // 關閉響應式檢測
Object.keys(result).forEach(key => {
// 對每一個屬性定義響應式屬性,並在非生產環境下,提供警告程序。
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
`避免直接修改注入的值,由於當提供的組件從新呈現時,更改將被覆蓋。正在修改的注入:「${key}」`,
vm
);
});
} else {
defineReactive(vm, key, result[key]);
}
});
toggleObserving(true); // 開啓響應式檢測
}
}
複製代碼
initState
/** * 初始化 props/ methods/ data/ computed/ watch/ 等選項。 */
export function initState(vm: Component) {
vm._watchers = [];
const opts = vm.$options;
if (opts.props) initProps(vm, opts.props);
if (opts.methods) initMethods(vm, opts.methods);
if (opts.data) {
initData(vm);
} else {
observe((vm._data = {}), true /* asRootData */);
}
if (opts.computed) initComputed(vm, opts.computed);
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
複製代碼
注: 這裏只是簡單展現了其初始化順序,其內部各個初始化方法將在
構建響應式系統
深挖。 這裏只須要明白一點,即初始化順序:props
=>methods
=>data
=>computed
=>watch
(根據上述順序,天然也就知道,爲何能夠在data
選項中使用props
去初始化值)
initProvide
export function initProvide(vm: Component) {
const provide = vm.$options.provide;
if (provide) {
vm._provided = typeof provide === 'function' ? provide.call(vm) : provide;
}
}
複製代碼
上述初始化部分的分析,只是簡單的梳理了其執行過程,若是想對其內部實現作更爲細緻的認識,能夠自行去看看代碼實現或上述說明提到的源碼解析的相關文章。
若存在掛載點,則執行掛載函數,渲染組件。掛載函數如何執行,實現機制如何,將在後文慢慢梳理出來。
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
複製代碼
總結:全文梳理了執行 new Vue()
調用 _init()
方法,接着又跟着代碼執行過程探討了內部實現。
承接上文 - 「試着讀讀 Vue 源代碼」初始化先後作了哪些事❓
承接下文 - 「試着讀讀Vue源代碼」響應式系統是如何構建的❓待續...