手寫Vue2核心(四):生命周期及組件的合并策略

屬性與生命周期合并策略


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
+ }
傳入組件中的options與Sub構(gòu)造函數(shù)

組件的渲染原理

回顧之前的渲染流程:解析成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
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市穴豫,隨后出現(xiàn)的幾起案子凡简,更是在濱河造成了極大的恐慌,老刑警劉巖精肃,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件秤涩,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡司抱,警方通過(guò)查閱死者的電腦和手機(jī)筐眷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)习柠,“玉大人浊竟,你說(shuō)我怎么就攤上這事〗蚧” “怎么了振定?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)肉拓。 經(jīng)常有香客問(wèn)我后频,道長(zhǎng),這世上最難降的妖魔是什么暖途? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任卑惜,我火速辦了婚禮,結(jié)果婚禮上驻售,老公的妹妹穿的比我還像新娘露久。我一直安慰自己,他們只是感情好欺栗,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布毫痕。 她就那樣靜靜地躺著,像睡著了一般迟几。 火紅的嫁衣襯著肌膚如雪消请。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,692評(píng)論 1 305
  • 那天类腮,我揣著相機(jī)與錄音臊泰,去河邊找鬼。 笑死蚜枢,一個(gè)胖子當(dāng)著我的面吹牛缸逃,可吹牛的內(nèi)容都是我干的针饥。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼需频,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼丁眼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起贺辰,我...
    開(kāi)封第一講書(shū)人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤户盯,失蹤者是張志新(化名)和其女友劉穎嵌施,沒(méi)想到半個(gè)月后饲化,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吗伤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年吃靠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片足淆。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡巢块,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出巧号,到底是詐尸還是另有隱情族奢,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布丹鸿,位于F島的核電站越走,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏靠欢。R本人自食惡果不足惜廊敌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望门怪。 院中可真熱鬧骡澈,春花似錦、人聲如沸掷空。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)坦弟。三九已至疼电,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間减拭,已是汗流浹背蔽豺。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拧粪,地道東北人修陡。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓沧侥,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親魄鸦。 傳聞我的和親對(duì)象是個(gè)殘疾皇子宴杀,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容