Vue 源碼解讀(5)—— 全局 API
- 2022 年 2 月 25 日
- 筆記
- 精通 Vue 技術棧的源碼原理
目標
深入理解以下全局 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。