屬性與生命周期合并策略
Vue.mixin實現(xiàn)
在vue中有一個靜態(tài)方法:Vue.mixin,用于屬性與生命周期的合并
vue3已經(jīng)廢棄,因為該方法存在一些問題:
- 可能被開發(fā)者濫用(全局混入,導致變量沖突)
- 來源不明確(某些方法與屬性需要去到minxin中查找)
在Vue上新增靜態(tài)方法,如之前一樣,使用混入的方式
// index.js
+ import { initGlobalAPI } from './global-api/index.js'
+ initGlobalAPI(Vue)
// global-api\index.js
import { mergeOptions } from "@/util.js"
export function initGlobalAPI (Vue) {
Vue.options = {} // 用來存儲全局的配置
// Vue還有一些其他的靜態(tài)方法諸如:filter directive component
Vue.mixin = function (mixin) {
this.options = mergeOptions(this.options, mixin)
return this
}
}
合并策略主要分為兩個:屬性合并與生命周期合并
屬性合并
屬性合并主要實現(xiàn)思路是對象合并,規(guī)則如下(其實就是Object.assgin的規(guī)則):
- 如果父組件有子組件也有,應該用子組件替換父組件
- 如果父組件有值,子組件沒有,用父組件的
當然這里用父子組件描述其實也不合適,但沒想到什么好的描述,其實就是一個先后順序,看誰先往Vue上注入(可以簡單理解為誰先執(zhí)行Vue.mixin)
// util.js
// 同nextTick,并沒有如源碼那樣拆分出來,有興趣的自行g(shù)ithub擼源碼
// 合并策略,屬性采用對象合并(Object.assgin規(guī)則),生命周期則包裝成數(shù)組,后面依次執(zhí)行
export function mergeOptions (parent, child) {
const options = {}
// 如果父親有兒子也有,應該用兒子替換父親;如果父親有值兒子沒有,用父親的
// {a: 1} {a: 2} => {a: 2}
// {a: 1} {b: 2} => {a:1, b: 2}
// 使用for,主要考慮到深拷貝
for (let key in parent) {
mergeField(key)
}
for (let key in child) {
if (!parent.hasOwnProperty(key)) { // 如果父組件也有該屬性,合并過了,子組件無需再處理
mergeField(key)
}
}
// vue這種做法,老是在函數(shù)中寫函數(shù)我也是醉了…
function mergeField (key) {
// data屬性的合并處理
if (isObject(parent[key]) && isObject(child[key])) {
options[key] = {...parent[key], ...child[key]}
} else {
if (child[key]) { // 如果兒子有值
options[key] = child[key]
} else {
options[key] = parent[key]
}
}
}
return options
}
生命周期的合并
生命周期合并,不同于屬性,函數(shù)是沒法合并的,需要依次執(zhí)行,實現(xiàn)的思路是隊列
但是Vue的生命周期方法有很多個,如果一直if...else if,那么將會很不恰當,解決的辦法是使用策略模式
// util.js
// 沒全寫,主要是實現(xiàn)合并原理
const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted'
]
const strats = {}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
// 鉤子合并策略,數(shù)組形式
function mergeHook (parentVal, childVal) {
if (childVal) {
if (parentVal) {
// 如果兒子有父親也有
return parentVal.concat(childVal)
} else {
// 如果兒子有父親沒有
return [childVal]
}
} else {
return parentVal // 兒子沒有直接采用父親
}
}
// 同上面同一文件,個人筆記可以diff,簡書不支持
// 合并策略,屬性采用對象合并(Object.assgin規(guī)則),生命周期則包裝成數(shù)組,后面依次執(zhí)行
export function mergeOptions (parent, child) {
// vue這種做法,老是在函數(shù)中寫函數(shù)我也是醉了…
function mergeField (key) {
// 策略模式,生命周期合并處理
+ if (strats[key]) {
+ return options[key] = strats[key](parent[key], child[key]) // 這里相當于調(diào)用mergeHook,因為沒完全實現(xiàn)(比如components等那些合并策略并沒有實現(xiàn))
+ }
}
return options
}
這里說一下為什么返回的一定為數(shù)組吧,如果只看上面局部代碼可能理解不了
初始化時(也就是第一次),傳入的Vue.options = {},因此第一次傳入的parentVal為undefined
而如果我們在Vue實例化時如果有傳入生命周期,走進策略中的時候,childVal就會有值,因此第一次返回結(jié)果必為return [childVal]

lifecycle中新增callHook方法,用于調(diào)用(在合適的時機調(diào)用對應的生命周期函數(shù))
// lifecycle.js
export function lifecycleMixin (Vue) {
Vue.prototype._update = function (vnode) {
+ vm.$el = patch(vm.$el, vnode) // 這里之前實現(xiàn)寫錯了,寫到$options.el去了,改回來
}
}
// 調(diào)用合并的生命周期,依次執(zhí)行
+ export function callHook (vm, hook) { // 發(fā)布模式
+ const handlers = vm.$options[hook]
+ if (handlers) {
+ // 這里的實現(xiàn)也就是為什么vue的什么周期不能用箭頭函數(shù),call將無效,this指向了window而不是vm
+ handlers.forEach(handlers => handlers.call(vm))
+ }
+ }
調(diào)用生命周期函數(shù)(僅作示例,一樣不會寫全)
+ import { mountComponent, callHook } from './lifecycle.js'
+ import { mergeOptions, nextTick } from '@/util'
// 通過原型混合的方式,往vue的原型添方法
export function initMixin (Vue) {
Vue.prototype._init = function (options) { // options是用戶傳入的對象
const vm = this
// 實例上有個屬性 $options ,表示的是用戶傳入的所有屬性
+ // vm.$options = options
+ // 這里vm.constructor.options不能使用this,否則調(diào)用時this就指向了子組件實例,而不是Vue了
+ vm.$options = mergeOptions(vm.constructor.options, options)
+ callHook(this, 'beforeCreate')
// 初始化狀態(tài)
initState(vm)
+ callHook(this, 'created')
}
Vue.prototype.$mount = function (el) {
+ vm.$el = el // 同上,之前寫錯了
// code...
mountComponent(vm, el) // 組件掛載
}
}
組件合并與渲染原理
組件的合并
內(nèi)部使用的Vue.extend,返回通過對象創(chuàng)建一個類,通過這個類取創(chuàng)建一個組件去使用
先查找自己身上是否存在,沒有則查找父親的__proto__,使用Object.create來繼承(這里的父子不是父子組件,需要理解為全局注冊的和局部注冊的組件)
// global-api\index.js
export function initGlobalAPI (Vue) {
Vue.options = {} // 用來存儲全局的配置
// filter directive component
Vue.mixin = function (mixin) {
this.options = mergeOptions(this.options, mixin)
return this
}
+ // 調(diào)用生成組件
+ Vue.options._base = Vue // 永遠指向Vue的構(gòu)造函數(shù)
+ Vue.options.components = {} // 用來存放組件的定義
+ Vue.component = function (id, definition) {
+ definition.name = definition.name || id // 組件名,如果定義中有name屬性則使用name,否則以組件名命名
+ definition = this.options._base.extend(definition) // 通過對象產(chǎn)生一個構(gòu)造函數(shù)
+ this.options.components[id] = definition
+ }
+ let cid = 0
+ // 子組件初始化時,會 new VueComponent(options),產(chǎn)生一個子類Sub
+ Vue.extend = function (options) {
+ const Super = this // Vue構(gòu)造函數(shù),此時還未被實例化
+ const Sub = function VueComponent (options) {
+ this._init(options)
+ }
+ Sub.cid = cid++ // 防止組件時同一個構(gòu)造函數(shù)產(chǎn)生的,因為不同組件可能命名卻是一樣,會導致createComponent中出問題
+ Sub.prototype = Object.create(Super.prototype) // 都是通過Vue來繼承的
+ Sub.prototype.constructor = Sub // 常規(guī)操作,原型變更,將實例所指向的原函數(shù)也改掉,這樣靜態(tài)屬性也會被同步過來
+ // 注意這一步不是在替換$options.component,而是在將Vue.component方法進行統(tǒng)一,都是使用的上面那個Vue.component = function (id, definition)函數(shù)
+ Sub.component = Super.component
+ // ...省略其余操作代碼
+ Sub.options = mergeOptions(Super.options, options) // 將全局組件與該實例化的組件options合并(注意之前的實現(xiàn),只會合并屬性與生命周期)
+ return Sub // 這個構(gòu)造函數(shù)是由對象(options)產(chǎn)生而來的
+ }
}
// util.js
const strats = {}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
+ // 組件合并策略
+ strats.components = function (parentVal, childVal) {
+ const res = Object.create(parentVal)
+ if (childVal) {
+ for (let key in childVal) {
+ res[key] = childVal[key]
+ }
+ }
+ return res
+ }

組件的渲染原理
回顧之前的渲染流程:解析成ast語法樹 -> 轉(zhuǎn)變?yōu)榭蓤?zhí)行的render(generate方法) -> 創(chuàng)建出vnode
而現(xiàn)在的問題在于,創(chuàng)建出來的vnode是一個自定義標簽節(jié)點,而不是真實Dom,所以應該生成vnode時,應該將真實的組件內(nèi)容替換掉這個自定義節(jié)點(組件)
因此在createElement(創(chuàng)建虛擬節(jié)點)時,我們需要區(qū)分該節(jié)點是自定義組件節(jié)點,還是真實節(jié)點。Vue源碼中是寫了大量的真實節(jié)點標簽,通過標簽名來進行識別
// utils.js
+ function makeUp (str) {
+ const map = {}
+
+ str.split(',').forEach(tagName => {
+ map[tagName] = true
+ })
+
+ return tag => map[tag] || false
+ }
+
+ // 標簽太多,隨便寫幾個,源碼里太多了。高階函數(shù),比起直接使用數(shù)組的include判斷,用字典時間復雜度為O(1)
+ export const isReservedTag = makeUp('a,p,div,ul,li,span,input,button')
通過isReservedTag方法,就能將自定義節(jié)點(組件名)與真實節(jié)點區(qū)分出來,如果是組件,那么去調(diào)用createComponent方法來創(chuàng)建對應的vnode
創(chuàng)建組件vnode時,還需要給組件添加生命周期(并非beforeCreate等vue的生命周期),因為不同于vue,組件是沒有$el的(這句話看不懂就想一下自己寫組件也不會在里面?zhèn)魅雃l吧),所以需要手動掛載來觸發(fā)后續(xù)的update
// vdom\index.js
- import { isObject } from "@/util.js"
+ import { isObject, isReservedTag } from "@/util.js"
// 創(chuàng)建 Dom虛擬節(jié)點(代碼邏輯變更)
export function createdElement (vm, tag, data = {}, ...children) {
// 需要對標簽名做過濾,因為有可能標簽名是一個自定義組件
+ if (isReservedTag(tag)) {
+ return vnode(vm, tag, data, data.key, children, undefined)
+ } else {
+ // 自定義組件
+ const Ctor = vm.$options.components[tag] // Ctor是個對象或者函數(shù)
+ // 核心:vue.extend,繼承父組件,通過原型鏈向上查找,封裝成函數(shù)
+ return createComponent(vm, tag, data, data.key, children, Ctor)
+ }
}
+ function createComponent (vm, tag, data, key, children, Ctor) {
+ if (isObject(Ctor)) { // 對象,是個子組件,也封裝成函數(shù),統(tǒng)一
+ Ctor = vm.$options._base.extend(Ctor)
+ }
+
+ // 給組件增加生命周期(源碼中是抽離出去的,所以需要將vnode傳進入,而不是直接使用Ctor)
+ data.hook = {
+ init (vnode) {
+ // 調(diào)用子組件的構(gòu)造函數(shù)
+ const child = vnode.componentInstance = new vnode.componentOptions.Ctor({})
+ child.$mount() // 手動掛載 vnode.componentInstance.$el = 真實的元素
+ }
+ }
+
+ // 組件的虛擬節(jié)點擁有 hook 和當前組件的 componentOptions ,Ctor中存放了組件的構(gòu)造函數(shù)
+ return vnode(vm, `vue-component-${Ctor.cid}-${tag}`, data, key, undefined, undefined, {Ctor})
+ }
function vnode (vm, tag, data, key, children, text, componentOptions) {
return {
vm,
tag,
children,
data,
key,
text,
+ componentOptions
}
}
有了組件的vnode后,在Vue初始化時(查看init.js邏輯),會調(diào)用$mount,而$mount中會掛載組件mountComponent,mountComponent中觸發(fā)vue._update來更新視圖,vue._update中會使用patch來生成真實節(jié)點,而上面也說過,組件是不會有$el的,所以直接通過vnode來創(chuàng)建真實節(jié)點即可,創(chuàng)建真實節(jié)點時,這里有點騷。正常人應該像前面一樣通過標簽名再來一次判斷,但是這里是通過去獲取是否有vnode.data.hook來判斷,有則調(diào)用init(vnode)直接去去調(diào)用實例化方法
// vdom\patch.js
export function patch(oldVnode, vnode) {
+ // 組件沒有oldVnode,直接創(chuàng)建元素
+ if (!oldVnode) {
+ return createdElm(vnode) // 根據(jù)虛擬節(jié)點創(chuàng)建元素
+ }
// 之前的code...
}
+ // 創(chuàng)建節(jié)點真實Dom
+ function createComponent (vnode) {
+ let i = vnode.data
+ // 先將vnode.data賦值給i,然后將i.hook賦值給i,如果i存在再將i.init賦值給i,瘋狂改變i的類型,雖然js中都屬于Object,但真的好嗎…
+ if ((i = i.hook) && (i = i.init)) {
+ i(vnode) // 調(diào)用組件的初始化方法
+ }
+
+ if (vnode.componentInstance) { // 如果虛擬節(jié)點上有組件的實例說明當前這個vnode是組件
+ return true
+ }
+
+ return false
+ }
function createdElm (vnode) { // 根據(jù)虛擬節(jié)點創(chuàng)建真實節(jié)點,不同于createElement
let { vm, tag, data, key, children, text } = vnode
if (typeof tag === 'string') {
+ // 可能是組件,如果是組件,就直接創(chuàng)造出組件的真實節(jié)點
+ if (createComponent(vnode)) {
+ // 如果返回true,說明這個虛擬節(jié)點是組件
+ return vnode.componentInstance.$el
+ }
vnode.el = document.createElement(tag) // 用vue的指令時,可以通過vnode拿到真實dom
updateProperties(vnode)
children.forEach(child => {
vnode.el.appendChild(createdElm(child)) // 遞歸創(chuàng)建插入節(jié)點,現(xiàn)代瀏覽器appendChild并不會插入一次回流一次
})
} else {
vnode.el = document.createTextNode(text)
}
return vnode.el
}