引出問題
首先我們來這麼一個問題, 這裡是完整的 jsfiddle demo or codepen demo
給一個元素繫結兩個邊框樣式, 右側和底部都為1px的紅色邊框
styleA: {
borderBottom: '1px solid red',
borderRight: '1px solid red'
};
複製程式碼
然後用一個按鈕(或者任何方式)將樣式換成下面的樣式, 一個1px的綠色邊框,和1px的紅色右側邊框。
styleB: {
border: '1px solid green',
borderRight: '1px solid red'
};
複製程式碼
我們期望的結果應該是右側邊框是紅色的,其餘三邊的邊框是綠色的,但實際結果卻是所有邊都是綠色的, 這裡已經出現了問題, 然後再點選按鈕,將樣式切換回去, 此時期望的結果應該是跟一開始一樣: 右側和底部都為1px的紅色邊框, 但實際結果卻是隻剩下底部的邊框是紅色的,右側的邊框就像消失了一樣。
那麼, 右側的邊框樣式是不是真的消失了呢? 是不是從第一次切換就消失了呢?(這好像也能符合第一次全都是綠色邊框的表現),是CSS
的bug嗎?
這個style
的替換過程是在Vue
裡幫我們實現的,是跟虛擬節點vNode
的渲染有關,接下來讓我們去Vue
的原始碼看一下這個問題到底是怎麼樣造成的。
Vue更新檢視機制
首先,vue檢視的更新通過updateComponent
進行, updateComponent
會執行一個update
的方法進行更新檢視,update會從根節點進行patch
操作, patch
操作會依次遍歷虛擬節點樹的所有vnode節點,深度優先的遍歷方式。
通常patch
操作會update以下幾個部分
0: ƒ updateAttrs(oldVnode, vnode)
1: ƒ updateClass(oldVnode, vnode)
2: ƒ updateDOMListeners(oldVnode, vnode)
3: ƒ updateDOMProps(oldVnode, vnode)
4: ƒ updateStyle(oldVnode, vnode)
5: ƒ update(oldVnode, vnode)
6: ƒ updateDirectives(oldVnode, vnode)
複製程式碼
這裡我們只需要關注第5個方法:updateStyle
, 那麼這個方法裡做了什麼呢?
看一下核心邏輯:
可以看到這段程式碼的主要邏輯是用新的樣式覆蓋舊的樣式,這裡的setProp是對element.style
進行修改,也就是原生CSSStyleDeclaration
物件的例項。
- 首先將不存在於newStyle中的oldStyle的樣式設定為
''
, - 然後再設定與oldStyle中樣式值不相等的newStyle的樣式,
看起來沒什麼問題,一切都很符合邏輯,那麼是什麼造成了上面的現象呢?
一切的罪魁禍首都在這個border
樣式的簡寫屬性(shorthand property)上。
簡寫屬性有什麼特殊的地方呢? 最直接的就是當對一個簡寫屬性賦值,例如:
border: 1px solid green;
複製程式碼
這個賦值會被轉換為:
borderWidth: "1px"
borderStyle: "solid"
borderColor: "green"
borderTop: "1px solid green"
borderTopColor: "green"
borderTopStyle: "solid"
borderTopWidth: "1px"
borderRight: "1px solid green"
borderRightColor: "green"
borderRightStyle: "solid"
borderRightWidth: "1px"
borderLeft: "1px solid green"
borderLeftColor: "green"
borderLeftStyle: "solid"
borderLeftWidth: "1px"
borderBottom: "1px solid green"
borderBottomColor: "green"
borderBottomStyle: "solid"
borderBottomWidth: "1px"
複製程式碼
也就是說borderTop
, borderLeft
, borderRight
, borderBottom
也都被賦值了.
原因分析
所以,回到上面的那個切換過程,根據updateStyle
原始碼進行分析:
-
從
styleA
切換為styleB
時,-
第一個
for
迴圈,borderBottom
不在 oldStyle 中,被清空,borderRight
在 oldStyle 中,保留了下來。 -
第二個
for
迴圈,border
不在 oldStyle 中,設定border
的值,注意此時borderTop
,borderLeft
,borderRight
,borderBottom
也都被賦值了,然後borderRight
與 oldStyle 中保留下來的值相等, 跳過這次賦值。 -
最後的結果就是
borderTop
,borderLeft
,borderRight
,borderBottom
都顯示border
的值。
-
-
從
styleB
切換回為styleA
時,-
第一個
for
迴圈,border
不在 oldStyle 中,border
的值被清空,此時borderTop
,borderLeft
,borderRight
,borderBottom
也都被清空,然後borderRight
在 oldStyle 中, 跳過這次賦值。 -
第二個
for
迴圈,borderBottom
不在 oldStyle 中,borderBottom
被賦值,borderRight
與 oldStyle 中保留下來的值相等, 跳過這次賦值 -
最後的結果也就是隻剩下了
borderBottom
的值。
-
解決方案
那麼,原理搞清楚了,有什麼好的解決方案呢? 這個問題在Vue的github上已經被提過issue了,看下尤雨溪的官方回覆
This is a wontfix. It's impractical to handle all the possible shorthand variations in the diffing algorithm. The solution is: do not use shorthand properties in inline styles.
If you really have to, e.g. you are allowing the user to edit the css arbitrarily, then the workaround is giving the element in question a key that equivalents to the hash of its inline styles. This forces the element to be replaced fresh when its inline styles change.
這個問題被定性為了一個wontfix
,但也給出了有效的解決方案:
- 給這個元素一個用樣式生成的hash值作為
key
, 當樣式有任何變化的時候,key
就會變化,在Vue
的更新渲染邏輯中,如果元素的key
發生變化,那麼oldstyle
就是空物件,就不會出現上面的問題了。