轉載自淺嘗輒止,React是如何工作的
大神們可以寫出“深入淺出”系列,小白就寫點"真·淺嘗輒止"系列的吧,主要便於自己理解和鞏固,畢竟一開始就上原始碼還是會頭大滴,於是就準備淺嘗輒止的瞭解下"React是如何工作的?"
React是怎麼工作的? 你知道Diff演算法嗎 ---xx面試官
小白的前端排坑指南:
How React.js works
Virtual Dom VS Browser Dom
React除了是MVC框架,資料驅動頁面的特點之外,核心的就是他很"快"。 按照普遍的說法:"因為直接操作DOM會帶來重繪、迴流等,帶來巨大的效能損耗而導致渲染慢等問題。React使用了虛擬DOM,每次狀態更新,React比較虛擬DOM的差異之後,再更改變化的內容,最後統一由React去修改真實DOM、完成頁面的更新、渲染。"
上面這段話,是我們都會說的,那麼一般到這裡,面試官就問了:"什麼是虛擬DOM,React是怎麼進行比較的?Diff演算法瞭解嗎?"。之前是有點崩潰的,於是決定淺嘗一下:
- 虛擬DOM是React的核心,它的本質是JavaScript物件;
- BrowserDOM(也就是頁面真實DOM)就是Browser物件了。
DOM沒什麼好說的,主要說下虛擬DOM的一些特點:
- 本質是JS物件,代表著真實的DOM
- 比真實DOM的比較和操作快的多
- 每秒可建立200,000個虛擬DOM節點
- 每次setState或despatch一個action,都會建立一次全新的虛擬dom
前幾點沒什麼好說的,注意第四點,也就是你每一個改動,每一個動作都會讓React去根據當前的狀態建立一個全新的Virtual DOM。
這裡每當Virtual DOM生成,都列印了出來,可以看到,它代表著真實DOM,而每次生成全新的,也是為了能夠比較old dom和new dom之前的差別。
Diff演算法
剛才提到了,React會抓取每個狀態下的內容,生成一個全新的Virtual DOM,然後通過和前一個的比較,找出不同和差異。React的Diff演算法有兩個約定:
- 兩個不同型別的元素,會產生兩個不同的樹
- 開發者,可以使用key關鍵字,告訴React哪些子元素在DOM下是穩定存在的、不變的。
第二點著重說一下,舉個例子:比如真實DOM的ul標籤下,有一系列的<li>
標籤,然而當你想要重新排列這個標籤時,如果你給了每個標籤一個key
值,React在比較差異的時候,就能夠知道"你還是你,只不過位置變化了"。 React除了要最快的找到差異外,還希望變化是最小的。如果加了key
,react就會保留例項,而不像之前一樣,完全創造一個全新的DOM。
來個更具體的:
1234
下一個狀態後,序列變為
1243
對於我們來講,其實就是調換了4和3的順序。可是怎麼讓React知道,原來的那個3
跑到了原來的4
後面了呢? 就是這個唯一的key
起了作用。
相關面試題:為什麼React中列表模板中要加入key
Diff運算例項
Diff在進行比較的時候,首先會比較兩個根元素,當差異是型別的改變的時候,可能就要花更多的“功夫”了
不同型別的dom元素
比如現在狀態有這樣的一個改變:
<div>
<Counter />
</div>
複製程式碼
<span>
<Counter />
</span>
複製程式碼複製程式碼
可以看到,從<div>
變成了<span>
,這種型別的改變,帶來的是直接對old tree的整體摧毀,包括子元素Counter
。 所以舊的例項Counter
會被完全摧毀後,建立一個新的例項來,顯然這種效率是低下的
同型別dom元素
當比較後發現兩個是同型別的,那好辦了,React會檢視其屬性的變化,然後直接修改屬性,原來的例項都得意保留,從而使得渲染高效,比如:
<div className="before" title="stuff" />
複製程式碼
<div className="after" title="stuff" />
複製程式碼複製程式碼
除了className
,包括style
也是一樣,增加、刪除、修改都不會對整個 dom tree進行摧毀,而是屬性的修改,保留其下面元素和節點
相同型別的元件元素
與上面類似,相同型別的元件元素,子元素的實力會保持,不會摧毀。 當元件更新時,例項保持不變,以便在渲染之間保持狀態。React更新底層元件例項的props以匹配新元素,並在底層例項上呼叫componentWillReceiveProps()
和componentWillUpdate()
。
接下來,呼叫render()
方法,diff演算法對前一個結果和新結果進行遞迴
key props
如果前面對key
的解釋,還不夠清除,這裡用一個真正的例項來說明key
的重要性吧。
- 場景一:在一個列表最後增加一個元素
<ul>
<li>first</li>
<li>second</li>
</ul>
------
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
複製程式碼複製程式碼
可以看到,在這種情況下,React只需要在最後insert
一個新元素即可,其他的都不需要變化,這個時候React是高效的,但是如果在場景二下:
- 場景二:在列表最前面插入一個元素
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
---
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
複製程式碼複製程式碼
這對React可能就是災難性的,因為React只知道前兩個元素不同,因此會完全創新一個新的元素,最後導致三個元素都是重新建立的,這大大降低了效率。這個時候,key
就排上用場了。當子元素有key
時,React使用key
將原始樹中的子元素與後續樹中的子元素相匹配。例如,在上面的低效示例中新增一個key
可以使樹轉換更高效:
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
------
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
複製程式碼複製程式碼
這樣,只有key
值為2014的是新建立的,而2015
和2016
僅僅是移動了位置而已。
策略
React是用什麼策略來比較兩顆tree之間的差異呢?這個策略是最核心的部分:
兩個樹的完全的 diff 演算法是一個時間複雜度為 O(n^3) 的問題。但是在前端當中,你很少會跨越層級地移動DOM元素。所以 Virtual DOM 只會對同一個層級的元素進行對比:
上面的div
只會和同一層級的div
對比,第二層級的只會跟第二層級對比。這樣演算法複雜度就可以達到 O(n)。
深度優先遍歷
在實際程式碼中,會對新舊兩棵樹進行一個深度優先的遍歷,這樣每個節點都會有一個唯一的標記,然後記錄差異
在深度優先遍歷的時候,每遍歷到一個節點就把該節點和新的的樹進行對比。如果有差異的話就記錄到一個物件裡面。
比如第上圖中的1號節點p,有了變化,這樣子就記錄如下:
patches[1] = [{difference}, {difference}...]//用陣列儲存新舊節點的差異
複製程式碼複製程式碼
ok,那麼差異型別呢,在上一節中已經說了,包括根元素的型別的不同分為兩大類,然後根據不同的情況採取不同的更換策略。
最後,就是在真實DOM進行操作,apply這些差異,更新和渲染了。
為什麼Redux 需要 reducers是純函式?
這又是一個很厲害的問題了,使用Redux的都知道,reducers會接收上一個state
和action
作為引數,然後返回一個新的state
,這個新的state
不能是在原來state
基礎上的修改。所以經常可以看到以下的寫法:
return Object.assign(...)
//或者----------
return {...state,xx:xxx}
複製程式碼複製程式碼
其作用,都是為了返回一個全新的物件。
為什麼reducers要求是純函式(返回全新的物件,不影響原物件)? --某面試官
純函式
從本質上講,純函式的定義如下:不修改函式的輸入值,依賴於外部狀態(比如資料庫,DOM和全域性變數),同時對於任何相同的輸入有著相同的輸出結果。
舉個例子,下面的add函式不修改變數a或b,同時不依賴外部狀態,對於相同的輸入始終返回相同的結果。
const add = (a,b) => {a + b};
複製程式碼複製程式碼
這就是一個純函式,結果對a、b沒有任何影響,回頭去看reducer,它符合純函式的所有特徵,所以就是一個純函式
為什麼必須是純函式?
先告訴你結果吧,如果在reducer中,在原來的state
上進行操作,並返回的話,並不會讓React重新渲染。 完全不會有任何變化!
接下來看下Redux的原始碼:
Redux接收一個給定的state(物件),然後通過迴圈將state的每一部分傳遞給每個對應的reducer。如果有發生任何改變,reducer將返回一個新的物件。如果不發生任何變化,reducer將返回舊的state。
Redux只通過比較新舊兩個物件的儲存位置來比較新舊兩個物件是否相同。如果你在reducer內部直接修改舊的state物件的屬性值,那麼新的state和舊的state將都指向同一個物件。因此Redux認為沒有任何改變,返回的state將為舊的state。
好了,也就是說,從原始碼的角度來講,redux要求開發者必須讓新的state
是全新的物件。那麼為什麼非要這麼麻煩開發者呢?
請看下面的例子:嘗試比較a和b是否相同
var a = {
name: 'jack',
friend: ['sam','xiaoming','cunsi'],
years: 12,
...//省略n專案
}
複製程式碼
var b = {
name: 'jack',
friend: ['sam','xiaoming','cunsi'],
years: 13,
...//省略n專案
}
複製程式碼複製程式碼
思路是怎樣的?我們需要遍歷物件,如果物件的屬性是陣列,還需要進行遞迴遍歷,去看內容是否一致、是否發生了變化。 這帶來的效能損耗是非常巨大的。 有沒有更好的辦法?
有!
//接上面的例子
a === b //false
複製程式碼複製程式碼
我不要進行深度比較,只是淺比較,引用值不一樣(不是同一個物件),那就是不一樣的。 這就是redux
的reducer
如此設計的原因了