react源碼學(xué)習(xí)(一)render過程

render過程

我們想了解react的工作機制,我們直接去看源碼很難去弄懂每一步到底是做什么的,在復(fù)雜的函數(shù)調(diào)用中我們很容易讓自己迷失,所以我決定跟隨一些常用方法來分析工作機制,第一篇就是ReactDOM.render這個入口方法,在講解中我會直接忽略dev和調(diào)試的代碼,因為這與工作機制無關(guān)。
先來看下主要的流程

render流程.png

先來看一下入口代碼

/**
 * 渲染dom的入口方法
 * @param {*} element
 * @param {*} container
 * @param {*} callback
 */
export function render(
  element: React$Element<any>,
  container: DOMContainer,
  callback: ?Function,
) {
  invariant(
    isValidContainer(container),
    'Target container is not a DOM element.',
  );
  return legacyRenderSubtreeIntoContainer(
    null,
    element,
    container,
    false,
    callback,
  );
}

/**
 * render方法真正調(diào)用的主方法
 * 主要步驟有初次渲染,創(chuàng)建fiberroot對象->將更新
 */
function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: DOMContainer,
  forceHydrate: boolean,
  callback: ?Function,
) {
  let root: RootType = (container._reactRootContainer: any);
  let fiberRoot;
  // 首次渲染時不存在這個元素,初次渲染進(jìn)入這個邏輯
  if (!root) {
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // 初次渲染不需要批處理要立即同步更新
    unbatchedUpdates(() => {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // 不是首次渲染,比如之后調(diào)用setState更新都會將更新加入隊列,等待事務(wù)調(diào)度更新
    updateContainer(children, fiberRoot, parentComponent, callback);
  }
  return getPublicRootInstance(fiberRoot);
}

這里我們實際調(diào)用的是legacyRenderSubtreeIntoContainer,將我們傳入的組件也就是element掛載到傳入的dom元素上。
首先我們會獲取dom上的一個root元素,如果沒有證明我們是初次渲染,如果不是調(diào)用更新的方法。
官網(wǎng)上有一段例子,我覺得能很好理解這個過程當(dāng)我們第一次執(zhí)行tick會走初次渲染的邏輯,后邊的我們會走更新的邏輯,這也是為什么我們不用setState也能達(dá)到更新頁面的效果

function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  // 重復(fù)調(diào)用
  ReactDOM.render(element, document.getElementById('root'));
}
setInterval(tick, 1000);

在react中我們將這個root叫fiberRoot元素,這是整個渲染樹唯一的根節(jié)點,上邊相應(yīng)的也會掛載很多屬性。這里我們先不去看這個數(shù)據(jù)結(jié)構(gòu)。只看大體流程這里我們將的是render所以只說初次渲染的邏輯
我們實際會在unbatchedUpdates中調(diào)用updateContainer
這個unbatchedUpdates實際上是一種強制同步更新的方法我們先看源碼。這里我們其實就是處理了傳入函數(shù)的executionContext上下文
executionContext &= ~BatchedContext;executionContext |= LegacyUnbatchedContext;這里的意思就是我們要將LegacyUnbatchedContext這種類型合并進(jìn)當(dāng)前上下文,在方法執(zhí)行完后再恢復(fù)之前的執(zhí)行環(huán)境。
當(dāng)在這種上下文的環(huán)境下react的更新會走同步的邏輯,因為這是第一次更新,用戶要盡快的看到頁面的內(nèi)容,所以不需要走異步更新的邏輯

/**
 * 同步更新任務(wù)
 */
export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
  const prevExecutionContext = executionContext;
  executionContext &= ~BatchedContext;
  executionContext |= LegacyUnbatchedContext;
  try {
    return fn(a);
  } finally {
    executionContext = prevExecutionContext;
    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batch
      flushSyncCallbackQueue();
    }
  }
}

接下來我們就要看看這個updateContainer了,這里我們進(jìn)入了更新的主邏輯。方便理解還是先貼出主要代碼。

/**
 * 更新的主邏輯,
 * 計算過期時間->創(chuàng)建更新的update對象->加入到調(diào)度隊列->并開啟任務(wù)調(diào)度
 */
export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): ExpirationTime {
  // 獲取root上的根fiber對象
  const current = container.current;
  // 獲取當(dāng)前的時間節(jié)點
  const currentTime = requestCurrentTimeForUpdate();
  // 計算當(dāng)前的到期時間
  const expirationTime = computeExpirationForFiber(
    currentTime,
    current,
    suspenseConfig,
  );
  // 處理context相關(guān)
  const context = getContextForSubtree(parentComponent);
  if (container.context === null) {
    container.context = context;
  } else {
    container.pendingContext = context;
  }
  // 生成update對象,是批處理更新的一個單元
  const update = createUpdate(expirationTime, suspenseConfig);
  // 為update對象具體要更新的參數(shù)賦值,傳入的是ReactElement元素
  update.payload = {element};
  // 將update將入fiber根對象上的任務(wù)隊列
  enqueueUpdate(current, update);
  // 開始執(zhí)行任務(wù)調(diào)度,在到期時間內(nèi)
  scheduleWork(current, expirationTime);

  return expirationTime;
}

主要流程就是如下步驟 計算過期時間->創(chuàng)建更新的update對象->加入到調(diào)度隊列->并開啟任務(wù)調(diào)度
什么是過期時間,說到這里要先說下react16之后的新概念fiber,能支持我們在執(zhí)行耗時任務(wù)的時候可以跳出來相應(yīng)一些高優(yōu)先級的事件,比如我們在一個循環(huán)中執(zhí)行一些復(fù)雜計算。但這時候用戶通過input打字,我們就要即時響應(yīng)輸入操作,這在原來是做不到的。我們來實現(xiàn)這個功能主要靠的就是expirationTime過期時間這個概念。保證任務(wù)要在這個時間段內(nèi)完成,如果超時了那么就要立即在下一個事件循環(huán)中完成
然后就是生成一個update對象用來記錄更新的內(nèi)容,將這個update對象插入rootFiber上的更新隊列(基于鏈表實現(xiàn))
最后開啟任務(wù)調(diào)度,這里render的執(zhí)行階段就執(zhí)行完了,接下來的任務(wù)就交給react的任務(wù)調(diào)度器去完成這也是下一篇要說的

expirationTime

先來看看關(guān)于過期時間的計算

// 值越大優(yōu)先級越高
export function msToExpirationTime(ms: number): ExpirationTime {
  // Always add an offset so that we don't clash with the magic number for NoWork.
  return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0);
}
function ceiling(num: number, precision: number): number {
  return (((num / precision) | 0) + 1) * precision;
}

function computeExpirationBucket(
  currentTime,
  expirationInMs,
  bucketSizeMs,
): ExpirationTime {
  return (
    MAGIC_NUMBER_OFFSET -
    ceiling(
      MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
      bucketSizeMs / UNIT_SIZE,
    )
  );
}

export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;
// 計算高優(yōu)先級的時間
export function computeInteractiveExpiration(currentTime: ExpirationTime) {
  return computeExpirationBucket(
    currentTime,
    HIGH_PRIORITY_EXPIRATION,
    HIGH_PRIORITY_BATCH_SIZE,
  );
}

export const LOW_PRIORITY_EXPIRATION = 5000;
export const LOW_PRIORITY_BATCH_SIZE = 250;
// 計算低優(yōu)先級的時間也就是過期時間
export function computeAsyncExpiration(
  currentTime: ExpirationTime,
): ExpirationTime {
  return computeExpirationBucket(
    currentTime,
    LOW_PRIORITY_EXPIRATION,
    LOW_PRIORITY_BATCH_SIZE,
  );
}

這里主要用的就是computeAsyncExpirationcomputeInteractiveExpiration這個兩個不同優(yōu)先級時間的計算,關(guān)于ceiling的計算我這里有一個例子

ceiling(10011, 10)//10020
ceiling(10019, 10)//10020

可以看到在計算值的時候會在每10個時間間隔內(nèi)的過期時間都相同,相對的Async的間隔為25,而Interactive的時間間隔為10。這也保證了在這個時間間隔內(nèi)的時間都會有相同的過期時間,這保證了在這段時間內(nèi)觸發(fā)的任務(wù)的優(yōu)先級相同。保證一同觸發(fā)的任務(wù)同時完成

再來看看當(dāng)前時間的獲取

/**
 * 計算當(dāng)前時間
 */
export function requestCurrentTimeForUpdate() {
  if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
    // We're inside React, so it's fine to read the actual time.
    // 執(zhí)行的上下文是render或者commit,在執(zhí)行階段獲取真實時間
    return msToExpirationTime(now());
  }
  // We're not inside React, so we may be in the middle of a browser event.
  // 如果我們沒在react內(nèi)部更新中,可能是在執(zhí)行瀏覽器的任務(wù)中
  if (currentEventTime !== NoWork) {
    // Use the same start time for all updates until we enter React again.
    return currentEventTime;
  }
  // This is the first update since React yielded. Compute a new start time.
  // 之前的任務(wù)已經(jīng)執(zhí)行完,開啟新的任務(wù)時候需要重新計算時間
  currentEventTime = msToExpirationTime(now());
  return currentEventTime;
}


首先在render和commit階段我們直接獲取當(dāng)前真實時間。
然后如果當(dāng)前有任務(wù)在執(zhí)行我們返回之前計算的當(dāng)前時間,這也就確保了幾毫秒之內(nèi)觸發(fā)任務(wù)我們會以相同的當(dāng)前時間計算。
最后如果沒有任務(wù)我們計算一個新的當(dāng)前時間并賦給全局變量。
然后就是過期時間的計算了在初次渲染時會直接返回同步更新的標(biāo)識

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 坐在咖啡店的那把藍(lán)色木椅上, 挺直著脊背,望著窗外的婆娑。 徐徐的微風(fēng)吹佛水中的倒影, 垂柳不卑不亢,不聲不響。 ...
    輕安安閱讀 488評論 1 1
  • 以地質(zhì)堅硬,不易磨損的毛竹為骨架,將36根傘骨一根根打磨成一頭扁,一頭圓的形狀,用柿子膠把傘形紗紙黏合在傘骨上;再...
    _生花_閱讀 205評論 0 2
  • 例子一 在頁面中實時顯示當(dāng)前的時間 例子二 過濾器修改date的屬性值這時候圖片出現(xiàn)了這種形式的過濾頁面 過濾器 ...
    Frank_Yi閱讀 289評論 0 0
  • 今日體驗 今天下班有點早,晚上給奧迪換風(fēng)扇,由于第一次拆,也沒有點思路的去干,確實空間有點小不好拿,經(jīng)過第一次的失...
    任武科閱讀 137評論 0 1

友情鏈接更多精彩內(nèi)容