React源碼解析,實現一個React

作者 | Video++極鏈科技前端Team超凡

整理 | 包包

前言

React 起源於 Facebook 的內部項目,是一個用於構建用戶界面的 Javascript 庫。其擁有較高的性能,代碼邏輯非常簡單,越來越多的人已開始關注和使用它。

本文希望通過參考 React 源碼,依葫蘆畫瓢地完成React的雛形。來幫助理解其內部的實現原理,知其然更要知其所以然。

虛擬DOM(Virtual DOM)

瞭解React的都知道,其高效的原因,是因為React按照頁面的DOM結構,利用Javascript在內存中構建了一套相同結構的虛擬內存樹模型,這個內存模型就稱為Virtual DOM。每當頁面產生了變化,React的diff算法會先在內存模型中進行比對,提取出差異點,在將Virtual DOM轉化為原生DOM輸出時,按照差異點,只patch出有變動的部分。

下面是VirtualDOM節點的定義:

React源碼解析,實現一個React

入口

一切都是從 React.render(, document.body) 開始的,所以先來看看 React是怎麼定義的?

React中主要包括:

• render(virtualDom, container) 命令式調用,一般用於應用入口,將虛擬DOM渲染在container容器中;

• createElement(name, props, children) 創建組件時使用,JSX是其語法糖;

• Component 以ES6中的類式語法聲明時使用。

createElement(type, props, children)

createElement()的主要作用是根據給定type創建Virtual DOM節點,JSX是它的語法糖形式;其type參數可以是原生的html標籤名(如:div、tag等),也可以是React組件類或函數。

組件的實現

React的所有組件,按照類型可以分為三種:

• 文本展示類型 (TextComponent)

• 原生DOM類型 (DomComponent)

• 自定義類型 (CompositeComponent)

每種類型的組件,都需要處理初始化更新兩種邏輯,具體會在下面兩個函數中實現:

• mountComponent(rootNodeId) 用於處理初始化邏輯

• updateComponent() 用於處理更新邏輯

初始化mountComponent()的實現

mountComponent() 的實現思路是,根據virtual Dom對象生成HTML代碼並返回。

首先定義類型組件的基類 Component ,它只是簡單地記錄了傳入的virtualDom對象,並初始化了組件節點ID。

React源碼解析,實現一個React

下面是不同類型組件初始化渲染邏輯的各自實現。

• TextComponent

作為純展示類型組件,TextComponent 只是簡單地將需要展示的內容,使用標籤包裝並返回就可以了。

React源碼解析,實現一個React

• DomComponent

DomComponent類型在處理原生DOM時,需要額外注意一下原生事件部分的處理。

React源碼解析,實現一個React

• CompositeComponent

在實現CompositeComponent類型的初始化渲染邏輯之前,先看一下React組件的定義語法。

React源碼解析,實現一個React

聲明語法中,App繼承自React.Component,所以我們先來實現Component這個類。

這裡的 React.Component 不要與上面的 Component 混淆, Component 是不同組件類型的基類,抽象了組件渲染與更新;而React.Component則是Composite這種類型組件聲明時的基類。

在 React.Component 中,簡單地聲明瞭控制數據流向的props屬性,以及組件實例內部用於觸發更新的setState()函數。

React源碼解析,實現一個React

在瞭解了 React.Component 的定義之後,我們回到 CompositeComponent ,開始實現mountComponent()的邏輯。

首先要了解的是,在composite類型組件中,vDom對象中的type,指向的是組件類的定義, 因此 mountComponent() 函數要做的工作,就是使用vDom的props屬性來創建一個type的實例。

React源碼解析,實現一個React

思考一下,在JSX語法中,解析器碰到 <myinput> 標籤後,就會去查找到 MyInput 的定義,上面說過JSX只是createElement的語法糖,因此背後調用的是 React.createElement(MyInput) 。在React規範中,可以使用類或函數來聲明組件,因此在 mountComponent() 中使用 new type() ,就可以構造出MyInput的實例了。

更新流程updateComponent()的實現

實現完組件的初始化之後,接下來要實現組件的更新邏輯。

React開放了 setState() 用於組件更新,回顧上面 React.Component 中 setState() 的定義, 實際調用的是 this._reactInternalInstance.updateComponent(null, newState) 這個函數。而 this._reactInternalInstance指向CompositeComponent,困此更新邏輯交回CompositeComponent.updateComponent()來完成。

• CompositeComponent

Composite類型組件的更新函數,需要處理兩種流程:

  1. 當被定義在其它組件的render函數中時,其包裹組件會構建出新的vDom對象,根據傳入新的vDom來處理更新;
  2. 當組件內部使用setState()觸發時,根據新的state來更新;

瞭解這兩種方式的區別,可以幫助我們理解下面updateComponent函數的實現。

React源碼解析,實現一個React

我們梳理一下更新流程:

  1. 組件在初始化時,記錄下了render組件的實例,即this._renderedComponent;
  2. 在更新環節,重新render()得到新的VDomnextRenderVDom;
  3. 通過比對前後兩個VDom的type和key,來判斷是執行原來_renderedComponent的updateComponent函數,還是重新生成新的組件;

上面使用到了shouldUpdateReactComponent這個比對函數,來對vDom的type和key進行比對,其實現如下:

React源碼解析,實現一個React

上面這個處理邏輯,就是diff算法的第一個規則: 當兩個VDom節點的類型不一致時,重新構建該組件的Virtual DOM樹結構。

• TextComponent Text類型組件作為顆粒度最小的組件,更新邏輯非常簡單,展示新的文本內容即可。

React源碼解析,實現一個React

• DomComponent

因為diff算法的介入,Dom類型的處理邏輯相對複雜。 可以分兩步來處理,第一步更新組件輸出的容器DOM上面的屬性;第二步處理子級DOM。

React源碼解析,實現一個React

_updateProperties()函數對比新舊props,完成屬性及事件的處理。 特別注意一下事件處理部分,需要註銷掉原來DOM上註冊的事件。

React源碼解析,實現一個React

_updateDOMChildren() 用於處理children部分的更新, 這部分的邏輯相對複雜,也是diff算法的優化點所在。

注:下面的說明中,以名稱中含'children'來標識 集合,'child'指代 集合項。

i. 使用 nextChildrenVDoms 數據生成新的nextChildrenComponent;

• DomComponent在初始化流程中,_mountComponent()函數會將組件集合保存下來,存入實例的_renderedChildrenComponent屬性中, 通過遍歷該屬性,可以取得childComponent實例上的_vDom;

• 使用vDom來生成標識索引key,並以childComponent作為索引值,生成childrenComponent的Map結構; (對於Compotite類型,使用vDom.key作為標識索引key; 對於Text和Dom類型,使用childComponent在childrenComponent中所處的索引位置作為標識索引key);

• 使用nextChildrenVDoms生成新nextChildrenComponent的Map結構; 在遍歷vDom集合的過程中,會使用上面的標識索引key生成規則,來進行判定,看是複用之前的組件實例觸發更新,還是創建一個新的組件;

ii. 經過上面一步得到Map結構的prevChildren和nextChildren之後, 會使用深度遍歷算法,遞歸地比對樹結構中,相同層級和位置的兩個組件,將差異點保存為特定的diff標識結構,存入diffQueue隊列中;

iii. 遍歷diffQueue,按照差異的類型,完成最終HTML DOM的變動;

首先是_updateDOMChildren()裡的的定義。由於在遞歸組件樹的節點時,存在多次觸發_updateDOMChildren()的情況; 因此使用_updateDepth變量,在比對操作前+1,完成後-1,來判定整個樹的更新是否全部完成,繼而調用_patch()完成HTML DOM的更新;

React源碼解析,實現一個React

下面的_diff()中,實現了更新步驟中的1 和2。

React源碼解析,實現一個React

值得注意的是_diff過程中lastIndex變量的作用,其記錄在遍歷過程中,每次訪問到的prevChildrenComponent中位置最靠後的組件,這是組件更新的一種排序上面的優化策略,可以參見這一篇文章當中的詳細介紹:不可思議的react diff。

在計算出diffQueue的差異隊列後,在_patch()函數中完成最終HTML DOM的更新:

React源碼解析,實現一個React

總結

至此,我們實現了一個簡易版本的React框架,完成了組件類的定義、初始化及更新; 並且梳理了核心diff算法。

下面簡單做一下總結:

• 組件分為3種類型來處理組件的初始化渲染和更新:TextComponent、DomComponent和CompositeComponent;

• virtualDom對象中,記錄了組件類型type,唯一標識key和屬性集合props;

• 組件是由virtual Dom創建而來,vDom上的type和key用來標識組件實例的唯一性;

• diff算法的核心,是對比新舊vDom對象,來完成部分組件實例的複用,並加入了排序優化策略。 通過javascript大量計算的代價,來換取減少頁面DOM重排的消耗,從而提高了渲染性能;

相關資料:

https://github.com/Matt-Esch/virtual-dom

https://zhuanlan.zhihu.com/p/20346379


分享到:


相關文章: