屬性與生命周期合并策略
Vue.mixin實(shí)現(xiàn)
在vue中有一個(gè)靜態(tài)方法:Vue.mixin
,用于屬性與生命周期的合并
vue3已經(jīng)廢棄葬毫,因?yàn)樵摲椒ù嬖谝恍﹩?wèn)題:
- 可能被開(kāi)發(fā)者濫用(全局混入镇辉,導(dǎo)致變量沖突)
- 來(lái)源不明確(某些方法與屬性需要去到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 = {} // 用來(lái)存儲(chǔ)全局的配置
// Vue還有一些其他的靜態(tài)方法諸如:filter directive component
Vue.mixin = function (mixin) {
this.options = mergeOptions(this.options, mixin)
return this
}
}
合并策略主要分為兩個(gè):屬性合并與生命周期合并
屬性合并
屬性合并主要實(shí)現(xiàn)思路是對(duì)象合并,規(guī)則如下(其實(shí)就是Object.assgin的規(guī)則):
- 如果父組件有子組件也有村砂,應(yīng)該用子組件替換父組件
- 如果父組件有值烂斋,子組件沒(méi)有,用父組件的
當(dāng)然這里用父子組件描述其實(shí)也不合適,但沒(méi)想到什么好的描述汛骂,其實(shí)就是一個(gè)先后順序罕模,看誰(shuí)先往Vue上注入(可以簡(jiǎn)單理解為誰(shuí)先執(zhí)行Vue.mixin
)
// util.js
// 同nextTick,并沒(méi)有如源碼那樣拆分出來(lái)帘瞭,有興趣的自行g(shù)ithub擼源碼
// 合并策略淑掌,屬性采用對(duì)象合并(Object.assgin規(guī)則),生命周期則包裝成數(shù)組蝶念,后面依次執(zhí)行
export function mergeOptions (parent, child) {
const options = {}
// 如果父親有兒子也有抛腕,應(yīng)該用兒子替換父親;如果父親有值兒子沒(méi)有媒殉,用父親的
// {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)) { // 如果父組件也有該屬性,合并過(guò)了廷蓉,子組件無(wú)需再處理
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ù)是沒(méi)法合并的刹悴,需要依次執(zhí)行,實(shí)現(xiàn)的思路是隊(duì)列
但是Vue
的生命周期方法有很多個(gè)攒暇,如果一直if...else if颂跨,那么將會(huì)很不恰當(dāng),解決的辦法是使用策略模式
// util.js
// 沒(méi)全寫扯饶,主要是實(shí)現(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 {
// 如果兒子有父親沒(méi)有
return [childVal]
}
} else {
return parentVal // 兒子沒(méi)有直接采用父親
}
}
// 同上面同一文件,個(gè)人筆記可以diff尾序,簡(jiǎn)書(shū)不支持
// 合并策略钓丰,屬性采用對(duì)象合并(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]) // 這里相當(dāng)于調(diào)用mergeHook兰怠,因?yàn)闆](méi)完全實(shí)現(xiàn)(比如components等那些合并策略并沒(méi)有實(shí)現(xiàn))
+ }
}
return options
}
這里說(shuō)一下為什么返回的一定為數(shù)組吧梦鉴,如果只看上面局部代碼可能理解不了
初始化時(shí)(也就是第一次),傳入的Vue.options = {}
揭保,因此第一次傳入的parentVal為undefined
而如果我們?cè)赩ue實(shí)例化時(shí)如果有傳入生命周期肥橙,走進(jìn)策略中的時(shí)候,childVal就會(huì)有值秸侣,因此第一次返回結(jié)果必為return [childVal]
lifecycle中新增callHook
方法存筏,用于調(diào)用(在合適的時(shí)機(jī)調(diào)用對(duì)應(yīng)的生命周期函數(shù))
// lifecycle.js
export function lifecycleMixin (Vue) {
Vue.prototype._update = function (vnode) {
+ vm.$el = patch(vm.$el, vnode) // 這里之前實(shí)現(xiàn)寫錯(cuò)了宠互,寫到$options.el去了,改回來(lái)
}
}
// 調(diào)用合并的生命周期椭坚,依次執(zhí)行
+ export function callHook (vm, hook) { // 發(fā)布模式
+ const handlers = vm.$options[hook]
+ if (handlers) {
+ // 這里的實(shí)現(xiàn)也就是為什么vue的什么周期不能用箭頭函數(shù)予跌,call將無(wú)效,this指向了window而不是vm
+ handlers.forEach(handlers => handlers.call(vm))
+ }
+ }
調(diào)用生命周期函數(shù)(僅作示例善茎,一樣不會(huì)寫全)
+ import { mountComponent, callHook } from './lifecycle.js'
+ import { mergeOptions, nextTick } from '@/util'
// 通過(guò)原型混合的方式券册,往vue的原型添方法
export function initMixin (Vue) {
Vue.prototype._init = function (options) { // options是用戶傳入的對(duì)象
const vm = this
// 實(shí)例上有個(gè)屬性 $options ,表示的是用戶傳入的所有屬性
+ // vm.$options = options
+ // 這里vm.constructor.options不能使用this垂涯,否則調(diào)用時(shí)this就指向了子組件實(shí)例汁掠,而不是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 // 同上,之前寫錯(cuò)了
// code...
mountComponent(vm, el) // 組件掛載
}
}
組件合并與渲染原理
組件的合并
內(nèi)部使用的Vue.extend
集币,返回通過(guò)對(duì)象創(chuàng)建一個(gè)類考阱,通過(guò)這個(gè)類取創(chuàng)建一個(gè)組件去使用
先查找自己身上是否存在,沒(méi)有則查找父親的__proto__
鞠苟,使用Object.create
來(lái)繼承(這里的父子不是父子組件乞榨,需要理解為全局注冊(cè)的和局部注冊(cè)的組件)
// global-api\index.js
export function initGlobalAPI (Vue) {
Vue.options = {} // 用來(lái)存儲(chǔ)全局的配置
// filter directive component
Vue.mixin = function (mixin) {
this.options = mergeOptions(this.options, mixin)
return this
}
+ // 調(diào)用生成組件
+ Vue.options._base = Vue // 永遠(yuǎn)指向Vue的構(gòu)造函數(shù)
+ Vue.options.components = {} // 用來(lái)存放組件的定義
+ Vue.component = function (id, definition) {
+ definition.name = definition.name || id // 組件名,如果定義中有name屬性則使用name当娱,否則以組件名命名
+ definition = this.options._base.extend(definition) // 通過(guò)對(duì)象產(chǎn)生一個(gè)構(gòu)造函數(shù)
+ this.options.components[id] = definition
+ }
+ let cid = 0
+ // 子組件初始化時(shí)吃既,會(huì) new VueComponent(options),產(chǎn)生一個(gè)子類Sub
+ Vue.extend = function (options) {
+ const Super = this // Vue構(gòu)造函數(shù)跨细,此時(shí)還未被實(shí)例化
+ const Sub = function VueComponent (options) {
+ this._init(options)
+ }
+ Sub.cid = cid++ // 防止組件時(shí)同一個(gè)構(gòu)造函數(shù)產(chǎn)生的鹦倚,因?yàn)椴煌M件可能命名卻是一樣,會(huì)導(dǎo)致createComponent中出問(wèn)題
+ Sub.prototype = Object.create(Super.prototype) // 都是通過(guò)Vue來(lái)繼承的
+ Sub.prototype.constructor = Sub // 常規(guī)操作冀惭,原型變更震叙,將實(shí)例所指向的原函數(shù)也改掉,這樣靜態(tài)屬性也會(huì)被同步過(guò)來(lái)
+ // 注意這一步不是在替換$options.component散休,而是在將Vue.component方法進(jìn)行統(tǒng)一媒楼,都是使用的上面那個(gè)Vue.component = function (id, definition)函數(shù)
+ Sub.component = Super.component
+ // ...省略其余操作代碼
+ Sub.options = mergeOptions(Super.options, options) // 將全局組件與該實(shí)例化的組件options合并(注意之前的實(shí)現(xiàn),只會(huì)合并屬性與生命周期)
+ return Sub // 這個(gè)構(gòu)造函數(shù)是由對(duì)象(options)產(chǎn)生而來(lái)的
+ }
}
// 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語(yǔ)法樹(shù) -> 轉(zhuǎn)變?yōu)榭蓤?zhí)行的render(generate方法) -> 創(chuàng)建出vnode
而現(xiàn)在的問(wèn)題在于戚丸,創(chuàng)建出來(lái)的vnode是一個(gè)自定義標(biāo)簽節(jié)點(diǎn)划址,而不是真實(shí)Dom,所以應(yīng)該生成vnode時(shí)限府,應(yīng)該將真實(shí)的組件內(nèi)容替換掉這個(gè)自定義節(jié)點(diǎn)(組件)
因此在createElement(創(chuàng)建虛擬節(jié)點(diǎn))時(shí)夺颤,我們需要區(qū)分該節(jié)點(diǎn)是自定義組件節(jié)點(diǎn),還是真實(shí)節(jié)點(diǎn)胁勺。Vue源碼中是寫了大量的真實(shí)節(jié)點(diǎn)標(biāo)簽世澜,通過(guò)標(biāo)簽名來(lái)進(jìn)行識(shí)別
// utils.js
+ function makeUp (str) {
+ const map = {}
+
+ str.split(',').forEach(tagName => {
+ map[tagName] = true
+ })
+
+ return tag => map[tag] || false
+ }
+
+ // 標(biāo)簽太多,隨便寫幾個(gè)姻几,源碼里太多了宜狐。高階函數(shù),比起直接使用數(shù)組的include判斷蛇捌,用字典時(shí)間復(fù)雜度為O(1)
+ export const isReservedTag = makeUp('a,p,div,ul,li,span,input,button')
通過(guò)isReservedTag
方法抚恒,就能將自定義節(jié)點(diǎn)(組件名)與真實(shí)節(jié)點(diǎn)區(qū)分出來(lái),如果是組件络拌,那么去調(diào)用createComponent
方法來(lái)創(chuàng)建對(duì)應(yīng)的vnode
創(chuàng)建組件vnode時(shí)俭驮,還需要給組件添加生命周期(并非beforeCreate等vue的生命周期),因?yàn)椴煌趘ue春贸,組件是沒(méi)有$el
的(這句話看不懂就想一下自己寫組件也不會(huì)在里面?zhèn)魅雃l吧)混萝,所以需要手動(dòng)掛載來(lái)觸發(fā)后續(xù)的update
// vdom\index.js
- import { isObject } from "@/util.js"
+ import { isObject, isReservedTag } from "@/util.js"
// 創(chuàng)建 Dom虛擬節(jié)點(diǎn)(代碼邏輯變更)
export function createdElement (vm, tag, data = {}, ...children) {
// 需要對(duì)標(biāo)簽名做過(guò)濾,因?yàn)橛锌赡軜?biāo)簽名是一個(gè)自定義組件
+ if (isReservedTag(tag)) {
+ return vnode(vm, tag, data, data.key, children, undefined)
+ } else {
+ // 自定義組件
+ const Ctor = vm.$options.components[tag] // Ctor是個(gè)對(duì)象或者函數(shù)
+ // 核心:vue.extend萍恕,繼承父組件逸嘀,通過(guò)原型鏈向上查找,封裝成函數(shù)
+ return createComponent(vm, tag, data, data.key, children, Ctor)
+ }
}
+ function createComponent (vm, tag, data, key, children, Ctor) {
+ if (isObject(Ctor)) { // 對(duì)象允粤,是個(gè)子組件崭倘,也封裝成函數(shù),統(tǒng)一
+ Ctor = vm.$options._base.extend(Ctor)
+ }
+
+ // 給組件增加生命周期(源碼中是抽離出去的类垫,所以需要將vnode傳進(jìn)入司光,而不是直接使用Ctor)
+ data.hook = {
+ init (vnode) {
+ // 調(diào)用子組件的構(gòu)造函數(shù)
+ const child = vnode.componentInstance = new vnode.componentOptions.Ctor({})
+ child.$mount() // 手動(dòng)掛載 vnode.componentInstance.$el = 真實(shí)的元素
+ }
+ }
+
+ // 組件的虛擬節(jié)點(diǎn)擁有 hook 和當(dāng)前組件的 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初始化時(shí)(查看init.js邏輯)残家,會(huì)調(diào)用$mount
,而$mount
中會(huì)掛載組件mountComponent
,mountComponent
中觸發(fā)vue._update
來(lái)更新視圖售躁,vue._update
中會(huì)使用patch
來(lái)生成真實(shí)節(jié)點(diǎn)坞淮,而上面也說(shuō)過(guò),組件是不會(huì)有$el
的陪捷,所以直接通過(guò)vnode來(lái)創(chuàng)建真實(shí)節(jié)點(diǎn)即可碾盐,創(chuàng)建真實(shí)節(jié)點(diǎn)時(shí),這里有點(diǎn)騷揩局。正常人應(yīng)該像前面一樣通過(guò)標(biāo)簽名再來(lái)一次判斷毫玖,但是這里是通過(guò)去獲取是否有vnode.data.hook
來(lái)判斷,有則調(diào)用init(vnode)
直接去去調(diào)用實(shí)例化方法
// vdom\patch.js
export function patch(oldVnode, vnode) {
+ // 組件沒(méi)有oldVnode凌盯,直接創(chuàng)建元素
+ if (!oldVnode) {
+ return createdElm(vnode) // 根據(jù)虛擬節(jié)點(diǎn)創(chuàng)建元素
+ }
// 之前的code...
}
+ // 創(chuàng)建節(jié)點(diǎn)真實(shí)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é)點(diǎn)上有組件的實(shí)例說(shuō)明當(dāng)前這個(gè)vnode是組件
+ return true
+ }
+
+ return false
+ }
function createdElm (vnode) { // 根據(jù)虛擬節(jié)點(diǎn)創(chuàng)建真實(shí)節(jié)點(diǎn)县忌,不同于createElement
let { vm, tag, data, key, children, text } = vnode
if (typeof tag === 'string') {
+ // 可能是組件掂榔,如果是組件继效,就直接創(chuàng)造出組件的真實(shí)節(jié)點(diǎn)
+ if (createComponent(vnode)) {
+ // 如果返回true,說(shuō)明這個(gè)虛擬節(jié)點(diǎn)是組件
+ return vnode.componentInstance.$el
+ }
vnode.el = document.createElement(tag) // 用vue的指令時(shí)装获,可以通過(guò)vnode拿到真實(shí)dom
updateProperties(vnode)
children.forEach(child => {
vnode.el.appendChild(createdElm(child)) // 遞歸創(chuàng)建插入節(jié)點(diǎn)瑞信,現(xiàn)代瀏覽器appendChild并不會(huì)插入一次回流一次
})
} else {
vnode.el = document.createTextNode(text)
}
return vnode.el
}