這是我參與8月更文挑戰的第5天,活動詳情查看:8月更文挑戰javascript
本文你將收穫:html
在使用 Vue 開發的時候,常用 混入(mixin) 發現真的好用,混入(mixin) 提供了一種很是靈活的方式,來分發 Vue 組件中的可複用功能。一個混入對象能夠包含任意組件選項。當組件使用混入對象時,全部混入對象的選項將被「混合」進入該組件自己的選項。vue
雖然好用,可是一直停留在看文檔會用,「知其然不知其因此然」,而且在以前組內分享時,有同窗也分享了關於 混入(mixin) 的問題,因此最近有興趣去看了一下實現原理,發現仍是有點繞,是真的有點繞(菜雞一枚 😆)。這篇文章分享一下本身的一些探索但願對你有幫組。整體來講其實就是探索兩個問題:java
咱們帶着問題往下看。git
import Vue from "vue";
import App from "./App.vue";
Vue.config.productionTip = false;
Vue.mixin({
data() {
return {
a: 1
};
}
});
new Vue({
render: (h) => h(App)
}).$mount("#app");
複製代碼
測試源碼github
<template>
<div class="hello"> <h1>{{ a }}</h1> </div>
</template>
<script> export default { name: "HelloWorld", props: { msg: String, }, mixins: [ { data() { return { a: 2, }; }, }, ], data() { return { // a: 2, }; }, }; </script>
複製代碼
測試源碼web
基礎 options 就是:components、directives、filters 三兄弟,這三兄弟在初始化全局 API 的時候就設置在 Vue.options 上。因此這三個是最早存在全局 options。 數組
混入分爲兩種狀況。markdown
不過全局混入,須要注意的是,混入的操做應該是在初始化實例以前,而不是以後,這樣混入 (mixin) 才能合併上你的自定義 options。app
每個組件在初始化的時候都會生成一個 vm (組件實例)。在建立組件實例以前,全局註冊的 options,其實會被傳遞引用到每一個組件中,目的是將和 全局 options 和 組件 options 合併起來,組件便能訪問到全局選項。因此的時機就是建立好組件實例以前。
對於全局註冊的 options ,Vue 背後偷偷給組件都合併一個全局選項的引用。可是爲保證全局 options 不被污染,又不可能每一個組件都深度克隆一份全局選項致使開銷過大,因此會根據不一樣的選項,作不一樣的處理。下面咱們就來看看混入合併的策略是什麼?
在這以前,回到上面的兩種混入,咱們發現混入合併最後都調用了 mergeOptions 這個方法。這個方法就是混入的重點。
// 合併剛纔從類的繼承鏈中獲取的配置對象及你本身在代碼中編寫的配置對象(從第一次合併確定是new Vue(options)這個 options
export function mergeOptions ( parent: Object, child: Object, vm?: Component ): Object {
...
// 組件屬性中的 props、inject、directive 等進行規範化
// 驗證開發者的代碼是否符合規範
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
if (!child._base) {
// 遍歷mixins,parent 先和 mixins 合併,而後在和 child 合併
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
// 處理 parent 的 key
// 先遍歷合併 parent 中的 key,存儲在 options
// 初始化時:parent 就是全局選項
for (key in parent) {
mergeField(key)
}
// 處理 child 的 key
// 在遍歷 child,合併補上 parent 中沒有的 key ,存儲在 options
// 初始化時:child 就是組件自定義選項
for (key in child) {
if (!hasOwn(parent, key)) { // 排除已經處理過的 parent 中的 key
mergeField(key)
}
}
// 獲得類型的合併函數,進行合併字段
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
複製代碼
源碼很長,關鍵代碼在最後的函數,這函數就是 「獲得類型的合併函數,進行合併字段」 ,這裏的類型多是:'data'、hook、'props'、'methods'、'inject'、'computed'、'provide'等等,也就是類型的不一樣,進行的合併策略也是不同的。固然若是都不存在,就走默認的處理 defaultStrat 。
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
複製代碼
組件 options > 全局 options
data,咱們在開發時,通常使用函數來定義,固然也可使用對象(比較少)。咱們以函數爲爲主線來討論混入策略。
這裏簡單解釋一下爲何通常狀況下,咱們使用函數來定義 data: 在 Vue 中組件是能夠複用的,一個組件被建立好以後,就能夠被用在其餘各個地方,而組件無論被複用了多少次,組件中的 data 數據應該是相互不影響的。基於數據不影響的理念,組件被複用一次,data 數據就應該被複制一次,data 是函數,每個函數都會有本身的存儲空間,函數每次執行都會建立本身的執行上下文,相互不影響。函數相似於給每一個組件實例建立一個私有的數據空間,讓各個組件實例維護各自的數據。而單純的寫成對象形式,就使得全部組件實例共用了一份 data,就會形成一個變了全都會變的結果。
function mergeData (to: Object, from: ?Object): Object {
if (!from) return to
let key, toVal, fromVal
const keys = hasSymbol
? Reflect.ownKeys(from)
: Object.keys(from)
for (let i = 0; i < keys.length; i++) {
key = keys[i]
// in case the object is already observed...
if (key === '__ob__') continue
toVal = to[key]
fromVal = from[key]
// 若是存在這個屬性,從新設置
if (!hasOwn(to, key)) {
set(to, key, fromVal)
} else if (
// 存在相同的屬性,就合併對象
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
mergeData(toVal, fromVal)
}
}
return to
}
/** * Data */
export function mergeDataOrFn ( parentVal: any, childVal: any, vm?: Component ): ?Function {
if (!vm) {
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
return function mergedInstanceDataFn () {
const instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}
strats.data = function ( parentVal: any, childVal: any, vm?: Component ): ?Function {
if (!vm) {
if (childVal && typeof childVal !== 'function') {
return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
}
複製代碼
源碼很長,若是你一行一行去看,真的難受。抽象一下其實就是:兩個 data 函數合併成一個函數返回,data 函數執行返回的數據對象也進行合併。
可是注意這裏的合併數據也是有優先級的。咱們經過一個例子來看看。
// 全局配置
Vue.mixin({
data() {
return {
a: 1
};
}
});
// 子組件
<template> <div class="child"> <h1>{{ a }}</h1> </div> </template>
<script> export default { name: "Child", mixins: [ { data() { return { a: 5, }; }, mixins: [ { data() { return { a: 4, }; }, }, ], }, ], data() { return { a: 6, }; }, }; </script>
複製代碼
這例子中,設置 4 類 data option 函數:
provide 的混入策略和 data 的混入策略一致。底層都是調用 mergeDataOrFn 函數實現。
// Vue 中全部的 hook 函數
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
]
// 爲全部的 hook 註冊回調,回調都是 mergeHook
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
// mergeHook 協同 dedupeHooks 的做用就是將 hook 函數存入數組
function mergeHook ( parentVal: ?Array<Function>, childVal: ?Function | ?Array<Function> ): ?Array<Function> {
const res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
return res
? dedupeHooks(res)
: res
}
function dedupeHooks (hooks) {
const res = []
for (let i = 0; i < hooks.length; i++) {
if (res.indexOf(hooks[i]) === -1) {
res.push(hooks[i])
}
}
return res
}
複製代碼
hook 的混入,相對 data 的混入來講,要簡單一些,就是把全部的鉤子函數保存進數組,雖然順序執行。
// 全局
Vue.mixin({
created() {
console.log(1);
}
});
// 子組件
<template> <div class="child"> </div> </template>
<script> export default { name: "Child", mixins: [ { created() { console.log(3); }, mixins: [ { created() { console.log(4); }, }, ], }, ], created() { console.log(2); }, }; </script>
複製代碼
測試源碼 hook 混入是存放在數組中,最後就變成了:
[
全局 mixin hook,
... ,
組件mixin-mixin hook,
組件mixin hook,
組件 hook
],
複製代碼
執行的時候,按照這個數組 順序執行。
watch 的混入策略和 hook 的混入策略思想是一致的,都是按照
[
全局 mixin watch,
... ,
組件 mixin-mixin watch,
組件 mixin watch,
組件 watch
]
複製代碼
這個順序混入合併 watch, 最後執行的時候順序執行(注意:雖然混入測試和 hook 同樣,可是底層實現仍是不同的,這裏就不貼源碼了)。
component、directives、filters 這三者是放在一塊兒來說哈,主要是這三者合併測試同樣,而且這三者最開始初始化全局 API 的時候就設置在 Vue.options 上。
// 中轉函數
function mergeAssets ( parentVal: ?Object, childVal: ?Object, vm?: Component, key: string ): Object {
const res = Object.create(parentVal || null)
if (childVal) {
return extend(res, childVal)
} else {
return res
}
}
// 爲 component、directives、filters 綁定回調
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
//
export function extend (to: Object, _from: ?Object): Object {
for (const key in _from) {
to[key] = _from[key]
}
return to
}
複製代碼
這裏最重要的就是 「const res = Object.create(parentVal || null) 」 這一行代碼,component、directives、filters 混入策略的精髓。
Object.create()方法建立一個新對象,使用現有的對象來提供新建立的對象的proto。
什麼意思了,簡單來講就是經過使用 Object.create 來建立對象,而且實現繼承,兩個對象繼承混入,經過原型鏈的方式不會相互覆蓋,而是 權重小 被放到 權重大 的原型上 (大佬的實現,就是牛逼)。
<script>
// 全局 filter
Vue.filter("g_filter",function (params) {})
// mixin 的 mixin
var mixin_mixin_filter={
filters:{
mixin_mixin_filter(){}
}
}
// mixin filter
var mixins_filter={
mixins:[mixin_mixin_filter],
filters:{
mixins_filter(){}
}
}
// 組件 filter
var vue = new Vue({
mixins:[mixins_filter],
filters:{
self_filter(){}
}
})
console.log(vue.$options);
</script>
複製代碼
在實現這個例子演示時,發生了一個小插曲。本想在 codesandbox 實現這個例子的,發如今 codesandbox 上實現的例子,發現和實際不太同樣,若是有興趣能夠研究一下,codesandbox.io/s/vue-mixin…
這四者的混入策略也是同樣的,因此放在一塊兒來講。並且它們的混入策略,也相對來講比較簡單。
strats.props =
strats.methods =
strats.inject =
strats.computed = function ( parentVal: ?Object, childVal: ?Object, vm?: Component, key: string ): ?Object {
if (childVal && process.env.NODE_ENV !== 'production') {
assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
const ret = Object.create(null)
extend(ret, parentVal)
if (childVal) extend(ret, childVal)
return ret
}
複製代碼
簡單的對象合併,key 值相同,優先級高的覆蓋優先級低的。組件 對象 > 組件 mixin 對象 > 組件 mixin -mixin 對象 > ... > 全局 mixin 對象。
以 methods 爲例:
// 全局配置
Vue.mixin({
methods: {
test() {
console.log(1);
}
}
});
// 子組件
<template> <div class="hello"></div> </template>
<script> export default { name: "HelloWorld", props: { msg: String, }, mixins: [ { methods: { test() { console.log(3); }, }, mixins: [ { methods: { test() { console.log(4); }, }, }, ], }, ], methods: { test() { console.log(2); }, }, created() { this.test(); }, }; </script>
複製代碼
這是默認的處理方式,也至關於一種兜底的方案,當上面全部的混入策略不存在的時候,就會用這種兜底方式,如 el,template,propData。他們的混入策略就是權重大的覆蓋權重小的。組件 > 組件 mixin > 組件 mixin -mixin > ... > 全局 mixin。
本文帶你們一塊兒探索了 Vue mixin 的策略,在不一樣場景有不一樣的混入策略,涉及到 data、provide、鉤子函數、watch、component、directives、filters、props、computed、methods、inject、el、template、propData 。從混入的方式來講,咱們能夠總結爲 5 個大的方向:
「知其然知其因此然」,抓着源碼研究了好久,也看了不少文章,總結出來,但願對你有幫助。
若是以爲寫得還行,幫忙點個贊吧。