日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

react源码解析之stack reconciler

發(fā)布時間:2024/4/17 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 react源码解析之stack reconciler 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

關(guān)于源碼解讀的系列文章,可以關(guān)注我的github的這個倉庫, 現(xiàn)在才剛剛寫,后續(xù)有空就寫點。爭取把react源碼剖析透學(xué)習(xí)透。有不正確的地方希望大家?guī)兔χ刚?。大家互相學(xué)習(xí),共同進(jìn)步。

本篇文章是官方文檔的翻譯,英文原文請訪問官網(wǎng)

這個章節(jié)是stack reconciler的一些實現(xiàn)說明.

它的技術(shù)性很強(qiáng)并假定你能完全理解React的公開API,以及它是如何劃分為核心、渲染器和協(xié)調(diào)器的。如果你對React代碼不是很熟悉,請先閱讀代碼概覽。

它還假定你能夠理解React組件、實例和元素的區(qū)別。

Stack reconciler 被用在React 15 以及更早的版本中, 它在源代碼中的位置是src/renderers/shared/stack/reconciler.

視頻:從零開始構(gòu)建React

Paul O'Shannessy給出了一個關(guān)于從零開始構(gòu)建React的討論,在很大程度上對本文檔給予了啟發(fā)。

本文檔與上邊的視頻都是對實際代碼庫的簡化,因此你可以通過熟悉兩者來更好地理解。

概述

協(xié)調(diào)器本身沒有公共 API. 但是諸如React DOM 和React Native的渲染器使用它依據(jù)用戶所編寫的React組件來有效地更新用戶界面.

以遞歸過程的形式裝載

讓我們考慮首次裝載組件的情形:

ReactDOM.render(<App />, rootEl); 復(fù)制代碼

React DOM會將 <App />傳遞給協(xié)調(diào)器。請記住, <App />是一個React元素,也就是說是對哪些要渲染的東西的說明。你可以把它看成一個普通的對象:

console.log(<App />); // { type: App, props: {} } 復(fù)制代碼

協(xié)調(diào)器(reconciler)會檢查 App是類還是函數(shù)。如果 App 是函數(shù),協(xié)調(diào)器會調(diào)用App(props)來獲取所渲染的元素。如果App是類,協(xié)調(diào)器則會使用new App(props)創(chuàng)建一個App實例,調(diào)用 componentWillMount() 生命周期方法,進(jìn)而調(diào)用 render() 方法來獲取所渲染的元素。無論如何,協(xié)調(diào)器都會學(xué)習(xí)App元素的“渲染行為”。

此過程是遞歸的。App 可能渲染為<Greeting />,而<Greeting />可能渲染為 <Button />,如此類推。因為協(xié)調(diào)器會依次學(xué)習(xí)他們各自將如何渲染,所以協(xié)調(diào)器會遞歸地“向下鉆取”所有用戶定義組件。

你可以通過如下偽代碼來理解該過程:

function isClass(type) {// React.Component的子類都會含有這一標(biāo)志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. // 此函數(shù)讀取一個React元素(例如<App />) // 并返回一個表達(dá)所裝載樹的DOM或內(nèi)部節(jié)點。 function mount(element) {var type = element.type;var props = element.props;// 我們以此判斷所渲染元素:// 是以函數(shù)型運行該類型// 還是創(chuàng)建新實例并調(diào)用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();}// 調(diào)用render()以獲取所渲染元素renderedElement = publicInstance.render();} else {// 組件函數(shù)renderedElement = type(props);}// 該過程是遞歸實現(xiàn),原因在于組件可能返回一個其它組件類型的元素。return mount(renderedElement);// 注意:該實現(xiàn)不完整,且將無窮遞歸! 它只處理<App />或<Button />等元素。尚不處理<div />或<p />等元素。 }var rootEl = document.getElementById('root'); var node = mount(<App />); rootEl.appendChild(node); 復(fù)制代碼

注意:

這的確是一段偽代碼。它與真實的實現(xiàn)不同。它會導(dǎo)致棧溢出,因為我們還沒有討論何時停止遞歸。

讓我們回顧一下上面示例中的幾個關(guān)鍵概念:

  • React元素是表示組件類型(例如 App)與屬性的普通對象。
  • 用戶定義組件(例如 App)可以為類或者函數(shù),但它們都會被“渲染為”元素。
  • “裝載”(Mounting)是一個遞歸過程,當(dāng)給定頂級React元素(例如)時創(chuàng)建DOM或內(nèi)部節(jié)點樹。

裝載主機(jī)元素(Mounting Host Elements)

該過程將沒有任何意義,如果最終沒有渲染內(nèi)容到屏幕上。

除了用戶定義的(“復(fù)合”)組件外, React元素還可能表示特定于平臺的(“主機(jī)”)組件。例如,Button可能會從其渲染方法中返回 <div /> 。

如果元素的type屬性是一個字符串,即表示我們正在處理一個主機(jī)元素(host element):

console.log(<div />); // { type: 'div', props: {} } 復(fù)制代碼

主機(jī)元素(host elements)不存在關(guān)聯(lián)的用戶定義代碼。

當(dāng)協(xié)調(diào)器遇到主機(jī)元素(host element)時,它會讓渲染器(renderer)裝載它(mounting)。例如,React DOM將會創(chuàng)建一個DOM節(jié)點。

如果主機(jī)元素(host element)有子級,協(xié)調(diào)器(reconciler)則會用上述相同算法遞歸地將它們裝載。而不管子級是主機(jī)元素(如<div><hr /></div>)還是混合元素(如<div><Button /></div>)或是兩者兼有。

由子級組件生成的DOM節(jié)點將被追加到DOM父節(jié)點,同時整的DOM結(jié)構(gòu)會被遞歸裝配。

注意:

協(xié)調(diào)器本身(reconciler)并不與DOM捆綁。裝載(mounting)的具體結(jié)果(有時在源代碼中稱為“裝載映像”)取決于渲染器(renderer),可能為 DOM節(jié)點(React DOM)、字符串(React DOM服務(wù)器)或表示本機(jī)視圖的數(shù)值(React Native)。

我們來擴(kuò)展一下代碼,以處理主機(jī)元素(host elements):

function isClass(type) {// React.Component 子類含有這一標(biāo)志return (Boolean(type.prototype) &&Boolean(type.prototype.isReactComponent)); }// 該函數(shù)僅處理含復(fù)合類型的元素。 例如,它處理<App />和<Button />,但不處理<div />。 function mountComposite(element) {var type = element.type;var props = element.props;var renderedElement;if (isClass(type)) {// 組件類var publicInstance = new type(props);// 設(shè)置屬性publicInstance.props = props;// 若必要,則調(diào)用生命周期函數(shù)if (publicInstance.componentWillMount) {publicInstance.componentWillMount();}renderedElement = publicInstance.render();} else if (typeof type === 'function') {// 組件函數(shù)renderedElement = type(props);}// 該過程是遞歸,一旦該元素為主機(jī)(如<div />}而非復(fù)合(如<App />)時,則逐漸結(jié)束return mount(renderedElement); }// 該函數(shù)僅處理含主機(jī)類型的元素(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);// 該代碼塊不可出現(xiàn)在協(xié)調(diào)器(reconciler)中。// 不同渲染器(renderers)可能會以不同方式初始化節(jié)點。// 例如,React Native會生成iOS或Android視圖。var node = document.createElement(type);Object.keys(props).forEach(propName => {if (propName !== 'children') {node.setAttribute(propName, props[propName]);}});// 裝載子節(jié)點children.forEach(childElement => {// 子節(jié)點有可能是主機(jī)元素(如<div />)或復(fù)合元素(如<Button />).// 所以我們應(yīng)該遞歸的裝載var childNode = mount(childElement);// 此行代碼仍是特定于渲染器的。不同的渲染器則會使用不同的方法node.appendChild(childNode);});// 返回DOM節(jié)點作為裝載結(jié)果// 此處即為遞歸結(jié)束.return node; }function mount(element) {var type = element.type;if (typeof type === 'function') {// 用戶定義的組件return mountComposite(element);} else if (typeof type === 'string') {// 平臺相關(guān)的組件,比如說瀏覽器中的div,ios和安卓中的視圖return mountHost(element);} }var rootEl = document.getElementById('root'); var node = mount(<App />); rootEl.appendChild(node); 復(fù)制代碼

該代碼能夠工作但仍與協(xié)調(diào)器(reconciler)的真正實現(xiàn)相差甚遠(yuǎn)。其所缺少的關(guān)鍵部分是對更新的支持。

介紹內(nèi)部實例

React 的關(guān)鍵特征是您可以重新渲染所有內(nèi)容, 它不會重新創(chuàng)建 DOM 或重置狀態(tài):

ReactDOM.render(<App />, rootEl); // 應(yīng)該重新使用現(xiàn)存的 DOM: ReactDOM.render(<App />, rootEl); 復(fù)制代碼

但是, 上面的實現(xiàn)只知道如何裝載初始樹。它無法對其執(zhí)行更新, 因為它沒有存儲所有必需的信息, 例如所有 publicInstance , 或者哪個 DOM 節(jié)點 對應(yīng)于哪些組件。

堆棧協(xié)調(diào)(stack reconciler)的基本代碼是通過使 mount () 函數(shù)成為一個方法并將其放在類上來解決這一問題。 這種方式有一些缺陷,但是目前代碼中仍然使用的是這種方式。不過目前我們也正在重寫協(xié)調(diào)器(reconciler)

我們將創(chuàng)建兩個類: DOMComponent 和 CompositeComponent , 而不是單獨的 mountHost 和 mountComposite 函數(shù)。

兩個類都有一個接受 element 的構(gòu)造函數(shù), 以及一個能返回已裝入節(jié)點的 mount () 方法。我們將用一個能實例化正確類的工廠函數(shù)替換掉之前 例子里的mount函數(shù):

function instantiateComponent(element) {var type = element.type;if (typeof type === 'function') {// 用戶自定義組件return new CompositeComponent(element);} else if (typeof type === 'string') {// 特定于平臺的組件return new DOMComponent(element);} } 復(fù)制代碼

首先, 讓我們考慮如何實現(xiàn) CompositeComponent:

class CompositeComponent {constructor(element) {this.currentElement = element;this.renderedComponent = null;this.publicInstance = null;}getPublicInstance() {// 針對復(fù)合組合, 返回類的實例.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);// 設(shè)置屬性publicInstance.props = props;// 如果有必要,調(diào)用生命周期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實例化內(nèi)部的child實例,這個實例有可能是DOMComponent,比如<div /> or <p />// 也可能是CompositeComponent 比如說<App /> or <Button />var renderedComponent = instantiateComponent(renderedElement);this.renderedComponent = renderedComponent;// 增加渲染輸出return renderedComponent.mount();} } 復(fù)制代碼

這與我們以前的 mountComposite() 實現(xiàn)沒有太大的不同, 但現(xiàn)在我們可以保存一些信息, 比如this.currentElement、this.renderedComponent 和 this.publicInstance ,這些保存的信息會在更新期間被使用。

請注意, CompositeComponent的實例與用戶提供的 element.type 的實例不是一回事。 CompositeComponent是我們的協(xié)調(diào)器(reconciler)的一個實現(xiàn)細(xì)節(jié), 從不向用戶公開。 用戶自定義類是我們從 element.type 讀取的,并且通過 CompositeComponent 創(chuàng)建它的一個實例。

為避免混亂,我們將CompositeComponent和DOMComponent的實例稱為“內(nèi)部實例”。 由于它們的存在, 我們可以將一些長壽數(shù)據(jù)(ong-lived)與它們關(guān)聯(lián)起來。只有渲染器(renderer)和協(xié)調(diào)器(reconciler)知道它們的存在。

另一方面, 我們將用戶定義的類的實例稱為 "公共實例"(public instance)。公共實例是您在 render() 和自定義組件的其他方法中看到的 this

mountHost() 函數(shù)被重構(gòu)為 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;} } 復(fù)制代碼

從 mountHost () 重構(gòu)后的主要區(qū)別在于, 我們現(xiàn)在將 this.node 和 this.renderedChildren 與內(nèi)部 DOM 組件實例相關(guān)聯(lián)。 我們還將使用它們在將來應(yīng)用非破壞性更新。

因此, 每個內(nèi)部實例 (復(fù)合實例或主機(jī)實例)(composite or host) 現(xiàn)在都指向內(nèi)部的子實例。為幫助可視化, 如果功能 <App> 組件呈現(xiàn) <Button> 類組件, 并且 <Button> 類呈現(xiàn)<div>, 則內(nèi)部實例樹將如下所顯示:

[object CompositeComponent] {currentElement: <App />,publicInstance: null,renderedComponent: [object CompositeComponent] {currentElement: <Button />,publicInstance: [object Button],renderedComponent: [object DOMComponent] {currentElement: <div />,node: [object HTMLDivElement],renderedChildren: []}} } 復(fù)制代碼

在 DOM 中, 您只會看到<div> 。但是, 內(nèi)部實例樹同時包含復(fù)合和主機(jī)內(nèi)部實例(composite and host internal instances)。

內(nèi)部的復(fù)合實例需要存儲下面的信息:

  • 當(dāng)前元素(The current element).
  • 如果元素類型是類, 則將類實例化并存為公共實例(The public instance if element type is a class).
  • 一個通過運行render()之后并傳入工廠函數(shù)而得到的內(nèi)部實例(renderedComponent)。它可以是一個DOMComponent或一個CompositeComponent。

內(nèi)部的主機(jī)實例需要存儲下面的信息:

  • 當(dāng)前元素(The current element).
  • DOM 節(jié)點(The DOM node).
  • 所有的內(nèi)部子實例,他們可以是 DOMComponent or a CompositeComponent。(All the child internal instances. Each of them can be either a DOMComponent or a CompositeComponent).

如果你很難想象一個內(nèi)部的實例樹是如何在更復(fù)雜的應(yīng)用中構(gòu)建的, React DevTools可以給出一個非常接近的近似,因為它突出顯示了帶有灰色的主機(jī)實例,以及用紫色表示的組合實例:

為了完成這個重構(gòu),我們將引入一個函數(shù),它將一個完整的樹掛載到一個容器節(jié)點,就像ReactDOM.render()。它返回一個公共實例,也類似于 ReactDOM.render():

function mountTree(element, containerNode) {// 創(chuàng)建頂級內(nèi)部實例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); 復(fù)制代碼

卸載(Unmounting)

現(xiàn)在,我們有了保存有它們的子節(jié)點和DOM節(jié)點的內(nèi)部實例,我們可以實現(xiàn)卸載。對于一個復(fù)合組件(composite component),卸載將調(diào)用一個生命周期鉤子然后遞歸進(jìn)行。

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();} } 復(fù)制代碼

對于DOMComponent,卸載操作讓每個孩子進(jìn)行卸載:

class DOMComponent {// ...unmount() {// Unmount all the childrenvar renderedChildren = this.renderedChildren;renderedChildren.forEach(child => child.unmount());} } 復(fù)制代碼

在實踐中,卸載DOM組件也會刪除事件偵聽器并清除一些緩存,為了便于理解,我們暫時跳過這些細(xì)節(jié)。

現(xiàn)在我們可以添加一個頂級函數(shù),叫作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 = ''; } 復(fù)制代碼

為了使其工作,我們需要從一個DOM節(jié)點讀取一個內(nèi)部根實例。我們將修改 mountTree() 以將 _internalInstance 屬性添加到DOM 根節(jié)點。 我們也將教mountTree()去銷毀任何現(xiàn)存樹,以便將來它可以被多次調(diào)用:

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; } 復(fù)制代碼

現(xiàn)在,可以反復(fù)運行unmountTree()或者 mountTree(),清除舊樹并且在組件上運行 componentWillUnmount() 生命周期鉤子。

更新(Updating)

在上一節(jié)中,我們實現(xiàn)了卸載。然而,如果每個組件的prop的變動都要卸載并掛載整個樹,這是不可接受的。幸好我們設(shè)計了協(xié)調(diào)器。 協(xié)調(diào)器(reconciler)的目標(biāo)是重用已存在的實例,以便保留DOM和狀態(tài):

var rootEl = document.getElementById('root');mountTree(<App />, rootEl); // 應(yīng)該重用現(xiàn)有的DOM: mountTree(<App />, rootEl); 復(fù)制代碼

我們將用一種方法擴(kuò)展我們的內(nèi)部實例。 除了 mount()和 unmount()。DOMComponent和 CompositeComponent將實現(xiàn)一個新的方法,它叫作 receive(nextElement):

class CompositeComponent {// ...receive(nextElement) {// ...} }class DOMComponent {// ...receive(nextElement) {// ...} } 復(fù)制代碼

它的工作是做任何必要的工作,以使組件(及其任何子節(jié)點) 能夠根據(jù) nextElement 提供的信息保持信息為最新狀態(tài)。

這是經(jīng)常被描述為"virtual DOM diffing"的部分,盡管真正發(fā)生的是我們遞歸地遍歷內(nèi)部樹,并讓每個內(nèi)部實例接收到更新指令。

更新復(fù)合組件(Updating Composite Components)

當(dāng)一個復(fù)合組件接收到一個新元素(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);}// ... 復(fù)制代碼

下一步,我們可以看一下渲染元素的type。如果自從上次渲染,type 沒有被改變,組件接下來可以被適當(dāng)更新。

例如,如果它第一次返回 <Button color="red" />,并且第二次返回 <Button color="blue" />,我們可以告訴內(nèi)部實例去 receive() 下一個元素:

// ...// 如果被渲染元素類型沒有被改變,// 重用現(xiàn)有的組件實例.if (prevRenderedElement.type === nextRenderedElement.type) {prevRenderedComponent.receive(nextRenderedElement);return;}// ...復(fù)制代碼

但是,如果下一個被渲染元素和前一個相比有一個不同的type ,我們不能更新內(nèi)部實例。因為一個 <button> 不“能變”為一個<input>.

相反,我們必須卸載現(xiàn)有的內(nèi)部實例并掛載對應(yīng)于渲染的元素類型的新實例。 例如,這就是當(dāng)一個之前被渲染的元素<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);} } 復(fù)制代碼

總而言之,當(dāng)一個復(fù)合組件(composite component)接收到一個新元素時,它可能會將更新委托給其渲染的內(nèi)部實例((rendered internal instance), 或者卸載它,并在其位置上掛一個新元素。

另一種情況下,組件將重新掛載而不是接收一個元素,并且這發(fā)生在元素的key變化時。本文檔中,我們不討論key 處理,因為它將使原本復(fù)雜的教程更加復(fù)雜。

注意,我們需要添加一個叫作getHostNode()的新方法到內(nèi)部實例(internal instance),以便可以定位特定于平臺的節(jié)點并在更新期間替換它。 它的實現(xiàn)對兩個類都很簡單:

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;} } 復(fù)制代碼

更新主機(jī)組件(Updating Host Components)

主機(jī)組件實現(xiàn)(例如DOMComponent), 是以不同方式更新.當(dāng)它們接收到一個元素時,它們需要更新底層特定于平臺的視圖。在 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]);}});// ... 復(fù)制代碼

接下來,主機(jī)組件需要更新它們的子元素。與復(fù)合組件不同的是,它們可能包含多個子元素。

在這個簡化的例子中,我們使用一個內(nèi)部實例的數(shù)組并對其進(jìn)行迭代,是更新或替換內(nèi)部實例,這取決于接收到的type是否與之前的type匹配。 真正的調(diào)解器(reconciler)同時在帳戶中獲取元素的key并且追蹤變動,除了插入與刪除,但是我們現(xiàn)在先忽略這一邏輯。

我們在列表中收集DOM操作,這樣我們就可以批量地執(zhí)行它們。

// ...// // 這些是React元素(element)數(shù)組:var prevChildren = prevProps.children || [];if (!Array.isArray(prevChildren)) {prevChildren = [prevChildren];}var nextChildren = nextProps.children || [];if (!Array.isArray(nextChildren)) {nextChildren = [nextChildren];}// 這些是內(nèi)部實例(internal instances)數(shù)組:var prevRenderedChildren = this.renderedChildren;var nextRenderedChildren = [];// 當(dāng)我們遍歷children時,我們將向數(shù)組中添加操作。var operationQueue = [];// 注意:以下章節(jié)大大減化!// 它不處理reorders,空children,或者keys。// 它只是用來解釋整個流程,而不是具體的細(xì)節(jié)。for (var i = 0; i < nextChildren.length; i++) {// 嘗試為這個子級獲取現(xiàn)存內(nèi)部實例。var prevChild = prevRenderedChildren[i];// 如果在這個索引下沒有內(nèi)部實例,那說明是一個child被添加了末尾。// 這時應(yīng)該去創(chuàng)建一個內(nèi)部實例,掛載它,并使用它的節(jié)點。if (!prevChild) {var nextChild = instantiateComponent(nextChildren[i]);var node = nextChild.mount();// 記錄一下我們將來需要append一個節(jié)點(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;// 如果我們不能更新現(xiàn)有的實例,我們就必須卸載它。然后裝一個新的替代它。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;}// 如果我們可以更新現(xiàn)存的內(nèi)部實例(internal instance),// 我們僅僅把下一個元素傳入其receive即可,讓其receive函數(shù)處理它的更新即可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;// ... 復(fù)制代碼

作為最后一步,我們執(zhí)行DOM操作。還是那句話,真正的協(xié)調(diào)器(reconciler)代碼更復(fù)雜,因為它還能處理移動:

// ...// 處理隊列里的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;}}} } 復(fù)制代碼

這是用來更新主機(jī)組件(host components)的。

頂級更新(Top-Level Updates)

現(xiàn)在 CompositeComponent 與 DOMComponent 都實現(xiàn)了 receive(nextElement) 方法, 我們現(xiàn)在可以改變頂級 mountTree() 函數(shù)了,當(dāng)元素(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;// 如果可以,使用現(xiàn)存根組件if (prevElement.type === element.type) {prevRootComponent.receive(element);return;}// 否則,卸載現(xiàn)存樹unmountTree(containerNode);}// ...} 復(fù)制代碼

現(xiàn)在調(diào)用 mountTree()兩次,同樣的類型不會先卸載再裝載了:

var rootEl = document.getElementById('root');mountTree(<App />, rootEl); // 復(fù)用現(xiàn)存 DOM: mountTree(<App />, rootEl); 復(fù)制代碼

These are the basics of how React works internally.

我們遺漏的還有什么?

與真正的代碼庫相比,這個文檔被簡化了。有一些重要的方面我們沒有提到:

  • 組件可以渲染null,而且,協(xié)調(diào)器(reconciler)可以處理數(shù)組中的“空槽(empty slots)”并顯示輸出。

  • 協(xié)調(diào)器(reconciler)可以從元素中讀取 key ,并且用它來建立在一個數(shù)組中內(nèi)部實例與元素的對應(yīng)關(guān)系。實際的 React 實現(xiàn)的大部分復(fù)雜性與此相關(guān)。

  • 除了復(fù)合和主機(jī)內(nèi)部實例類之外,還存在用于“文本”和“空”組件的類。它們表示文本節(jié)點和通過渲染 null得到的“空槽”。

  • 渲染器(Renderers)使用injection 將主機(jī)內(nèi)部類傳遞給協(xié)調(diào)器(reconciler)。例如,React DOM 告訴協(xié)調(diào)器使用 ReactDOMComponent 作為主機(jī)內(nèi)部實現(xiàn)實例。

  • 更新子列表的邏輯被提取到一個名為 ReactMultiChild 的mixin中,它被主機(jī)內(nèi)部實例類實現(xiàn)在 React DOM和 React Native時都使用。

  • 協(xié)調(diào)器也實現(xiàn)了在復(fù)合組件(composite components)中支持setState()。事件處理程序內(nèi)部的多個更新將被打包成一個單一的更新。

  • 協(xié)調(diào)器(reconciler)還負(fù)責(zé)復(fù)合組件和主機(jī)節(jié)點的refs。

  • 在DOM準(zhǔn)備好之后調(diào)用的生命周期鉤子,例如 componentDidMount() 和 componentDidUpdate(),收集到“回調(diào)隊列”,并在單個批處理中執(zhí)行。

  • React 將當(dāng)前更新的信息放入一個名為“事務(wù)”的內(nèi)部對象中。事務(wù)對于跟蹤掛起的生命周期鉤子的隊列、 為了warning而嵌套的當(dāng)前DOM(the current DOM nesting for the warnings)以及任何“全局”到特定的更新都是有用的。 事務(wù)還可以確保在更新后“清除所有內(nèi)容”。例如,由 React DOM提供的事務(wù)類在任何更新之后恢復(fù)input的選中與否。

直接查看代碼(Jumping into the Code)

  • 在 ReactMount 中可以查看此教程中類似 mountTree() 和 unmountTree() 的代碼. 它負(fù)責(zé)裝載(mounting)和卸載(unmounting)頂級組件。 ReactNativeMount is its React Native analog.

  • ReactDOMComponent 在教程中與DOMComponent等同. 它實現(xiàn)了 React DOM渲染器(renderer)的主機(jī)組件類(host component class。 ReactNativeBaseComponent is its React Native analog.

  • ReactCompositeComponent 在教程中與 CompositeComponent 等同. 它處理調(diào)用用戶定義的組件并維護(hù)它們的狀態(tài)。

  • instantiateReactComponent 包含選擇正確的內(nèi)部實例類并運行element的構(gòu)造函數(shù)。在本教程中,它與instantiateComponent()等同。

  • ReactReconciler 是一個具有 mountComponent(), receiveComponent(), 和 unmountComponent() 方法的封裝. 它調(diào)用內(nèi)部實例的底層實現(xiàn),但也包含了所有內(nèi)部實例實現(xiàn)共享的代碼。

  • ReactChildReconciler 根據(jù)元素的 key ,實現(xiàn)了mounting、updating和unmounting的邏輯.

  • ReactMultiChild 獨立于渲染器的操作隊列,實現(xiàn)了處理child的插入、刪除和移動

  • 由于遺留的原因 mount(), receive(), and unmount() 被稱作 mountComponent(), receiveComponent(), and unmountComponent() 但是他們卻接收elements

  • 內(nèi)部實例的屬性以一個下劃線開始, 例如, _currentElement. 在整個代碼庫中,它們被認(rèn)為是只讀的公共字段。

未來方向(Future Directions)

堆棧協(xié)調(diào)器具有固有的局限性, 如同步和無法中斷工作或分割成區(qū)塊。 我們正在實現(xiàn)一個新的協(xié)調(diào)器Fiber reconciler, 你可以在這里看它的具體思路 將來我們會用fiber協(xié)調(diào)器代替stack協(xié)調(diào)器(譯者注:其實現(xiàn)在react16已經(jīng)發(fā)布,在react16中fiber算法已經(jīng)取代了stack算法)

下一步(Next Steps)

閱讀next section以了解有關(guān)協(xié)調(diào)器的當(dāng)前實現(xiàn)的詳細(xì)信息。

總結(jié)

以上是生活随笔為你收集整理的react源码解析之stack reconciler的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。