學react哪家強,阿寬頻你看React的生命週期

彭道寬發表於2018-10-22

前言

其實,我也不懂,關於原始碼,我看了兩次,只看懂了40%,丟臉了,但是還是硬著頭皮,寫篇文章,mark一下吧,原始碼來自 《深入React技術棧》這本書....我只是大自然的搬運工

React 生命週期

學習一個框架,最重要的莫過於對生命週期的理解了。嗯,很懵,但是人傻就是要多看書,多看掘金上的優秀文章,看了兩篇React生命週期的文章之後,大概也能懂得個大概。就記錄一下吧 ~

[注意] 這是 react16.3之前的生命週期

先看圖,再看字

《深入React技術棧》中對生命週期的說明:

學react哪家強,阿寬頻你看React的生命週期

渲染的過程:

學react哪家強,阿寬頻你看React的生命週期

上圖中的getDefaultProps和getInitialState分別對應ES6中的static defaultProps = {}與建構函式construct中的this.state ={}賦值

學react哪家強,阿寬頻你看React的生命週期

生命週期 - 初次渲染

    一個初始化元件 (以ES6 classes為例子)

    // 當使用 ES6 classes 編寫 React 元件時,其實就是呼叫內部方法 createClass 建立元件

    import React, { Component } from 'react'

    class Index extends Component {

      static propTypes = {
        // code...
      }

      static defaultProps = {
        // code...
      }

      constructor(props) {
        super(props)
        this.state = {
          // code...
        }
      }

      componentWillMount () {
        // code...
      }

      componentDidMount () {
        
      }

      render () {
        return (
          // code...
        )
      }
    }
複製程式碼

我們來看看《深入React技術》中如何解讀原始碼

  var React = {
    // ...
    createClass: ReactClass.createClass,
    // ...
  }

  var ReactClass = {
    createClass: function(spec) {
      var Constructor = function(props, context, updater) {
        // 自動繫結
        if (this.__reactAutoBindPairs.length) {
          bindAutoBindMethods(this);
        }
        this.props = props;
        this.context = context;
        this.refs = emptyObject;
        this.updater = updater || ReactNoopUpdateQueue;
        this.state = null;
        // ReactClass 沒有建構函式,通過 getInitialState 和 componentWillMount 來代替
        var initialState = this.getInitialState ? this.getInitialState() : null;
        this.state = initialState;
      }

      // 原型繼承父類
      Constructor.prototype = new ReactClassComponent();
      Constructor.prototype.constructor = Constructor;
      Constructor.prototype.__reactAutoBindPairs = [];

      // 合併 mixin
      injectedMixins.forEach(
        mixSpecIntoComponent.bind(null, Constructor)
      );
      mixSpecIntoComponent(Constructor, spec);

      // 所有 mixin 合併後初始化 defaultProps(在整個生命週期中,getDefaultProps 只執行一次)
      if (Constructor.getDefaultProps) {
        Constructor.defaultProps = Constructor.getDefaultProps();
      }

      // 減少查詢並設定原型的時間
      for (var methodName in ReactClassInterface) {
        if (!Constructor.prototype[methodName]) {
          Constructor.prototype[methodName] = null;
        }
      }

      return Constructor;
    }
  }



  // React Constructor 說明

  React規定constructor有三個引數,分別是props、context和updater。

  · props是屬性,它是不可變的。
  
  · context是全域性上下文。
  
  · updater是包含一些更新方法的物件

  // this.setState最終呼叫的是this.updater.enqueueSetState方法
  
  // this.forceUpdate最終呼叫的是this.updater.enqueueForceUpdate方法

複製程式碼
學react哪家強,阿寬頻你看React的生命週期

mountComponent 元件掛載程式碼

  // 當元件掛載時,會分配一個遞增編號,表示執行 ReactUpdates 時更新元件的順序
  var nextMountID = 1

  // 初始化元件,渲染標記,註冊事件監聽器
  mountComponent: function(transaction, nativeParent, nativeContainerInfo, context) {
    // 當前元素對應的上下文
    this._context = context
    this._mountOrder = nextMountID
    this._nativeParent = nativeParent
    this._nativeContainerInfo = nativeContainerInfo

    var publicProps = this._processProps(this._currentElement.props)
    var publicContext = this._processContext(context)
    var Component = this._currentElement.type

    // 初始化公共類
    var inst = this._constructComponent(publicProps, publicContext)

    var renderedElement;

    // 判斷元件是否為無狀態元件,無狀態元件沒有狀態更新佇列,它只專注於渲染
    if (!shouldConstruct(Component) && (inst == null || inst.render == null)) {
      renderedElement = inst
      warnIfInvalidElement(Component, renderedElement)
      inst = new StatelessComponent(Component)
    }

    // 這些初始化引數本應該在建構函式中設定,在此設定是為了便於進行簡單的類抽象 
    inst.props = publicProps
    inst.context = publicContext
    inst.refs = emptyObject
    inst.updater = ReactUpdateQueue

    this._instance = inst
    

    // 將例項儲存為一個引用 
    ReactInstanceMap.set(inst, this)
    
    // 初始化 state
    var initialState = inst.state
    if (initialState === undefined) {
      inst.state = initialState = null
    }

    // 初始化更新佇列 
    this._pendingStateQueue = null
    this._pendingReplaceState = false 
    this._pendingForceUpdate = false

    var markup;

    // 如果掛載出現錯誤
    if (inst.upstable_handleError) {
      markup = this.performInitialMountWithErrorHandling(renderedElement, nativeParent, nativeContainerInfo, transaction, context)
    } else {
      // 初始化掛載
      markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction,
context)
    }

    // 如果存在 componentDidMount , 則呼叫
    if (inst.componentDidMount) {
      transaction.getReactMountReady().enqueue(inst.componentDidMount, inst)
    }

    return markup
  }

  // 掛載錯誤處理
  performInitialMountWithErrorHandling: function(renderedElement, nativeParent, nativeContainerInfo, transaction, context) {
    var markup;
    var checkpoint = transaction.checkpoint()

    try {
      // 捕捉錯誤,沒有錯誤則初始化掛載
      markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction,
context)
    } catch (e) {
      transaction.rollback(checkpoint)
      this._instance.unstable_handleError(e)
      if (this._pendingStateQueue) {
        this._instance.state = this._processPendingState(this._instance.props, this._instance.context)
      }

      checkpoint = transaction.checkpoint()
      // 如果捕捉到錯誤,則執行 unmountComponent 後,再初始化掛載
      this._renderedComponent.unmountComponent(true)
      
      transaction.rollback(checkpoint)

      markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context)

    }

    return markup

  }


  // 掛載元件
  performInitialMount: function(renderedElement, nativeParent, nativeContainerInfo, transaction, context) {
    var inst = this._instance
    
    // 如果存在 componentWillMount, 則呼叫
    if (inst.componentWillMount) {
      inst.componentWillMount()

      // componentWillMount 呼叫 setState 時,不會觸發 re-render 而是自動提前合併

      if (this._pendingStateQueue) {
        inst.state = this._processPendingState(inst.props, inst.context)
      }
    }

    // 如果不是無狀態元件,即可開始渲染
    if (renderedElement === undefined) {
      renderedElement = this._renderValidatedComponent()
    }

    this._renderedNodeType = ReactNodeTypes.getType(renderedElement)
     // 得到 _currentElement 對應的 component 類例項 
    this._renderedComponent = this._instantiateReactComponent(renderedElement)

    // render 遞迴渲染
    var markup = ReactReconciler.mountComponent(this._renderedComponent, transaction, nativeParent,
nativeContainerInfo, this._processChildContext(context))

    return markup
  }
複製程式碼

總結一下 - 初次渲染 ?

1 . 當使用 ES6 classes 編寫 React 元件時,其實就是呼叫內部方法 createClass 建立元件, 該方法返回一個Constructor(props, context, updater) 用來生成元件例項,我們發現在呼叫React.createClass,已經執行了getDefaultProps(),並將其賦值於Constructor的原型中

2 . 由於通過ReactCompositeComponentBase 返回的是一個虛擬節點,所以需要利用 instantiateReactComponent去得到例項,再使用 mountComponent 拿到結果作為當前自定義元素的結果

當使用 React 建立元件時,首先會呼叫 instantiateReactComponent,這是初始化元件的入口 函式,它通過判斷 node 型別來區分不同元件的入口 (具體看下邊說明)

3 . 在React中,因為所有class元件都要繼承自Component類或者PureComponent類,因此和原生class寫法一樣,要在constructor裡首先呼叫super方法,才能獲得this。通過 mountComponent 掛載元件,初始化序號、標記等引數,判斷是否為無狀態元件,並進行 對應的元件初始化工作,比如初始化 props、context 等引數。利用 getInitialState 獲取初始化 state、初始化更新佇列和更新狀態。

4 . 若存在 componentWillMount,則執行。如果此時在 componentWillMount 中呼叫 setState 方法,是不會觸發 re-render的,而是會進行 state 合併,且 inst.state = this._processPendingState (inst.props, inst.context) 是在 componentWillMount 之後執行的,因此 componentWillMount 中 的 this.state 並不是最新的,在 render 中才可以獲取更新後的 this.state。

React 是利用更新佇列 this._pendingStateQueue 以及更新狀態 this._pendingReplaceState 和 this._pendingForceUpdate 來實現 setState 的非同步更新機制。也就是說 this.setState 最終呼叫的是this.updater.enqueueSetState方法

5 . 當渲染完成後,若存在 componentDidMount,則呼叫。其實,mountComponent 本質上是通過遞迴渲染內容的,由於遞迴的特性,父元件的 componentWillMount 在其子元件的 componentWillMount 之前呼叫,而父元件的 componentDidMount 在其子元件的 componentDidMount 之後呼叫。

學react哪家強,阿寬頻你看React的生命週期

額外補充

instantiateReactComponent 入口元件

  · 當 node 為空時,說明 node 不存在,則初始化空元件 ReactEmptyComponent.create(instantiateReactComponent)。

  · 當 node 型別為物件時,即是 DOM 標籤元件或自定義元件,那麼如果 element 型別為字串時 ,則初始化 DOM 標籤元件ReactNativeComponent.createInternalComponent (element),否則初始化自定義元件 ReactCompositeComponentWrapper()

  · 當 node 型別為字串或數字時,則初始化文字元件 ReactNativeComponent.createInstanceForText(node)。

  · 如果是其他情況,則不作處理

  // instantiateReactComponent 方法原始碼, 初始化元件入口

  function instantiateReactComponent(node, parentCompositeType) {
    var instance;

    // 空元件 (ReactEmptyComponent)
    if (node === null || node === false) {
      instance = ReactEmptyComponent.create(instantiateReactComponent)
    }

    // 物件型別
    if (typeof node === 'object') {
      var element = node
      if (typeof element === 'string') {
        instance = ReactNativeComponent.createInternalComponent (element)
      } else if (isInternalComponentType(element.type)) {
          // 不是字串表示的自定義元件暫無法使用,此處將不做元件初始化操作
          instance = new element.type(element)
      } else {
        // 自定義元件
        instance = new ReactCompositeComponentWrapper()
      }
    } else if (typeof node === 'string' || typeof node === 'number') {
      // 字串或數字
      instance = ReactNativeComponent.createInstanceForText(node)
    } else {
      // 不做處理
    }

    // 設定例項
    instance.construct(node)
    // 初始化引數
    instance._mountIndex = 0
    instance._mountImage = null

    return instance
    
  }
  
複製程式碼

生命週期 - 更新階段

學react哪家強,阿寬頻你看React的生命週期

updateComponent 負責管理生命週期中的 componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render 和 componentDidUpdate

首先通過 updateComponent 更新元件,如果前後元素不一致,說明需要進行元件更新

如果存在 componentWillReceiveProps, 則執行。如果此時在 componentWillReceiveProps 中調 用 setState,是不會觸發 re-render 的,而是會進行 state 合併。且在 componentWillReceiveProps、 shouldComponentUpdate 和 componentWillUpdate 中也還是無法獲取到更新後的 this.state,即此 時訪問的 this.state 仍然是未更新的資料,需要設定 inst.state = nextState 後才可以,因此 只有在 render 和 componentDidUpdate 中才能獲取到更新後的 this.state。

呼叫 shouldComponentUpdate 判斷是否需要進行元件更新,如果存在 componentWillUpdate, 則執行。

updateComponent 本質上也是通過遞迴渲染內容的,由於遞迴的特性,父元件的 componentWillUpdate 是在其子元件的 componentWillUpdate 之前呼叫的,而父元件的 componentDidUpdate 也是在其子元件的 componentDidUpdate 之後呼叫的。

當渲染完成之後,若存在 componentDidUpdate,則觸發

相關原始碼

  // receiveComponent 是通過呼叫 updateComponent 進行元件更新的
  receiveComponent: function(nextElement, transaction, nextContext) {
    var prevElement = this._currentElement; var prevContext = this._context;
    this._pendingElement = null;
    this.updateComponent(transaction, prevElement, nextElement, prevContext, nextContext);
  },

  updateComponent: function(transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext) {
    var inst = this._instance
    var willReceive = false
    var nextContext
    var nextProps

    // 上下文是否改變
    if (this._context === nextUnmaskedContext) {
      nextContext = inst.context
    } else {
      nextContext = this._processContext(nextUnmaskedContext)
      willReceive = true 
    }

    if (preParentElement === nextParentElement) {
      // 元素相同,跳過元素型別檢測
      nextProps = nextParentElement.props
    } else {
      // 檢查元素的型別
      nextProps = this._processProps(nextParentElement.props)
      willReceive = true
    }

    // 如果存在 compnentWillReceiveProps ,則呼叫
    if (inst.componentWillReceiveProps && willReceive) {
      inst.componentWillReceiveProps(nextProps, nextContext)
    }

    // 將新的state合併到更新的佇列中, 此時的 nextState 是最新的 state
    var nextState = this._processPendingState(nextProps, nextContext)

    // 根據更新佇列和 shouldComponentUpdate 的狀態來判斷是否需要更新元件
    var shouldUpdate = this._pendingForceUpdate || !inst.shouldComponentUpdate || inst.shouldComponentUpdate(nextProps, nextState, nextContext)

    if (shouldUpdate) {
      // 重置更新佇列
      this._pendingForceUpdate = false
      // 即將更新 this.props 、 this.state 、 this.context
      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
    }
  },

  // 當確定元件需要更新時,則呼叫
  _performComponentUpdate: function(nextElement, nextProps, nextState, nextContext, transaction, unmaskedContext) {
    var inst = this._instance
    var hasComponentDidUpdate = Boolean(inst.componentDidUpdate)

    var preProps
    var preState
    var preContext

    // 如果存在 componentDidUpdate , 則將當前的 state, props, context 儲存一份
    if (hasComponentDidUpdate) {
      preProps = inst.props
      preState = inst.state
      preContext = inst.context
    }

    // 如果存在 componentWillUpdate ,則呼叫
    if (inst.componentWillUpdate) {
      inst.componetWillUpdate(nextProps, nextState, nextContext)
    }

    this._currentElement = nextParentElement
    this._context = unmaskedContext

    // 更新 this.props 、 this.state 、 this.context
    inst.props = nextProps
    inst.state = nextState
    inst.context = nextContext

    // 呼叫 render 渲染元件
    this._updateRenderedComponent(transaction, unmaskedContext)

    // 當元件完成更新後,如果存在 componentDidUpdate,則呼叫 
    if (hasComponentDidUpdate) {
      transaction.getReactMountReady().enqueue( 
        inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext),inst
      ) 
    }

  }

  // 呼叫 render 渲染元件
  _updateRenderedComponent: function (transaction, context) {
    var preComponentInstance = this._renderedComponet
    var preRenderedElement = prevComponentInstance._currentElement
    var nextRenderedElement = this._renderValidatedComonet()

    // 如果需要更新,則呼叫ReactReconciler.receiveComponent 繼續更新元件
    if (shouldUpdateReactComponent(preRenderedElement, nextRenderedElement)) {
      ReactReconciler.receiveComponent(preComponentInstance, nextRenderedElement, transaction, this._processChildContext(context)) 
    } else {
      // 如果不需要更新, 則渲染元件
      var oldNativeNode = ReactReconciler.getNativeNode(preComponentInstance)
      ReactReconciler.unmountComponent(preComponentInstance)

      this._renderedNodeType = ReactNodeTypes.getType(nextRenderedElement)

      // 得到 nextRenderedElement 對應的component 類例項

      this._renderedComponet = this._instantiateReactComponent(nextRenderedElement)

      // 使用 render 遞迴渲染
      var nextMarkup = ReactReconciler.mountComponent(this._renderedComponent,transaction, this._nativeParent, this._nativeContainerInfo, this._processChildContext(context))

      tgus._replaceNodeWithMarkup(oldNativeNode, nextMarkup)
    }
  }
複製程式碼

setState 迴圈呼叫的風險

禁止在 shouldComponentUpdate 和 componentWillUpdate 中呼叫this.setState,因為這樣會造成迴圈呼叫,直到耗光瀏覽器記憶體後奔潰,那麼為什麼不能呢 ?


1 . 呼叫 setState 時,實際上會執行 enqueueSetState 方法,並對partialState和_pendingStateQueue更新佇列進行合併操作,最終通過 enqueueUpdate 執行 state 的更新

2 . 而 performUpdateIfNecessary 方法會獲取 _pendingElement、_pendingStateQueue、_pendingForceUpdate,並呼叫 receiveComponent 和 updateComponent 方法進行元件更新

3 . 如 果 在 shouldComponentUpdate 或 componentWillUpdate 方 法 中 調 用 setState , 此 時 this._pendingStateQueue != null,則 performUpdateIfNecessary 方法就會呼叫 updateComponent 方法進行元件更新,但 updateComponent 方法又會呼叫 shouldComponentUpdate 和 componentWill- Update 方法,因此造成迴圈呼叫,使得瀏覽器記憶體佔滿後崩潰

複製程式碼

生命週期 - 解除安裝階段

unmountComponent 負責管理生命週期中的 componentWillUnmount

如果存在 componentWillUnmount,則執行並重置所有相關引數、更新佇列以及更新狀態,如 果此時在 componentWillUnmount 中呼叫 setState,是不會觸發 re-render 的,這是因為所有更新 佇列和更新狀態都被重置為 null,並清除了公共類,完成了元件解除安裝操作

相關原始碼

  unmountComponent: function(safely) {
    if (!this._renderedComponent) {
      return
    }

    var inst = this._instance

    // 如果存在 componentWillUnmount, 則呼叫
    if (inst.componentWillUnmount) {
      if (safely) {
        var name = this.getName() + '.componentWillUnmount()'
        ReactErrorUtils.invokeGuardedCallback(name, inst.componentWillUnmount.bind(inst))
      } else {
        inst.componentWillUnmount()
      }
    }

    // 如果元件已經渲染,則對元件進行 unmountComponent 操作
    if (this._renderedComponent) {
      ReactReconciler.unmountComponent(this._renderedComponent, safely)
      this._renderedNodeType = null
      this._renderedComponent = null
      this._instance = null
    }

    // 重置相關引數、更新佇列以及更新狀態 
    this._pendingStateQueue = null   // 更新佇列
    this._pendingReplaceState = false // 更新狀態
    this._pendingForceUpdate = false  
    this._pendingCallbacks = null
    this._pendingElement = null
    this._context = null
    this._rootNodeID = null
    this._topLevelWrapper = null
    // 清除公共類
    ReactInstanceMap.remove(inst)

  }

複製程式碼

在 React 開發中,一個很重要的原則就是讓元件儘可能是無狀態的,無狀態元件沒有狀態,沒有生命週期,只是簡單地接受 props 渲染生成 DOM 結構,是一個 純粹為渲染而生的元件。

再看一個圖總結生命週期

學react哪家強,阿寬頻你看React的生命週期

相關連結

Blog: blog.pengdaokuan.cn:4001

Github: github.com/PDKSophia/b…

相關文章