React 原始碼學習(八):元件更新

Zongzi發表於2019-04-08

閱讀原始碼成了今年的學習目標之一,在選擇 Vue 和 React 之間,我想先閱讀 React 。 在考慮到讀哪個版本的時候,我想先接觸到原始碼早期的思想可能會更輕鬆一些,最終我選擇閱讀 0.3-stable 。 那麼接下來,我將從幾個方面來解讀這個版本的原始碼。

  1. React 原始碼學習(一):HTML 元素渲染
  2. React 原始碼學習(二):HTML 子元素渲染
  3. React 原始碼學習(三):CSS 樣式及 DOM 屬性
  4. React 原始碼學習(四):事務機制
  5. React 原始碼學習(五):事件機制
  6. React 原始碼學習(六):元件渲染
  7. React 原始碼學習(七):生命週期
  8. React 原始碼學習(八):元件更新

是什麼引起元件更新

引發元件更新的方法就是 this.setState ,按照註釋程式碼看來 this.setState 是不可變的,則 this._pendingState 是用來存放掛起的 state ,他不會直接更新到 this.state ,讓我們來看到程式碼:

// core/ReactCompositeComponent.js
var ReactCompositeComponentMixin = {
  setState: function(partialState) {
    // 如果“掛起狀態”存在,則與之合併,否則與現有狀態合併。
    this.replaceState(merge(this._pendingState || this.state, partialState));
  },
  replaceState: function(completeState) {
    var compositeLifeCycleState = this._compositeLifeCycleState;
    // 生命週期校驗
    invariant(
      this._lifeCycleState === ReactComponent.LifeCycle.MOUNTED ||
      compositeLifeCycleState === CompositeLifeCycle.MOUNTING,
      'replaceState(...): Can only update a mounted (or mounting) component.'
    );
    invariant(
      compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE &&
      compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING,
      'replaceState(...): Cannot update while unmounting component or during ' +
      'an existing state transition (such as within `render`).'
    );

    // 將合併完的狀態給掛起狀態,若不滿足下面更新條件,則只存入掛起狀態結束此函式
    this._pendingState = completeState;

    // 如果我們處於安裝或接收道具的中間,請不要觸發狀態轉換,因為這兩者都已經這樣做了。
    // 若複合元件生命週期不在掛載中和更新 props 時,我們會操作更新方法
    if (compositeLifeCycleState !== CompositeLifeCycle.MOUNTING &&
        compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_PROPS) {
      // 變更復合元件生命週期為更新 state
      this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;

      // 準備更新 state ,並釋放掛起狀態
      var nextState = this._pendingState;
      this._pendingState = null;

      // 進入 React 排程事務,加入 _receivePropsAndState 方法
      var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
      transaction.perform(
        this._receivePropsAndState,
        this,
        this.props,
        nextState,
        transaction
      );
      ReactComponent.ReactReconcileTransaction.release(transaction);

      // 排程事務完成後置空複合元件生命週期
      this._compositeLifeCycleState = null;
    }
  },
};
複製程式碼

setState 觸發了什麼

那麼到此為止,你可以知道 this.setState 並非事實更新 this.state 的,比如我們看到在 componentWillMount 中去使用 this.setState 並不會馬上更新到 this.state ,那麼我們繼續閱讀後面程式碼:

// core/ReactCompositeComponent.js
var ReactCompositeComponentMixin = {
  _receivePropsAndState: function(nextProps, nextState, transaction) {
    // shouldComponentUpdate 方法不存在或返回 true
    if (!this.shouldComponentUpdate ||
        this.shouldComponentUpdate(nextProps, nextState)) {
      // Will set `this.props` and `this.state`.
      this._performComponentUpdate(nextProps, nextState, transaction);
    } else {
      // 如果確定某個元件不應該更新,我們仍然需要設定props和state。
      // shouldComponentUpdate 返回 false 的情況
      this.props = nextProps;
      this.state = nextState;
    }
  },
  _performComponentUpdate: function(nextProps, nextState, transaction) {
    // 存入舊的 props 和 state
    // 用於傳入 componentDidUpdate
    var prevProps = this.props;
    var prevState = this.state;

    if (this.componentWillUpdate) {
      this.componentWillUpdate(nextProps, nextState, transaction);
    }

    // 更新 props 和 state
    this.props = nextProps;
    this.state = nextState;

    // 更新元件
    this.updateComponent(transaction);

    if (this.componentDidUpdate) {
      transaction.getReactOnDOMReady().enqueue(
        this,
        this.componentDidUpdate.bind(this, prevProps, prevState)
      );
    }
  },
  updateComponent: function(transaction) {
    // 這裡的更新比較硬核
    // 先把已渲染的舊的元件賦值至 currentComponent
    var currentComponent = this._renderedComponent;
    // 直接渲染新的元件(是不是很硬核)
    var nextComponent = this._renderValidatedComponent();
    // 如果是同樣的元件則進入此判斷
    // 通過 constructor 來判斷是否為同一個
    // 比如:
    // React.DOM.a().constructor !== React.DOM.p().constructor
    // React.DOM.a().constructor === React.DOM.a().constructor
    // 或
    // React.createClass({ render: () => null }).constructor ===
    // React.createClass({ render: () => null }).constructor
    if (currentComponent.constructor === nextComponent.constructor) {
      // 若新的元件 props 下 isStatic 為真則不更新
      // 知道這一個可以對部分元件進行手動優化,以免不必要的計算
      if (!nextComponent.props.isStatic) {
        // 這裡會呼叫對應的方法
        // ReactCompositeComponent.receiveProps
        // ReactNativeComponent.receiveProps
        // ReactTextComponent.receiveProps
        // 除了 ReactTextComponent 都會呼叫 ReactComponent.Mixin.receiveProps 來更新 ref 相關
        // 這個我們稍後來解讀
        currentComponent.receiveProps(nextComponent.props, transaction);
      }
    } else {
      // These two IDs are actually the same! But nothing should rely on that.
      // 舊的 _rootNodeID 和新的 _rootNodeID
      var thisID = this._rootNodeID;
      var currentComponentID = currentComponent._rootNodeID;
      // 解除安裝舊元件
      currentComponent.unmountComponent();
      // 掛載新元件(也挺硬核的)
      var nextMarkup = nextComponent.mountComponent(thisID, transaction);
      // 在新 _rootNodeID 下更新 markup 標記
      ReactComponent.DOMIDOperations.dangerouslyReplaceNodeWithMarkupByID(
        currentComponentID,
        nextMarkup
      );
      // 賦值新的元件
      this._renderedComponent = nextComponent;
    }
  },
};
複製程式碼

各個元件的 receiveProps 方法

上面程式碼看來,一個是不替換元件的情況下更新元件,另一個則是直接更新 markup 標記。我們按照順序一個個看過來吧,先看到 ReactCompositeComponent.receiveProps

// core/ReactCompositeComponent.js
var ReactCompositeComponentMixin = {
  receiveProps: function(nextProps, transaction) {
    // 校驗引數
    if (this.constructor.propDeclarations) {
      this._assertValidProps(nextProps);
    }
    // 更新 ref
    ReactComponent.Mixin.receiveProps.call(this, nextProps, transaction);

    // 更新複合元件生命週期為更新 props
    this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;
    // 執行鉤子函式,在這個函式內執行 this.setState 是不會立即更新 this.state 的
    if (this.componentWillReceiveProps) {
      this.componentWillReceiveProps(nextProps, transaction);
    }
    // 進入複合元件生命週期更新 state
    this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;
    // When receiving props, calls to `setState` by `componentWillReceiveProps`
    // will set `this._pendingState` without triggering a re-render.
    // 如果上面執行過 componentWillReceiveProps ,並且裡面操作了 this.setState
    // 那麼 this._pendingState 會有值,並且是與 this.state 合併過的
    var nextState = this._pendingState || this.state;
    // 釋放 this._pendingState
    this._pendingState = null;
    // 執行的是 currentComponent._receivePropsAndState 方法
    // 但是這個 currentComponent 一定是 ReactCompositeComponent
    this._receivePropsAndState(nextProps, nextState, transaction);
    // 置空複合元件生命週期
    this._compositeLifeCycleState = null;
  },
};
複製程式碼

再是我們來看看 ReactNativeComponent.receiveProps

// core/ReactNativeComponent.js
ReactNativeComponent.Mixin = {
  receiveProps: function(nextProps, transaction) {
    // 日常校驗
    invariant(
      this._rootNodeID,
      'Trying to control a native dom element without a backing id'
    );
    assertValidProps(nextProps);
    // 日常更新 ref
    ReactComponent.Mixin.receiveProps.call(this, nextProps, transaction);
    // 重點來了,更新 DOM 屬性
    this._updateDOMProperties(nextProps);
    // 更新 DOM 子節點
    this._updateDOMChildren(nextProps, transaction);
    // 都執行完後更新 props
    this.props = nextProps;
  },
  _updateDOMProperties: function(nextProps) {
    // 這裡開始解讀更新 DOM 屬性
    // 儲存舊 props
    var lastProps = this.props;
    // 遍歷新 props
    for (var propKey in nextProps) {
      var nextProp = nextProps[propKey];
      var lastProp = lastProps[propKey];
      // 以新 props 鍵為準取對應的值
      // 若 2 個值相等則跳過
      if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) {
        continue;
      }
      // CSS 樣式
      if (propKey === STYLE) {
        if (nextProp) {
          nextProp = nextProps.style = merge(nextProp);
        }
        var styleUpdates;
        // 遍歷 nextProp
        for (var styleName in nextProp) {
          if (!nextProp.hasOwnProperty(styleName)) {
            continue;
          }
          // 舊的 styleName 與新的 styleName 值不同時
          // 將新的值加入 styleUpdates
          if (!lastProp || lastProp[styleName] !== nextProp[styleName]) {
            if (!styleUpdates) {
              styleUpdates = {};
            }
            styleUpdates[styleName] = nextProp[styleName];
          }
        }
        // 操作更新 CSS 樣式
        if (styleUpdates) {
          // ReactComponent.DOMIDOperations => ReactDOMIDOperations
          // 他會通過 ID 對真實 node 進行相應的更新
          ReactComponent.DOMIDOperations.updateStylesByID(
            this._rootNodeID,
            styleUpdates
          );
        }
        // 判斷若是 dangerouslySetInnerHTML 則在不同的情況下進行相應的更新
      } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
        var lastHtml = lastProp && lastProp.__html;
        var nextHtml = nextProp && nextProp.__html;
        if (lastHtml !== nextHtml) {
          ReactComponent.DOMIDOperations.updateInnerHTMLByID(
            this._rootNodeID,
            nextProp
          );
        }
        // 判斷 content 的情況更新
      } else if (propKey === CONTENT) {
        ReactComponent.DOMIDOperations.updateTextContentByID(
          this._rootNodeID,
          '' + nextProp
        );
        // 對事件進行監聽
        // 比較好奇的是舊的 propKey 若存在著事件監聽,這裡似乎沒有做什麼處理
        // 這樣不就記憶體溢位了嗎?難道說不會有這種情況???
        // 想多了啦,更新 props 的情況,同樣的事件會被覆蓋
        // 在對應 this._rootNodeID 的情況下。(希望如此,沒有證實過,但是理解如此)
      } else if (registrationNames[propKey]) {
        putListener(this._rootNodeID, propKey, nextProp);
      } else {
        // 剩餘的就是更新 DOM 屬性啦
        ReactComponent.DOMIDOperations.updatePropertyByID(
          this._rootNodeID,
          propKey,
          nextProp
        );
      }
    }
  },
  _updateDOMChildren: function(nextProps, transaction) {
    // 來更新 DOM 子節點了
    // 當前 this.props.content 型別
    var thisPropsContentType = typeof this.props.content;
    // 是否 thisPropsContentEmpty 為空
    var thisPropsContentEmpty =
      this.props.content == null || thisPropsContentType === 'boolean';
    // 新的 nextProps.content 型別
    var nextPropsContentType = typeof nextProps.content;
    // 是否 nextPropsContentEmpty 為空
    var nextPropsContentEmpty =
      nextProps.content == null || nextPropsContentType === 'boolean';

    // 最後使用的 content :
    // 若 thisPropsContentEmpty 不為空則取 this.props.content 否則
    // this.props.children 型別為 string 或 number 的情況下取 this.props.children 否則
    // null
    var lastUsedContent = !thisPropsContentEmpty ? this.props.content :
      CONTENT_TYPES[typeof this.props.children] ? this.props.children : null;

    // 使用內容 content :
    // 若 nextPropsContentEmpty 不為空則取 nextProps.content 否則
    // nextProps.children 型別為 string 或 number 的情況下取 nextProps.children 否則
    // null
    var contentToUse = !nextPropsContentEmpty ? nextProps.content :
      CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;

    // Note the use of `!=` which checks for null or undefined.

    // 最後使用的 children :
    // 若 lastUsedContent 不為 null or undefined 則取 null 否則
    // 取 this.props.children ,以 content 優先
    var lastUsedChildren =
      lastUsedContent != null ? null : this.props.children;
    // 使用 children :
    // 若 contentToUse 不為 null or undefined 則取 null 否則
    // 取 nextProps.children ,以 content 優先
    var childrenToUse = contentToUse != null ? null : nextProps.children;

    // 需要使用 content 情況
    if (contentToUse != null) {
      // 是否需要移除 children 判斷結果:
      // 最後使用的 children 存在並且 children 不再需要使用
      var childrenRemoved = lastUsedChildren != null && childrenToUse == null;
      if (childrenRemoved) {
        // 更新子節點
        this.updateMultiChild(null, transaction);
      }
      // 若沒滿足上面條件則說明不需要更新掉 children
      // 並且新舊 content 不相等的情況下進行 DOM 操作
      if (lastUsedContent !== contentToUse) {
        ReactComponent.DOMIDOperations.updateTextContentByID(
          this._rootNodeID,
          '' + contentToUse
        );
      }
    } else {
      // 反之看是否需要移除 content
      // 若最後使用的 content 存在且 content 不再需要使用
      var contentRemoved = lastUsedContent != null && contentToUse == null;
      if (contentRemoved) {
        // 進行 DOM 操作
        ReactComponent.DOMIDOperations.updateTextContentByID(
          this._rootNodeID,
          ''
        );
      }
      // 更新子節點
      // 壓扁更新,與掛載時一樣
      this.updateMultiChild(flattenChildren(nextProps.children), transaction);
    }
  },
};
複製程式碼

Diff

關於 DOM 操作一系列的方法這裡不準備做解讀,可以直接檢視原始碼 core/ReactDOMIDOperations.js ,道理都是一樣的。但是,這裡需要看下 updateMultiChild 方法,因為這裡已經涉及到 Diff 實現,但是在講 Diff 之前,我們先把 ReactTextComponent.receiveProps 給解讀掉,其實方法裡面很簡單,就是操作了 ReactDOMIDOperations 相關的方法,具體實現直接看原始碼就行,那麼接下來,我們來看到 updateMultiChild

// core/ReactMultiChild.js
// 直接看到 updateMultiChild
var ReactMultiChildMixin = {
  enqueueMarkupAt: function(markup, insertAt) {
    this.domOperations = this.domOperations || [];
    this.domOperations.push({insertMarkup: markup, finalIndex: insertAt});
  },
  enqueueMove: function(originalIndex, finalIndex) {
    this.domOperations = this.domOperations || [];
    this.domOperations.push({moveFrom: originalIndex, finalIndex: finalIndex});
  },
  enqueueUnmountChildByName: function(name, removeChild) {
    if (ReactComponent.isValidComponent(removeChild)) {
      this.domOperations = this.domOperations || [];
      this.domOperations.push({removeAt: removeChild._domIndex});
      removeChild.unmountComponent && removeChild.unmountComponent();
      delete this._renderedChildren[name];
    }
  },

  /**
   * Reconciles new children with old children in three phases.
   *
   * - Adds new content while updating existing children that should remain.
   * - Remove children that are no longer present in the next children.
   * - As a very last step, moves existing dom structures around.
   * - (Comment 1) `curChildrenDOMIndex` is the largest index of the current
   *   rendered children that appears in the next children and did not need to
   *   be "moved".
   * - (Comment 2) This is the key insight. If any non-removed child's previous
   *   index is less than `curChildrenDOMIndex` it must be moved.
   *
   * @param {?Object} children Flattened children object.
   */
  updateMultiChild: function(nextChildren, transaction) {
    // 一些補全判斷操作
    if (!nextChildren && !this._renderedChildren) {
      return;
    } else if (nextChildren && !this._renderedChildren) {
      this._renderedChildren = {}; // lazily allocate backing store with nothing
    } else if (!nextChildren && this._renderedChildren) {
      nextChildren = {};
    }
    // 用於更新子節點時,記錄的父節點 ID 字首加 dot
    var rootDomIdDot = this._rootNodeID + '.';
    // DOM markup 標記緩衝
    var markupBuffer = null;  // Accumulate adjacent new children markup.
    // DOM markup 標記緩衝等待插入的數量
    var numPendingInsert = 0; // How many root nodes are waiting in markupBuffer
    // 新子節點的迴圈用索引 index
    var loopDomIndex = 0;     // Index of loop through new children.
    var curChildrenDOMIndex = 0;  // See (Comment 1)
    // 遍歷新的 children
    for (var name in nextChildren) {
      if (!nextChildren.hasOwnProperty(name)) {continue;}
      var curChild = this._renderedChildren[name];
      var nextChild = nextChildren[name];
      // 通過 constructor 來判斷 curChild 和 nextChild 是否為同一個
      if (shouldManageExisting(curChild, nextChild)) {
        if (markupBuffer) {
          // 若 DOM markup 標記緩衝存在,將其加入佇列
          // 標記位置為 loopDomIndex - numPendingInsert
          // 這裡和下面是一樣的道理,請看到迴圈結束後
          this.enqueueMarkupAt(markupBuffer, loopDomIndex - numPendingInsert);
          // 清空 DOM markup 標記緩衝
          markupBuffer = null;
        }
        // 初始化 DOM markup 標記緩衝等待插入的數量為 0
        numPendingInsert = 0;
        // _domIndex 在掛載中依次按照順序進行排序,若他小於目前的子節點順序
        // 則進行移動操作,移動操作則是記錄原 index 和現 index (也就是新子節點的迴圈用索引 index )
        if (curChild._domIndex < curChildrenDOMIndex) { // (Comment 2)
          // 我沒有辦法聯想到此情況
          this.enqueueMove(curChild._domIndex, loopDomIndex);
        }
        // curChildrenDOMIndex 則取大值
        curChildrenDOMIndex = Math.max(curChild._domIndex, curChildrenDOMIndex);
        // 硬核式遞迴更新!!同樣會進入到 Diff
        !nextChild.props.isStatic &&
          curChild.receiveProps(nextChild.props, transaction);
        // 更新 _domIndex 屬性
        curChild._domIndex = loopDomIndex;
      } else {
        // 若 curChild 和 nextChild 不為同一個的時候
        if (curChild) {               // !shouldUpdate && curChild => delete
          // 解除安裝舊子節點加入佇列,並操作解除安裝元件
          this.enqueueUnmountChildByName(name, curChild);
          // curChildrenDOMIndex 則取大值
          curChildrenDOMIndex =
            Math.max(curChild._domIndex, curChildrenDOMIndex);
        }
        if (nextChild) {              // !shouldUpdate && nextChild => insert
          // 對應位置傳入新子節點
          this._renderedChildren[name] = nextChild;
          // 生成新的 markup 標記
          // ID 為父 ID 加 dot 加現在的 name
          var nextMarkup =
            nextChild.mountComponent(rootDomIdDot + name, transaction);
          // 累加 DOM markup 標記緩衝
          markupBuffer = markupBuffer ? markupBuffer + nextMarkup : nextMarkup;
          // DOM markup 標記緩衝等待插入的數量
          numPendingInsert++;
          // 新的子節點 _domIndex 更新
          nextChild._domIndex = loopDomIndex;
        }
      }
      // 若新子節點存在,則新子節點的迴圈用索引 index 累加 1
      loopDomIndex = nextChild ? loopDomIndex + 1 : loopDomIndex;
    }
    if (markupBuffer) {
      // 將 DOM markup 標記緩衝加入佇列
      // 這裡的 loopDomIndex - numPendingInsert 可以解釋下
      // 會使得 markupBuffer 存在的情況就是進入第二個分支,那麼同樣的,
      // 會使得 numPendingInsert 增加的情況也是第二個分支,那麼在這裡插入的 DOM markup 標記
      // 是最後插入的,他需要從整個迴圈 DOM 索引減去等待數量來確定插入位置
      // 舉個例子,你在進入第二個分支時,舊節點存在的情況下一定會被移除
      // 新節點存在的情況下一定會被生成 DOM markup 標記 並且累加相應的數量
      // loopDomIndex 也會隨之增加,loopDomIndex 也一定大於等於 numPendingInsert
      // 如:舊節點 <div></div><p></p>
      // 新節點 <div></div><span></span><p></p>
      // 這種情況下 loopDomIndex 為 3 , numPendingInsert 為 2 ,插入位置為 1
      this.enqueueMarkupAt(markupBuffer, loopDomIndex - numPendingInsert);
    }
    // 遍歷舊 children
    for (var childName in this._renderedChildren) { // from other direction
      if (!this._renderedChildren.hasOwnProperty(childName)) { continue; }
      var child = this._renderedChildren[childName];
      if (child && !nextChildren[childName]) {
        // 舊的存在,新的不存在加入佇列
        this.enqueueUnmountChildByName(childName, child);
      }
    }
    // 執行 DOM 操作佇列
    this.processChildDOMOperationsQueue();
  },
  processChildDOMOperationsQueue: function() {
    if (this.domOperations) {
      // 執行佇列
      ReactComponent.DOMIDOperations
        .manageChildrenByParentID(this._rootNodeID, this.domOperations);
      this.domOperations = null;
    }
  },
};
複製程式碼

在上面這個執行佇列,我們需要看到相關的 DOM 操作:

// domUtils/DOMChildrenOperations.js
var MOVE_NODE_AT_ORIG_INDEX = keyOf({moveFrom: null});
var INSERT_MARKUP = keyOf({insertMarkup: null});
var REMOVE_AT = keyOf({removeAt: null});

var manageChildren = function(parent, childOperations) {
  // 用於獲得 DOM 中原生的 Node
  // 符合 MOVE_NODE_AT_ORIG_INDEX 和 REMOVE_AT
  var nodesByOriginalIndex = _getNodesByOriginalIndex(parent, childOperations);
  if (nodesByOriginalIndex) {
    // 移除對應的 Node
    _removeChildrenByOriginalIndex(parent, nodesByOriginalIndex);
  }
  // 對應的插入
  _placeNodesAtDestination(parent, childOperations, nodesByOriginalIndex);
};
複製程式碼

refs 引用

那麼到此, Diff 實現算是解讀完成,最後關於 ref 我們在這裡也直接解讀掉, ref 為引用,看到官方註釋:“ ReactOwners are capable of storing references to owned components. ”,那麼首先我們得知道 [OWNER] 是什麼,他是:“引用元件所有者的屬性鍵。”,那麼他的值就是該元件的所有者(也就是父元件例項),這句話的依據在哪裡呢?

// core/ReactCompositeComponent.js
var ReactCompositeComponentMixin = {
  _renderValidatedComponent: function() {
    // render 方法執行前,我們將 this 也就是當前複合元件傳入 ReactCurrentOwner.current
    // render 方法執行結束後,我們將置空 ReactCurrentOwner.current
    ReactCurrentOwner.current = this;
    var renderedComponent = this.render();
    ReactCurrentOwner.current = null;
    return renderedComponent;
  },
};
複製程式碼

那麼執行 render 方法時,發生了什麼?回憶一下。返回的是 ReactCompositeComponent 或者 ReactNativeComponent 或者 ReactTextComponent ,那麼他們在被例項化的過程中獲得了 ReactCurrentOwner.current

// core/ReactComponent.js
var ReactComponent = {
  Mixin: {
    construct: function(initialProps, children) {
      // Record the component responsible for creating this component.
      // 記錄負責建立此元件的元件。
      // 將其記錄下來。
      this.props[OWNER] = ReactCurrentOwner.current;
    },
  }
};
複製程式碼

那麼講了這麼多,他和 ref 有什麼關係呢,那還確實有關係。在掛載、更新、解除安裝元件時都會發生 ref 的更新,若你對子元件新增了 ref 屬性,那麼他對應的鍵會出現在他擁有者的 this.refs 上,那麼你就可以通過擁有者呼叫引用上的方法。

那麼到此,實現元件更新。

相關文章