react16 渲染流程


前言

react升級到16之后,架構發生了比較大的變化,現在不看,以后怕是看不懂了,react源碼看起來也很麻煩,也有很多不理解的地方。
大體看了一下渲染過程。

react16架構的變化

react api的變化就不說了。react架構從stack變到了“fiber”。
最大的變化就是支持了任務幀,把各個任務都增加了優先級,同步和異步。比如用戶輸入input是優先級比較高的,它可以打斷低優先級的任務。
比如再處理dom diff的時候耗時嚴重,fiber任務處理大概會有50ms的幀時長,超過這個時間就會先去看看有沒高優任務去做。然后回來做低優先級任務。

  • 優先級高的任務可以中斷低優先級的任務。然后再重新,注意是重新執行優先級低的任務。
  • 還增加了異步任務,調用requestIdleCallback api,瀏覽器空閑的時候執行。(不過用戶操作默認是同步的,暫時還沒開放這個特性)
  • dom diff樹變成了鏈表,一個dom對應兩個fiber(一個鏈表),對應兩個隊列,這都是為找到被中斷的任務,重新執行而設計的。

設計成鏈表之后就更方便了。遞歸改循環,結點關系更好維護了,看下圖就一目了然。

渲染流程

代碼太多,就不貼出來了,整理了一個圖。這里就對圖說下流程。(以下是個人理解,歡迎指正)
這篇源碼分析還是挺不錯的(http://zxc0328.github.io/2017/09/28/react-16-source/)

調用setState后,其實是調用了this.updater.enqueueSetState,這個函數首先從全局拿到React組件實例對應的fiber,然后拿到了fiber的優先級。最后調用了addUpdate向隊列中推入需要更新的fiber,並調用scheduleUpdate觸發調度器調度一次新的更新。

setState其實是把任務推到隊列里,然后調用調度器處理更新

addUpdate

然后看下addUpdate,其實是把任務封裝成update,然后把update推入updateQueue,

type Update = {
  priorityLevel: PriorityLevel,
  partialState: PartialState<any, any>,
  callback: Callback | null,
  isReplace: boolean,
  isForced: boolean,
  isTopLevelUnmount: boolean,
  next: Update | null,
};

type UpdateQueue = {
  first: Update | null,
  last: Update | null,
  hasForceUpdate: boolean,
  callbackList: null | Array<Callback>,

  // Dev only
  isProcessing?: boolean,
};

每個react 結點都有2個fiber鏈表,一個叫current fiber,一個叫alternate fiber,而每個鏈表又對應兩個updateQueue。
而currentFiber.alternate = alternateFiber; alternateFiber.alternate = currentFiber。通過alternate屬性連接起來。初始化的時候,alternate fiber是current fiber 的clone。
處理diff的時候,操作的是alternateFiber,處理完diff,讓currentFiber = alternateFiber;這樣一個處理就完成了。
如果被高優插入,current queue的存在就可以做備份,需要繼續處理。

insertUpdate函數,將update按優先級插入這兩個queue。

到此,一般工作做完了。另一半就是react的核心,調度算法了。

scheduleUpdate

fiber的調度是一個鏈表,從當前的結點,一直遍歷到根結點(每個fiber都有fiber.return,它的值是它的父元素)。fiber的調度是從根結點進行的。遍歷過程中會更新父結點的pendingWorkPriority(當前fiber的優先級)。標志這個結點上等待更新的事務的優先級。

當遍歷到HostRoot(根結點)時,開始真正的調度工作。

會根據任務的優先級來決定是不是執行這次更新,

  • 如果優先級為sychronousPriority(比如用戶輸入), 就執行同步更新,立馬執行。
  • 如果是TaskPriority,它的執行一般是在batchUpdate里面進行更新。(比如我回調里執行setSate,它的任務優先級就是TaskPriority),它會在dispatch的callback里面等callback執行完再執行更新。這樣是對應setstate的batchUpdate。后面再講這部分。如果是TaskPriority,直接return。
  • 如果不是這兩個優先級的話,表明這次更新是異步的,那就在瀏覽器空閑的時間處理它。調用的是scheduleDeferredCallback,會根據平台注入函數,比如瀏覽器的api是requestIdleCallback,如果瀏覽器不支持react還有個polyfill。

我們繼續跟着同步任務處理走,同步任務調用performWork:

performWork

這里會開始任務的處理,調度完成。
performWork的作用就是“刷新”待更新隊列,執行待更新的事務。
處理邏輯主循環是workLoop,也會調用一次scheduleDeferredCallback,未來處理一次更新,來完成workLoop未完成的任務。

workLoop

如果沒有alternate fiber首先會根據update queue創建fiber,進入的是createWorkInProgress函數。createWorkInProgress 這個函數會構建一顆樹的頂端,賦值給全局變量 nextUnitOfWork ,通過迭代的方式,不斷更新 nextUnitOfWork 直到遍歷完所有樹的節點。

然后就會處理幀任務,進入函數首要處理就是看下上次處理有沒有沒被處理的任務,然后根據時間片處理任務。
如果任務在deadline之前沒有commit,就會被標記pendingCommit,workLoop首先會處理pendingCommit。
接下來是處理邏輯:

  • 如果下一個update是同步的,就調用performUnitOfWork進行reconcilation,最后調用commitAllWork把它渲染到dom。
  • 解決完同步的任務,這時候看下時間片,有沒有超時,如果沒有,調用performUnitOfWork執行異步任務。
  • 如果還有時間就commitAllWork,如果沒有時間就留到下一幀提交。

react16 將任務的處理分成reconcilation和render,reconcilation包括dom diff,處理react組件的屬性和鈎子等。單獨把render到dom抽出來,以便跨平台。

一次更新是同步還是異步是由優先級決定的,異步渲染默認是關閉的。用戶代碼的優先級是同步的。

performUnitOfWork

performUnitOfWork是執行的reconcilation階段,它又拆分成beginWork和complete,beginWork做的是產出effect list,complete做的是處理react實例的生命周期和處理屬性等操作。

effect list是dom diff的產出,其實是被打過tag的diff數組。render階段會根據effect list render到dom

beginWork

beginWork就會根據不同的fiber tag做不同的處理,比如ClassComponent(React組件實例)調用updateClassComponent,hostComponent(真實dom)調用updateHostComponent。

updateClassComponent

如果為空,初始化一個react組件,調用組件的componentWillMount,如果新老props不一樣,調用componentWillReceiveProps。這時候會檢測shouldComponentUpdate,如果為true,就調用實例的render渲染出children,然后調用reconcileChildren對dom進行diff。
reconcileChildren其實是調用的reconcileChildrenArray。進行大名鼎鼎的dom diff操作。

updateHostComponent

真實dom直接進行reconcileChildrenArray dom diff

reconcileChildrenArray(dom diff)

React的reconcile算法采用的是層次遍歷,然后在層次上面進行簡化的兩端比較法,因為fiber樹是單鏈表結構,沒有子節點數組這樣的數據結構。也就沒有可以供兩端同時比較的尾部游標。所以React的這個算法是一個簡化的兩端比較法,只從頭部開始比較。

從頭部遍歷。第一次遍歷新數組,對上了,新老index都++,比較新老數組哪些元素是一樣的,(通過updateSlot,比較key),如果是同樣的就update。第一次遍歷完了,如果新數組遍歷完了,那就可以把老數組中剩余的fiber刪除了。
如果老數組完了新數組還沒完,那就把新數組剩下的都插入。
如果這些情況都不是,就把所有老數組元素按key放map里,然后遍歷新數組,插入老數組的元素,這是移動的情況。
最后再刪除沒有被上述情況涉及的元素

completeUnitOfWork

completeUnitOfWork是reconcile的complete階段,主要是更新props和調用生命周期方法等等。
主要的邏輯是調用completeWork完成收尾,然后將當前子樹的effect list插入到HostRoot的effect list中(這塊主要是收集effect tag到頂端的effect,然后直接render,不需要遍歷鏈表了)

completeWork

這里也是根據不同的fiber tag進行對應的處理。
主要是HostComponent的處理,檢查結點是不是需要更新,如果需要就打個tag,標記為update的side-effect。更新新老ref。

commitAllWork

這塊就是render到dom的操作了。根據dom diff產生的effect list進行dom的增刪改。還會調用一些生命周期,比如刪除組件調用componentWillUnmount。

如果effect發生在ClassComponent上,調用組件的componentDidMount
componentDidUpdate。
在render完暴露鈎子以便調用包括
componentDidMount、componentDidUpdate和componentWillUnmount

到這全部流程就結束了。

帶來的問題

  • render是不會被打斷的,可以被打斷的只有reconcilation階段,而且被打斷之后,低優先級的任務是重新執行,這就導致了reconcilation階段的一些操作會被多次執行。
    reconcilation階段的react生命周期函數不能保證只被調用一次,所以相關邏輯需要整理。

  • 如果高優先級的任務一直存在,那么低優先級的任務則永遠無法進行,組件永遠無法繼續渲染。

  • 因為以上兩個問題,到現在為止react16還不敢直接開啟默認渲染。

fiber對象

為幫助理解,這里寫一下fiber對象。

const Fiber = {
  tag: HOST_COMPONENT,
  type: 'div',
  return: parentFiber,
  child: childFiber,
  sibling: null,
  alternate: currentFiber,
  stateNode: document.createElement('div') | instance,
  props: { children: [], className: 'foo' },
  partialState: null,
  effectTag: PLACEMENT,
  effects: [],
  firstEffect: null,
  lastEffect: null
}

return,child,sibling都標識着該結點的下個結點的連接,鏈表就是通過這3個屬性遍歷的。
effectTag 和 effects 這兩個屬性為的是記錄每個節點 Diff 后需要變更的狀態,比如刪除,移動,插入,替換,更新等
alternate就是之前提過的連接兩個fiber 鏈表的屬性,通過這個屬性獲得兩個鏈表
stateNode,用於記錄當前 fiber 所對應的真實 DOM 結點 或者 當前虛擬組件的實例,這么做的原因第一是為了實現 Ref ,第二是為了實現 DOM 的跟蹤。
firstEffect 、lastEffect 等是用來保存中斷前后 effect 的狀態,用戶中斷后恢復之前的操作。
tag:

export type TypeOfWork = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;

export const IndeterminateComponent = 0; // 尚不知是類組件還是函數式組件
export const FunctionalComponent = 1; // 函數式組件
export const ClassComponent = 2; // Class類組件
export const HostRoot = 3; // 組件樹根組件,可以嵌套
export const HostPortal = 4; // 子樹. Could be an entry point to a different renderer.
export const HostComponent = 5; // 標准組件,如地div, span等
export const HostText = 6; // 文本
export const CallComponent = 7; // 組件調用
export const CallHandlerPhase = 8; // 調用組件方法
export const ReturnComponent = 9; // placeholder(占位符)
export const Fragment = 10; // 片段

fiber優先級

同步任務包括SynchronousPriority和TaskPriority,剩下的異步任務會在接下來的幾幀完成。

{
  NoWork: 0, // No work is pending.
  SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects.
  TaskPriority: 2, // Completes at the end of the current tick.
  HighPriority: 3, // Interaction that needs to complete pretty soon to feel responsive.
  LowPriority: 4, // Data fetching, or result from updating stores.
  OffscreenPriority: 5, // Won't be visible but do the work in case it becomes visible.
}

QA

fiber的異步任務掛起和恢復?任務調度的粒度?

任務的調度是在workLoop中進行的,workLoop首先會判斷有沒有之前沒有commitAll的(這塊其實在做渲染),沒有的話就看下時間,執行performUnitOfWork。(這里還會判斷一下如果沒有下個工作的結點了,就判斷一下再commitAll)。這是掛起的過程,直接return掉。粒度顯而易見就是每個performUnitOfWork函數,每個結點的performUnitOfWork就是最小工作粒度。
任務的恢復是在調用workLoop之后,判斷一下之前的任務有沒有被return。然后調用scheduleDeferredCallback來執行一下workLoop來解決之前沒被處理的事務。

如何進行批量的setstate處理?異步setstate?

比如

 handleClick = (e) => {
        e.stopPropagation();
        this.setState({
            title: 'click2'
        })
        this.setState({
            title: 'click3'
        })
        this.setState({
            title: 'click4'
        })
    }

觸發 dispatchEvent 回調函數的處理過程中,會執行到 batchedUpdates。

function batchedUpdates(fn, a) {
    var previousIsBatchingUpdates = isBatchingUpdates; // 批量處理
    isBatchingUpdates = true;
    try {
        return fn(a); // // 此過程中可能改變state所以需要再performWork
    } finally {
        isBatchingUpdates = previousIsBatchingUpdates;
        if (!isBatchingUpdates && !isRendering) {
            performWork(Sync, null);
        }
    }
}

isBatchingUpdates被置為true,這三個 setState 都不能立即執行performWork。

if (isBatchingUpdates) {
    if (isUnbatchingUpdates) {
        nextFlushedRoot = root;
        nextFlushedExpirationTime = Sync;
        performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime);
    }
    return;
}
if (expirationTime === Sync) {
    performWork(Sync, null);
} else {
    scheduleCallbackWithExpiration(expirationTime);
}

而會在這個callback執行之后調用performWork,這樣就完成了batch 處理。

為什么settimeout可以跳過batchUpdate呢?

setTimeout 的回調函數須等 dispatchEvent 函數執行完,也就是要等到 performWork 執行,然而在 performWork 中, nextFlushedRoot 為 null , while 循環無法進行。

function performWork () {
    while (nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork && (minExpirationTime === NoWork || nextFlushedExpirationTime <= minExpirationTime) && !deadlineDidExpire) {
        performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime);
        // Find the next highest priority work.
        findHighestPriorityRoot();
    }
}

執行 setTimeout 的回調函數時, isBatchingUpdates 已經變為 false,所以每次 setState 都會觸發 performWork 。

回顧

這里總結一下react的渲染流程:把新state push 到2個fiber queue里面,通過alternate queue構建一個alternate fiber之后所有的tag都打在這個fiber上面,遍歷到根結點,從根結點進行dom diff的打tag處理。打完tag,收集tag(收集到頂端的currentFiber.effect上),收集的tag列表(effect List)。到此進入render過程,直接根據effect List操作dom。到此完成。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM