標籤:

React系列:React架構

分析React.render(<App/>,rootEl)的過程,

幾個概念

React元素(React Elements):如<App/>,可以把React元素視為對象如{type:App,props:{}},render操作會檢查App的類型:

  • 如果App是函數,那麼執行App(props)操作
  • 如果App是class,那麼會執行new App(props)並且調用componentWillMount()操作,然後調用其render方法進行渲染,返回渲染後的元素。

上面是個遞歸的過程,App的渲染結果可能是<Greeting/>,<Greeting/>又可能渲染為<Button/>,其偽代碼如下:

function isClass(type) {n // React.Component subclasses have this flagn return (n Boolean(type.prototype) &&n Boolean(type.prototype.isReactComponent)n );n}nn// This function takes a React element (e.g. <App />)n// and returns a DOM or Native node representing the mounted tree.nfunction mount(element) {n var type = element.type;n var props = element.props;nn // We will determine the rendered elementn // by either running the type as functionn // or creating an instance and calling render().n var renderedElement;n if (isClass(type)) {n // Component classn var publicInstance = new type(props);n // Set the propsn publicInstance.props = props;n // Call the lifecycle if necessaryn if (publicInstance.componentWillMount) {n publicInstance.componentWillMount();n }n // Get the rendered element by calling render()n renderedElement = publicInstance.render();n } else {n // Component functionn renderedElement = type(props);n }nn // This process is recursive because a component mayn // return an element with a type of another component.n return mount(renderedElement);nn // Note: this implementation is incomplete and recurses infinitely!n // It only handles elements like <App /> or <Button />.n // It doesnt handle elements like <div /> or <p /> yet.n}nnvar rootEl = document.getElementById(root);nvar node = mount(<App />);nrootEl.appendChild(node);n

要點:

  • React元素僅僅是表示React組件type和props的對象
  • 用戶自定義組件可以是純函數也可以是含有render方法的組件類。
  • 」mount「過程是一個遞歸過程,其可以生成DOM tree或Native tree。

React元素不僅可以表示用戶自定義組件也可以表示宿主組件,如果React元素的type類型為string則表示這是一個宿主組件。宿主組件上並未關聯任何用戶自定義代碼。當reconciler遇到一個宿主組件,會讓renderer負責渲染該組件,對於ReactDOM來說就是生產一個DOM節點,而對於React Native來說則是生成一個Native元素。如果宿主元素含有孩子節點,那麼孩子節點仍然按照上面的演算法進行渲染,孩子元素是宿主組件還是用戶自定義組件並不影響。孩子節點渲染完後,將其append到父節點即可。由此可以看出用戶自定義組件和宿主組件的渲染是相互遞歸的。

注意到reconciler並未與DOM綁定,實際的mount結果實際上取決於renderer,其可以是一個DOM節點(React DOM)也可以是一個字元串(React Server)還可以是一個native界面(React Native)。

如果將考慮宿主組件的渲染,則上面的演算法會轉變如下:

function isClass(type) {n // React.Component subclasses have this flagn return (n Boolean(type.prototype) &&n Boolean(type.prototype.isReactComponent)n );n}nn// This function only handles elements with a composite type.n// For example, it handles <App /> and <Button />, but not a <div />.nfunction mountComposite(element) {n var type = element.type;n var props = element.props;nn var renderedElement;n if (isClass(type)) {n // Component classn var publicInstance = new type(props);n // Set the propsn publicInstance.props = props;n // Call the lifecycle if necessaryn if (publicInstance.componentWillMount) {n publicInstance.componentWillMount();n }n renderedElement = publicInstance.render();n } else if (typeof type === function) {n // Component functionn renderedElement = type(props);n }nn // This is recursive but well eventually reach the bottom of recursion whenn // the element is host (e.g. <div />) rather than composite (e.g. <App />):n return mount(renderedElement);n}nn// This function only handles elements with a host type.n// For example, it handles <div /> and <p /> but not an <App />.nfunction mountHost(element) {n var type = element.type;n var props = element.props;n var children = props.children || [];n if (!Array.isArray(children)) {n children = [children];n }n children = children.filter(Boolean);nn // This block of code shouldnt be in the reconciler.n // Different renderers might initialize nodes differently.n // For example, React Native would create iOS or Android views.n var node = document.createElement(type);n Object.keys(props).forEach(propName => {n if (propName !== children) {n node.setAttribute(propName, props[propName]);n }n });nn // Mount the childrenn children.forEach(childElement => {n // Children may be host (e.g. <div />) or composite (e.g. <Button />).n // We will also mount them recursively:n var childNode = mount(childElement);nn // This line of code is also renderer-specific.n // It would be different depending on the renderer:n node.appendChild(childNode);n });nn // Return the DOM node as mount result.n // This is where the recursion ends.n return node;n}nnfunction mount(element) {n var type = element.type;n if (typeof type === function) {n // User-defined componentsn return mountComposite(element);n } else if (typeof type === string) {n // Platform-specific componentsn return mountHost(element);n }n}nnvar rootEl = document.getElementById(root);nvar node = mount(<App />);nrootEl.appendChild(node);n

React的一個重要特性是重渲染DOM時並不會重新創建DOM。但是目前的實現僅知道如何mount初始化的DOM樹,我們並不能對其進行更新,因為其沒有存儲所有必須的信息。為了區別自定義組件和宿主組件的的mount操作,分別定義而兩個類DOMComponent和CompositeComponent。

兩個類都有mount方法其返回載入的節點,因此可以將全局的mount方法替換為兩個類的方法。

function instantiateComponent(element) {n var type = element.type;n if (typeof type === function) {n // User-defined componentsn return new CompositeComponent(element);n } else if (typeof type === string) {n // Platform-specific componentsn return new DOMComponent(element);n } n}n

CompositeComponent的實現如下

class CompositeComponent {n constructor(element) {n this.currentElement = element;n this.renderedComponent = null;n this.publicInstance = null;n }nn getPublicInstance() {n // For composite components, expose the class instance.n return this.publicInstance;n }nn mount() {n var element = this.currentElement;n var type = element.type;n var props = element.props;nn var publicInstance;n var renderedElement;n if (isClass(type)) {n // Component classn publicInstance = new type(props);n // Set the propsn publicInstance.props = props;n // Call the lifecycle if necessaryn if (publicInstance.componentWillMount) {n publicInstance.componentWillMount();n }n renderedElement = publicInstance.render();n } else if (typeof type === function) {n // Component functionn publicInstance = null;n renderedElement = type(props);n }nn // Save the public instancen this.publicInstance = publicInstance;nn // Instantiate the child internal instance according to the element.n // It would be a DOMComponent for <div /> or <p />,n // and a CompositeComponent for <App /> or <Button />:n var renderedComponent = instantiateComponent(renderedElement);n this.renderedComponent = renderedComponent;nn // Mount the rendered outputn return renderedComponent.mount();n }n}n

這樣就可以存儲額外的信息了,如this.currentElement, this.renderedComponent, this.publicInstances,更新時可以使用這些信息了。

注意到CompositeComponent和用戶提供的element.type的實例不同,CompositeComponent是reconciler的實現細節,因此並未向用戶公開。用於自定義的class對應的是element.type,其作為CompositeComponent的參數,CompositeComponent負責實例化它。

為了與用戶定義組件類的實例區分,我們把CompositeComponent的實例稱為internal instances。其存在目的在於維護一些長久存在的數據,只有renderer和reconciler知曉其存在。與之相對,我們把用戶自定義類的實例稱為」public instance「,public instance就是render方法里的this綁定的對象。

我們也要將mountHost重構為DOMComponent的mount:

class DOMComponent {n constructor(element) {n this.currentElement = element;n this.renderedChildren = [];n this.node = null;n }nn getPublicInstance() {n // For DOM components, only expose the DOM node.n return this.node;n }nn mount() {n var element = this.currentElement;n var type = element.type;n var props = element.props;n var children = props.children || [];n if (!Array.isArray(children)) {n children = [children];n }nn // Create and save the noden var node = document.createElement(type);n this.node = node;nn // Set the attributesn Object.keys(props).forEach(propName => {n if (propName !== children) {n node.setAttribute(propName, props[propName]);n }n });nn // Create and save the contained children.n // Each of them can be a DOMComponent or a CompositeComponent,n // depending on whether the element type is a string or a function.n var renderedChildren = children.map(instantiateComponent);n this.renderedChildren = renderedChildren;nn // Collect DOM nodes they return on mountn var childNodes = renderedChildren.map(child => child.mount());n childNodes.forEach(childNode => node.appendChild(childNode));nn // Return the DOM node as mount resultn return node;n }n}n

對mountHost進行重構後,我們在internal instance上關聯了this.node和this.renderedChildren信息,這些信息可以用於後續的更新操作。

composite internal instance需要存儲如下信息:

  • 當前元素
  • 如果type是class,需要存儲public instance
  • 渲染的internal instance,

host internal insance需要存儲如下信息:

  • 當前元素
  • DOM節點
  • 所有的孩子 internal instance。

渲染完組件後就可以將其掛載到指定容器上,mountTree完成該操作,其和ReactDOM.render()操作類似。

function mountTree(element, containerNode) {n // Destroy any existing treen if (containerNode.firstChild) {n unmountTree(containerNode);n }nn // Create the top-level internal instancen var rootComponent = instantiateComponent(element);nn // Mount the top-level component into the containern var node = rootComponent.mount();n containerNode.appendChild(node);nn // Save a reference to the internal instancen node._internalInstance = rootComponent;nn // Return the public instance it providesn var publicInstance = rootComponent.getPublicInstance();n return publicInstance;n}nnvar rootEl = document.getElementById(root);nmountTree(<App />, rootEl);n

現在可以執行unmount操作

CompositeComponent實現如下:

class CompositeComponent {nn // ...nn unmount() {n // Call the lifecycle hook if necessaryn var publicInstance = this.publicInstance;n if (publicInstance) {n if (publicInstance.componentWillUnmount) {n publicInstance.componentWillUnmount();n }n }nn // Unmount the single rendered componentn var renderedComponent = this.renderedComponent;n renderedComponent.unmount();n }n}n

DOMComponent實現如下:

class DOMComponent {nn // ...nn unmount() {n // Unmount all the childrenn var renderedChildren = this.renderedChildren;n renderedChildren.forEach(child => child.unmount());n }n}n

在實踐中,unmount DOM組件也會刪除相應的事件監聽器和清除緩存。

這樣可以實現top-level的unmounTree操作了,其與ReactDOM.unmountComponentAtNode實現相似。

function unmountTree(containerNode) {n // Read the internal instance from a DOM node:n // (This doesnt work yet, we will need to change mountTree() to store it.)n var node = containerNode.firstChild;n var rootComponent = node._internalInstance;nn // Unmount the tree and clear the containern rootComponent.unmount();n containerNode.innerHTML = ;n}n

實現完unmount操作後接著就可以實現update操作了。reconciler要儘可能的重用已有的實例,這樣儘可能的保留DOM和狀態。

var rootEl = document.getElementById(root);nnmountTree(<App />, rootEl);n// Should reuse the existing DOM:nmountTree(<App />, rootEl);n

為了實現更新操作,DOMComponent和CompositeComponent需要實現一個receive(nextElement)方法。

class CompositeComponent {n // ...nn receive(nextElement) {n // ...n }n}nnclass DOMComponent {n // ...nn receive(nextElement) {n // ...n }n}n

該方法主要利用nextElement提供的信息更新當前的currentElement。其內部實現依賴於」Virtual DOM diff「演算法,其具體的操作簡而言之就是遍歷更新子樹。

當一個composite component接受到一個新的element,其會運行componentWillUpdate操作,然後使用新的props屬性重新渲染元素:

class CompositeComponent {nn // ...nn receive(nextElement) {n var prevProps = this.currentElement.props;n var publicInstance = this.publicInstance;n var prevRenderedComponent = this.renderedComponent;n var prevRenderedElement = prevRenderedComponent.currentElement;nn // Update *own* elementn this.currentElement = nextElement;n var type = nextElement.type;n var nextProps = nextElement.props;nn // Figure out what the next render() output isn var nextRenderedElement;n if (isClass(type)) {n // Component classn // Call the lifecycle if necessaryn if (publicInstance.componentWillUpdate) {n publicInstance.componentWillUpdate(nextProps);n }n // Update the propsn publicInstance.props = nextProps;n // Re-rendern nextRenderedElement = publicInstance.render();n } else if (typeof type === function) {n // Component functionn nextRenderedElement = type(nextProps);n }nn // ...n

如果上次的元素type和本次的相同,那麼可以實現就地更新。例如上次是<Button color="red"/>而本次是<Button color="blue"/>,我么可以調用internal instance的receive操作實現更新。

// ...nn // If the rendered element type has not changed,n // reuse the existing component instance and exit.n if (prevRenderedElement.type === nextRenderedElement.type) {n prevRenderedComponent.receive(nextRenderedElement);n return;n }nn // ...n

如果和上次的元素type不同,那麼僅通過更新internal instance就不行了,我們不能通過更新屬性將<input/>轉換為<button/>,此時我們需要卸載上一元素,載入新的元素。

// ...nn // If we reached this point, we need to unmount the previouslyn // mounted component, mount the new one, and swap their nodes.nn // Find the old node because it will need to be replacedn var prevNode = prevRenderedComponent.getHostNode();nn // Unmount the old child and mount a new childn prevRenderedComponent.unmount();n var nextRenderedComponent = instantiateComponent(nextRenderedElement);n var nextNode = nextRenderedComponent.mount();nn // Replace the reference to the childn this.renderedComponent = nextRenderedComponent;nn // Replace the old node with the new onen // Note: this is renderer-specific code andn // ideally should live outside of CompositeComponent:n prevNode.parentNode.replaceChild(nextNode, prevNode);n }n

簡而言之,當composite component接收一個新的元素,其可能更新內部的internal instance,也可能就地卸載上一個並載入新的元素。

還有一種情況也可能導致在卸載舊的更新新的,這就是如果元素的key值發生變化。我們需要添加一個getHostNode方法來定位平台相關的node,以便於更新操作時進行替換。其實現如下:

class CompositeComponent {n // ...nn getHostNode() {n // Ask the rendered component to provide it.n // This will recursively drill down any composites.n return this.renderedComponent.getHostNode();n }n}nnclass DOMComponent {n // ...nn getHostNode() {n return this.node;n } n}n

Host Component的更新操作與Composite不同,當其接收一個新的元素,其通過更新底層的平台相關的視圖實現更新,對於DOM來說就是根系DOM的屬性。

class DOMComponent {n // ...nn receive(nextElement) {n var node = this.node;n var prevElement = this.currentElement;n var prevProps = prevElement.props;n var nextProps = nextElement.props; n this.currentElement = nextElement;nn // Remove old attributes.n Object.keys(prevProps).forEach(propName => {n if (propName !== children && !nextProps.hasOwnProperty(propName)) {n node.removeAttribute(propName);n }n });n // Set next attributes.n Object.keys(nextProps).forEach(propName => {n if (propName !== children) {n node.setAttribute(propName, nextProps[propName]);n }n });nn // ...n

host component需要更新其孩子節點,同composite components不同,其可能含有超過一個孩子節點。通過比較孩子元素當前type和以前的type決定是gengx或者替換。reconciler的實現實際上還會使用key屬性來追蹤孩子節點,以重利用節點。其實現簡化如下

// ...nn // These are arrays of React elements:n var prevChildren = prevProps.children || [];n if (!Array.isArray(prevChildren)) {n prevChildren = [prevChildren];n }n var nextChildren = nextProps.children || [];n if (!Array.isArray(nextChildren)) {n nextChildren = [nextChildren];n }n // These are arrays of internal instances:n var prevRenderedChildren = this.renderedChildren;n var nextRenderedChildren = [];nn // As we iterate over children, we will add operations to the array.n var operationQueue = [];nn // Note: the section below is extremely simplified!n // It doesnt handle reorders, children with holes, or keys.n // It only exists to illustrate the overall flow, not the specifics.nn for (var i = 0; i < nextChildren.length; i++) {n // Try to get an existing internal instance for this childn var prevChild = prevRenderedChildren[i];nn // If there is no internal instance under this index,n // a child has been appended to the end. Create a newn // internal instance, mount it, and use its node.n if (!prevChild) {n var nextChild = instantiateComponent(nextChildren[i]);n var node = nextChild.mount();nn // Record that we need to append a noden operationQueue.push({type: ADD, node});n nextRenderedChildren.push(nextChild);n continue;n }nn // We can only update the instance if its elements type matches.n // For example, <Button size="small" /> can be updated ton // <Button size="large" /> but not to an <App />.n var canUpdate = prevChildren[i].type === nextChildren[i].type;nn // If we cant update an existing instance, we have to unmount itn // and mount a new one instead of it.n if (!canUpdate) {n var prevNode = prevChild.node;n prevChild.unmount();nn var nextChild = instantiateComponent(nextChildren[i]);n var nextNode = nextChild.mount();nn // Record that we need to swap the nodesn operationQueue.push({type: REPLACE, prevNode, nextNode});n nextRenderedChildren.push(nextChild);n continue;n }nn // If we can update an existing internal instance,n // just let it receive the next element and handle its own update.n prevChild.receive(nextChildren[i]);n nextRenderedChildren.push(prevChild);n }nn // Finally, unmount any children that dont exist:n for (var j = nextChildren.length; j < prevChildren.length; j++) {n var prevChild = prevRenderedChildren[j];n var node = prevChild.node;n prevChild.unmount();nn // Record that we need to remove the noden operationQueue.push({type: REMOVE, node});n }nn // Point the list of rendered children to the updated version.n this.renderedChildren = nextRenderedChildren;nn // ...nn // Process the operation queue.n while (operationQueue.length > 0) {n var operation = operationQueue.shift();n switch (operation.type) {n case ADD:n this.node.appendChild(operation.node);n break;n case REPLACE:n this.node.replaceChild(operation.nextNode, operation.prevNode);n break;n case REMOVE:n this.node.removeChild(operation.node);n break;n }n }n

CompositeComponent和DOMComponent實現好receive(nextElement)方法後,mountTree的實現可以更改。

function mountTree(element, containerNode) {n // Check for an existing treen if (containerNode.firstChild) {n var prevNode = containerNode.firstChild;n var prevRootComponent = prevNode._internalInstance;n var prevElement = prevRootComponent.currentElement;nn // If we can, reuse the existing root componentn if (prevElement.type === element.type) {n prevRootComponent.receive(element);n return;n }nn // Otherwise, unmount the existing treen unmountTree(containerNode);n }nn // ...nn}n

這樣多次使用同一type元素調用mountTree就可以重用以前的元素了。


推薦閱讀:

Redux非同步方案選型
React 是如何重新定義前端開發的
阿里雲前端周刊 - 第 21 期
感謝《深入淺出React和Redux》的所有讀者

TAG:React |