騎蟲歷險記 01 | 消失的scopeId

《騎鵝歷險記》的主角是一個喜歡欺負小動物的小男孩,他由於戲弄小精靈而變成小人,而後意外的騎在一隻志向遠大的鵝身上,和路過的大雁們周遊各地八個月,最後變成了一個講禮貌的好孩子。小時候想變成小人跟着鵝處處旅行,長大之後成爲了製造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.placeholderdiv.item[vnode1,vnode2]

[vnode1]變化爲[vnode2,vnode1],在算法的執行流程中, 將會命中紅線所示的流程,進入更新子節點的子流程:

IMG_08ED14A51F43-1.jpeg

在更新子節點的流程中,vue會判斷vnode1和vnode2相同、能夠複用,從而將vnode1中保存的真實dom給vnode2使用。即,把div.placeholder 更新爲div.item,可是setScopeId只會在建立新的element(真實DOM)的時候執行。因此,div.itemscopeId在這裏丟失了。

解決問題的方法就很簡單了:只要不讓兩個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.itemdiv.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在總體流程上設計和架構的不一樣,在實際應用中的表現也會大相徑庭。

(注:只從實際開發中遇到的一個問題作一個發散式的探索,不對兩個框架作具體評價,二者各有優點,應當根據具體狀況具體選擇)

參考文獻

vue-loader文檔

相關文章
相關標籤/搜索