深入理解react(源碼分析)

MissZxy 8年前發布 | 14K 次閱讀 React 源碼分析 ReactNative 移動開發

  • 理解ReactElement和ReactClass的概念

    • ReactElement

    • ReactClass

    </li>
  • react渲染過程

  • react更新機制

  • reactdiff算法

  • react的優點與總結

  • </ul>

    理解ReactElement和ReactClass的概念

    首先讓我們理解兩個概念:

    ReactElement

    一個描述DOM節點或component實例的字面級對象。它包含一些信息,包括組件類型 type 和屬性 props 。就像一個描述DOM節點的元素(虛擬節點)。它們可以被創建通過 React.createElement 方法或 jsx 寫法

    分為 DOM Element 和 Component Elements 兩類:

    • DOM Elements

    當節點的type屬性為字符串時,它代表是普通的節點,如 div,span

    {
      type: 'button',
      props: {
        className: 'button button-blue',
        children: {
          type: 'b',
          props: {
            children: 'OK!'
          }
        }
      }
    }
    • Component Elements

    當節點的type屬性為一個函數或一個類時,它代表自定義的節點

    class Button extends React.Component {
      render() {
        const { children, color } = this.props;
        return {
          type: 'button',
          props: {
            className: 'button button-' + color,
            children: {
              type: 'b',
              props: {
                children: children
              }
            }
          }
        };
      }
    }

    // Component Elements { type: Button, props: { color: 'blue', children: 'OK!' } }</code></pre>

    ReactClass

    ReactClass是平時我們寫的Component組件(類或函數),例如上面的 Button 類。ReactClass實例化后調用render方法可返回 DOM Element 。

    react渲染過程

    過程理解:

    // element是 Component Elements
    ReactDOM.render({
      type: Form,
      props: {
        isSubmitted: false,
        buttonText: 'OK!'
      }
    }, document.getElementById('root'));
    1. 調用 React.render 方法,將我們的 element 根虛擬節點渲染到 container 元素中。 element 可以是一個字符串文本元素,也可以是如上介紹的 ReactElement (分為DOM Elements, Component Elements)。

    2. 根據 element 的類型不同,分別實例化 ReactDOMTextComponent , ReactDOMComponent , ReactCompositeComponent 類。這些類用來管理 ReactElement ,負責將不同的 ReactElement 轉化成DOM(mountComponent方法),負責更新DOM(receiveComponent方法,updateComponent方法, 如下會介紹)等。

    3. ReactCompositeComponent 實例調用 mountComponent 方法后內部調用 render 方法,返回了 DOM Elements 。再對如圖的步驟:two:遞歸。

    react更新機制

    每個類型的元素都要處理好自己的更新:

    1. 自定義元素的更新,主要是更新render出的節點,做甩手掌柜交給render出的節點的對應component去管理更新。

    2. text節點的更新很簡單,直接更新文案。

    3. 瀏覽器基本元素的更新,分為兩塊:

      1. 先是更新屬性,對比出前后屬性的不同,局部更新。并且處理特殊屬性,比如事件綁定。

      2. 然后是子節點的更新,子節點更新主要是找出差異對象,找差異對象的時候也會使用上面的shouldUpdateReactComponent來判斷,如果是可以直接更新的就會遞歸調用子節點的更新,這樣也會遞歸查找差異對象。不可直接更新的刪除之前的對象或添加新的對象。之后根據差異對象操作dom元素(位置變動,刪除,添加等)。

      </li> </ol>

      第一步:調用this.setState

      ReactClass.prototype.setState = function(newState) {
          //this._reactInternalInstance是ReactCompositeComponent的實例
          this._reactInternalInstance.receiveComponent(null, newState);
      }

      第二步:調用內部receiveComponent方法

      這里主要分三種情況,文本元素,基本元素,自定義元素。

      自定義元素:

      receiveComponent方法 

      // receiveComponent方法
      ReactCompositeComponent.prototype.receiveComponent = function(nextElement, transaction, nextContext) {
          var prevElement = this._currentElement;
          var prevContext = this._context;

      this._pendingElement = null;
      
      this.updateComponent(
        transaction,
        prevElement,
        nextElement,
        prevContext,
        nextContext
      );
      
      

      }</code></pre>

      updateComponent方法 

      // updateComponent方法
      ReactCompositeComponent.prototype.updateComponent = function(
          transaction,
          prevParentElement,
          nextParentElement,
          prevUnmaskedContext,
          nextUnmaskedContext
      ) {
         // 簡寫.....

      // 不是state更新而是props更新
      if (prevParentElement !== nextParentElement) {
        willReceive = true;
      }
      
      if (willReceive && inst.componentWillReceiveProps) {
          // 調用生命周期componentWillReceiveProps方法
      }
      
      // 是否更新元素
      if (inst.shouldComponentUpdate) {
          // 如果提供shouldComponentUpdate方法
          shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext);
      } else {
          if (this._compositeType === CompositeTypes.PureClass) {
            // 如果是PureClass,淺層對比props和state
            shouldUpdate =
              !shallowEqual(prevProps, nextProps) ||
              !shallowEqual(inst.state, nextState);
          }
      }
      
      if (shouldUpdate) {
        // 更新元素
        this._performComponentUpdate(
          nextParentElement,
          nextProps,
          nextState,
          nextContext,
          transaction,
          nextUnmaskedContext
        );
      } else {
        // 不更新元素,但仍然設置props和state
        this._currentElement = nextParentElement;
        this._context = nextUnmaskedContext;
        inst.props = nextProps;
        inst.state = nextState;
        inst.context = nextContext;
      }
      
      

      // .......

      }</code></pre>

      內部_performComponentUpdate方法

      // 內部_updateRenderedComponentWithNextElement方法
          ReactCompositeComponent.prototype._updateRenderedComponentWithNextElement = function() {

      // 判定兩個element需不需要更新
      if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
        // 如果需要更新,就繼續調用子節點的receiveComponent的方法,傳入新的element更新子節點。
        ReactReconciler.receiveComponent(
          prevComponentInstance,
          nextRenderedElement,
          transaction,
          this._processChildContext(context)
        );
      } else {
        // 卸載之前的子節點,安裝新的子節點
        var oldHostNode = ReactReconciler.getHostNode(prevComponentInstance);
        ReactReconciler.unmountComponent(
          prevComponentInstance,
          safely,
          false /* skipLifecycle */
        );
      
        var nodeType = ReactNodeTypes.getType(nextRenderedElement);
        this._renderedNodeType = nodeType;
        var child = this._instantiateReactComponent(
          nextRenderedElement,
          nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */
        );
        this._renderedComponent = child;
      
        var nextMarkup = ReactReconciler.mountComponent(
          child,
          transaction,
          this._hostParent,
          this._hostContainerInfo,
          this._processChildContext(context),
          debugID
        );
      
      }</code></pre> 
      

      shouldUpdateReactComponent函數 

      function shouldUpdateReactComponent(prevElement, nextElement){
        var prevEmpty = prevElement === null || prevElement === false;
        var nextEmpty = nextElement === null || nextElement === false;
        if (prevEmpty || nextEmpty) {
          return prevEmpty === nextEmpty;
        }
      
        var prevType = typeof prevElement;
        var nextType = typeof nextElement;
      
        if (prevType === 'string' || prevType === 'number') {
          // 如果先前的ReactElement對象類型是字符串或數字,新的ReactElement對象類型也是字符串或數字,則需要更新,新的ReactElement對象類型是對象,則不應該更新,直接替換。
          return (nextType === 'string' || nextType === 'number');
        } else {
            // 如果先前的ReactElement對象類型是對象,新的ReactElement對象類型也是對象,并且標簽類型和key值相同,則需要更新
          return (
            nextType === 'object' &&
            prevElement.type === nextElement.type &&
            prevElement.key === nextElement.key
          );
        }
      }

      文本元素:

      receiveComponent方法

      ReactDOMTextComponent.prototype.receiveComponent(nextText, transaction) {
           //跟以前保存的字符串比較
          if (nextText !== this._currentElement) {
            this._currentElement = nextText;
            var nextStringText = '' + nextText;
            if (nextStringText !== this._stringText) {
              this._stringText = nextStringText;
              var commentNodes = this.getHostNode();
              // 替換文本元素
              DOMChildrenOperations.replaceDelimitedText(
                commentNodes[0],
                commentNodes[1],
                nextStringText
              );
            }
          }
        }

      基本元素:

      receiveComponent方法 

      ReactDOMComponent.prototype.receiveComponent = function(nextElement, transaction, context) {
          var prevElement = this._currentElement;
          this._currentElement = nextElement;
          this.updateComponent(transaction, prevElement, nextElement, context);
      }

      updateComponent方法

      ReactDOMComponent.prototype.updateComponent = function(transaction, prevElement, nextElement, context) {
          // 略.....
          //需要單獨的更新屬性
          this._updateDOMProperties(lastProps, nextProps, transaction, isCustomComponentTag);
          //再更新子節點
          this._updateDOMChildren(
            lastProps,
            nextProps,
            transaction,
            context
          );
      
          // ......
      }

      this._updateDOMChildren 方法內部調用diff算法,請看下一節........

      react Diff算法

      diff算法

      _updateChildren: function(nextNestedChildrenElements, transaction, context) {
          var prevChildren = this._renderedChildren;
          var removedNodes = {};
          var mountImages = [];
      
          // 獲取新的子元素數組
          var nextChildren = this._reconcilerUpdateChildren(
            prevChildren,
            nextNestedChildrenElements,
            mountImages,
            removedNodes,
            transaction,
            context
          );
      
          if (!nextChildren && !prevChildren) {
            return;
          }
      
          var updates = null;
          var name;
          var nextIndex = 0;
          var lastIndex = 0;
          var nextMountIndex = 0;
          var lastPlacedNode = null;
      
          for (name in nextChildren) {
            if (!nextChildren.hasOwnProperty(name)) {
              continue;
            }
            var prevChild = prevChildren && prevChildren[name];
            var nextChild = nextChildren[name];
            if (prevChild === nextChild) {
                // 同一個引用,說明是使用的同一個component,所以我們需要做移動的操作
                // 移動已有的子節點
                // NOTICE:這里根據nextIndex, lastIndex決定是否移動
              updates = enqueue(
                updates,
                this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex)
              );
      
              // 更新lastIndex
              lastIndex = Math.max(prevChild._mountIndex, lastIndex);
              // 更新component的.mountIndex屬性
              prevChild._mountIndex = nextIndex;
      
            } else {
              if (prevChild) {
                // 更新lastIndex
                lastIndex = Math.max(prevChild._mountIndex, lastIndex);
              }
      
              // 添加新的子節點在指定的位置上
              updates = enqueue(
                updates,
                this._mountChildAtIndex(
                  nextChild,
                  mountImages[nextMountIndex],
                  lastPlacedNode,
                  nextIndex,
                  transaction,
                  context
                )
              );
      
      
              nextMountIndex++;
            }
      
            // 更新nextIndex
            nextIndex++;
            lastPlacedNode = ReactReconciler.getHostNode(nextChild);
          }
      
          // 移除掉不存在的舊子節點,和舊子節點和新子節點不同的舊子節點
          for (name in removedNodes) {
            if (removedNodes.hasOwnProperty(name)) {
              updates = enqueue(
                updates,
                this._unmountChild(prevChildren[name], removedNodes[name])
              );
            }
          }
        }

      react的優點與總結

      優點

      • 虛擬節點。在UI方面,不需要立刻更新視圖,而是生成虛擬DOM后統一渲染。

      • 組件機制。各個組件獨立管理,層層嵌套,互不影響,react內部實現的渲染功能。

      • 差異算法。根據基本元素的key值,判斷是否遞歸更新子節點,還是刪除舊節點,添加新節點。

      總結

      想要更好的利用react的虛擬DOM,diff算法的優勢,我們需要正確的優化、組織react頁面。例如將一個頁面render的 ReactElement 節點分解成多個組件。在需要優化的組件手動添加 shouldComponentUpdate 來避免不需要的 re-render 。

       

      來自:https://segmentfault.com/a/1190000007252756

       

 本文由用戶 MissZxy 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!