React16.2的fiber架構詳解(3)

React16是否能異步渲染,在於內部一個變量。在開始以前,咱們須要準備一個例子。javascript

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>by 司徒正美</title>
    <meta name="viewport" content="width=device-width">
    <!-- <script type='text/javascript' src="./src/React2.js"></script>-->
    <script type='text/javascript' src="./test/react.js"></script>
    <script type='text/javascript' src="./test/react-dom.js"></script>
    <script src="test/babel.js"></script>
</head>

<body>
    <div id="test"></div>
    <div id="content"></div>
</body>
<script type="text/babel">
    var container = document.getElementById("test");
    class Root extends React.Component{
        constructor(props){
            super(props)
            this.props = props
        }
        render(){
            console.log("Root render..", Date.now())
            return <div><A /></div>
        }
    }
    class A extends React.Component{
        constructor(props){
            super(props)
            this.props = props
        }
        render(){
            console.log("A render..", Date.now())
            return <div>{1111}</div>
        }
    }
    ReactDOM.render(<Root />, container, function(){
        console.log("callback",Date.now())
    })
   console.log("end", Date.now())
</script>

</html>

當中的react.js與react-dom.js是React16.4beta,你們能夠在bootcdn上下載。html

前文已經提過,ReactDOM.render/hydrate/unstable_renderSubtreeIntoContainer/unmountComponentAtNode都是legacyRenderSubtreeIntoContainer方法的加殼方法。java

圖片描述

legacyRenderSubtreeIntoContainer裏面調用legacyCreateRootFromDOMContainer建立一個ReactRoot對象,而後再調用其render或legacy_renderSubtreeIntoContainer方法node

//by 司徒正美
function legacyCreateRootFromDOMContainer(container, forceHydrate) {
  var shouldHydrate = forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
  // First clear any existing content.
  if (!shouldHydrate) {
    var warned = false;
    var rootSibling = void 0;
    while (rootSibling = container.lastChild) {
      {
        if (!warned && rootSibling.nodeType === ELEMENT_NODE && rootSibling.hasAttribute(ROOT_ATTRIBUTE_NAME)) {
          warned = true;
          warning_1(false, 'render(): Target node has markup rendered by React, but there ' + 'are unrelated nodes as well. This is most commonly caused by ' + 'white-space inserted around server-rendered markup.');
        }
      }
      container.removeChild(rootSibling);
    }
  }
  {
    if (shouldHydrate && !forceHydrate && !warnedAboutHydrateAPI) {
      warnedAboutHydrateAPI = true;
      lowPriorityWarning$1(false, 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' + 'will stop working in React v17. Replace the ReactDOM.render() call ' + 'with ReactDOM.hydrate() if you want React to attach to the server HTML.');
    }
  }
  // Legacy roots are not async by default.
  var isAsync = false;
  console.log("new ReactRoot",container, isAsync, shouldHydrate)
  return new ReactRoot(container, isAsync, shouldHydrate);
}

留意裏面的isAsync,是寫死的,強制使用同步,咱們能夠改一改,就能使用異步react

var isAsync = true;
本節的內容就準備讀如何 異步渲染

ReactRoot以前已經說過,再貼一下源碼。babel

圖片描述架構

從DOMRenderer.updateContainer到達updateContainerAtExpirationTime到達scheduleWork到達scheduleWork到達scheduleWorkImpl到達requestWork,咱們一路加點註釋app

下面是scheduleWorkImpl的代碼:dom

圖片描述

requestWork裏面才進行同步異步邏輯分家異步

圖片描述

scheduleCallbackWithExpiration是幹了什麼呢?它會斷定是否工做與不幹做,工做就是從新計算過時時間,而後執行scheduleDeferredCallback方法。scheduleDeferredCallback有兩個參數,第一個是回調函數,第二個是對象,裏面的timeout決定它在執行scheduleDeferredCallback最遲多少ms才執行。

scheduleDeferredCallback是何方神聖呢?它是大名鼎鼎的requestIdleCallback

clipboard.png

clipboard.png

requestIdleCallback的語法以下:

圖片描述

performAsyncWork與performSyncWork也是一對兄弟。

//by 司徒正美

  function performAsyncWork(dl) {
    performWork(NoWork, true, dl);
  }

  function performSyncWork() {
    performWork(Sync, false, null);
  }

performWork的源碼

function performWork(minExpirationTime, isAsync, dl) {
    deadline = dl;
    // Keep working on roots until there's no more work, or until the we reach
    // the deadline.
    findHighestPriorityRoot();
    if (enableUserTimingAPI && deadline !== null) {
      var didExpire = nextFlushedExpirationTime < recalculateCurrentTime();
      stopRequestCallbackTimer(didExpire);
    }
    if (isAsync) {
      while (nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork && 
        (minExpirationTime === NoWork ||
         minExpirationTime >= nextFlushedExpirationTime) && (!deadlineDidExpire || recalculateCurrentTime() >= nextFlushedExpirationTime)) {
        performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, !deadlineDidExpire);
        findHighestPriorityRoot();
      }
    } else {
      while (nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork && 
        (minExpirationTime === NoWork || minExpirationTime >= nextFlushedExpirationTime)) {
        performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, false);
        findHighestPriorityRoot();
      }
    }
    // We're done flushing work. Either we ran out of time in this callback,
    // or there's no more work left with sufficient priority.
    // If we're inside a callback, set this to false since we just completed it.
    if (deadline !== null) {
      callbackExpirationTime = NoWork;
      callbackID = -1;
    }
    // If there's work left over, schedule a new callback.
    if (nextFlushedExpirationTime !== NoWork) {
      scheduleCallbackWithExpiration(nextFlushedExpirationTime);
    }
    // Clean-up.
    deadline = null;
    deadlineDidExpire = false;

    finishRendering();
  }

下面是同步與異步的執行狀況

圖片描述

圖片描述

但異步模式爲何會調用兩次render呢?估計這還在測試階段,許多BUG。咱們追蹤到finishClassComponent方法,看到它的render方法:

圖片描述

咱們再改一下Root組件的代碼,添加一個componentDidMount.

class Root extends React.Component{
        constructor(props){
            super(props)
            this.props = props
            this.state = {
                x: 1
            }
        }
        render(){
            console.log("Root render..", Date.now())
            return <h1><A x={this.state.x} /></h1>
        }
        componentDidMount(){
            console.log("Root componentDidMount")
            this.setState({
                x: 2
            })
        }
    }
     class A extends React.Component{
        constructor(props){
            super(props)
            this.props = props
            this.state = {
                text: props.x
            }
        }
        componentWillReceiveProps(p){
            this.setState({
               text: p.x
            })
        }
        render(){
            console.log("A render..", Date.now())
            return <h2>{this.state.text}</h2>
        }
    }

clipboard.png

咱們再看一下fiber樹。fiber有許多種類型,但主要是四種, ClassFiber, FunctionFiber, HostComponentFiber, HostTextFiber,分別對應原來的類組件,無狀態組件,元素虛擬節點,文本虛擬節點。Fiber表面上比React15的虛擬DOM多了一些屬性,如parent, child, sibling。換言之,fiber能夠像真實DOM同樣上下右遍歷(沒有左)。

clipboard.png

React16的源碼裏面有兩個方法beginWorkfinishWork重要方法。beginWork,就是從一個Fiber開始,初始化它的state(若是fiber.type爲函數,則new 實例或一個相似相似的東西,若是type爲標籤名,則建立元素節點或文本節點),並遍歷它的第一重孩子,讓孩子們加上parent,sibling(注意這時孩子沒有stateNode)。最後返回第一個孩子,做爲剛纔fiber的child。 而後對這個child再執行beginWork操做。

beginWork的過程當中,確到組件,須要用到context,context是來自contextStack。這是一個全局對象。在頂層,默認會push一個空對象。而後到達某個組件時,peek一下(不使用pop方法)。 若是這個組件有getChildContext方法呢,這時就會產生一個新context, push進去。

有些fiber是沒有孩子的,好比說文本節點,或一些元素節點,這樣它開始 finishWork操做,找它的sibling,對sibling進行beginWork操做,沒有sibling就往上找,這時就會再次訪問到某個組件,若是這個組件有getChildContext,因而就pop一下。

finishWork還有一個重要任務,就是收集DOM操做指令,一開始全部fiber的effects都PLACEMENT,叫作置換,其實至關於append。每次往上找時,父fiber就把它全部孩子的effect收集一下,最後到頂層Root組件時, contextStack爲空,而effects則裝得滿滿的,而後交給commintAllWork執行這些指令。

fiber架構是很好地解決context的往下傳送問題。
相關文章
相關標籤/搜索