Vue源碼主線路整理

第二次來梳理Vue源碼邏輯了踊兜。第一次因為不熟悉犹赖,梳理的很細致才弄懂试浙。第二次就更有大局觀一些了董瞻,這次我主要抓住流程的重點順利走完流程就好了。主路線是按執(zhí)行順序進行排列的田巴。

入口是哪里開始的钠糊?

init()。合并配置壹哺,初始化事件中心抄伍、初始化生命周期、初始化數(shù)據(jù)響應化斗躏、初始化渲染等等逝慧。

render函數(shù)是什么時候生成的?

vm.$mount啄糙。把el或者?template字符串或.vue文件dom文檔笛臣,解析成render函數(shù)(我們使用webpack的話,直接解析成render函數(shù),此步驟我們在webapck中完成)。

render函數(shù)-》Vnode-》真實DOM 的步驟隧饼?

mountComponent(主要代碼:updateComponent = () => { vm._update(vm._render(), hydrating)} 和?vm._watcher = new Watcher(vm, updateComponent, noop))沈堡。其核心就是先調(diào)用?vm._render?方法,生成虛擬 Node燕雁。再實例化一個渲染W(wǎng)atcher诞丽,在它的回調(diào)函數(shù)中會調(diào)用?updateComponent?方法,最終調(diào)用?vm._update更新 DOM拐格。

render函數(shù)是如何變成Vnode的僧免?

vm._render,把render函數(shù)渲染成vnode捏浊。模板編譯成的?render?函數(shù)使用vm._c?方法渲染懂衩,用戶手寫?render函數(shù)用vm.$createElement方法渲染。?這倆個方法都在入口的初始化渲染中進行了定義。它們傳入的參數(shù)相同浊洞,并且內(nèi)部都調(diào)用了?createElement?方法牵敷。createElement,創(chuàng)建vnode并返回法希。該方法有5個參數(shù)枷餐,也就是創(chuàng)建vnode的原材料,context (VNode 的上下文環(huán)境)苫亦;tag (標簽,是一個字符串或者一個?Component)毛肋;data (VNode的數(shù)據(jù));children( VNode 的子節(jié)點,是任意類型的)著觉;normalizationType(子節(jié)點規(guī)范的類型)村生。

注意
1、編譯生成的render函數(shù)的children數(shù)組中內(nèi)容(包括普通節(jié)點和組件節(jié)點)已經(jīng)是vnode類型了饼丘,在編譯階段已經(jīng)完成了趁桃,只是對某些情況需要規(guī)范數(shù)據(jù)格式。所以下面的render函數(shù)轉(zhuǎn)化成vnode數(shù)據(jù)格式肄鸽,是對根節(jié)點做的操作卫病,而對children只是做了規(guī)范化操作。
2典徘、createElement方法執(zhí)行了多少次蟀苛?tag標簽又是什么?在編譯成render函數(shù)時逮诲,children已經(jīng)是vnode類型怎么理解帜平,學到編譯階段就明白了。

在編譯模板的時候梅鹦,會解析到各個節(jié)點的 tag裆甩,在生成 render 函數(shù)的時候,作為參數(shù)傳入齐唆,并最終傳給 createElement 函數(shù)嗤栓。這個 _c 最終都會執(zhí)行 createElement,你可以看到箍邮,對于一個組件嵌套多層節(jié)點的情況茉帅,會生成多個 createElement 函數(shù),整個 render 函數(shù)返回的 vnode锭弊,其實是一個 vnode tree堪澎。

createElement主要做了那些事情?

1味滞、規(guī)范vnode的子節(jié)點children全封。render?函數(shù)如果是編譯生成的马昙,理論上編譯生成的?children?都已經(jīng)是 VNode 類型了(除了functional component?函數(shù)式組件返回的是一個數(shù)組而不是一個根節(jié)點)桃犬。children是一個數(shù)組刹悴,組員是一個個vnode,它們也有自己的children數(shù)組攒暇,這樣就形成了一個 VNode Tree土匀,它很好的描述了我們的 DOM Tree。其中對children的處理有2個方法simpleNormalizeChildren(針對render函數(shù)是編譯生成且函數(shù)式組件返回的是一個數(shù)組而不是一個根節(jié)點)和normalizeArrayChildren(針對v-solt / v-for編譯生成有嵌套數(shù)據(jù)的情況或者用戶手寫render函數(shù))形用,打平children數(shù)組就轧。
2、根據(jù)tag不同類型田度,以不同方式創(chuàng)建vnode妒御。1、若tag是string?類型镇饺,若是內(nèi)置節(jié)點乎莉,就直接創(chuàng)建vnode,若是已注冊的組件名奸笤,就createComponent惋啃,創(chuàng)建組件類型vnode,否則創(chuàng)建未知標簽vnode监右。2边灭、若tag是Component類型(比如<router-link><slot>),createComponent健盒,創(chuàng)建組件類型vnode節(jié)點绒瘦。? ?

注意:這里對不同tag類型用不同方式生成vnode節(jié)點,是對main.js中new Vue({options})中options中的模版?zhèn)魅敕绞阶雠袛唷?扣癣、直接寫<template>模版惰帽。2、注冊component搏色,寫component善茎。3、直接寫render函數(shù)频轿。來用不同方式生成vnode節(jié)點垂涯。

組件是如何通過createComponent生成vnode的?

createComponent(Ctor: Class<Component> | Function | Object | void,??data: ?VNodeData,??context: Component,??children: ?Array<VNode>,??tag?: string)航邢,創(chuàng)建組件類型vnode耕赘。核心流程有3步,構造子類構造函數(shù)膳殷,安裝組件勾子函數(shù)和實例化vnode操骡。這三步同時也是在為接下來組件從vode->真實DOM的patch步驟做準備九火。

????1、Vue.extend册招,構造子類構造函數(shù)岔激,init()執(zhí)行initGlobalAPI(初始化全局API)時定義了Vue.options_base = Vue,init()也通過mergeOptions把Vue中一些option擴展到了vm.$options上是掰,所以在組件實例中我們能通過vm.$options._base取到Vue虑鼎。在Vue.prototype上有Vue.extend方法,其作用是構造一個Vue的子類键痛。它使用原型繼承(a.prototype = object.create(b.prototype))把一個純對象轉(zhuǎn)換成一個繼承于?Vue?的構造器?Sub?并返回炫彩,然后對?Sub?這個對象本身擴展屬性(如擴展?options、添加全局 API 等)絮短,并且對配置中的?props?和?computed?做了初始化江兢;最后對于這個?Sub?構造函數(shù)做了緩存,避免多次執(zhí)行?Vue.extend?的時候?qū)ν粋€子組件重復構造(比如多個組件引用同一個組件的情況)丁频。當我們?nèi)嵗?Sub?的時候杉允,就會執(zhí)行?this._init?邏輯再次走到了?Vue?實例的初始化邏輯。
????2限府、installComponentHooks夺颤,安裝組件勾子函數(shù)。?Vue.js 使用的 Virtual DOM 參考的是開源庫?snabbdom胁勺,它的一個特點是在 VNode 的 patch 流程中對外暴露了各種時機的鉤子函數(shù)世澜,方便我們做一些額外的事情,Vue.js 也是充分利用這一點署穗,在初始化一個 Component 類型的 VNode 的過程中實現(xiàn)了幾個鉤子函數(shù)寥裂。installComponentHooks?的過程就是把?componentVNodeHooks?的鉤子函數(shù)合并到?data.hook中,在 VNode 執(zhí)行?patch?的過程中執(zhí)行相關的鉤子函數(shù)案疲。
????3封恰、實例化VNode,通過?new VNode?實例化一個?vnode?并返回褐啡。需要注意的是和普通元素節(jié)點的?vnode?不同诺舔,組件的?vnode?是沒有?children?的。
????注意:這個vnode是組件vnode(主要作用是傳遞數(shù)據(jù))备畦,后面組件實例執(zhí)行_inite->$mount->mountComponent->_render()低飒,_render時還會生成一個vnode,是組件的渲染vnode(即組件內(nèi)部的真正要掛載的內(nèi)容的vnode)

vnode是如何變成真實DOM的懂盐?

update褥赊,把 VNode 渲染成真實的 DOM。它被調(diào)用的時機有 2 個莉恼,一個是首次渲染拌喉,一個是數(shù)據(jù)更新的時候速那。其核心就是調(diào)用vm.__patch__,它通過nodeOps(操作dom標簽)和modules(操作dom標簽屬性)來個模塊來生成真實DOM尿背。因為vue可以跑在不同平臺(web/weex)端仰,他通過函數(shù)柯理化實現(xiàn),在createPatchFunction方法中 ....(一系列判斷邏輯)?return patch()残家,一方面兩個模塊參數(shù)通過閉包固化,不用重復傳入榆俺,其二一系列判斷邏輯固化,不用二次執(zhí)行,代碼更精練邏輯更清晰坞淮。
????首次渲染,vm.$el(index.html中id為app的Dom對象)作為初始節(jié)點vnode的父節(jié)點陪捷,執(zhí)行createElm方法回窘。1、把當前vnode創(chuàng)建真實DOM(如果?vnode?節(jié)點不包含?tag市袖,則它有可能是一個注釋或者純文本節(jié)點啡直,可以直接插入到父元素中)。2苍碟、通過createChildren創(chuàng)建子節(jié)點(其中就是對children數(shù)組中每一個值執(zhí)行creatElm創(chuàng)建真實DOM)酒觅。3、執(zhí)行insert把節(jié)點掛載到父節(jié)點上微峰。如此循化下去舷丹,節(jié)點的創(chuàng)建順序是父->子,節(jié)點掛載順序是子->父蜓肆。

組件是如何從vnode-》真實DOM的颜凯?

patch方法遇到組件時發(fā)生了什么?

patch的過程會執(zhí)行createComponent仗扬,是組件節(jié)點的話會執(zhí)行組件生成vnode時安裝的組件勾子函數(shù)init症概,init方法會執(zhí)行:
1、createComponentInstanceForVnode(vnode(當前正在執(zhí)行createElm的組件vnode)早芭,activeInstance(在patch的入口_update方法中申明中彼城,當前vm實例,也是當前執(zhí)行上下文)))。在其內(nèi)定義了options對象{ __isComponent: true,?_parentVnode: vnode, parent:??activeInstance}退个。最后執(zhí)行“生成vnode時構造的Vue子類實例”并把options傳入Ctor(options)募壕,子組件的實例化在其內(nèi)執(zhí)行(重新執(zhí)行_init方法),_init(options(第一步定義的options))帜乞。
2司抱、用$mount?方法掛載子組件。

組件是如何執(zhí)行_init方法的黎烈?

首先习柠,合并option變?yōu)閳?zhí)行initInternalComponent(vm(當前子組件vm實例),options(上一步定義的options))匀谣,方法:opts= vm.$options,opts.parent = options.parent(在patch的入口_update方法中申明资溃,父級vm實例,也是父級執(zhí)行上下文)武翎、opts._parentVnode = parentVnode(當前正在執(zhí)行createElm的組件vnode,組件的占位節(jié)點vnode)溶锭,它們是把之前我們通過?createComponentInstanceForVnode?函數(shù)傳入的幾個參數(shù)合并到內(nèi)部的選項?$options?里了宝恶。
其次,在初始化生命周期initLifecycle時:const options=vm.$options趴捅。let parent=options.parent垫毙。parent=parent.$parent。parent.$children.push(vm)拱绑。vm.$parent=parent综芥。確定了子組件實例和父組件實例的關系
其他的部分也和Vue實例一樣猎拨,初始化事件中心膀藐、初始化生命周期、初始化數(shù)據(jù)響應化红省、初始化渲染等等额各。

組件是如何掛載的?

因為組件options中沒有el屬性吧恃,所以它自己接管了$mount過程虾啦,也就是init方法的第二步。它最終會調(diào)用?mountComponent?方法蚜枢,進而執(zhí)行?vm._render()?方法:
1缸逃、vm.$vnode=_parentVnode(把在createComponent生成組件vnode賦給_parentVnode,作為當前組件的占位節(jié)點)厂抽。
2需频、vnode=render.call(vm._renderProxy,vm.$createElement)(這一步得到組件的渲染vnode,是真正要替換占位節(jié)點,要去掛載的vnode)筷凤。
3昭殉、vnode.parent=_parentVnode,確定組件占位節(jié)點vnode和渲染vnode的父子關系藐守。

接下來組件要通過vm._update(vnode(這里是_render生成的組件渲染vnode))去掛載了:
1挪丢、vm._vnode = vnode(渲染vnode),也就是說:vm._vnode.parent =?vm.$vnode卢厂。
2乾蓬、const prevActiveInstance = activeInstance,activeInstance = vm慎恒。activeInstance是一個init時初始化的全局變量任内,它儲存了當前子組件vm的實例作為當前執(zhí)行上下文(它是組件渲染vnode的上下文)撵渡,prevActiveInstance儲存了父級vm實例(它是組件占位節(jié)點vnode的上下文)。
3死嗦、_patch_的過程還是執(zhí)行createElm方法趋距,這次我們傳入的?vnode?是組件渲染的?vnode,也就是我們之前說的?vm._vnode越除。如果組件的根節(jié)點是個普通元素节腐,那么?vm._vnode?也是普通的?vnode,那么這里?createComponent(vnode, insertedVnodeQueue, parentElm, refElm)?的返回值是 false摘盆。接下來的過程就和我們開頭一樣了翼雀,先創(chuàng)建一個父節(jié)點占位符,然后再遍歷所有子 VNode 遞歸調(diào)用?createElm骡澈,在遍歷的過程中锅纺,如果遇到子 VNode 是一個組件的 VNode,則重復本節(jié)開始的過程肋殴,這樣通過一個遞歸的方式就可以完整地構建了整個組件樹。

Vue的合并配置是什么坦弟?

Vue中合并配置有兩種場景:1护锤、外部調(diào)用new Vue(options)。2酿傍、創(chuàng)建組件vnode時通過Vue.extend(options)來構造繼承自Vue的子類構造函數(shù)烙懦。兩種場景構造函數(shù)都會執(zhí)行實例的_init(options)方法,其中包含合并配置赤炒。

外部調(diào)用new Vue(options)如何進行配置合并氯析?

1、_init()中執(zhí)行vm.$options=mergeOptions(Vue.options,options,vm)來合并莺褒。Vue.options是在src/core/global-api/index.js(initGlobalAPI(Vue))中定義掩缓。
? ??initGlobalAPI(Vue)它主要是干了2件事情:1、在Vue.options對象下創(chuàng)建屬性名為(component(注冊組件), directive(注冊指令), filter(數(shù)據(jù)過濾))的空對象遵岩。2你辣、把一些內(nèi)置組件(keep-alive等)擴展到Vue.options.component中。
????回到mergeOptions()尘执,它的主要功能就是把parent和child兩個對象根據(jù)一些合并策略舍哄,合并成一個對象并返回。1誊锭、遞歸把child.extends和child.mixins合并到parent上表悬。2、分別遍歷parent和child調(diào)用mergeField(key)丧靡,對于不同的key由不同的合并策略蟆沫,例如對于生命周期勾子函數(shù)籽暇,如果Vue.options和options都定義了相同生命周期勾子函數(shù),則把兩個勾子函數(shù)合并成數(shù)組饥追。
總結就是图仓,把Vue實例默認配置和new Vue(options)中options配置,配置合并但绕。

組件場景如何進行配置合并救崔?

1、src/core/global-api/extend.js(Vue.extend(options))捏顺,其中會把子組件傳入對象(data,created等)和Vue.options一起合并到 子組件實例的 vm.options中蛙卤。
2、在組件vnode進行patch時德玫,會執(zhí)行 創(chuàng)建vnode時掛載的勾子函數(shù) 即“patch方法遇到組件時發(fā)生了什么扳碍?”這一步。初始化子組件實例拆座,再次進行配置合并(initInternalComponent(vm, options)?邏輯)主巍。先把上一步合并得到的options賦給vm.$options,再把實例化子組件時傳入的options(父實例,組件vnode占位節(jié)點parentVnode)保存到vm.$options 即"組件是如何執(zhí)行_init方法的挪凑?"這一步的配置合并孕索,另外還保留了 parentVnode 配置中的如?propsData?等其它的屬性。
總結就是躏碳,把子組件實例和父組件實例配置 合并搞旭,把子組件占位vnode和子組件實例 配置合并。

大部分庫菇绵、框架的設計都是類似的肄渗,自身定義了一些默認配置,同時又可以在初始化階段傳入一些定義配置咬最,然后去merge默認配置翎嫡,來達到定制化不同需求的目的。只不過在Vue場景下丹诀,會對merge的過程做一些精細化控制钝的。

生命周期是怎么樣的?

每個Vue實例在被創(chuàng)建之前都要經(jīng)過一系列的初始化過程铆遭。例如需要設置數(shù)據(jù)監(jiān)聽硝桩、模版編譯、掛載實例到DOM枚荣、在數(shù)據(jù)變化時更新DOM等碗脊。同時在這個過程中也會運行一些叫生命周期的鉤子函數(shù),給予用戶機會在一些特定的場景下添加他們自己的代碼。

源碼中最終執(zhí)行生命周期的函數(shù)都是調(diào)用callHook(hook)方法(src/core/instance/lifecycle)衙伶,根據(jù)傳入的hook字符串去拿到vm.$options[hook]對應的回調(diào)函數(shù)數(shù)組祈坠,然后遍歷執(zhí)行,執(zhí)行時候把vm作為函數(shù)執(zhí)行的上下文矢劲。vm.$options[hook]的相關生命周期鉤子函數(shù)的內(nèi)容定義在“配置合并”的時候完成了赦拘,因此?callhook?函數(shù)的功能就是調(diào)用某個生命周期鉤子注冊的所有回調(diào)函數(shù)。

各個生命周期鉤子函數(shù)的調(diào)用時機芬沉?

beforeCreate & created(src/core/instance/init.js中_init方法)躺同。beforeCreate?和?created?的鉤子調(diào)用是在?initState?的前后,initState?的作用是初始化?props丸逸、data蹋艺、methods、watch黄刚、computed?等屬性捎谨。
????那么顯然?beforeCreate?的鉤子函數(shù)中就不能獲取到?props、data?中定義的值憔维,也不能調(diào)用?methods?中定義的函數(shù)涛救。
????如果組件在加載的時候需要和后端有交互,放在這倆個鉤子函數(shù)執(zhí)行都可以业扒。
????如果是需要訪問?props州叠、data?等數(shù)據(jù)的話,就需要使用?created?鉤子函數(shù)凶赁。
????之后我們會介紹 vue-router 和 vuex 的時候會發(fā)現(xiàn)它們都混合了?beforeCreatd?鉤子函數(shù)。

beforeMount & mounted(src/core/instance/lifecycle.js中mountComponent方法)逆甜。在mountComponent一開始就執(zhí)行了beforeMount鉤子虱肄。
mounted執(zhí)行分兩種情況
1、期間通過render生成vnode和patch掛載真實Dom交煞,最后如果Vue實例為根實例(vm.$vnode=null,組件占位節(jié)點vnode為null)咏窿,則執(zhí)行mounted勾子函數(shù)。
2素征、如果是子組件實例集嵌,在從父-》子生成真實DOM,子-》父 進行真實DOM掛載的過程中御毅,掛載完畢就會執(zhí)行insert方法(子-》父)根欧,其中包含執(zhí)行mounted勾子函數(shù)。
總結beforeMount的執(zhí)行順序是先父后子端蛆,mounted執(zhí)行順序是先子后父凤粗。

beforeUpdate & updated。mountComponent中會new Watcher()創(chuàng)建渲染watcher今豆,在監(jiān)聽到組件響應式數(shù)據(jù)變化時嫌拣,會nextTick異步執(zhí)行beforeUpdate柔袁。之后再判斷如果 watcher是渲染watcher 且 已經(jīng)完成mounted步驟,就會執(zhí)行updated鉤子函數(shù)异逐。

beforeDestroy & destroyed(src/core/instance/lifecycle.js)捶索。beforeDestroy?和?destroyed?鉤子函數(shù)的執(zhí)行時機在組件銷毀的階段。
????beforeDestroy?鉤子函數(shù)的執(zhí)行時機是在?$destroy?函數(shù)執(zhí)行最開始的地方灰瞻,接著執(zhí)行了一系列的銷毀動作腥例,包括從?parent?的?$children?中刪掉自身,刪除?watcher箩祥,當前渲染的 VNode 執(zhí)行銷毀鉤子函數(shù)等院崇,執(zhí)行完畢后再調(diào)用?destroy?鉤子函數(shù)。
????在?$destroy?的執(zhí)行過程中袍祖,它又會執(zhí)行?vm.__patch__(vm._vnode, null)?觸發(fā)它子組件的銷毀鉤子函數(shù)底瓣,這樣一層層的遞歸調(diào)用,所以?destroy?鉤子函數(shù)執(zhí)行順序是先子后父蕉陋,和?mounted?過程一樣捐凭。

activated & deactivated。activated?和?deactivated?鉤子函數(shù)是專門為?keep-alive?組件定制的鉤子凳鬓。

組件注冊是怎么樣的茁肠?

組件注冊分為全局注冊局部注冊

要注冊一個全局組件缩举,可以使用?Vue.component(tagName, options)(src/core/global-api/assets.js)垦梆。
????全局注冊Vue.component()方法會執(zhí)行Vue.options.components[id] = Vue.extend(options),把組件實例掛載到Vue.options.components.id(即組件名)上仅孩。那么全局注冊的組件實例就掛載在根Vue配置上了托猩。
????在Vue.extend時,會把“開發(fā)人員寫的子組件配置”和“父組件默認配置” 合并辽慕。那么全局組件實例就掛載在當前子組件實例配置上了京腥。所有組件都會經(jīng)歷這一步,所以都可以取到全局組件的實例進行初始化溅蛉。

局部組件公浪,在子組件創(chuàng)建vnode時,執(zhí)行Vue.extend(options)會把“開發(fā)人員寫的子組件配置”和父組件默認配置 合并傳入當前子組件實例配置上船侧,其中子組件配置options包含了我們注冊的components組件對象欠气。那么子組件就可以取到局部組件的實例進行初始化。

注意:我們知道組件vnode生成總共3步勺爱。1晃琳、“構造子類構造函數(shù)”。2、“掛載組件鉤子函數(shù)(patch時候執(zhí)行)”卫旱。3人灼、"返回組件vnode實例"。全局組件的1在vue.component()方法中完成了顾翼,23在createElement中的creatComponent中完成投放。局部組件123都在createElement中的creatComponent中完成。至于一個組件會執(zhí)行幾次createElement适贸,在"render函數(shù)是如何變成Vnode的灸芳?"中的注意中已經(jīng)說明。

總結:當前組件實例中要能夠拿到注冊組件的實例拜姿,必須要先把注冊組件實例放入當前組件實例配置中烙样,而后在patch階段中才能取到它進行初始化。不管全局還是局部蕊肥,都在當前組件創(chuàng)建vnode構造子類構造函數(shù)中的配置合并階段谒获,把注冊組件實例傳給了當前組件實例作為配置。全局是Vue.options配置傳過來的壁却,局部是當前組件開發(fā)人員傳入的配置傳過來的批狱。

異步組件是怎么樣的?

在我們平時的開發(fā)工作中展东,為了減少首屏代碼體積赔硫,往往會把一些非首屏的組件設計成異步組件,按需加載盐肃。區(qū)別就是第二個參數(shù)由 對象 變成了 函數(shù)爪膊。它進入createElement后,也會取執(zhí)行createComponent砸王。他通過resolveAsyncComponent(asyncFactory, baseCtor, context)得到子類構造函數(shù)Ctor惊完。

異步組件的注冊三種方式:

普通異步組件注冊
promise形式注冊異步組件
高級異步組件注冊

普通異步組件:
? ? 1、vue.component('name', func)傳入一個函數(shù)处硬,結果是vue.options.components[name]=func。
? ? 2拇派、生成組件vnode執(zhí)行createElement-》createComponent荷辕。createComponent中因為ctor不存在,會執(zhí)行Ctor=resolveAsyncComponent(asyncFactory(異步函數(shù)),baseCtor(Vue),context(當前執(zhí)行createElement的上下文))件豌。
? ? 2.1疮方、第一次執(zhí)行resolveAsyncComponent:
????????把當前上下文(vm)放入asyncFactory.contexts中(哪個組件中有該異步組件,就會執(zhí)行這個過程,把有該異步組件的組件實例收集起來)
? ? ? ? (這個過程是異步的,會先執(zhí)行下面完同步代碼)執(zhí)行異步加載去加載組件對象res,加載到后會把組件拿去構造子類構造函數(shù)Vue.extends(res) 得到ctor茧彤,把ctor放入asyncFactory.resolved中骡显。再執(zhí)行forceRender方法,它遍歷每一個asyncFactory.contexts中的組件實例vm,去拿到vm._watcher(組件渲染watcher)執(zhí)行updata()來重新渲染惫谤,之后再次進入createElement壁顶,進入到4
? ? ?2.2溜歪、resolveAsyncComponent返回一個undefined若专。
? ? 3、(到這里第一輪同步代碼執(zhí)行結束)回到createComponent蝴猪,因為resolveAsyncComponent返回undefined调衰,會去執(zhí)行createAsyncPlaceholder。它創(chuàng)建了一個空的vnode節(jié)點(即注釋節(jié)點)自阱。
? ? 4嚎莉、(第二輪同步代碼),生成組件vnode執(zhí)行createElement-》createComponent沛豌。createComponent中因為ctor不存在趋箩,會執(zhí)行Ctor=resolveAsyncComponent()。此時發(fā)現(xiàn)asyncFactory.resolved中有ctor琼懊,返回ctor阁簸。接下來和普通組件流程一樣,去“掛載勾子函數(shù)”和“返回組件vnode”哼丈。

Promise異步組件:
? ? 和普通異步組件基本相同启妹。在2.1去執(zhí)行異步加載res對象后,它不用回調(diào)函數(shù)來處理異步加載的返回結果醉旦,而是同步直接判斷res是否是對象(import()異步加載會直接同步返回promise對象)饶米,是promise對象就由promise(resolve,reject)去處理返回結果。其實就是一個語法糖车胡。

高級異步組件注冊:
? ? 傳入一個函數(shù)檬输,這個函數(shù)返回一個配置對象。我們可以定義加載組件(component)對象匈棘,加載中(loading)組件對象丧慈,加載失敗(err)組件對象,過A時間開始(前提是未加載到組件)顯示加載中組件主卫,過B時間(前提是未加載到組件)顯示加載失敗組件逃默。
? ? 一開始若定義了相關組件都會給 構造子類構造函數(shù) 并賦值給asyncFactory.xxx中。
????先判斷delay簇搅,若為0直接顯示loading組件完域,否則過A時間 且 加載結果沒有出來,就設置asyncFactory.loading=true執(zhí)行forceRender重新渲染瘩将,再次createElement-》createComponent執(zhí)行到這里面的時候會把loading組件構造器返回出去吟税。
? ? 再判斷timeout凹耙,若timeout存在,則過B時間 且?加載結果沒有出來或者加載結果為失敗肠仪,就設置asyncFactory.error=true執(zhí)行forceRender重新渲染肖抱,再次createElement-》createComponent執(zhí)行到這里面的時候會把error組件構造器返回出去。

Vue的響應式原理是什么藤韵?

前面分析了在組件化的前提下虐沥,Vue如何把初始化數(shù)據(jù)渲染成DOM。接下來會通過分析“數(shù)據(jù)變化觸發(fā)DOM重新渲染”來深入了解 Vue的響應式原理泽艘。

一欲险、創(chuàng)建響應式對象

????_init初始化時,initState(vm)完成了響應式對象的創(chuàng)建匹涮,主要是對?props天试、methods、data然低、computed?和?wathcer?等屬性做了初始化操作喜每,這里我們重點分析?props?和?data。
? ? initProps初始化過程主要2件事:
? ? 1雳攘、調(diào)用defineReactive方法把每個prop對應的值變成響應式带兜。
? ? 2、通過defindProperty的set和get方法把對vm.key(即我們在組件中用的this.key)的訪問代理到vm._props.key吨灭。
? ??initData初始化過程主要2件事:
? ? 1刚照、調(diào)用?observe?方法觀測整個?data?的變化,把?data?也變成響應式喧兄。
? ? 2无畔、通過defindProperty的set和get方法把對vm.key(即我們在組件中用的this.key)的訪問代理到vm._props.key。

????observe方法作用:主要就是取Observer實例吠冤。根據(jù) "data.__ob__" 判斷是否實例化過Observe浑彰,若已經(jīng)實例化過Observe,直接取ob =?data.__ob__拯辙,并返回ob郭变。否則實例化Observe,ob = new Observe(data)涯保,并返回ob饵较。
? ??Observer方法作用:
? ? 1、const dep = new Dep() 實例化Dep對象遭赂。
? ? 2、給data.__ob__賦值 this(即當前Observer實例)横辆。
? ? 3撇他、判斷data是否是數(shù)組茄猫,是就循環(huán)每個子data調(diào)用observe方法。否則就對data每個屬性調(diào)用defineReactive方法困肩。
? ? defineReactive方法作用:主要功能就是定義響應式對象划纽。
? ? ? 1、const dep = new Dep() 實例化dep做為容器锌畸,get中用來收集依賴勇劣,set中用來對依賴派發(fā)更新。
? ? ? 2潭枣、get比默,用來收集依賴(收集訂閱該數(shù)據(jù)的渲染_watcher)
? ? ? 3、set盆犁,用來派發(fā)更新(當該數(shù)據(jù)發(fā)生變化時命咐,通知訂閱該數(shù)據(jù)的_watcher重新渲染)

二、依賴收集

????????組件執(zhí)行$mount調(diào)用的mountComponent中定義了updateComponent方法谐岁,并實例化了一個渲染Watcher(vm, updateComponent, noop)醋奠。
? ? new Watcher(vm, updateComponent, noop)的流程:
? ? 1、vm._watcher=this(把this(當前Watcher類)賦值給當前vm實例的渲染_watcher)伊佃。
? ? 2窜司、定義一個deps和一個newDeps數(shù)組,用來收集該渲染W(wǎng)atcher訂閱了的響應式數(shù)據(jù)的dep對象航揉。
? ? 3塞祈、this.getter = updateComponent(如果傳入的第二個參數(shù)是函數(shù),把函數(shù)賦值給 this.getter)迷捧。
? ? 4织咧、this.value=this.get()
? ? ? ? 4.1漠秋、pushTarget(this)笙蒙。讓當前watcher實例 成為 全局watcher(同一時間內(nèi), 只能有一個渲染_watcher成為全局watcher)。
? ? ? ? 注意:這里有一個“壓椙旖酰”機制捅位。一個targetStack儲存歷史渲染W(wǎng)atcher。一個Dep.target儲存全局Watcher搂抒。當一個渲染_watcherA要成為全局Watcher時艇搀,發(fā)現(xiàn)現(xiàn)在_watcherB是全局Watcher,則把_watcherB推入targetStack求晶,_watcherA上位焰雕。_watcherB想出來上位,也可以通過Dep.target=targetStack.pop()把_watcherA擠掉芳杏。
????????這個機制是為了解決組件嵌套組建情況矩屁,comA中有一個comB辟宗。_watcherA在位時,執(zhí)行到patch階段吝秕,comB的vm實例化開始泊脐,_watcherB上位,_watcherA被推入targetStack烁峭。當comB掛載完畢容客,又把_watcherA請出來上位,繼續(xù)渲染下面的內(nèi)容约郁。
? ? ? ? 4.2缩挑、let?value=this.getter.call(vm,vm)。相當于執(zhí)行vm._update(vm._render(), hydrating)棍现。vm_render()生成vnode時调煎,會對vm上數(shù)據(jù)進行訪問,觸發(fā)數(shù)據(jù)對象的 getter己肮。
? ? ? ? 4.3士袄、進入defineReactive的get函數(shù)內(nèi),如果當前Dep.target存在(4.1已經(jīng)賦值)谎僻,就執(zhí)行Dep.target.addDep(dep)娄柳。1、把dep收集進Dep.target(全局Watcher)的newDeps中艘绍。2赤拒、把(全局Watcher)收集進dep.subs。完成依賴收集诱鞠,數(shù)據(jù)變更要派發(fā)更新就去dep.subs中找訂閱該數(shù)據(jù)的渲染_watcher挎挖。所以在?vm._render()?過程中,會觸發(fā)所有數(shù)據(jù)的 getter航夺,這樣實際上已經(jīng)完成了一個依賴收集的過程蕉朵。
? ? ? ? 4.4、traverse,這個是要遞歸去訪問?value阳掐,觸發(fā)它所有子項的?getter始衅。
? ? ? ? 4.5、popTarget()缭保,即4.1中“壓椦凑ⅲ”機制中的targetStack.pop()。就是把?Dep.target?恢復成上一個狀態(tài)艺骂,因為當前 vm 的數(shù)據(jù)依賴收集已經(jīng)完成诸老,那么對應的渲染Dep.target?也需要改變。
? ? ? ? 4.6钳恕、this.cleanupDeps()别伏,依賴清空吮廉。因為Vue 是數(shù)據(jù)驅(qū)動的,所以每次數(shù)據(jù)變化都會重新 render畸肆,那么?vm._render()?方法又會再次執(zhí)行,并再次觸發(fā)數(shù)據(jù)的 getters宙址,所以?Wathcer?在構造函數(shù)中會初始化 2 個?Dep?實例數(shù)組轴脐,newDeps?表示新添加的?Dep?實例數(shù)組,而?deps?表示上一次添加的?Dep?實例數(shù)組抡砂。
? ? ? ?在執(zhí)行?cleanupDeps?函數(shù)的時候大咱,會首先遍歷?deps,移除對?dep.subs?數(shù)組中?Wathcer?的訂閱注益,然后newDeps?和?deps?交換碴巾,并把?newDepIds?和?newDeps?清空。那么為什么需要做?deps?訂閱的移除呢丑搔,在添加?deps?的訂閱過程厦瓢,已經(jīng)能通過?id?去重避免重復訂閱了。
? ??????考慮到一種場景啤月,我們的模板會根據(jù)?v-if?去渲染不同子模板 a 和 b煮仇,當我們滿足某種條件的時候渲染 a 的時候,會訪問到 a 中的數(shù)據(jù)谎仲,這時候我們對 a 使用的數(shù)據(jù)添加了 getter浙垫,做了依賴收集,那么當我們?nèi)バ薷?a 的數(shù)據(jù)的時候郑诺,理應通知到這些訂閱者夹姥。那么如果我們一旦改變了條件渲染了 b 模板,又會對 b 使用的數(shù)據(jù)添加了 getter辙诞,如果我們沒有依賴移除的過程辙售,那么這時候我去修改 a 模板的數(shù)據(jù),會通知 a 數(shù)據(jù)的訂閱的回調(diào)倘要,這顯然是有浪費的圾亏。因此 Vue 設計了在每次添加完新的訂閱,會移除掉舊的訂閱封拧,這樣就保證了在我們剛才的場景中志鹃,如果渲染 b 模板的時候去修改 a 模板的數(shù)據(jù),a 數(shù)據(jù)訂閱回調(diào)已經(jīng)被移除了泽西,所以不會有任何浪費曹铃,真的是非常贊嘆 Vue 對一些細節(jié)上的處理。

三捧杉、派發(fā)更新

當響應式數(shù)據(jù)發(fā)生變化陕见,執(zhí)行dep.notify()秘血。遍歷所有的?subs(渲染_watcher?的實例數(shù)組),然后調(diào)用每一個 _watcher的?update?方法(src/core/observer/watcher.js)评甜。對于 _watcher?的不同狀態(tài)灰粮,會執(zhí)行不同的邏輯,computed?和?sync?等狀態(tài)先不分析忍坷,在一般組件數(shù)據(jù)更新的場景粘舟,會走到最后一個?queueWatcher(this)?的邏輯( src/core/observer/scheduler.js )。
? ??這里引入了一個隊列的概念佩研,這也是 Vue 在做派發(fā)更新的時候的一個優(yōu)化的點柑肴,它并不會每次數(shù)據(jù)改變都觸發(fā)?watcher?的回調(diào),而是把這些?watcher?先添加到一個隊列里旬薯,然后在?nextTick?后執(zhí)行?flushSchedulerQueue(src/core/observer/scheduler.js)晰骑。這里有幾個細節(jié)要注意一下:1、首先用?has?對象保證同一個?Watcher?只添加一次绊序。2硕舆、接著對?flushing的判斷,else 部分的邏輯稍后講政模。3岗宣、最后通過?waiting?保證對?nextTick(flushSchedulerQueue)的調(diào)用只有一次flushSchedulerQueue異步執(zhí)行。
? ? 淋样、隊列排序
? ? queue.sort((a, b) => a.id - b.id)?對隊列做了從小到大的排序耗式,這么做主要有以下要確保以下幾點:
1.組件的更新由父到子;因為父組件的創(chuàng)建過程是先于子的趁猴,所以?watcher?的創(chuàng)建也是先父后子刊咳,執(zhí)行順序也應該保持先父后子。
2.用戶的自定義?watcher?要優(yōu)先于渲染?watcher?執(zhí)行儡司;因為用戶自定義?watcher?是在渲染?watcher?之前創(chuàng)建的娱挨。
3.如果一個組件在父組件的?watcher?執(zhí)行期間被銷毀,那么它對應的?watcher?執(zhí)行都可以被跳過捕犬,所以父組件的?watcher?應該先執(zhí)行跷坝。
? ? 隊列遍歷
? ??在對?queue?排序后碉碉,接著就是要對它做遍歷柴钻,拿到對應的?watcher,執(zhí)行?watcher.run()垢粮。這里需要注意一個細節(jié)贴届,在遍歷的時候每次都會對?queue.length?求值,因為在?watcher.run()?的時候,很可能用戶會再次添加新的?watcher毫蚓,這樣會再次執(zhí)行到?queueWatcher占键。
? ? 三、狀態(tài)恢復
? ??這個過程就是執(zhí)行?resetSchedulerState?函數(shù)(src/core/observer/scheduler.js)元潘。邏輯非常簡單畔乙,就是把這些控制流程狀態(tài)的一些變量恢復到初始值,把?watcher?隊列清空翩概。

接下來我們繼續(xù)分析?watcher.run() (src/core/observer/watcher.js)
????????run?函數(shù)實際上就是執(zhí)行?this.getAndInvoke?方法啸澡,并傳入?watcher?的回調(diào)函數(shù)。getAndInvoke?函數(shù)邏輯也很簡單氮帐,先通過?this.get()?得到它當前的值,然后做判斷洛姑,如果滿足新舊值不等上沐、新值是對象類型、deep?模式任何一個條件楞艾,則執(zhí)行?watcher?的回調(diào)参咙,注意回調(diào)函數(shù)執(zhí)行的時候會把第一個和第二個參數(shù)傳入新值?value?和舊值?oldValue,這就是當我們添加自定義?watcher?的時候能在回調(diào)函數(shù)的參數(shù)中拿到新舊值的原因硫眯。
? ??????對于渲染?watcher?而言蕴侧,它在執(zhí)行?this.get()?方法求值的時候,會執(zhí)行?getter?方法(updateComponent)两入,這就是當我們?nèi)バ薷慕M件相關的響應式數(shù)據(jù)的時候净宵,會觸發(fā)組件重新渲染的原因,接著就會重新執(zhí)行?patch?的過程裹纳,但它和首次渲染有所不同择葡。

組件如何更新的?

現(xiàn)在我們知道剃氧,響應式數(shù)據(jù)發(fā)生變化時候敏储,會觸發(fā)訂閱該數(shù)據(jù)的渲染W(wǎng)atcher的回調(diào)函數(shù)(updateComponent),進而執(zhí)行組件更新朋鞍。
首先進行sameVNode(oldVnode, vnode)判斷新舊節(jié)點是否相同已添,來走到不通的邏輯。
若新舊節(jié)點不同:直接替換節(jié)點滥酥。其步驟分3步:
1更舞、createElm()創(chuàng)建新節(jié)點并插入Dom。
2恨狈、更新父的占位符節(jié)點疏哗。找到當前?vnode?的父的占位符節(jié)點,先執(zhí)行各個?module?的?destroy?的鉤子函數(shù),如果當前占位符是一個可掛載的節(jié)點返奉,則執(zhí)行?module?的?create?鉤子函數(shù)贝搁。
3、刪除舊節(jié)點芽偏。
若新舊節(jié)點相同雷逆,會調(diào)用?patchVNode?方法,它是把新的?vnodepatch?到舊的?vnode?上污尉,我把它拆成四步驟:
1膀哲、執(zhí)行?prepatch?鉤子函數(shù)。當更新的?vnode?是一個組件?vnode?的時候被碗,會執(zhí)行?prepatch?的方法拿到新的?vnode?的組件配置以及組件實例某宪,去執(zhí)行?updateChildComponent?方法,updateChildComponent?的邏輯也非常簡單锐朴,由于更新了?vnode兴喂,那么?vnode?對應的實例?vm?的一系列屬性也會發(fā)生變化,包括占位符?vm.$vnode?的更新焚志、slot?的更新衣迷,listeners?的更新,props?的更新等等酱酬。
2壶谒、執(zhí)行?update?鉤子函數(shù)∩殴粒回到?patchVNode?函數(shù)汗菜,在執(zhí)行完新的?vnode?的?prepatch?鉤子函數(shù),會執(zhí)行所有?module?的?update?鉤子函數(shù)以及用戶自定義?update?鉤子函數(shù)挑社,對于?module?的鉤子函數(shù)呵俏,之后我們會有具體的章節(jié)針對一些具體的 case 分析。
3滔灶、完成?patch?過程普碎。如果?vnode?是個文本節(jié)點且新舊文本不相同,則直接替換文本內(nèi)容录平。如果不是文本節(jié)點拒迅,則判斷它們的子節(jié)點挠他,并分了幾種情況處理:1、oldCh?與?ch?都存在且不相同時,使用?updateChildren?函數(shù)來更新子節(jié)點灯抛,這個后面重點講焚鲜。2.如果只有?ch?存在求橄,表示舊節(jié)點不需要了镇匀。如果舊的節(jié)點是文本節(jié)點則先將節(jié)點的文本清除,然后通過?addVnodes?將?ch?批量插入到新節(jié)點?elm?下。3.如果只有?oldCh?存在彼水,表示更新的是空節(jié)點崔拥,則需要將舊的節(jié)點通過?removeVnodes?全部清除。4.當只有舊節(jié)點是文本節(jié)點的時候凤覆,則清除其節(jié)點文本內(nèi)容链瓦。
4、執(zhí)行?postpatch?鉤子函數(shù)盯桦。再執(zhí)行完?patch?過程后慈俯,會執(zhí)行?postpatch?鉤子函數(shù),它是組件自定義的鉤子函數(shù)拥峦,有則執(zhí)行贴膘。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市略号,隨后出現(xiàn)的幾起案子步鉴,更是在濱河造成了極大的恐慌,老刑警劉巖璃哟,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異喊递,居然都是意外死亡随闪,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門骚勘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來铐伴,“玉大人,你說我怎么就攤上這事俏讹〉毖纾” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵泽疆,是天一觀的道長户矢。 經(jīng)常有香客問我,道長殉疼,這世上最難降的妖魔是什么梯浪? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮瓢娜,結果婚禮上挂洛,老公的妹妹穿的比我還像新娘。我一直安慰自己眠砾,他們只是感情好虏劲,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般柒巫。 火紅的嫁衣襯著肌膚如雪励堡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天吻育,我揣著相機與錄音念秧,去河邊找鬼。 笑死布疼,一個胖子當著我的面吹牛摊趾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播游两,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼砾层,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了贱案?” 一聲冷哼從身側(cè)響起肛炮,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎宝踪,沒想到半個月后侨糟,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡瘩燥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年秕重,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片厉膀。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡溶耘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出服鹅,到底是詐尸還是另有隱情凳兵,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布企软,位于F島的核電站庐扫,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏仗哨。R本人自食惡果不足惜聚蝶,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望藻治。 院中可真熱鬧碘勉,春花似錦、人聲如沸桩卵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至胜嗓,卻和暖如春高职,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背辞州。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工怔锌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人变过。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓埃元,卻偏偏與公主長得像,于是被迫代替她去往敵國和親媚狰。 傳聞我的和親對象是個殘疾皇子岛杀,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348

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