數(shù)據(jù)驅(qū)動(dòng)
釋疑:
那為什么不用class來(lái)寫(xiě)vue而是用構(gòu)造函數(shù)的形式呢?
這是因?yàn)閂ue這個(gè)對(duì)象的方法太多了,很多都需要放在不同的模塊來(lái)寫(xiě)的,在不同的模塊我只要在vue的prototype上去添加方法就好了。如果是class,就不好添加了,只能在一個(gè)class上添加,繼承的話調(diào)用的也是其他class了,所以采用構(gòu)造函數(shù)的形式。
構(gòu)造函數(shù)和class的使用場(chǎng)景區(qū)別
當(dāng)一個(gè)對(duì)象或模塊頻繁用到繼承或者內(nèi)部屬性比較固定,采用OOB的思想,即采用class,如果一個(gè)模塊非常大,要分散到多個(gè)子模塊去編寫(xiě),且不怎么用到繼承采用構(gòu)造函數(shù)的形式。
整體流程:
new Vue => init => $mount => compile => render => vnode => update(patch) => dom
new Vue & init:
混入了一些全局方法
$mount
調(diào)用render函數(shù),沒(méi)有則創(chuàng)建一個(gè)
調(diào)用beforeMount,beforeUpdate,mounted鉤子
通過(guò)update方法渲染dom
返回vm
render
render就是createElement方法
Visual DOM
一個(gè)描述DOM的類
createElement
格式化children
創(chuàng)建vnode
update
其本質(zhì)就是調(diào)用了patch方法
patch方法就是將vnode遍歷創(chuàng)建DOM并插入(中間處理了文本節(jié)點(diǎn),注釋節(jié)點(diǎn))
組件化
Vue的組件化我們需要了解這么幾個(gè)部分:
1. createComponent(創(chuàng)建組件)
創(chuàng)建子類構(gòu)造函數(shù)
-
安裝組件鉤子,組件鉤子包括(組件的生命周期會(huì)在這幾個(gè)鉤子內(nèi)部具體調(diào)用)
- init
- prepatch
- insert
- destroy
創(chuàng)建vnode實(shí)例
2. patch
組件的patch的時(shí)候會(huì)合并配置項(xiàng),最后調(diào)用$mount(通過(guò)update)去進(jìn)行最后的渲染。
ps:
先父組件后子組件的有:
beforeCreate
created
beforeMount
beforeUpdate
updated
beforeDestroy
先子組件后父組件的有:
mounted
destroyed
3. 合并配置
vue源碼內(nèi)部會(huì)根據(jù)參數(shù)的不同采取不同的合并策略進(jìn)行配置合并。
4. 生命周期
1.beforeCreate & created
beforeCreate 在initState之前,而initState 的作用是初始化 props、data、methods、watch、computed 等屬性,所以beforeCreate拿不到。
-
beforeMount & mounted
- beforeMount 調(diào)用在_render之前
- mounted 調(diào)用在_update之后
- 子組件在insert方法中調(diào)用了mounted
beforeUpdate & updated
這2個(gè)鉤子會(huì)在mounted之后觸發(fā)。beforeDestroy & destroyed
在 $destroy 的執(zhí)行過(guò)程中,它又會(huì)執(zhí)行 vm.patch(vm._vnode, null) 觸發(fā)它父組件的銷毀鉤子函數(shù),這樣一層層的遞歸調(diào)用,所以 destroy 鉤子函數(shù)執(zhí)行順序是先子后父,和 mounted 過(guò)程一樣。
5. 組件注冊(cè)
- vue在初始化全局API的時(shí)候會(huì)將所有的組件合并到配置項(xiàng)中(全局注冊(cè)會(huì)合并到Vue原型鏈上的opinion,否則添加到該組件名下的配置項(xiàng)中)
- 我們?cè)谑褂玫倪^(guò)程中會(huì)根據(jù)tag的名字去拿,如果拿不到用駝峰的形式去拿,還拿不到用短橫線的形式去拿,這就是為什么我們注冊(cè)組件后使用的時(shí)候可以用駝峰和短橫線的原因。
6. 異步組件
vue將異步組件分成了普通組件,promsie組件和高階組件分別進(jìn)行處理。
- 先定義通用的forceRender, resolve, reject3個(gè)方法
- 由于異步組件組的解析過(guò)程中數(shù)據(jù)沒(méi)有發(fā)生變化,因此需要通過(guò)foceRender中的foreceupdate去強(qiáng)制更新
- 最后拿到解析后的組件,通過(guò)extend方法轉(zhuǎn)成組件的構(gòu)造函數(shù)。
- 由于之前定義好了resolve,reject,webpack通過(guò)import的方式返回的是一個(gè)promsie對(duì)象,那么該promsie最后的resovle和reject也會(huì)變成入?yún)⒈唤Y(jié)合進(jìn)來(lái)。
- 高級(jí)組件就是加了loading和timeout字段,由于vue解析異步組件的時(shí)候會(huì)先判斷是不是對(duì)象,對(duì)象的話就回去拿loading,和timeout字段。這里注意的一點(diǎn)是如果這是延遲為0,那么第一次解析的時(shí)候會(huì)返回我們定義好的loading組件否則在之后會(huì)創(chuàng)建一個(gè)注釋vnode,最后在foreRender的過(guò)程中會(huì)被替換。loading和timeout最后都會(huì)通過(guò)forceRender去渲染最終的結(jié)果。
- 由于開(kāi)始都回生成一個(gè)占位注釋節(jié)點(diǎn),等拿到組件后會(huì)去調(diào)用foreRender方法去更新,所以所有的異步組件其實(shí)都是渲染了2次。
Vue響應(yīng)式系統(tǒng)
1. 創(chuàng)建響應(yīng)式對(duì)象:
observe(xxx) => xxx是數(shù)組 ?遞歸observe :walk(調(diào)用defineReactive), observe整個(gè)動(dòng)作是vue核心庫(kù)需要的,最后變成響應(yīng)式對(duì)象是靠defineReactive實(shí)現(xiàn)的。
依賴收集和派發(fā)更新有2個(gè)概念:
- dep ,這是一個(gè)包含 依賴id, 訂閱者(watcher), 依賴數(shù)組的類,這類主要是用來(lái)管理watcher的。
- watcher,這個(gè)類的作用主要是數(shù)據(jù)發(fā)生變化的時(shí)候,調(diào)用各個(gè)方法產(chǎn)生計(jì)算和更改視圖的,包含了各種屬性。
2. 依賴收集:
依賴收集發(fā)生在數(shù)據(jù)屬性的get階段。干了這么一件事:
將當(dāng)前wather變成持有這個(gè)dep的訂閱者。
依賴收集要清空原來(lái)依賴,使用的新的依賴。防止重復(fù)訂閱的浪費(fèi)。
3. 派發(fā)更新:
派發(fā)更新主要發(fā)生在數(shù)據(jù)屬性的set階段, 做了這么幾個(gè)步驟:
- 判斷新值和舊值如果相等或者同為NaN,則直接返回。
- 通過(guò)observe將新值變?yōu)轫憫?yīng)式對(duì)象
- 通過(guò)dep.notify()派發(fā)更新。
派發(fā)更新的流程:
值發(fā)生改變 => dep.notify()=> 遍歷觸發(fā)訂閱的wathcer的update=> 根據(jù)是否同步等條件最后觸發(fā)watcher.run方法(渲染函數(shù)觸發(fā)get執(zhí)行更新DOM)=> 觸發(fā)watcher的回調(diào)(用戶可以拿到新值和舊值)
4. [檢測(cè)變化的注意事項(xiàng)]
通過(guò)a.b = 1給對(duì)象新添加屬性。
通過(guò)arr[0] = 1直接給原數(shù)組的元素賦值。
通過(guò)vm.items.length = newLength修改數(shù)組長(zhǎng)度。
上述3中可以通過(guò)set方法變成響應(yīng)式。核心還是通過(guò)defineReactive和dep.notify()
5. 計(jì)算屬性VS偵聽(tīng)屬性
-
計(jì)算屬性在創(chuàng)建wathcer的時(shí)候會(huì)置上一個(gè)標(biāo)志位lazy,做了2層優(yōu)化;
- 在初始化的時(shí)候不會(huì)去計(jì)算
- 在更新時(shí)候比較前后值是否一樣否則不會(huì)渲染(所有響應(yīng)式數(shù)據(jù)都一樣)。
watch
就是通過(guò)traverse做了遞歸響應(yīng)式。
watcher的種類:
deep watcher(遞歸添加watcher)
user watcher(用戶的watcher)
computed watcher
sync watcher(可以在user watcher里配置)
renderwatcher(渲染 watcher,一般來(lái)說(shuō)處的位置靠上)
6. 組件更新:
說(shuō)下數(shù)據(jù)變化到更新整個(gè)流程:
依賴發(fā)生變化 => watcher.getter => vm._update => vm.patch
判斷是否已是同一個(gè)節(jié)點(diǎn)的邏輯:
key => tag => isComment => data => sameInputType
如果新舊節(jié)點(diǎn)不同,那么主要分3步進(jìn)行:
- 創(chuàng)建新節(jié)點(diǎn)
- 更新父的占位節(jié)點(diǎn)
- 刪除舊節(jié)點(diǎn)
如果新舊節(jié)點(diǎn)相同:
- 執(zhí)行prepatch鉤子函數(shù)(去拿新的 vnode 的組件配置以及組件實(shí)例,去執(zhí)行 updateChildComponent 方法)
- 執(zhí)行 update 鉤子函數(shù)
- 完成 patch 過(guò)程
patch大致邏輯是這樣的:
- 新節(jié)點(diǎn)存在文本節(jié)點(diǎn)的情況下,當(dāng)舊節(jié)點(diǎn)的子節(jié)點(diǎn)和新節(jié)點(diǎn)的子節(jié)點(diǎn)都存在的情況下,開(kāi)始diff算法。
- 當(dāng)舊節(jié)點(diǎn)沒(méi)有子節(jié)點(diǎn)的情況下,檢查新節(jié)點(diǎn)子節(jié)點(diǎn)的key的重復(fù)性.如果舊節(jié)點(diǎn)存在文本節(jié)點(diǎn),則清空文本節(jié)點(diǎn)。然后批量將新節(jié)點(diǎn)的子節(jié)點(diǎn)添加到elem后面
- 如果舊節(jié)點(diǎn)存在子節(jié)點(diǎn),則移除所以的子節(jié)點(diǎn)。
- 如果新節(jié)點(diǎn)沒(méi)有子節(jié)點(diǎn)但是舊節(jié)點(diǎn)有子節(jié)點(diǎn),則清空舊節(jié)點(diǎn)的文本內(nèi)容
- 若新舊節(jié)點(diǎn)文本內(nèi)容不同則替換。
diff算法其實(shí)遵循幾個(gè)原則:
能移動(dòng)就移動(dòng)
先比頭和尾
頭尾比不了,就找中間key或索引一樣的比,然后添加到舊頭
新vnode較長(zhǎng)則在舊vnode上添加node,較短則刪除。
7. Props;
- 規(guī)范化
當(dāng) props 是一個(gè)數(shù)組,每一個(gè)數(shù)組元素 prop 只能是一個(gè) string,表示 prop 的 key,轉(zhuǎn)成駝峰格式,prop 的類型為空。
當(dāng) props 是一個(gè)對(duì)象,對(duì)于 props 中每個(gè) prop 的 key,我們會(huì)轉(zhuǎn)駝峰格式,而它的 value,如果不是一個(gè)對(duì)象,我們就把它規(guī)范成一個(gè)對(duì)象。
如果 props 既不是數(shù)組也不是對(duì)象,就拋出一個(gè)警告。 - 初始化(校驗(yàn)、響應(yīng)式和代理)
釋疑:
1. 為什么說(shuō)Vue是異步更新,因?yàn)閐ep.notify()調(diào)用了watcher的udpate方法,這個(gè)方法調(diào)用了queueWatcher方方法,最終調(diào)用了nextTick這個(gè)異步方法,所以是異步更新的。
2. 在依賴收集階段如果碰到對(duì)象里面的屬性是數(shù)組的,如果數(shù)組的值是基本類型的,那么通過(guò)Object.defineproperty無(wú)法對(duì)其響應(yīng)式化,因此數(shù)組元素的變化是無(wú)法觸發(fā)更新的
我們能在watch拿到新值和舊值的原因是因?yàn)樵趙ather執(zhí)行run的時(shí)候,會(huì)將新舊值傳到回調(diào)里
3. push, unshift, splice是響應(yīng)式操作的原因是Vue重寫(xiě)了這3個(gè)方法。
4. 為什么每次獲取計(jì)算屬性的值時(shí)都要進(jìn)行依賴收集呢,而不是僅進(jìn)行一次性的依賴收集?原因是,計(jì)算屬性的依賴項(xiàng)可能會(huì)改變,這次有x個(gè)依賴項(xiàng),下次可能有y個(gè)依賴項(xiàng)。比如三元表達(dá)式
編譯
- 解析模板字符串就是根據(jù)通過(guò)正則按照不同的情況生成一個(gè)js對(duì)象,
- 優(yōu)化語(yǔ)法書(shū)其實(shí)就是標(biāo)記靜態(tài)節(jié)點(diǎn)和靜態(tài)根,提高編譯的效率。
- 生成代碼就是根據(jù)不同的條件將代碼串生成可以執(zhí)行的代碼。
擴(kuò)展
1. event
先通過(guò)正則把節(jié)點(diǎn)上的元素都解析出來(lái),并對(duì)事件是原生事件還是自定義事件加以區(qū)分。然后把所有的事件用 vm._events 存儲(chǔ)起來(lái). on 就是往_events 里push,off就是對(duì)_events的元素進(jìn)行刪除,once就是兩者結(jié)合一下。
2. v-model
v-model其實(shí)是一種語(yǔ)法糖,其本質(zhì)利用了父子組件的通信完成的。
3. slot
普通插槽是在父組件編譯和渲染階段生成 vnodes,所以數(shù)據(jù)的作用域是父組件實(shí)例,子組件渲染的時(shí)候直接拿到這些渲染好的 vnodes。而對(duì)于作用域插槽,父組件在編譯和渲染階段并不會(huì)直接生成 vnodes,而是在父節(jié)點(diǎn) vnode 的 data 中保留一個(gè) scopedSlots 對(duì)象,存儲(chǔ)著不同名稱的插槽以及它們對(duì)應(yīng)的渲染函數(shù),只有在編譯和渲染子組件階段才會(huì)執(zhí)行這個(gè)渲染函數(shù)生成 vnodes,由于是在子組件環(huán)境執(zhí)行的,所以對(duì)應(yīng)的數(shù)據(jù)作用域是子組件實(shí)例。
4. keep-alive
vue內(nèi)部對(duì)keep-alive做了特殊處理,在執(zhí)行prepatch階段會(huì)重新渲染緩存的組件,沒(méi)有重新生成一個(gè)vue實(shí)例,因此也沒(méi)有標(biāo)準(zhǔn)組件的生命周期。
transition && transition-group
自動(dòng)嗅探目標(biāo)元素是否應(yīng)用了 CSS 過(guò)渡或動(dòng)畫(huà),如果是,在恰當(dāng)?shù)臅r(shí)機(jī)添加/刪除 CSS 類名。
如果過(guò)渡組件提供了 JavaScript 鉤子函數(shù),這些鉤子函數(shù)將在恰當(dāng)?shù)臅r(shí)機(jī)被調(diào)用。
如果沒(méi)有找到 JavaScript 鉤子并且也沒(méi)有檢測(cè)到 CSS 過(guò)渡/動(dòng)畫(huà),DOM 操作 (插入/刪除) 在下一幀中立即執(zhí)行。
所以真正執(zhí)行動(dòng)畫(huà)的是我們寫(xiě)的 CSS 或者是 JavaScript 鉤子函數(shù),而 Vue 的 <transition> 只是幫我們很好地管理了這些 CSS 的添加/刪除,以及鉤子函數(shù)的執(zhí)行時(shí)機(jī)。
transition && transition-group