㈠ 【面試題解析】從 Vue 源碼分析 key 的作用
最近看了面試題中有一個這樣的題, v-for 為什麼要綁定 key?
Vue 中 key 很多人都弄不清楚有什麼作用,甚至還有些人認為不綁定 key 就會報錯。
其實沒綁定 key 的話,Vue 還是可以正常運行的,報警告是因為沒通過 Eslint 的檢查。
接下來將通過源碼一步步分析這個 key 的作用。
Virtual DOM 最主要保留了 DOM 元素的層級關系和一些基本屬性,本質上就是一個 JS 對象。相對於真實的 DOM,Virtual DOM 更簡單,操作起來速度更快。
如果需要改變 DOM,則會通過新舊 Virtual DOM 對比,找出需要修改的節點進行真實的 DOM 操作,從而減小性能消耗。
傳統的 Diff 演算法需要遍歷一個樹的每個節點,與另一棵樹的每個節點對比,時間復雜度為 O(n²)。
Vue 採用的 Diff 演算法則通過逐級對比,大大降低了復雜性,時間復雜度為 O(n)。
VNode 更新首先會經過 patch 函數, patch 函數源碼如下:
vnode 表示更新後的節點,oldVnode 表示更新前的節點,通過對比新舊節點進行操作。
1、vnode 未定義,oldVnode 存在則觸發 destroy 的鉤子函數
2、oldVnode 未定義,則根據 vnode 創建新的元素
3、oldVnode 不為真實元素並且 oldVnode 與 vnode 為同一節點,則會調用 patchVnode 觸發更新
4、oldVnode 為真實元素或者 oldVnode 與 vnode 不是同一節點,另做處理
接下來會進入 patchVnode 函數,源碼如下:
1、vnode 的 text 不存在,則會比對 oldVnode 與 vnode 的 children 節點進行更新操作
2、vnode 的 text 存在,則會修改 DOM 節點的 text
接下來在 updateChildren 函數內就可以看到 key 的用處。
key 的作用主要是給 VNode 添加唯一標識,通過這個 key,可以更快找到新舊 VNode 的變化,從而進一步操作。
key 的作用主要表現在以下這段源碼中。
updateChildren 過程為:
1、分別用兩個指針(startIndex, endIndex)表示 oldCh 和 newCh 的頭尾節點
2、對指針所對應的節點做一個兩兩比較,判斷是否屬於同一節點
3、如果4種比較都沒有匹配,那麼判斷是否有 key,有 key 就會用 key 去做一個比較;無 key 則會通過遍歷的形式進行比較
4、比較的過程中,指針往中間靠,當有一個 startIndex > endIndex,則表示有一個已經遍歷完了,比較結束
從 VNode 的渲染過程可以得知,Vue 的 Diff 演算法先進行的是同級比較,然後再比較子節點。
子節點比較會通過 startIndex、endIndex 兩個指針進行兩兩比較,再通過 key 比對子節點。如果沒設置 key,則會通過遍歷的方式匹配節點,增加性能消耗。
所以不綁定 key 並不會有問題,綁定 key 之後在性能上有一定的提升。
綜上,key 主要是應用在 Diff 演算法中,作用是為了更快速定位出相同的新舊節點,盡量減少 DOM 的創建和銷毀的操作。
希望以上內容能夠對各位小夥伴有所幫助,祝大家面試順利。
Vue 的文檔中對 key 的說明如下:
關於就地修改,關鍵在於 sameVnode 的實現,源碼如下:
可以看出,當 key 未綁定時,主要通過元素的標簽等進行判斷,在 updateChildren 內會將 oldStartVnode 與 newStartVnode 判斷為同一節點。
如果 VNode 中只包含了文本節點,在 patchVnode 中可以直接替換文本節點,而不需要移動節點的位置,確實在不綁定 key 的情況下效率要高一丟丟。
某些情況下不綁定 key 的效率更高,那為什麼大部分Eslint的規則還是要求綁定 key 呢?
因為在實際項目中,大多數情況下 v-for 的節點內並不只有文本節點,那麼 VNode 的位元組點就要進行銷毀和創建的操作。
相比替換文本帶來的一丟丟提升,這部分會消耗更多的性能,得不償失。
了解了就地修改,那麼我們在一些簡單節點上可以選擇不綁定 key,從而提高性能。
如果你喜歡我的文章,希望可以關注一下我的公眾號【前端develop】
㈡ vue-lazyload 源碼解析
/src/lazy.js
定義變數接收實例化參數。
lazy.js 默認導出一個函數,該函數返回一個 Lazy 類,形成閉包,保持對 Vue 的引用。
判斷是否支持Webp圖片
/src/listener.js
定義變數接收實例化參數。
filter 方法將配置的 filter 對象中的方法執行,接收兩個參數,一個為 ReactiveListener 實例,一個為 options 參數對象。
initState 方法給元素添加 data-set 屬性,值為圖片地址 src,並且定義了圖片狀態對象 state 。在 Lazy 中已經根據像素比選擇了最適配屏幕的圖片,顧這里不需要考慮 srcset 屬性。另外,我們自定義指令是 v-lazy,到目前為止,還沒有給圖片的 src 屬性賦值。
render 方法,是在 Lazy 中實例化 ReactiveListener 時傳遞過來的參數。
回過頭再來結合 lazy.js 中的 lazyLoadHandler 方法與 ReactiveListener 暴露的方法來看。
/src/lazy-container.js
LazyContainer 的核心是 container 下的選擇器selector(默認 img 標簽)遍歷後調用 lazy 的 add 方法進行綁定,自定義指令 v-lazyload-container。
/src/lazy-component.js
上述實現元素綁定主要是通過自定義指令 v-lazy , v-lazy-container 。那麼 LazyComponent 則是通過注冊的 lazy-component 組件,完成綁定,默認渲染成為 div 標簽,作為 img 的容器。
/src/lazy-image.js
通 LazyComponent 組件,只不過 LazyImage 注冊的 lazy-image 組件,渲染成的是 img 標簽,多了 src 屬性。
通過自定義指令 v-lazy 將設置背景圖的元素或者 img元素,通過 _addListenerTarget 方法收集與數組 TargetQueue 中,並遍歷觸發懶載入的方法, addEventListener 綁定在該元素上,觸發的事件為 lazyLoadHandler ;
在需要懶載入的元素上設置屬性 data-src ,這是期望的圖片地址(filter 配置項可以預先過濾賦值),元素上自定義 lazyLoad 表示圖片狀態(狀態變更後,adapter 中觸發回調);
ListenerQueue 數組中收集的是 ReactiveListener 類的實例,主要是用於懶載入不同狀態下的圖片載入,loading - loaded - error;
當觸發 EventListener 了,執行 lazyLoadHandler 方法,根據演算法,進入 viewport 後, ReactiveListener 元素如果與觸發元素匹配,則進行圖片的載入及渲染。
㈢ vue生命周期詳解
vue源碼中最終執行生命周期函數都是調用 callHook 方法, callHook 函數的邏輯很簡單,根據傳入的生命周期類型 hook ,去拿到 vm.$options[hook] 對應的回調函數數組,然後遍歷執行,執行的時候把 vm 作為函數執行的上下文。
1. new Vue(options) :創建一個vm實例;
2. mergeOptions(resolveConstructorOptions(vm.constructor), options, vm) :合並Vue構造函數里options和傳入的options或合並父子的options。比如:在mergeOptions函數中會調用mergeHook方法合並生命周期的鉤子函數,mergeHook方法原理是只有父時返回父,只有子時返回數組類型的子。父、子都存在時,將子添加在父的後面返回組合而成的數組。這也是父子均有鉤子函數的時候,先執行父的後執行子的的原因;
3. initLifecycle(vm)、initEvents(vm)、initRender(vm) :在創建的vm實例上初始化生命周期、事件、渲染相關的屬性;
4. callHook(vm, 'beforeCreate') :調用beforeCreate生命周期鉤子函數;
5. initInjections(vm)、initState(vm)、initProvide(vm) :初始化數據:inject、state、provide。initState 的作用是初始化 props、data、methods、watch、computed 等屬性;
6. callHook(vm, 'created') :調用created生命周期鉤子函數;
7. vm.$mount(vm.$options.el) : $mount 方法在多個文件中都有定義,如"src/platform/web/entry-runtime-with-compiler.js"、"src/platform/web/runtime/index.js"、"src/platform/weex/runtime/index.js"。因為 $mount 方法的實現是和平台、構建方式相關的。以"entry-runtime-with-compiler.js"為例,關鍵步驟是查看 vm.$options 中是否有render方法,如果沒有則會根據el和template屬性確定最終的template字元串,再調用 compileToFunctions 方法將template字元串轉為render方法,最後,調用原先原型上的$mount方法,即開始執行"lifecycle.js"中 mountComponent 方法;
8. callHook(vm, 'beforeMount') :調用beforeMount生命周期鉤子函數;
9. vm._render() => vm._update() => vm.__patch__() :先執行vm._render方法,即調用createElement生成虛擬DOM,即VNode ,每個VNode有children ,children 每個元素也是⼀個 VNode,這樣就形成了⼀個 VNode Tree;再調用vm._update方法進行首次渲染,vm._update方法核心是調用vm. patch 方法,這個方法跟vm.$mount一樣跟平台相關;vm. patch 方法則是根據生成的VNode Tree遞歸createElm方法創建真實Dom Tree掛載到Dom上;
10. callHook(vm, 'mount') :調用mount生命周期鉤子函數:VNode patch 到 Dom 之後會執行 'invokeInsertHook'函數,把 insertedVnodeQueue 中保存的mount鉤子函數執行一遍,insertedVnodeQueue隊列中的鉤子函數是在根據VNode Tree遞歸createElm方法創建真實Dom Tree過程生成的鉤子函數順序隊列,因此mounted鉤子函數的執行順序是先子後父;
11. data changes :數據更新,nextTick中執行 flushSchelerQueue 方法,該方法會執行watcher隊列中的watcher;
12. callHook(vm, 'beforeUpdate') :執行watcher時會執行watcher的before方法,即調用beforeUpdate生命周期鉤子函數;
13. Virtual DOM re-render and patch :重新render生成新的Virtual DOM,並且patch到DOM上;
14. callHook(vm, 'updated') :調用updated生命周期鉤子函數;
15. vm.$destroy() :啟動卸銷毀過程;
16. callHook(vm, 'beforeDestroy') :調用beforeDestroy生命周期鉤子函數;
17. Teardown watchers, childcomponents and event listeners :執行一系列銷毀動作,在 $destroy 的執行過程中,它又會執行 vm.__patch__(vm._vnode, null) 觸發它子組件的銷毀鉤子函數,這樣一層層的遞歸調用,所以 destroyed 鉤子函數執行順序是先子後父,和 mounted 過程一樣。
18. callHook(vm, 'destroyed ') :調用destroyed 生命周期鉤子函數。
㈣ vue3源碼分析-實現props,emit,事件處理等
>
本期來實現, setup裡面使用props,父子組件通信props和emit等 ,所有的源碼請查看
在render函數中, 可以通過this,來訪問setup返回的內容,還可以訪問this.$el等
由於是測試dom,jest需要提前注入下面的內容,讓document裡面有app節點,下面測試用例類似在html中定義一個app節點哦
本功能的測試用例正式開始
上面的測試用例
解決這兩個需求:
針對上面的分析,需要在setupStatefulComponent中來創建proxy並且綁定到instance當中,並且setup的執行結果如果是對象,也已經存在instance中了,可以通過instance.setupState來進行獲取
通過上面的操作,從render中this.xxx獲取setup返回對象的內容就ok了,接下來處理el
需要在mountElement中,創建節點的時候,在vnode中綁定下,el,並且在setupStatefulComponent 中的代理對象中判斷當前的key
看似沒有問題吧,但是實際上是有問題的,請仔細思考一下, mountElement是不是比setupStatefulComponent 後執行,setupStatefulComponent執行的時候,vnode.el不存在,後續mountelement的時候,vnode就會有值,那麼上面的測試用例肯定是報錯的,$el為null
解決這個問題的關鍵,mountElement的載入順序是 render -> patch -> mountElement,並且render函數返回的subtree是一個vnode,改vnode中上面是mount的時候,已經賦值好了el,所以在patch後執行下操作
在vue中,可以使用onEvent來寫事件,那麼這個功能是怎麼實現的呢,咋們一起來看看
在本功能的測試用例中,可以分析以下內容:
解決問題:
這個功能比較簡單,在處理prop中做個判斷, 屬性是否滿足 /^on[A-Z]/i這個格式,如果是這個格式,則進行事件注冊,但是vue3會做事件緩存,這個是怎麼做到?
緩存也好實現,在傳入當前的el中增加一個屬性 el._vei || (el._vei = {}) 存在這里,則直接使用,不能存在則創建並且存入緩存
事件處理就ok啦
父子組件通信,在vue中是非常常見的,這里主要實現props與emit
根據上面的測試用例,分析props的以下內容:
解決問題:
問題1: 想要在子組件的setup函數中第一個參數, 使用props,那麼在setup函數調用的時候,把當前組件的props傳入到setup函數中即可 問題2: render中this想要問題,則在上面的那個代理中,在 加入一個判斷,key是否在當前instance的props中 問題3: 修改報錯,那就是只能讀,可以使用以前實現的 api shallowReadonly來包裹一下 既可
做完之後,可以發現咋們的測試用例是運行沒有毛病的
上面實現了props,那麼emit也是少不了的,那麼接下來就來實現下emit
根據上面的測試用例,可以分析出:
解決辦法: 問題1: emit 是setup的第二個參數, 那麼可以在setup函數調用的時候,傳入第二個參數 問題2: 關於emit的第一個參數, 可以做條件判斷,把xxx-xxx的形式轉成xxxXxx的形式,然後加入on,最後在props中取找,存在則調用,不存在則不調用 問題3:emit的第二個參數, 則使用剩餘參數即可
到此就圓滿成功啦!