首先這篇文章是讀 vue.js
源代碼的梳理性文章,文章分塊梳理,記錄着本身的一些理解及大體過程;更重要的一點是但願在 vue.js 3.0
發佈前深刻的瞭解其原理。html
若是你從未看過或者接觸過 vue.js
源代碼,建議你參考如下列出的 vue.js
解析的相關文章,由於這些文章更細緻的講解了這個工程,本文只是以一些 demo
演示某一功能點或 API
實現,力求簡要梳理過程。vue
若是搞清楚了工程目錄及入口,建議直接去看代碼,這樣比較高效 ( 遇到難以理解對應着回來看看別人的講解,加以理解便可 )node
文章所涉及到的代碼,基本都是縮減版,具體還請參閱 vue.js - 2.5.17。git
若有任何疏漏和錯誤之處歡迎指正、交流。github
/** * 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 源代碼」初始化先後作了哪些事情❓。數組
_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
具體代碼實現,就不過多贅述了; 接下來着重看被監控的幾個步驟主要作了什麼?markdown
若是就單單看代碼,可能就不太直觀且不易理解;不如直接用 Demo 代入斷點調試看看每一步是如何作的,那將會使你對代碼的運行有更直觀的理解與認識。app
<!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
參數以下斷點圖所:dom
根據上述 Demo
咱們着重分析執行代碼即 mergeOptions
函數,根據代碼可知該函數是對咱們傳入的options
作了一層處理,而後賦值給實例屬性$options
。ide
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源代碼」響應式系統是如何構建的❓待續...