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)系作者
  • 序言:七十年代末盐捷,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子默勾,更是在濱河造成了極大的恐慌碉渡,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件母剥,死亡現(xiàn)場離奇詭異滞诺,居然都是意外死亡,警方通過查閱死者的電腦和手機环疼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進(jìn)店門习霹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人炫隶,你說我怎么就攤上這事淋叶。” “怎么了伪阶?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵煞檩,是天一觀的道長。 經(jīng)常有香客問我栅贴,道長斟湃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任筹误,我火速辦了婚禮桐早,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘厨剪。我一直安慰自己,他們只是感情好友存,可當(dāng)我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布祷膳。 她就那樣靜靜地躺著,像睡著了一般屡立。 火紅的嫁衣襯著肌膚如雪直晨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天,我揣著相機與錄音勇皇,去河邊找鬼罩句。 笑死,一個胖子當(dāng)著我的面吹牛敛摘,可吹牛的內(nèi)容都是我干的门烂。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼兄淫,長吁一口氣:“原來是場噩夢啊……” “哼屯远!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起捕虽,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤慨丐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后泄私,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體房揭,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年晌端,在試婚紗的時候發(fā)現(xiàn)自己被綠了崩溪。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡斩松,死狀恐怖伶唯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情惧盹,我是刑警寧澤乳幸,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站钧椰,受9級特大地震影響粹断,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜嫡霞,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一瓶埋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧诊沪,春花似錦养筒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至渐裸,卻和暖如春巫湘,著一層夾襖步出監(jiān)牢的瞬間装悲,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工尚氛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留诀诊,地道東北人。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓阅嘶,卻偏偏與公主長得像属瓣,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子奈懒,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容

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