《騎鵝歷險記》的主角是一個喜歡欺負小動物的小男孩,他由於戲弄小精靈而變成小人,而後意外的騎在一隻志向遠大的鵝身上,和路過的大雁們周遊各地八個月,最後變成了一個講禮貌的好孩子。小時候想變成小人跟着鵝處處旅行,長大之後成爲了製造bug的程序員,隨着它們去調用棧、去庫和框架的源碼裏歷險。另外一種圓夢。javascript
觀察下面這段vue代碼:css
<template>
<div>
<button @click="load">load</button>
<list>
<div class='item' v-for="item in list"></div>
</list>
</div>
</template>
<style lang="scss" scoped>
.item {
//...
}
</style>
複製代碼
這個組件實現的功能是:渲染一個列表,點擊load按鈕,list
字段從[]
切換爲[{id: 1}]
。html
代碼中的List組件很是簡單:vue
<div class="list">
<slot></slot>
<div class="placeholder">placeholder</div>
</div>
複製代碼
基於以上的代碼,出現的問題是:在點擊load
按鈕、數據加載進來以後,.item
的樣式卻並不會生效。java
在瞭解這個問題出現的緣由以前,須要瞭解一下scopeId
的相關原理[1]:node
當 <style>
標籤有 scoped
屬性時,它的 CSS 只做用於當前組件中的元素。react
<template>
<div class="example">hi</div>
</template>
<style scoped>
.example {
color: red;
}
</style>
複製代碼
轉換結果:程序員
<template>
<div class="example" data-v-f3f3eg9>hi</div>
</template>
<style>
.example[data-v-f3f3eg9] {
color: red;
}
</style>
複製代碼
這裏的data-v-f3f3eg9
就是scopeId
。其中,給樣式增長scopeId
的動做發生在vue-loader
中,給html增長scopeId
的動做在vue
中執行:在解析模板、生成vnode的時候,會給當前組件裏對應的元素添加對應的屬性值:data-v-scopeId
。算法
對於給定的具體案例,在點擊load按鈕前,生成的實際html代碼爲:markdown
<div>
<button data-v-123>load</button>
<div data-v-456 class="list">
<div class="placeholder">placeholder</div>
</div>
</div>
複製代碼
在點擊load按鈕後,生成的真實html代碼爲:
<div>
<button data-v-123>load</button>
<div data-v-456 class="list">
<div class="item">1</div>
<div class="placeholder">placeholder</div>
</div>
</div>
複製代碼
能夠看到,div.item這個元素的scopeId消失了,從而致使攜帶了scopeId的css代碼沒法對它生效。
前文中提到了,給html增長scopeId
的動做在vue
中執行:在解析模板、生成vnode的時候,會給當前組件裏對應的元素添加對應的屬性值。
vnode是真實DOM節點的代理,爲了下降DOM操做帶來的昂貴性能開銷,會先用性能更好的JavaScript計算出真實DOM的最終改動,再將改動應用到真實的DOM上,減小修改更新真實DOM新的次數。這個計算的過程叫作dom diff。
當點擊load按鈕,vue的雙向綁定特性觸發dom diff算法,更新div.list
節點進行相應的增刪添加操做。
在這個具體的例子中:
舊的子節點列表只有一個vnode
,也就是div.placeholder
對應的vnode
:[vnode1]
新的子節點列表有兩個vnode
,也就是div.placeholder
和div.item
:[vnode1,vnode2]
從[vnode1]
變化爲[vnode2,vnode1]
,在算法的執行流程中, 將會命中紅線所示的流程,進入更新子節點的子流程:
在更新子節點的流程中,vue會判斷vnode1和vnode2相同、能夠複用,從而將vnode1中保存的真實dom給vnode2使用。即,把div.placeholder
更新爲div.item
,可是setScopeId
只會在建立新的element(真實DOM)的時候執行。因此,div.item
的scopeId
在這裏丟失了。
解決問題的方法就很簡單了:只要不讓兩個vnode被斷定相同從而進行element的複用,就能夠生成新的element,從而建立出對應的scopeId。
判斷vnode是否相同的代碼sameVnode
方法代碼以下:
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
複製代碼
從這段代碼來看,解決問題的方法彷佛很簡單,設置key值、讓sameVnode的返回值爲false便可。
在使用v-for
的時候,不設置key值是一個寫進了源碼警告的操做,以上的代碼彷佛有生搬硬造之嫌。可是還有一種狀況, 在設置了key值以後,scopeId也會消失:
觀察如下代碼:
<template>
<div>
<button @click="load">load</button>
<list>
<van-cell class='item' v-for="item in item.list" :key="item.id">
{{ item.id }}
</div>
</list>
</div>
</template>
複製代碼
van-cell
是有贊開源的組件庫vant
中的一個函數式組件。
若是建立的是類組件,vue會直接建立vnode,把data中的屬性(包括key值)放到vnode中;若是建立的是函數式組件,vue會使用組件本身提供的渲染函數。
而van-cell
這個函數式組件提供的渲染函數並不接收key屬性,也就是van-cell
建立的vnode的key爲undefined。這時候,sameVnode
的返回值仍是true,依然會進行element的複用。
react也有本身的dom diff算法,在react中編寫一樣功能的代碼,是否會出現一樣的問題呢?觀察如下代碼:
const List = ({ list }: { list: number[] }) => {
return (
<div> {list.map((item) => { return <Cell item={item}></Cell>; })} <div>placeholder</div> </div>
);
};
const Cell = ({ item }: { item: number }) => {
return <div className={s.item}>{item}</div>;
};
複製代碼
react判斷是否可複用的標準是:key是否相同,type是否相同。在這個例子裏,彷佛也會由於div.item
和div.placeholder
沒有key且type相同而錯誤將div.placeholer
用於div.item
的複用。
可是,react從jsx生成fiber node的時候,map操做會隱式生成一個fragment
元素,所以,在點擊load按鈕以前和點擊load按鈕以後,div.list
對應的fiber node的children均爲[fragment, div.placeholder]
,區別在於點擊load以後,fragment
的children從[]
變爲了[div.item]
。
所以,即便沒有設置key,react依然能準確的生成新的div.item
對應的fiber node。
vue把模版編譯成vnode,而後做用於真實DOM。react把jsx代碼編譯成fiber node,而後做用於真實DOM。看上去彷佛沒多大區別,就像披薩不過是大餅上撒了肉和蔬菜。vnode和fiber在結構、設計上的不一樣,vue和react在總體流程上設計和架構的不一樣,在實際應用中的表現也會大相徑庭。
(注:只從實際開發中遇到的一個問題作一個發散式的探索,不對兩個框架作具體評價,二者各有優點,應當根據具體狀況具體選擇)
參考文獻