這一部分講述的是堆棧調解器的實現
React
的API
可以被分爲三部分,核心,渲染器,調解器,如果你對代碼庫可能有點不瞭解的話,可以看我的博客
其中堆棧調解器是React
產品中最重要的部分,被React DOM
和React Native
渲染器共同使用,它的代碼地址src/renderers/shared/stack/reconciler
。
1.從零開始構建React
的歷史
Paul O'Shannessy
給予React
開發一個非常大的靈感,它對最終的代碼庫的文檔和解說可以得到更好的理解,所以有時間可以去看看。
2.概要
調解器自身並不是一個公共API
,而渲染器則作爲一個接口,通過用戶寫的React
組件來被React DOM
和React Native
兩個渲染器有效的更新。
3.綁定就是遞歸
下面是一個綁定組件的實例
ReactDOM.render(<App />, rootEl);
<App/>
是一個記錄了該如何去渲染的React
對象元素,React DOM
將<App />
作爲對象傳遞給調解器,你可以認爲是如下這樣的對象
console.log(<App />);
// { type: App, props: {} }
如果
App
是一個組件類或者函數類,調解器就會檢查他們。如果
App
是一個函數,調解器就會調用App(props)
去渲染元素。
如果App
是一個類,調解器就會通過new App(props)
去實例化一個App
對象,然後調用componentWillMount()
生命週期方法,接着是調用render()
方法去渲染元素。
無論是哪一種方法,調解器都能夠知道如何去渲染App
元素。
這個過程是遞歸的,App
可能渲染出<Greeting/>
,Greeting
可能會渲染出<Button />
,接着不斷處理,調解器通過知道一個組件如何渲染的來往深度處理用戶自定義的組件。
你可以認爲這個過程是如下的僞代碼:
function isClass(type) {
// React.Component子類都會有這些標記
return (
Boolean(type.prototype) &&
Boolean(type.prototype.isReactComponent)
);
}
// 這個函數會處理React元素
// 返回一個代表需要被綁定的樹的DOM或者Native節點
function mount(element) {
var type = element.type;
var props = element.props;
// 我們決定要被渲染的元素
// 它可能是一個函數運行的結果
// 也可能是一個類實例調用render運行後的結果
var renderedElement;
if (isClass(type)) {
// 類組件
var publicInstance = new type(props);
// 設置props
publicInstance.props = props;
// 如果有必要調用生命週期方法
if (publicInstance.componentWillMount) {
publicInstance.componentWillMount();
}
// 調用render得到要被渲染的元素
renderedElement = publicInstance.render();
} else {
// 函數式組件
renderedElement = type(props);
}
// 由於組件可能會返回另外一個組件
// 所以這個過程是一個遞歸的過程
return mount(renderedElement);
// 注意:這個實現也是不完整的,因爲遞歸依舊沒有被停止
// 它只能處理自定義組合組件,比如<App />或<Button />
// 而不能處理host組件,比如<div />或<p />
}
var rootEl = document.getElementById('root');
var node = mount(<App />);
rootEl.appendChild(node);
注意
上述的真是一段僞代碼,和真實的實現相差太遠,直接一看都知道問題,遞歸無法截止,所以上述只是一個簡單的思路,代碼需要完善。
我們先總結一下上述代碼的幾點關鍵點:
React
元素表現爲一個對象的話,可以用組件的type
和props
來表示,這就是React.createElement
的任務了自定義的組件(比如
App
)可以是類或者是函數,他們都會返回需要渲染的元素綁定是一個創建
DOM
或者Native
節點樹的遞歸過程
4.綁定Host
元素
如果我們不在視圖中顯示什麼的話,那麼上述的那些代碼都是沒有用的,顯示出結果是我們的目的。
除了用戶自定義的組合組件,React
元素還可能是Host
組件,比如Button
在render
方法中可能會返回<div/>
。
如果元素的type
是一個字符串,我們就需要按照host
元素來處理:
console.log(<div />);
// { type: 'div', props: {} }
當調解器檢測到是一個host
元素後,它會讓渲染器去關心如何綁定它,例如,React DOM
渲染器可能就創建一個DOM
節點,而其它的渲染器又會創建其它,而這些都不是調解器關心的,這裏大家可以留個心思,真是的渲染過程永遠都是渲染器的事,跟調解器半毛錢的關係都沒有。
如果一個host
元素有孩子,調解器就會用同樣的方法遞歸去綁定他們(記住是綁定而不是渲染),孩子是host
就host
處理,孩子是組合就按照組合的方式處理。
DOM
節點被孩子組件創建出來加入父親DOM
節點中,不斷遞歸的處理,最後完整的DOM
結構就組裝而成了。
注意
調解器自己並不會依賴DOM
,綁定的最終結果取決於渲染器,如果是DOM
節點就是React DOM
渲染器處理的,如果是字符串則是React DOM Server
渲染器處理的,如果是一個代表着Native
視圖的數字則是React Native
渲染器處理的,這些都不是調解器需要關心的。
如果你擴展之前講的代碼處理host
元素,那麼僞代碼就變爲了如下形式:
function isClass(type) {
// React.Component子類都會有這些標記
return (
Boolean(type.prototype) &&
Boolean(type.prototype.isReactComponent)
);
}
// 這個函數只是用來處理組合組件的
// 比如 <App />和<Button />, 不能處理<div />.
function mountComposite(element) {
var type = element.type;
var props = element.props;
var renderedElement;
if (isClass(type)) {
// 類組件實例化
var publicInstance = new type(props);
// 設置props
publicInstance.props = props;
//調用生命週期函數
if (publicInstance.componentWillMount) {
publicInstance.componentWillMount();
}
renderedElement = publicInstance.render();
} else if (typeof type === 'function') {
// 函數式組件
renderedElement = type(props);
}
// 當我們遇到的元素是個Host組件而不是一個組合組件時
// 這個遞歸過程就會停止
return mount(renderedElement);
}
// 這個函數只是用來處理host組件的
// 比如 <div />和<p />, 不能處理<App />.
function mountHost(element) {
var type = element.type;
var props = element.props;
var children = props.children || [];
if (!Array.isArray(children)) {
children = [children];
}
children = children.filter(Boolean);
// 這一部分代碼不應該存在調解器中
// 因爲不同的渲染器初始化節點方式是可能不同的
// 比如說,React Native會創建IOS或者Android視圖
var node = document.createElement(type);
Object.keys(props).forEach(propName => {
if (propName !== 'children') {
node.setAttribute(propName, props[propName]);
}
});
// 綁定子級
children.forEach(childElement => {
// 孩子可能是host或者組合組件
// 然後就是遞歸處理他們
var childNode = mount(childElement);
// 這部分代碼也是特殊的
// 形式方法的不同取決於不同的渲染器
node.appendChild(childNode);
});
// 返回DOM節點作爲綁定的結果
// 遞歸過程從此處返回
return node;
}
function mount(element) {
var type = element.type;
if (typeof type === 'function') {
// 用戶自定義組件
return mountComposite(element);
} else if (typeof type === 'string') {
// host組件
return mountHost(element);
}
}
var rootEl = document.getElementById('root');
var node = mount(<App />);
rootEl.appendChild(node);
這一部分代碼裏真正的調解器相差依舊是非常遙遠的,它連更新功能都沒有實現。
5.內部實例
React
的一個非常關鍵的特點就是你可以重複渲染任何東西,這個重複渲染的過程並不會重新創建DOM
或者是重設置state
,這是非常有意思的,而是不渲染,有趣。
ReactDOM.render(<App />, rootEl);
// 下面的代碼並不會有什麼性能損失,因爲下面的代碼相當於沒有執行,有趣
ReactDOM.render(<App />, rootEl);
然而,我們之前的代碼真是一個最簡單的構造初始綁定樹的方式了,因爲我們在前面的代碼沒有進行更新階段的數據儲備,所以根本無法實行更新的操作,比如說publicInstances
,或者哪個組件對應着哪個DOM
節點,這些數據在初始化綁定後統統不知道,這就非常尷尬了。
這個堆棧調解器代碼庫就通過一個在類中的mount()
函數方法來解決它,當然這個方法有一定缺陷,我們會嘗試重寫調解器,不過,它是怎麼工作的呢?
我們不再使用mountHost
和mountComposite
函數,我們用兩個類來取代他們:DOMComponent
和CompositeComponent
,因爲對象就可以存儲數據,函數一般都是純函數不影響數據儲備。
這兩個類都有一個接受element
爲參數的構造函數和一個mount
方法去返回被綁定的節點,而全局的mount()
函數被如下的代碼替換掉了:
function instantiateComponent(element) {
var type = element.type;
if (typeof type === 'function') {
// 用戶自定義組件
return new CompositeComponent(element);
} else if (typeof type === 'string') {
// host組件
return new DOMComponent(element);
}
}
首先我們先編寫CompositeComponent
類的實現:
class CompositeComponent {
constructor(element) {
this.currentElement = element;
this.renderedComponent = null;
this.publicInstance = null;
}
getPublicInstance() {
// For composite components, expose the class instance.針對組合組件暴露出它的類實例
return this.publicInstance;
}
mount() {
var element = this.currentElement;
var type = element.type;
var props = element.props;
var publicInstance;
var renderedElement;
if (isClass(type)) {
// 組件類
publicInstance = new type(props);
// 設置props
publicInstance.props = props;
// 調用生命週期函數
if (publicInstance.componentWillMount) {
publicInstance.componentWillMount();
}
renderedElement = publicInstance.render();
} else if (typeof type === 'function') {
// 函數式組件沒有實例
publicInstance = null;
renderedElement = type(props);
}
// 保存實例
this.publicInstance = publicInstance;
// 根據元素實例化孩子內部實例
// 這個實例可能是DOMComponent
// 也可能是CompositeComponent
var renderedComponent = instantiateComponent(renderedElement);
this.renderedComponent = renderedComponent;
// 將綁定的數據輸出
return renderedComponent.mount();
}
}
這個和前面的mountComposite
的實現沒有多少不同,但是我們保存了一些對我們更新數據有用的信息,比如this.currentElement
,this.renderedComponent
和this.publicInstance
。
有一點要注意,我們的CompositeComponent
實例和用戶自己實例化element.type
是不相同的,CompositeComponenet
是調解器內部的實現無法被外界使用,或者是沒有暴露出來,你並不知道它,而用戶自己通過new element.type()
是不一樣的,你可以直接對他進行處理,換一句話說的是我們在類中實現的getPublicInstance()
函數就是讓我們得到一個公共操作的實例,但是更加底層內部的實例呢,我們很明顯不能操作,也就只能操作當前這一層當做公共接口暴露出來的實例了。
爲了避免出現混亂,我將CompositeComponent
和DOMComponent
稱爲內部實例,他們的存在可以長久的保存着數據,而只有渲染器和調解器能夠直接處理他們。
與此相反,我們將用戶自定義類的實例稱爲公共實例(外部實例),這個公共實例你可以直接操作。
mountHost
函數被重構成了DOMComponent
中的mount
函數。
class DOMComponent {
constructor(element) {
this.currentElement = element;
this.renderedChildren = [];
this.node = null;
}
getPublicInstance() {
return this.node;
}
mount() {
var element = this.currentElement;
var type = element.type;
var props = element.props;
var children = props.children || [];
if (!Array.isArray(children)) {
children = [children];
}
// 創建並保存節點
var node = document.createElement(type);
this.node = node;
// 設置節點屬性
Object.keys(props).forEach(propName => {
if (propName !== 'children') {
node.setAttribute(propName, props[propName]);
}
});
// 創建和保存被包含的孩子
// 他們可能是DOMComponent或者是CompositeCompoennt
// 這取決與他們的type是字符串還是function
var renderedChildren = children.map(instantiateComponent);
this.renderedChildren = renderedChildren;
// 收集要綁定的節點
var childNodes = renderedChildren.map(child => child.mount());
childNodes.forEach(childNode => node.appendChild(childNode));
// 將DOM節點作爲綁定的結果返回
return node;
}
}
這個和之前的代碼主要的不同就是我們重構了moutHost()
並且保存了當前的node
和renderedChildren
來關聯內部的DOM
組件實例,以後我們就可以無需聲明就可以使用他們。
總的來說,每一個內部實例,不管是組合也好,host
也好,現在都會有指向他們的孩子內部實例的變量保存,從而構成一個內部實例鏈,爲了更加形象的來說明他,如果<App>
是一個函數式組件<Button>
是一個類組件,而Button
又渲染出了<div>
那麼最後的內部實例鏈就如同下面所示。
[object CompositeComponent] {
currentElement: <App />,
publicInstance: null,
renderedComponent: [object CompositeComponent] {
currentElement: <Button />,
publicInstance: [object Button],
renderedComponent: [object DOMComponent] {
currentElement: <div />,
node: [object HTMLDivElement],
renderedChildren: []
}
}
}
在DOM
中你講只會看到<div>
,然而內部實例樹既包括組合內部實例又包括host
內部實例。
組合內部實例需要保存如下一些東西:
當前的元素
如果元素的
type
是一個類,那麼要保存公共實例當前的元素不是
DOMComponent
就是CompositeComponent
,我們需要保存他們渲染的內部實例。
host
內部實例需要保存的:
當前的元素
DOM
節點所有的孩子內部實例,他們可能是
DOMComponent
也可能是CompositeComponent
你可以想象一個內部實例樹怎麼去構建一個複雜的應用呢,React DevTools
可以給你一個非常直觀的結果,它可以用灰色高亮host
實例,用紫色來高亮組合實例
然而如果要完成最終的重構,我們還要設計一個函數去進行真實的綁定操作像是ReactDOM.render()
,它還會返回一個公共實例,前面的mount得到的只是要進行綁定的節點,而沒有進行真實的綁定。
function mountTree(element, containerNode) {
// 創建頂部內部實例
var rootComponent = instantiateComponent(element);
// 將頂部組件加入最終DOM容器中實現真正的綁定
var node = rootComponent.mount();
containerNode.appendChild(node);
// 返回公共實例
var publicInstance = rootComponent.getPublicInstance();
return publicInstance;
}
var rootEl = document.getElementById('root');
mountTree(<App />, rootEl);
6.卸載
現在我們已經有了保存着孩子和DOM
節點的內部實例,接下來我們就可以實現卸載,對於Composite Component
,卸載會遞歸的調用生命週期函數。
class CompositeComponent {
// ...
unmount() {
// 調用生命週期函數
var publicInstance = this.publicInstance;
if (publicInstance) {
if (publicInstance.componentWillUnmount) {
publicInstance.componentWillUnmount();
}
}
// 卸載渲染
var renderedComponent = this.renderedComponent;
renderedComponent.unmount();
}
}
對於DOMComponent
,需要告訴每一個孩子都要卸載
class DOMComponent {
// ...
unmount() {
// 卸載所有的孩子
var renderedChildren = this.renderedChildren;
renderedChildren.forEach(child => child.unmount());
}
}
實際上,卸載DOM
組件也需要移除事件監聽器,清除緩衝,不過這些細節我先暫時跳過。
我們現在再增加一個全新的全局函數unmountTree(containerNode)
,他的功能和ReactDOM.unmountComponentAtNode()
類似。與mountTree
功能相反
function unmountTree(containerNode) {
// 從DOM節點中讀取一個內部實例
// 這一個_internalInstance內部實例我們會在mountTree給它增加
var node = containerNode.firstChild;
var rootComponent = node._internalInstance;
// 卸載樹並清理容器
rootComponent.unmount();
containerNode.innerHTML = '';
}
爲了讓上述的代碼可以正常的工作了,我們需要爲DOM
節點讀取一個root
內部實例,我們修改mountTree()
增加一個_internalInstance
屬性爲rootDOM
節點,我們要告訴mountTree
應該摧毀已經存在的樹,這樣纔可以多次調用:
function mountTree(element, containerNode) {
// 摧毀已經存在的樹
if (containerNode.firstChild) {
unmountTree(containerNode);
}
// 創建一個頂級的內部實例
var rootComponent = instantiateComponent(element);
// 將頂級內部實例的渲染結果加入DOM中
var node = rootComponent.mount();
containerNode.appendChild(node);
// 保存內部實例
node._internalInstance = rootComponent;
// 返回一個公共實例
var publicInstance = rootComponent.getPublicInstance();
return publicInstance;
}
7.更新
在上面,我們實現了卸載,然而如果每一次prop
改變都卸載舊的樹,構造性的樹,React
就沒有存在的必要了,而調解器的作用就是爲了重複使用已經存在實例,從而達到性能的提升。
var rootEl = document.getElementById('root');
mountTree(<App />, rootEl);
// 下面的語句相當於沒有執行
mountTree(<App />, rootEl);
我們將擴展我們的內部實例實現一個方法,DOMComponent
和CompositeComponent
都需要實現一個新的函數叫做receive(nextElement)
class CompositeComponent {
// ...
receive(nextElement) {
// ...
}
}
class DOMComponent {
// ...
receive(nextElement) {
// ...
}
}
這個函數的工作就是讓組件和它的孩子可以及時的瞭解到nextElement
的信息,進行更新。
這一部分在前面被描述爲“虛擬DOM diff
”,通過我們沿着內部實例樹遞歸往下走,讓每一個內部實例都可以接受到更新。
8.更新組合組件
當一個組合組件接受到一個新的元素的時候,我們會運行componeentWillUpdate()
生命週期函數,然後會通過新的porps
去重渲染組件,得到一個新的渲染元素。
class CompositeComponent {
// ...
receive(nextElement) {
var prevProps = this.currentElement.props;
var publicInstance = this.publicInstance;
var prevRenderedComponent = this.renderedComponent;
var prevRenderedElement = prevRenderedComponent.currentElement;
// 更新自己的元素
this.currentElement = nextElement;
var type = nextElement.type;
var nextProps = nextElement.props;
// 得出新render內容
var nextRenderedElement;
if (isClass(type)) {
if (publicInstance.componentWillUpdate) {
publicInstance.componentWillUpdate(nextProps);
}
// 更新props
publicInstance.props = nextProps;
// 重新渲染
nextRenderedElement = publicInstance.render();
} else if (typeof type === 'function') {
nextRenderedElement = type(nextProps);
}
// ...
得到了nextRenderedElement
我們就可以查看渲染的元素的type
,跟我之前將更新的時候的判斷是一樣的,如果type
沒有改變,那麼就向下遞歸,而不改變當前組件。
比如說,如果render
第一次返回了<Button color="red"/>
,第二次返回<Button color="blue">
,我們就可以告訴相應的內部實例receive
新元素。
// ...
// 如果渲染的元素type沒有變化
// 則重使用已經存在的實例,不去新創建實例
if (prevRenderedElement.type === nextRenderedElement.type) {
prevRenderedComponent.receive(nextRenderedElement);
return;
}
// ...
但是,如果新渲染的元素和之前的元素type
不一樣的話,那麼我們就無法進行更新操作了,因爲<button>
是無法成爲<input>
、所以,我們不得不卸載摧毀已經存在的內部實例然後裝載相應的新的渲染元素:
// ...
// 得到舊的節點
var prevNode = prevRenderedComponent.getHostNode();
// 卸載舊的孩子裝載新的孩子
prevRenderedComponent.unmount();
var nextRenderedComponent = instantiateComponent(nextRenderedElement);
var nextNode = nextRenderedComponent.mount();
// 替換引用
this.renderedComponent = nextRenderedComponent;
// 注意:這部分代碼理論上應該放在CompositeComponent外面而不是裏面
prevNode.parentNode.replaceChild(nextNode, prevNode);
}
}
綜上所述,一個組合組件接收到一個新的元素,他會直接更新內部實例,或者是卸載舊的實例,裝載新的實例。
這裏還有另外一種情況,當一個元素的key
發生變化時組件就是被重裝載而不是接受一個新的元素,當然我們這裏先不討論key
造成的改變,因爲它的會比較複雜。
值得注意的是,我們需要爲內部實例增加一個叫做getHostNode()
的函數,以至於在更新階段找到指定的節點然後更新它,下面是它在兩個類中的簡單實現:
class CompositeComponent {
// ...
getHostNode() {
// 遞歸處理
return this.renderedComponent.getHostNode();
}
}
class DOMComponent {
// ...
getHostNode() {
return this.node;
}
}
9.更新Host
組件
Host
組件的實現就像是DOMComponent
,和CompositeComponent
更新截然不同,當他們接收到一個元素時,他們需要更新底層的DOM
節點,就React
的DOM
而言,他們會更新DOM
屬性:
class DOMComponent {
// ...
receive(nextElement) {
var node = this.node;
var prevElement = this.currentElement;
var prevProps = prevElement.props;
var nextProps = nextElement.props;
this.currentElement = nextElement;
// 移除舊的屬性
Object.keys(prevProps).forEach(propName => {
if (propName !== 'children' && !nextProps.hasOwnProperty(propName)) {
node.removeAttribute(propName);
}
});
// 設置新的屬性
Object.keys(nextProps).forEach(propName => {
if (propName !== 'children') {
node.setAttribute(propName, nextProps[propName]);
}
});
// ...
然後,host
組件就開始更新他們的孩子,不像組合組件那樣,他們只會包含最多一個孩子。
在下面這個例子中,我是用一個數組的內部實例,然後遍歷它,或者是進行更新還是替換內部實例取決於新的元素和舊的元素的type
是否相同。真正的調解器還會使用元素的key
去處理,當然我們暫時不會涉及這個處理邏輯。
我們可以收集DOM
節點需要操作的孩子然後批量的處理他們來節省時間:
// ...
// 在這裏React元素是一個數組
var prevChildren = prevProps.children || [];
if (!Array.isArray(prevChildren)) {
prevChildren = [prevChildren];
}
var nextChildren = nextProps.children || [];
if (!Array.isArray(nextChildren)) {
nextChildren = [nextChildren];
}
// 在這裏內部實例也是一個數組
var prevRenderedChildren = this.renderedChildren;
var nextRenderedChildren = [];
// 我們迭代孩子增加操作到數組中
var operationQueue = [];
// Note: 這一份代碼是極度簡化了的
// 他沒有處理渲染以及key的問題
// 他只是用來說明整個處理流程而不是細節
for (var i = 0; i < nextChildren.length; i++) {
// 儘可能得到該孩子的內部實例
var prevChild = prevRenderedChildren[i];
// 如果下標表示的沒有內部實例
// 那麼會增加一個孩子到數組結尾
// 並創建一個新的內部實例,掛載它,以及使用它。
if (!prevChild) {
var nextChild = instantiateComponent(nextChildren[i]);
var node = nextChild.mount();
// 記錄我們需要增加的節點
operationQueue.push({type: 'ADD', node});
nextRenderedChildren.push(nextChild);
continue;
}
// 如果元素的類型匹配,那麼我們只更新實例.
// 比如, <Button size="small" /> 會被更新爲
// <Button size="large" /> 但是不能是一個 <App />.
var canUpdate = prevChildren[i].type === nextChildren[i].type;
// 如果我們沒有更新已經存在的實例,那麼我們就不得不卸載然後
// 再掛載新的節點去代替舊的
if (!canUpdate) {
var prevNode = prevChild.node;
prevChild.unmount();
var nextChild = instantiateComponent(nextChildren[i]);
var nextNode = nextChild.mount();
// 記錄我們需要交換的節點
operationQueue.push({type: 'REPLACE', prevNode, nextNode});
nextRenderedChildren.push(nextChild);
continue;
}
// 如果我們更新了已經存在的內部實例
// 那就直接接收下一個新的元素,用它來更新自己
prevChild.receive(nextChildren[i]);
nextRenderedChildren.push(prevChild);
}
// 最後卸載不存在的孩子
for (var j = nextChildren.length; j < prevChildren.length; j++) {
var prevChild = prevRenderedChildren[j];
var node = prevChild.node;
prevChild.unmount();
// 記錄我們需要移除的節點
operationQueue.push({type: 'REMOVE', node});
}
// 將孩子的列表更新到最新的版本
this.renderedChildren = nextRenderedChildren;
// ...
在最後一步我們執行了DOM
操作,同樣的,真正的調解器代碼會更加複雜,因爲他還要處理移除情況。
// ...
// Process the operation queue.
while (operationQueue.length > 0) {
var operation = operationQueue.shift();
switch (operation.type) {
case 'ADD':
this.node.appendChild(operation.node);
break;
case 'REPLACE':
this.node.replaceChild(operation.nextNode, operation.prevNode);
break;
case 'REMOVE':
this.node.removeChild(operation.node);
break;
}
}
}
}
10.頂級更新
現在CompositeComponent
和DOMComponent
都實現了receive(nextElement)
方法,我們需要改變全局函數mountTree()
增加在元素type
相同的時候的處理。
function mountTree(element, containerNode) {
// 檢查已經存在的樹
if (containerNode.firstChild) {
var prevNode = containerNode.firstChild;
var prevRootComponent = prevNode._internalInstance;
var prevElement = prevRootComponent.currentElement;
// 如果可以,重利用已經存在的root組件
if (prevElement.type === element.type) {
prevRootComponent.receive(element);
return;
}
// 否則,卸載已經存在的樹
unmountTree(containerNode);
}
// ...
}
如今,兩次調用mountTree()
,他能充分的利用已經存在的資源
var rootEl = document.getElementById('root');
mountTree(<App />, rootEl);
mountTree(<App />, rootEl);
11.我們遺漏了什麼
我的這篇博客寫的內容和真實的代碼相差甚遠,這裏我提幾個沒有涉及到的但又非常重要的幾點。
組件是可以返回
null
的,調解器可以處理空的數組和渲染出空的元素調解器也能從元素中讀取
key
屬性,使用它們來建立內部實例對應的數組中的元素,這樣可以提高性能除了是組合和
host
內部實例外,還可以是text
或者空的組件,他們表示text
節點和我們render
返回null
渲染器使用注入方式將
host
內部類傳遞給調解器,比如,React DOM
渲染器會告訴調解器使用ReactDOMComponent
作爲host
的內部實例的實現。更新的列表孩子的邏輯被提取到了一個叫做
ReactMultiChild
的mixin
中,這個東西可以被在React DOM
和React Native
渲染器中的host
內部實例所使用。調解器也實現了對
setState()
的支持,多個更新事件可以批量處理調解器還對組合組件和
host
組件的ref
進行了處理在
DOM
準備好之後,生命週期鉤子componentDidMount()
和componentDidUpdate()
也會調用,他們被加入到了一個“回調隊列”中,去順序的批量的處理他們React
將當前信息更新到內部實例中去的過程叫做“事務”,事務方式處理對於等待生命週期鉤子的隊列的跟蹤非常有用。事務能保證React在更新成功後可以清除所有東西。比如,事務類提供ReactDOM
更新出現問題時可以恢復到更新之前的狀態。
12.代碼比較
ReactMount
的代碼有點像mountTree
和unmountTree
用於組件的綁定和卸載,在React Native
中是這個ReactNativeMount
ReactDOMComponent
和DOMComponent
是類似的,它就是React DOM
渲染器host
組件類的實現,ReactNativeBaseComponent
則是React Native
的。ReactCompositeComponent
和CompositeComponent
是類似的,它用來處理用戶自定義組件和他們的狀態instantiateReactComponent
則構造一個元素實例,和instantiateComponent
相似ReactReconciler
則是mountComponent
,receiveComponent
,和unmountComponent
的集合,它在底層實現了內部實例,但是也包括所有內部實例共享的代碼。ReactChildReconciler
則是根據孩子元素的key
去綁定孩子,更新孩子和卸載孩子。ReactMultiChild
作爲一個獨立渲染器來處理孩子的插入和刪除工作。因爲歷史遺留問題,在React代碼中,
mount()
,receive()
, 和unmount()
名字改爲了mountComponent()
,receiveComponent()
, 和unmountComponent()
,但是他們依舊可以接受元素內部實例由於是私有的,一般都以下劃線命名開頭。
12.將來的發展方向
在前一章中我提到過一個調解器叫做纖維調解器,它就是用來取代堆棧調解器的,因爲堆棧調解器具有一定的侷限性,同步問題,或者是無法分離代碼,耦合度比較大,等等等,個人想法而已。
下一篇將講什麼暫時不知道,嘿