react源码解析之stack reconciler
關于源碼解讀的系列文章,可以關注我的github的這個倉庫, 現在才剛剛寫,后續有空就寫點。爭取把react源碼剖析透學習透。有不正確的地方希望大家幫忙指正。大家互相學習,共同進步。
本篇文章是官方文檔的翻譯,英文原文請訪問官網
這個章節是stack reconciler的一些實現說明.
它的技術性很強并假定你能完全理解React的公開API,以及它是如何劃分為核心、渲染器和協調器的。如果你對React代碼不是很熟悉,請先閱讀代碼概覽。
它還假定你能夠理解React組件、實例和元素的區別。
Stack reconciler 被用在React 15 以及更早的版本中, 它在源代碼中的位置是src/renderers/shared/stack/reconciler.
視頻:從零開始構建React
Paul O'Shannessy給出了一個關于從零開始構建React的討論,在很大程度上對本文檔給予了啟發。
本文檔與上邊的視頻都是對實際代碼庫的簡化,因此你可以通過熟悉兩者來更好地理解。
概述
協調器本身沒有公共 API. 但是諸如React DOM 和React Native的渲染器使用它依據用戶所編寫的React組件來有效地更新用戶界面.
以遞歸過程的形式裝載
讓我們考慮首次裝載組件的情形:
ReactDOM.render(<App />, rootEl); 復制代碼React DOM會將 <App />傳遞給協調器。請記住, <App />是一個React元素,也就是說是對哪些要渲染的東西的說明。你可以把它看成一個普通的對象:
console.log(<App />); // { type: App, props: {} } 復制代碼協調器(reconciler)會檢查 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)); }// This function takes a React element (e.g. <App />) // and returns a DOM or Native node representing the mounted tree. // 此函數讀取一個React元素(例如<App />) // 并返回一個表達所裝載樹的DOM或內部節點。 function mount(element) {var type = element.type;var props = element.props;// 我們以此判斷所渲染元素:// 是以函數型運行該類型// 還是創建新實例并調用render()。var renderedElement;if (isClass(type)) {// Component classvar publicInstance = new type(props);// Set the propspublicInstance.props = props;// Call the lifecycle if necessaryif (publicInstance.componentWillMount) {publicInstance.componentWillMount();}// 調用render()以獲取所渲染元素renderedElement = publicInstance.render();} else {// 組件函數renderedElement = type(props);}// 該過程是遞歸實現,原因在于組件可能返回一個其它組件類型的元素。return mount(renderedElement);// 注意:該實現不完整,且將無窮遞歸! 它只處理<App />或<Button />等元素。尚不處理<div />或<p />等元素。 }var rootEl = document.getElementById('root'); var node = mount(<App />); rootEl.appendChild(node); 復制代碼注意:
這的確是一段偽代碼。它與真實的實現不同。它會導致棧溢出,因為我們還沒有討論何時停止遞歸。
讓我們回顧一下上面示例中的幾個關鍵概念:
- React元素是表示組件類型(例如 App)與屬性的普通對象。
- 用戶定義組件(例如 App)可以為類或者函數,但它們都會被“渲染為”元素。
- “裝載”(Mounting)是一個遞歸過程,當給定頂級React元素(例如)時創建DOM或內部節點樹。
裝載主機元素(Mounting Host Elements)
該過程將沒有任何意義,如果最終沒有渲染內容到屏幕上。
除了用戶定義的(“復合”)組件外, React元素還可能表示特定于平臺的(“主機”)組件。例如,Button可能會從其渲染方法中返回 <div /> 。
如果元素的type屬性是一個字符串,即表示我們正在處理一個主機元素(host element):
console.log(<div />); // { type: 'div', props: {} } 復制代碼主機元素(host elements)不存在關聯的用戶定義代碼。
當協調器遇到主機元素(host element)時,它會讓渲染器(renderer)裝載它(mounting)。例如,React DOM將會創建一個DOM節點。
如果主機元素(host element)有子級,協調器(reconciler)則會用上述相同算法遞歸地將它們裝載。而不管子級是主機元素(如<div><hr /></div>)還是混合元素(如<div><Button /></div>)或是兩者兼有。
由子級組件生成的DOM節點將被追加到DOM父節點,同時整的DOM結構會被遞歸裝配。
注意:
協調器本身(reconciler)并不與DOM捆綁。裝載(mounting)的具體結果(有時在源代碼中稱為“裝載映像”)取決于渲染器(renderer),可能為 DOM節點(React DOM)、字符串(React DOM服務器)或表示本機視圖的數值(React Native)。
我們來擴展一下代碼,以處理主機元素(host elements):
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);// 設置屬性publicInstance.props = props;// 若必要,則調用生命周期函數if (publicInstance.componentWillMount) {publicInstance.componentWillMount();}renderedElement = publicInstance.render();} else if (typeof type === 'function') {// 組件函數renderedElement = type(props);}// 該過程是遞歸,一旦該元素為主機(如<div />}而非復合(如<App />)時,則逐漸結束return mount(renderedElement); }// 該函數僅處理含主機類型的元素(handles elements with a host type)。 例如,它處理<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);// 該代碼塊不可出現在協調器(reconciler)中。// 不同渲染器(renderers)可能會以不同方式初始化節點。// 例如,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 => {// 子節點有可能是主機元素(如<div />)或復合元素(如<Button />).// 所以我們應該遞歸的裝載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') {// 平臺相關的組件,比如說瀏覽器中的div,ios和安卓中的視圖return mountHost(element);} }var rootEl = document.getElementById('root'); var node = mount(<App />); rootEl.appendChild(node); 復制代碼該代碼能夠工作但仍與協調器(reconciler)的真正實現相差甚遠。其所缺少的關鍵部分是對更新的支持。
介紹內部實例
React 的關鍵特征是您可以重新渲染所有內容, 它不會重新創建 DOM 或重置狀態:
ReactDOM.render(<App />, rootEl); // 應該重新使用現存的 DOM: ReactDOM.render(<App />, rootEl); 復制代碼但是, 上面的實現只知道如何裝載初始樹。它無法對其執行更新, 因為它沒有存儲所有必需的信息, 例如所有 publicInstance , 或者哪個 DOM 節點 對應于哪些組件。
堆棧協調(stack reconciler)的基本代碼是通過使 mount () 函數成為一個方法并將其放在類上來解決這一問題。 這種方式有一些缺陷,但是目前代碼中仍然使用的是這種方式。不過目前我們也正在重寫協調器(reconciler)
我們將創建兩個類: DOMComponent 和 CompositeComponent , 而不是單獨的 mountHost 和 mountComposite 函數。
兩個類都有一個接受 element 的構造函數, 以及一個能返回已裝入節點的 mount () 方法。我們將用一個能實例化正確類的工廠函數替換掉之前 例子里的mount函數:
function instantiateComponent(element) {var type = element.type;if (typeof type === 'function') {// 用戶自定義組件return new CompositeComponent(element);} else if (typeof type === 'string') {// 特定于平臺的組件return new DOMComponent(element);} } 復制代碼首先, 讓我們考慮如何實現 CompositeComponent:
class CompositeComponent {constructor(element) {this.currentElement = element;this.renderedComponent = null;this.publicInstance = null;}getPublicInstance() {// 針對復合組合, 返回類的實例.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);// 設置屬性publicInstance.props = props;// 如果有必要,調用生命周期if (publicInstance.componentWillMount) {publicInstance.componentWillMount();}renderedElement = publicInstance.render();} else if (typeof type === 'function') {// Component functionpublicInstance = null;renderedElement = type(props);}// Save the public instancethis.publicInstance = publicInstance;// 通過element實例化內部的child實例,這個實例有可能是DOMComponent,比如<div /> or <p />// 也可能是CompositeComponent 比如說<App /> or <Button />var renderedComponent = instantiateComponent(renderedElement);this.renderedComponent = renderedComponent;// 增加渲染輸出return renderedComponent.mount();} } 復制代碼這與我們以前的 mountComposite() 實現沒有太大的不同, 但現在我們可以保存一些信息, 比如this.currentElement、this.renderedComponent 和 this.publicInstance ,這些保存的信息會在更新期間被使用。
請注意, CompositeComponent的實例與用戶提供的 element.type 的實例不是一回事。 CompositeComponent是我們的協調器(reconciler)的一個實現細節, 從不向用戶公開。 用戶自定義類是我們從 element.type 讀取的,并且通過 CompositeComponent 創建它的一個實例。
為避免混亂,我們將CompositeComponent和DOMComponent的實例稱為“內部實例”。 由于它們的存在, 我們可以將一些長壽數據(ong-lived)與它們關聯起來。只有渲染器(renderer)和協調器(reconciler)知道它們的存在。
另一方面, 我們將用戶定義的類的實例稱為 "公共實例"(public instance)。公共實例是您在 render() 和自定義組件的其他方法中看到的 this
mountHost() 函數被重構為 DOMComponent 類上的 mount()方法, 也看起來很熟悉:
class DOMComponent {constructor(element) {this.currentElement = element;this.renderedChildren = [];this.node = null;}getPublicInstance() {// For DOM components, only expose the DOM node.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];}// Create and save the nodevar node = document.createElement(type);this.node = node;// Set the attributesObject.keys(props).forEach(propName => {if (propName !== 'children') {node.setAttribute(propName, props[propName]);}});// Create and save the contained children.// Each of them can be a DOMComponent or a CompositeComponent,// depending on whether the element type is a string or a function.var renderedChildren = children.map(instantiateComponent);this.renderedChildren = renderedChildren;// Collect DOM nodes they return on mountvar childNodes = renderedChildren.map(child => child.mount());childNodes.forEach(childNode => node.appendChild(childNode));// Return the DOM node as mount resultreturn node;} } 復制代碼從 mountHost () 重構后的主要區別在于, 我們現在將 this.node 和 this.renderedChildren 與內部 DOM 組件實例相關聯。 我們還將使用它們在將來應用非破壞性更新。
因此, 每個內部實例 (復合實例或主機實例)(composite or 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> 。但是, 內部實例樹同時包含復合和主機內部實例(composite and host internal instances)。
內部的復合實例需要存儲下面的信息:
- 當前元素(The current element).
- 如果元素類型是類, 則將類實例化并存為公共實例(The public instance if element type is a class).
- 一個通過運行render()之后并傳入工廠函數而得到的內部實例(renderedComponent)。它可以是一個DOMComponent或一個CompositeComponent。
內部的主機實例需要存儲下面的信息:
- 當前元素(The current element).
- DOM 節點(The DOM node).
- 所有的內部子實例,他們可以是 DOMComponent or a CompositeComponent。(All the child internal instances. Each of them can be either a DOMComponent or a CompositeComponent).
如果你很難想象一個內部的實例樹是如何在更復雜的應用中構建的, React DevTools可以給出一個非常接近的近似,因為它突出顯示了帶有灰色的主機實例,以及用紫色表示的組合實例:
為了完成這個重構,我們將引入一個函數,它將一個完整的樹掛載到一個容器節點,就像ReactDOM.render()。它返回一個公共實例,也類似于 ReactDOM.render():
function mountTree(element, containerNode) {// 創建頂級內部實例var rootComponent = instantiateComponent(element);// 將頂級組件裝載到容器中var node = rootComponent.mount();containerNode.appendChild(node);// 返回它所提供的公共實例var publicInstance = rootComponent.getPublicInstance();return publicInstance; }var rootEl = document.getElementById('root'); mountTree(<App />, rootEl); 復制代碼卸載(Unmounting)
現在,我們有了保存有它們的子節點和DOM節點的內部實例,我們可以實現卸載。對于一個復合組件(composite component),卸載將調用一個生命周期鉤子然后遞歸進行。
class CompositeComponent {// ...unmount() {// Call the lifecycle hook if necessaryvar publicInstance = this.publicInstance;if (publicInstance) {if (publicInstance.componentWillUnmount) {publicInstance.componentWillUnmount();}}// Unmount the single rendered componentvar renderedComponent = this.renderedComponent;renderedComponent.unmount();} } 復制代碼對于DOMComponent,卸載操作讓每個孩子進行卸載:
class DOMComponent {// ...unmount() {// Unmount all the childrenvar renderedChildren = this.renderedChildren;renderedChildren.forEach(child => child.unmount());} } 復制代碼在實踐中,卸載DOM組件也會刪除事件偵聽器并清除一些緩存,為了便于理解,我們暫時跳過這些細節。
現在我們可以添加一個頂級函數,叫作unmountTree(containerNode),它與ReactDOM.unmountComponentAtNode()類似:
function unmountTree(containerNode) {// Read the internal instance from a DOM node:// (This doesn't work yet, we will need to change mountTree() to store it.)var node = containerNode.firstChild;var rootComponent = node._internalInstance;// Unmount the tree and clear the containerrootComponent.unmount();containerNode.innerHTML = ''; } 復制代碼為了使其工作,我們需要從一個DOM節點讀取一個內部根實例。我們將修改 mountTree() 以將 _internalInstance 屬性添加到DOM 根節點。 我們也將教mountTree()去銷毀任何現存樹,以便將來它可以被多次調用:
function mountTree(element, containerNode) {// Destroy any existing treeif (containerNode.firstChild) {unmountTree(containerNode);}// Create the top-level internal instancevar rootComponent = instantiateComponent(element);// Mount the top-level component into the containervar node = rootComponent.mount();containerNode.appendChild(node);// Save a reference to the internal instancenode._internalInstance = rootComponent;// Return the public instance it providesvar publicInstance = rootComponent.getPublicInstance();return publicInstance; } 復制代碼現在,可以反復運行unmountTree()或者 mountTree(),清除舊樹并且在組件上運行 componentWillUnmount() 生命周期鉤子。
更新(Updating)
在上一節中,我們實現了卸載。然而,如果每個組件的prop的變動都要卸載并掛載整個樹,這是不可接受的。幸好我們設計了協調器。 協調器(reconciler)的目標是重用已存在的實例,以便保留DOM和狀態:
var rootEl = document.getElementById('root');mountTree(<App />, rootEl); // 應該重用現有的DOM: mountTree(<App />, rootEl); 復制代碼我們將用一種方法擴展我們的內部實例。 除了 mount()和 unmount()。DOMComponent和 CompositeComponent將實現一個新的方法,它叫作 receive(nextElement):
class CompositeComponent {// ...receive(nextElement) {// ...} }class DOMComponent {// ...receive(nextElement) {// ...} } 復制代碼它的工作是做任何必要的工作,以使組件(及其任何子節點) 能夠根據 nextElement 提供的信息保持信息為最新狀態。
這是經常被描述為"virtual DOM diffing"的部分,盡管真正發生的是我們遞歸地遍歷內部樹,并讓每個內部實例接收到更新指令。
更新復合組件(Updating Composite Components)
當一個復合組件接收到一個新元素(element)時,我們運行componentWillUpdate()生命周期鉤子。
然后,我們使用新的props重新render組件,并獲得下一個render的元素(rendered element):
class CompositeComponent {// ...receive(nextElement) {var prevProps = this.currentElement.props;var publicInstance = this.publicInstance;var prevRenderedComponent = this.renderedComponent;var prevRenderedElement = prevRenderedComponent.currentElement;// Update *own* elementthis.currentElement = nextElement;var type = nextElement.type;var nextProps = nextElement.props;// Figure out what the next render() output isvar nextRenderedElement;if (isClass(type)) {// Component class// Call the lifecycle if necessaryif (publicInstance.componentWillUpdate) {publicInstance.componentWillUpdate(nextProps);}// Update the propspublicInstance.props = nextProps;// Re-rendernextRenderedElement = publicInstance.render();} else if (typeof type === 'function') {// Component functionnextRenderedElement = type(nextProps);}// ... 復制代碼下一步,我們可以看一下渲染元素的type。如果自從上次渲染,type 沒有被改變,組件接下來可以被適當更新。
例如,如果它第一次返回 <Button color="red" />,并且第二次返回 <Button color="blue" />,我們可以告訴內部實例去 receive() 下一個元素:
// ...// 如果被渲染元素類型沒有被改變,// 重用現有的組件實例.if (prevRenderedElement.type === nextRenderedElement.type) {prevRenderedComponent.receive(nextRenderedElement);return;}// ...復制代碼但是,如果下一個被渲染元素和前一個相比有一個不同的type ,我們不能更新內部實例。因為一個 <button> 不“能變”為一個<input>.
相反,我們必須卸載現有的內部實例并掛載對應于渲染的元素類型的新實例。 例如,這就是當一個之前被渲染的元素<button />之后又被渲染成一個 <input /> 的過程:
// ...// If we reached this point, we need to unmount the previously// mounted component, mount the new one, and swap their nodes.// Find the old node because it will need to be replacedvar prevNode = prevRenderedComponent.getHostNode();// Unmount the old child and mount a new childprevRenderedComponent.unmount();var nextRenderedComponent = instantiateComponent(nextRenderedElement);var nextNode = nextRenderedComponent.mount();// Replace the reference to the childthis.renderedComponent = nextRenderedComponent;// Replace the old node with the new one// Note: this is renderer-specific code and// ideally should live outside of CompositeComponent:prevNode.parentNode.replaceChild(nextNode, prevNode);} } 復制代碼總而言之,當一個復合組件(composite component)接收到一個新元素時,它可能會將更新委托給其渲染的內部實例((rendered internal instance), 或者卸載它,并在其位置上掛一個新元素。
另一種情況下,組件將重新掛載而不是接收一個元素,并且這發生在元素的key變化時。本文檔中,我們不討論key 處理,因為它將使原本復雜的教程更加復雜。
注意,我們需要添加一個叫作getHostNode()的新方法到內部實例(internal instance),以便可以定位特定于平臺的節點并在更新期間替換它。 它的實現對兩個類都很簡單:
class CompositeComponent {// ...getHostNode() {// 請求渲染的組件提供它(Ask the rendered component to provide it).// 這將遞歸地向下鉆取任何組合(This will recursively drill down any composites).return this.renderedComponent.getHostNode();} }class DOMComponent {// ...getHostNode() {return this.node;} } 復制代碼更新主機組件(Updating Host Components)
主機組件實現(例如DOMComponent), 是以不同方式更新.當它們接收到一個元素時,它們需要更新底層特定于平臺的視圖。在 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;// Remove old attributes.Object.keys(prevProps).forEach(propName => {if (propName !== 'children' && !nextProps.hasOwnProperty(propName)) {node.removeAttribute(propName);}});// Set next attributes.Object.keys(nextProps).forEach(propName => {if (propName !== 'children') {node.setAttribute(propName, nextProps[propName]);}});// ... 復制代碼接下來,主機組件需要更新它們的子元素。與復合組件不同的是,它們可能包含多個子元素。
在這個簡化的例子中,我們使用一個內部實例的數組并對其進行迭代,是更新或替換內部實例,這取決于接收到的type是否與之前的type匹配。 真正的調解器(reconciler)同時在帳戶中獲取元素的key并且追蹤變動,除了插入與刪除,但是我們現在先忽略這一邏輯。
我們在列表中收集DOM操作,這樣我們就可以批量地執行它們。
// ...// // 這些是React元素(element)數組:var prevChildren = prevProps.children || [];if (!Array.isArray(prevChildren)) {prevChildren = [prevChildren];}var nextChildren = nextProps.children || [];if (!Array.isArray(nextChildren)) {nextChildren = [nextChildren];}// 這些是內部實例(internal instances)數組:var prevRenderedChildren = this.renderedChildren;var nextRenderedChildren = [];// 當我們遍歷children時,我們將向數組中添加操作。var operationQueue = [];// 注意:以下章節大大減化!// 它不處理reorders,空children,或者keys。// 它只是用來解釋整個流程,而不是具體的細節。for (var i = 0; i < nextChildren.length; i++) {// 嘗試為這個子級獲取現存內部實例。var prevChild = prevRenderedChildren[i];// 如果在這個索引下沒有內部實例,那說明是一個child被添加了末尾。// 這時應該去創建一個內部實例,掛載它,并使用它的節點。if (!prevChild) {var nextChild = instantiateComponent(nextChildren[i]);var node = nextChild.mount();// 記錄一下我們將來需要append一個節點(node)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.getHostNode();prevChild.unmount();var nextChild = instantiateComponent(nextChildren[i]);var nextNode = nextChild.mount();// 記錄一下我們將來需要替換這些nodesoperationQueue.push({type: 'REPLACE', prevNode, nextNode});nextRenderedChildren.push(nextChild);continue;}// 如果我們可以更新現存的內部實例(internal instance),// 我們僅僅把下一個元素傳入其receive即可,讓其receive函數處理它的更新即可prevChild.receive(nextChildren[i]);nextRenderedChildren.push(prevChild);}// 最后,卸載(unmount)哪些不存在的childrenfor (var j = nextChildren.length; j < prevChildren.length; j++) {var prevChild = prevRenderedChildren[j];var node = prevChild.getHostNode();prevChild.unmount();// 記錄一下我們將來需要remove這些nodeoperationQueue.push({type: 'REMOVE', node});}// Point the list of rendered children to the updated version.this.renderedChildren = nextRenderedChildren;// ... 復制代碼作為最后一步,我們執行DOM操作。還是那句話,真正的協調器(reconciler)代碼更復雜,因為它還能處理移動:
// ...// 處理隊列里的operation。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;}}} } 復制代碼這是用來更新主機組件(host components)的。
頂級更新(Top-Level Updates)
現在 CompositeComponent 與 DOMComponent 都實現了 receive(nextElement) 方法, 我們現在可以改變頂級 mountTree() 函數了,當元素(element)的type相同時,我們可以使用receive了。
function mountTree(element, containerNode) {// Check for an existing treeif (containerNode.firstChild) {var prevNode = containerNode.firstChild;var prevRootComponent = prevNode._internalInstance;var prevElement = prevRootComponent.currentElement;// 如果可以,使用現存根組件if (prevElement.type === element.type) {prevRootComponent.receive(element);return;}// 否則,卸載現存樹unmountTree(containerNode);}// ...} 復制代碼現在調用 mountTree()兩次,同樣的類型不會先卸載再裝載了:
var rootEl = document.getElementById('root');mountTree(<App />, rootEl); // 復用現存 DOM: mountTree(<App />, rootEl); 復制代碼These are the basics of how React works internally.
我們遺漏的還有什么?
與真正的代碼庫相比,這個文檔被簡化了。有一些重要的方面我們沒有提到:
-
組件可以渲染null,而且,協調器(reconciler)可以處理數組中的“空槽(empty slots)”并顯示輸出。
-
協調器(reconciler)可以從元素中讀取 key ,并且用它來建立在一個數組中內部實例與元素的對應關系。實際的 React 實現的大部分復雜性與此相關。
-
除了復合和主機內部實例類之外,還存在用于“文本”和“空”組件的類。它們表示文本節點和通過渲染 null得到的“空槽”。
-
渲染器(Renderers)使用injection 將主機內部類傳遞給協調器(reconciler)。例如,React DOM 告訴協調器使用 ReactDOMComponent 作為主機內部實現實例。
-
更新子列表的邏輯被提取到一個名為 ReactMultiChild 的mixin中,它被主機內部實例類實現在 React DOM和 React Native時都使用。
-
協調器也實現了在復合組件(composite components)中支持setState()。事件處理程序內部的多個更新將被打包成一個單一的更新。
-
協調器(reconciler)還負責復合組件和主機節點的refs。
-
在DOM準備好之后調用的生命周期鉤子,例如 componentDidMount() 和 componentDidUpdate(),收集到“回調隊列”,并在單個批處理中執行。
-
React 將當前更新的信息放入一個名為“事務”的內部對象中。事務對于跟蹤掛起的生命周期鉤子的隊列、 為了warning而嵌套的當前DOM(the current DOM nesting for the warnings)以及任何“全局”到特定的更新都是有用的。 事務還可以確保在更新后“清除所有內容”。例如,由 React DOM提供的事務類在任何更新之后恢復input的選中與否。
直接查看代碼(Jumping into the Code)
-
在 ReactMount 中可以查看此教程中類似 mountTree() 和 unmountTree() 的代碼. 它負責裝載(mounting)和卸載(unmounting)頂級組件。 ReactNativeMount is its React Native analog.
-
ReactDOMComponent 在教程中與DOMComponent等同. 它實現了 React DOM渲染器(renderer)的主機組件類(host component class。 ReactNativeBaseComponent is its React Native analog.
-
ReactCompositeComponent 在教程中與 CompositeComponent 等同. 它處理調用用戶定義的組件并維護它們的狀態。
-
instantiateReactComponent 包含選擇正確的內部實例類并運行element的構造函數。在本教程中,它與instantiateComponent()等同。
-
ReactReconciler 是一個具有 mountComponent(), receiveComponent(), 和 unmountComponent() 方法的封裝. 它調用內部實例的底層實現,但也包含了所有內部實例實現共享的代碼。
-
ReactChildReconciler 根據元素的 key ,實現了mounting、updating和unmounting的邏輯.
-
ReactMultiChild 獨立于渲染器的操作隊列,實現了處理child的插入、刪除和移動
-
由于遺留的原因 mount(), receive(), and unmount() 被稱作 mountComponent(), receiveComponent(), and unmountComponent() 但是他們卻接收elements
-
內部實例的屬性以一個下劃線開始, 例如, _currentElement. 在整個代碼庫中,它們被認為是只讀的公共字段。
未來方向(Future Directions)
堆棧協調器具有固有的局限性, 如同步和無法中斷工作或分割成區塊。 我們正在實現一個新的協調器Fiber reconciler, 你可以在這里看它的具體思路 將來我們會用fiber協調器代替stack協調器(譯者注:其實現在react16已經發布,在react16中fiber算法已經取代了stack算法)
下一步(Next Steps)
閱讀next section以了解有關協調器的當前實現的詳細信息。
總結
以上是生活随笔為你收集整理的react源码解析之stack reconciler的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [剑指offer] 数组中只出现一次的数
- 下一篇: Linux系统集群架构线上项目配置实战(