淺嘗輒止,React是如何工作的

Vincent Ko發表於2018-08-22

大神們可以寫出“深入淺出”系列,小白就寫點"真·淺嘗輒止"系列的吧,主要便於自己理解和鞏固,畢竟一開始就上原始碼還是會頭大滴,於是就準備淺嘗輒止的瞭解下"React是如何工作的?"

React是怎麼工作的? 你知道Diff演算法嗎 ---xx面試官

小白的前端排坑指南:

1.VK的秋招前端奇遇記(一)

2.VK的秋招前端奇遇記(二)

3.VK的秋招前端奇遇記(三)

4.番外篇:前端面試&筆試演算法 Algorithm

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的一些特點:

  1. 本質是JS物件,代表著真實的DOM
  2. 比真實DOM的比較和操作快的多
  3. 每秒可建立200,000個虛擬DOM節點
  4. 每次setState或despatch一個action,都會建立一次全新的虛擬dom

前幾點沒什麼好說的,注意第四點,也就是你每一個改動,每一個動作都會讓React去根據當前的狀態建立一個全新的Virtual DOM。

淺嘗輒止,React是如何工作的

這裡每當Virtual DOM生成,都列印了出來,可以看到,它代表著真實DOM,而每次生成全新的,也是為了能夠比較old dom和new dom之前的差別。

Diff演算法

剛才提到了,React會抓取每個狀態下的內容,生成一個全新的Virtual DOM,然後通過和前一個的比較,找出不同和差異。React的Diff演算法有兩個約定:

  1. 兩個不同型別的元素,會產生兩個不同的樹
  2. 開發者,可以使用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的是新建立的,而20152016僅僅是移動了位置而已。

策略

React是用什麼策略來比較兩顆tree之間的差異呢?這個策略是最核心的部分:

兩個樹的完全的 diff 演算法是一個時間複雜度為 O(n^3) 的問題。但是在前端當中,你很少會跨越層級地移動DOM元素。所以 Virtual DOM 只會對同一個層級的元素進行對比:

淺嘗輒止,React是如何工作的

上面的div只會和同一層級的div對比,第二層級的只會跟第二層級對比。這樣演算法複雜度就可以達到 O(n)。

深度優先遍歷

在實際程式碼中,會對新舊兩棵樹進行一個深度優先的遍歷,這樣每個節點都會有一個唯一的標記,然後記錄差異

淺嘗輒止,React是如何工作的

在深度優先遍歷的時候,每遍歷到一個節點就把該節點和新的的樹進行對比。如果有差異的話就記錄到一個物件裡面。

比如第上圖中的1號節點p,有了變化,這樣子就記錄如下:

patches[1] = [{difference}, {difference}...]//用陣列儲存新舊節點的差異
複製程式碼

ok,那麼差異型別呢,在上一節中已經說了,包括根元素的型別的不同分為兩大類,然後根據不同的情況採取不同的更換策略。

最後,就是在真實DOM進行操作,apply這些差異,更新和渲染了。


為什麼Redux 需要 reducers是純函式?

這又是一個很厲害的問題了,使用Redux的都知道,reducers會接收上一個stateaction作為引數,然後返回一個新的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的原始碼:

淺嘗輒止,React是如何工作的

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
複製程式碼

我不要進行深度比較,只是淺比較,引用值不一樣(不是同一個物件),那就是不一樣的。 這就是reduxreducer如此設計的原因了

參考資料

1.為什麼Redux需要reducers是純函式

2.深度剖析:如何實現一個 Virtual DOM 演算法

3.Learn how to code: how react.js works

相關文章