目標
深入理解以下全域性 API 的實現原理。
-
Vue.use
-
Vue.mixin
-
Vue.component
-
Vue.filter
-
Vue.directive
-
Vue.extend
-
Vue.set
-
Vue.delete
-
Vue.nextTick
原始碼解讀
從該系列的第一篇文章 Vue 原始碼解讀(1)—— 前言 中的 原始碼目錄結構
介紹中可以得知,Vue 的眾多全域性 API 的實現大部分都放在 /src/core/global-api
目錄下。這些全域性 API 原始碼閱讀入口則是在 /src/core/global-api/index.js
檔案中。
入口
/src/core/global-api/index.js
/**
* 初始化 Vue 的眾多全域性 API,比如:
* 預設配置:Vue.config
* 工具方法:Vue.util.xx
* Vue.set、Vue.delete、Vue.nextTick、Vue.observable
* Vue.options.components、Vue.options.directives、Vue.options.filters、Vue.options._base
* Vue.use、Vue.extend、Vue.mixin、Vue.component、Vue.directive、Vue.filter
*
*/
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
// Vue 的眾多預設配置項
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
// Vue.config
Object.defineProperty(Vue, 'config', configDef)
/**
* 暴露一些工具方法,輕易不要使用這些工具方法,處理你很清楚這些工具方法,以及知道使用的風險
*/
Vue.util = {
// 警告日誌
warn,
// 類似選項合併
extend,
// 合併選項
mergeOptions,
// 設定響應式
defineReactive
}
// Vue.set / delete / nextTick
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 響應式方法
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
// Vue.options.compoents/directives/filter
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
// 將 Vue 建構函式掛載到 Vue.options._base 上
Vue.options._base = Vue
// 在 Vue.options.components 中新增內建元件,比如 keep-alive
extend(Vue.options.components, builtInComponents)
// Vue.use
initUse(Vue)
// Vue.mixin
initMixin(Vue)
// Vue.extend
initExtend(Vue)
// Vue.component/directive/filter
initAssetRegisters(Vue)
}
Vue.use
/src/core/global-api/use.js
/**
* 定義 Vue.use,負責為 Vue 安裝外掛,做了以下兩件事:
* 1、判斷外掛是否已經被安裝,如果安裝則直接結束
* 2、安裝外掛,執行外掛的 install 方法
* @param {*} plugin install 方法 或者 包含 install 方法的物件
* @returns Vue 例項
*/
Vue.use = function (plugin: Function | Object) {
// 已經安裝過的外掛列表
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
// 判斷 plugin 是否已經安裝,保證不重複安裝
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// 將 Vue 建構函式放到第一個引數位置,然後將這些引數傳遞給 install 方法
const args = toArray(arguments, 1)
args.unshift(this)
if (typeof plugin.install === 'function') {
// plugin 是一個物件,則執行其 install 方法安裝外掛
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
// 執行直接 plugin 方法安裝外掛
plugin.apply(null, args)
}
// 在 外掛列表中 新增新安裝的外掛
installedPlugins.push(plugin)
return this
}
Vue.mixin
/src/core/global-api/mixin.js
/**
* 定義 Vue.mixin,負責全域性混入選項,影響之後所有建立的 Vue 例項,這些例項會合並全域性混入的選項
* @param {*} mixin Vue 配置物件
* @returns 返回 Vue 例項
*/
Vue.mixin = function (mixin: Object) {
// 在 Vue 的預設配置項上合併 mixin 物件
this.options = mergeOptions(this.options, mixin)
return this
}
mergeOptions
src/core/util/options.js
/**
* 合併兩個選項,出現相同配置項時,子選項會覆蓋父選項的配置
*/
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
// 標準化 props、inject、directive 選項,方便後續程式的處理
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
// 處理原始 child 物件上的 extends 和 mixins,分別執行 mergeOptions,將這些繼承而來的選項合併到 parent
// mergeOptions 處理過的物件會含有 _base 屬性
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, 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)
}
}
// 合併選項,childVal 優先順序高於 parentVal
function mergeField (key) {
// strat 是合併策略函式,如何 key 衝突,則 childVal 會 覆蓋 parentVal
const strat = strats[key] || defaultStrat
// 值為如果 childVal 存在則優先使用 childVal,否則使用 parentVal
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
Vue.component、Vue.filter、Vue.directive
/src/core/global-api/assets.js
這三個 API 實現比較特殊,但是原理又很相似,所以就放在了一起實現。
const ASSET_TYPES = ['component', 'directive', 'filter']
/**
* 定義 Vue.component、Vue.filter、Vue.directive 這三個方法
* 這三個方法所做的事情是類似的,就是在 this.options.xx 上存放對應的配置
* 比如 Vue.component(compName, {xx}) 結果是 this.options.components.compName = 元件建構函式
* ASSET_TYPES = ['component', 'directive', 'filter']
*/
ASSET_TYPES.forEach(type => {
/**
* 比如:Vue.component(name, definition)
* @param {*} id name
* @param {*} definition 元件建構函式或者配置物件
* @returns 返回元件建構函式
*/
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
if (type === 'component' && isPlainObject(definition)) {
// 如果元件配置中存在 name,則使用,否則直接使用 id
definition.name = definition.name || id
// extend 就是 Vue.extend,所以這時的 definition 就變成了 元件建構函式,使用時可直接 new Definition()
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
// this.options.components[id] = definition
// 在例項化時通過 mergeOptions 將全域性註冊的元件合併到每個元件的配置物件的 components 中
this.options[type + 's'][id] = definition
return definition
}
}
})
Vue.extend
/src/core/global-api/extend.js
/**
* Each instance constructor, including Vue, has a unique
* cid. This enables us to create wrapped "child
* constructors" for prototypal inheritance and cache them.
*/
Vue.cid = 0
let cid = 1
/**
* 基於 Vue 去擴充套件子類,該子類同樣支援進一步的擴充套件
* 擴充套件時可以傳遞一些預設配置,就像 Vue 也會有一些預設配置
* 預設配置如果和基類有衝突則會進行選項合併(mergeOptions)
*/
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
/**
* 利用快取,如果存在則直接返回快取中的建構函式
* 什麼情況下可以利用到這個快取?
* 如果你在多次呼叫 Vue.extend 時使用了同一個配置項(extendOptions),這時就會啟用該快取
*/
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name)
}
// 定義 Sub 建構函式,和 Vue 建構函式一樣
const Sub = function VueComponent(options) {
// 初始化
this._init(options)
}
// 通過原型繼承的方式繼承 Vue
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
// 選項合併,合併 Vue 的配置項到 自己的配置項上來
Sub.options = mergeOptions(
Super.options,
extendOptions
)
// 記錄自己的基類
Sub['super'] = Super
// 初始化 props,將 props 配置代理到 Sub.prototype._props 物件上
// 在元件內通過 this._props 方式可以訪問
if (Sub.options.props) {
initProps(Sub)
}
// 初始化 computed,將 computed 配置代理到 Sub.prototype 物件上
// 在元件內可以通過 this.computedKey 的方式訪問
if (Sub.options.computed) {
initComputed(Sub)
}
// 定義 extend、mixin、use 這三個靜態方法,允許在 Sub 基礎上再進一步構造子類
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// 定義 component、filter、directive 三個靜態方法
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// 遞迴元件的原理,如果元件設定了 name 屬性,則將自己註冊到自己的 components 選項中
if (name) {
Sub.options.components[name] = Sub
}
// 在擴充套件時保留對基類選項的引用。
// 稍後在例項化時,我們可以檢查 Super 的選項是否具有更新
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// 快取
cachedCtors[SuperId] = Sub
return Sub
}
function initProps (Comp) {
const props = Comp.options.props
for (const key in props) {
proxy(Comp.prototype, `_props`, key)
}
}
function initComputed (Comp) {
const computed = Comp.options.computed
for (const key in computed) {
defineComputed(Comp.prototype, key, computed[key])
}
}
Vue.set
/src/core/global-api/index.js
Vue.set = set
set
/src/core/observer/index.js
/**
* 通過 Vue.set 或者 this.$set 方法給 target 的指定 key 設定值 val
* 如果 target 是物件,並且 key 原本不存在,則為新 key 設定響應式,然後執行依賴通知
*/
export function set (target: Array<any> | Object, key: any, val: any): any {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 更新陣列指定下標的元素,Vue.set(array, idx, val),通過 splice 方法實現響應式更新
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// 更新物件已有屬性,Vue.set(obj, key, val),執行更新即可
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
// 不能向 Vue 例項或者 $data 新增動態新增響應式屬性,vmCount 的用處之一,
// this.$data 的 ob.vmCount = 1,表示根元件,其它子元件的 vm.vmCount 都是 0
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// target 不是響應式物件,新屬性會被設定,但是不會做響應式處理
if (!ob) {
target[key] = val
return val
}
// 給物件定義新屬性,通過 defineReactive 方法設定響應式,並觸發依賴更新
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
Vue.delete
/src/core/global-api/index.js
Vue.delete = del
del
/src/core/observer/index.js
/**
* 通過 Vue.delete 或者 vm.$delete 刪除 target 物件的指定 key
* 陣列通過 splice 方法實現,物件則通過 delete 運算子刪除指定 key,並執行依賴通知
*/
export function del (target: Array<any> | Object, key: any) {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// target 為陣列,則通過 splice 方法刪除指定下標的元素
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = (target: any).__ob__
// 避免刪除 Vue 例項的屬性或者 $data 的資料
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
// 如果屬性不存在直接結束
if (!hasOwn(target, key)) {
return
}
// 通過 delete 運算子刪除物件的屬性
delete target[key]
if (!ob) {
return
}
// 執行依賴通知
ob.dep.notify()
}
Vue.nextTick
/src/core/global-api/index.js
Vue.nextTick = nextTick
nextTick
/src/core/util/next-tick.js
關於 nextTick 方法更加詳細解析,可以檢視上一篇文章 Vue 原始碼解讀(4)—— 非同步更新。
const callbacks = []
/**
* 完成兩件事:
* 1、用 try catch 包裝 flushSchedulerQueue 函式,然後將其放入 callbacks 陣列
* 2、如果 pending 為 false,表示現在瀏覽器的任務佇列中沒有 flushCallbacks 函式
* 如果 pending 為 true,則表示瀏覽器的任務佇列中已經被放入了 flushCallbacks 函式,
* 待執行 flushCallbacks 函式時,pending 會被再次置為 false,表示下一個 flushCallbacks 函式可以進入
* 瀏覽器的任務佇列了
* pending 的作用:保證在同一時刻,瀏覽器的任務佇列中只有一個 flushCallbacks 函式
* @param {*} cb 接收一個回撥函式 => flushSchedulerQueue
* @param {*} ctx 上下文
* @returns
*/
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 用 callbacks 陣列儲存經過包裝的 cb 函式
callbacks.push(() => {
if (cb) {
// 用 try catch 包裝回撥函式,便於錯誤捕獲
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
// 執行 timerFunc,在瀏覽器的任務佇列中(首選微任務佇列)放入 flushCallbacks 函式
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
總結
-
面試官 問:Vue.use(plugin) 做了什麼?
答:
負責安裝 plugin 外掛,其實就是執行外掛提供的 install 方法。
-
首先判斷該外掛是否已經安裝過
-
如果沒有,則執行外掛提供的 install 方法安裝外掛,具體做什麼有外掛自己決定
-
-
面試官 問:Vue.mixin(options) 做了什麼?
答:
負責在 Vue 的全域性配置上合併 options 配置。然後在每個元件生成 vnode 時會將全域性配置合併到元件自身的配置上來。
-
標準化 options 物件上的 props、inject、directive 選項的格式
-
處理 options 上的 extends 和 mixins,分別將他們合併到全域性配置上
-
然後將 options 配置和全域性配置進行合併,選項衝突時 options 配置會覆蓋全域性配置
-
-
面試官 問:Vue.component(compName, Comp) 做了什麼?
答:
負責註冊全域性元件。其實就是將元件配置註冊到全域性配置的 components 選項上(options.components),然後各個子元件在生成 vnode 時會將全域性的 components 選項合併到區域性的 components 配置項上。
-
如果第二個引數為空,則表示獲取 compName 的元件建構函式
-
如果 Comp 是元件配置物件,則使用 Vue.extend 方法得到元件建構函式,否則直接進行下一步
-
在全域性配置上設定元件資訊,
this.options.components.compName = CompConstructor
-
-
面試官 問:Vue.directive('my-directive', {xx}) 做了什麼?
答:
在全域性註冊 my-directive 指令,然後每個子元件在生成 vnode 時會將全域性的 directives 選項合併到區域性的 directives 選項中。原理同 Vue.component 方法:
-
如果第二個引數為空,則獲取指定指令的配置物件
-
如果不為空,如果第二個引數是一個函式的話,則生成配置物件 { bind: 第二個引數, update: 第二個引數 }
-
然後將指令配置物件設定到全域性配置上,
this.options.directives['my-directive'] = {xx}
-
-
面試官 問:Vue.filter('my-filter', function(val) {xx}) 做了什麼?
答:
負責在全域性註冊過濾器 my-filter,然後每個子元件在生成 vnode 時會將全域性的 filters 選項合併到區域性的 filters 選項中。原理是:
-
如果沒有提供第二個引數,則獲取 my-filter 過濾器的回撥函式
-
如果提供了第二個引數,則是設定
this.options.filters['my-filter'] = function(val) {xx}
。
-
-
面試官 問:Vue.extend(options) 做了什麼?
答:
Vue.extend 基於 Vue 建立一個子類,引數 options 會作為該子類的預設全域性配置,就像 Vue 的預設全域性配置一樣。所以通過 Vue.extend 擴充套件一個子類,一大用處就是內建一些公共配置,供子類的子類使用。
-
定義子類建構函式,這裡和 Vue 一樣,也是呼叫 _init(options)
-
合併 Vue 的配置和 options,如果選項衝突,則 options 的選項會覆蓋 Vue 的配置項
-
給子類定義全域性 API,值為 Vue 的全域性 API,比如
Sub.extend = Super.extend
,這樣子類同樣可以擴充套件出其它子類 -
返回子類 Sub
-
-
面試官 問:Vue.set(target, key, val) 做了什麼
答:
由於 Vue 無法探測普通的新增 property (比如 this.myObject.newProperty = 'hi'),所以通過 Vue.set 為向響應式物件中新增一個 property,可以確保這個新 property 同樣是響應式的,且觸發檢視更新。
-
更新陣列指定下標的元素:Vue.set(array, idx, val),內部通過 splice 方法實現響應式更新
-
更新物件已有屬性:Vue.set(obj, key ,val),直接更新即可 =>
obj[key] = val
-
不能向 Vue 例項或者 $data 動態新增根級別的響應式資料
-
Vue.set(obj, key, val),如果 obj 不是響應式物件,會執行
obj[key] = val
,但是不會做響應式處理 -
Vue.set(obj, key, val),為響應式物件 obj 增加一個新的 key,則通過 defineReactive 方法設定響應式,並觸發依賴更新
-
-
面試官 問:Vue.delete(target, key) 做了什麼?
答:
刪除物件的 property。如果物件是響應式的,確保刪除能觸發更新檢視。這個方法主要用於避開 Vue 不能檢測到 property 被刪除的限制,但是你應該很少會使用它。當然同樣不能刪除根級別的響應式屬性。
-
Vue.delete(array, idx),刪除指定下標的元素,內部是通過 splice 方法實現的
-
刪除響應式物件上的某個屬性:Vue.delete(obj, key),內部是執行
delete obj.key
,然後執行依賴更新即可
-
-
面試官 問:Vue.nextTick(cb) 做了什麼?
答:
Vue.nextTick(cb) 方法的作用是延遲迴調函式 cb 的執行,一般用於
this.key = newVal
更改資料後,想立即獲取更改過後的 DOM 資料:this.key = 'new val' Vue.nextTick(function() { // DOM 更新了 })
其內部的執行過程是:
-
this.key = 'new val
,觸發依賴通知更新,將負責更新的 watcher 放入 watcher 佇列 -
將重新整理 watcher 佇列的函式放到 callbacks 陣列中
-
在瀏覽器的非同步任務佇列中放入一個重新整理 callbacks 陣列的函式
-
Vue.nextTick(cb) 來插隊,將 cb 函式放入 callbacks 陣列
-
待將來的某個時刻執行重新整理 callbacks 陣列的函式
-
然後執行 callbacks 陣列中的眾多函式,觸發 watcher.run 的執行,更新 DOM
-
由於 cb 函式是在後面放到 callbacks 陣列,所以這就保證了先完成的 DOM 更新,再執行 cb 函式
-
連結
- 配套視訊,微信公眾號回覆:"精通 Vue 技術棧原始碼原理視訊版" 獲取
- 精通 Vue 技術棧原始碼原理 專欄
- github 倉庫 liyongning/Vue 歡迎 Star
感謝各位的:點贊、收藏和評論,我們下期見。
當學習成為了習慣,知識也就變成了常識。 感謝各位的 點贊、收藏和評論。
新視訊和文章會第一時間在微信公眾號傳送,歡迎關注:李永寧lyn
文章已收錄到 github 倉庫 liyongning/blog,歡迎 Watch 和 Star。