React源碼04 - Fiber Scheduler (調(diào)度器)

創(chuàng)建更新之后,找到 Root 然后進(jìn)入調(diào)度哗咆,同步和異步操作完全不同咽扇,實(shí)現(xiàn)更新分片的性能優(yōu)化。

主流的瀏覽器刷新頻率為 60Hz洁段,即每(1000ms / 60Hz)16.6ms 瀏覽器刷新一次应狱。JS可以操作 DOM,JS線程GUI渲染線程 是互斥的祠丝。所以 **JS腳本執(zhí)行 **和 **瀏覽器布局疾呻、繪制 **不能同時(shí)執(zhí)行。
在每16.6ms時(shí)間內(nèi)写半,需要完成如下工作:

JS腳本執(zhí)行 -----  樣式布局 ----- 樣式繪制

既然以瀏覽器是否有剩余時(shí)間作為任務(wù)中斷的標(biāo)準(zhǔn)岸蜗,那么我們需要一種機(jī)制,當(dāng)瀏覽器在每一幀 16.6ms 中執(zhí)行完自己的 GUI 渲染線程后叠蝇,還有剩余時(shí)間的話能通知我們執(zhí)行 react 的異步更新任務(wù)璃岳,react 執(zhí)行時(shí)會(huì)自己計(jì)時(shí),如果時(shí)間到了悔捶,而 react 依然沒有執(zhí)行完铃慷,則會(huì)掛起自己,并把控制權(quán)還給瀏覽器蜕该,以便瀏覽器執(zhí)行更高優(yōu)先級(jí)的任務(wù)犁柜。然后 react 在下次瀏覽器空閑時(shí)恢復(fù)執(zhí)行。而如果是同步任務(wù)堂淡,則不會(huì)中斷馋缅,會(huì)一直占用瀏覽器直到頁面渲染完畢坛怪。

其實(shí)部分瀏覽器已經(jīng)實(shí)現(xiàn)了這個(gè)API,這就是 requestIdleCallback(字面意思:請求空閑回調(diào))股囊。但是由于以下因素袜匿,React 放棄使用:

  • 瀏覽器兼容性
  • 觸發(fā)頻率不穩(wěn)定,受很多因素影響稚疹。比如當(dāng)我們的瀏覽器切換tab后居灯,之前tab注冊的 requestIdleCallback 觸發(fā)的頻率會(huì)變得很低。

React 實(shí)現(xiàn)了功能更完備的 requestIdleCallback polyfill(使用window.requestAnimationFrame() 和 JavaScript 任務(wù)隊(duì)列進(jìn)行模擬)内狗,這就是 Scheduler怪嫌,除了在空閑時(shí)觸發(fā)回調(diào)的功能外,Scheduler 還提供了多種調(diào)度優(yōu)先級(jí)供任務(wù)設(shè)置柳沙。

當(dāng) Scheduler 將任務(wù)交給 Reconciler 后岩灭,Reconciler 會(huì)為變化的虛擬 DOM 打上代表增/刪/更新的標(biāo)記,類似這樣:

// 這種二進(jìn)制存儲(chǔ)數(shù)據(jù):
// 設(shè)置:集合 | 目標(biāo)
// 查詢:集合 & 目標(biāo)
// 取消:集合 & ~目標(biāo)

export const Placement = /*             */ 0b0000000000010;
export const Update = /*                */ 0b0000000000100;
export const PlacementAndUpdate = /*    */ 0b0000000000110;
export const Deletion = /*              */ 0b0000000001000;

整個(gè) Scheduler 與 Reconciler 的工作都在內(nèi)存中進(jìn)行赂鲤。只有當(dāng)所有組件都完成 Reconciler 的工作后噪径,才會(huì)統(tǒng)一交給 Renderer。

Renderer 根據(jù) Reconciler 為虛擬 DOM 打的標(biāo)記数初,同步執(zhí)行對應(yīng)的 DOM 操作找爱。


image.png
image.png

其中紅框中的步驟隨時(shí)可能由于以下原因被中斷:

  • 有其他更高優(yōu)任務(wù)需要先更新
  • 當(dāng)前幀沒有剩余時(shí)間

由于紅框中的工作都在內(nèi)存中進(jìn)行,不會(huì)更新頁面上的DOM泡孩,即使反復(fù)中斷用戶也不會(huì)看見更新不完全的DOM车摄。
因此可以說 Scheduler 和 Reconciler 是和平臺(tái)無關(guān)的,而和平臺(tái)相關(guān)的是 Renderer仑鸥。

大致更新調(diào)度的流程:

  • 首先通過 ReactDOM.render/setState/forceUpdate 產(chǎn)生更新吮播。
  • 找到產(chǎn)生更新的節(jié)點(diǎn)所對應(yīng)的 FiberRoot,將其加入到調(diào)度器中(多次調(diào)用 ReactDOM.render 就會(huì)有多個(gè) root)眼俊。
  • 根據(jù) expirationTime 判斷是同步更新意狠,還是異步更新。具體就是在上一篇提到的泵琳,在 computeExpirationForFiber() 計(jì)算過期時(shí)間時(shí)摄职,根據(jù) fiber.mode & ConcurrentMode 模式是否開啟誊役,來計(jì)算同步的過期時(shí)間获列,和異步的過期時(shí)間,也就是 同步更新任務(wù)異步更新任務(wù) 蛔垢。同步更新任務(wù)沒有 deadline(用于 react 執(zhí)行的分片時(shí)間)击孩,會(huì)立即執(zhí)行,不會(huì)被中斷鹏漆。而異步更新任務(wù)有 deadline巩梢,需要等待調(diào)度创泄,并可能會(huì)中斷。在此過程中括蝠,同步任務(wù)和異步任務(wù)最終會(huì)匯聚到一起鞠抑,根據(jù)是否有 deadline,會(huì)進(jìn)入同步循環(huán)條件或異步循環(huán)條件忌警,而這個(gè)循環(huán)就是指遍歷整棵 Fiber 樹的每個(gè)節(jié)點(diǎn)進(jìn)行更新的操作搁拙。同步任務(wù)遍歷完更新完就完了,而異步任務(wù)在更新節(jié)點(diǎn)時(shí)會(huì)受到分片調(diào)度的控制法绵。
  • 異步更新任務(wù)被加入到 Scheduler 的 callbackList 中等待調(diào)度箕速。調(diào)度時(shí),會(huì)檢查是否有任務(wù)已經(jīng)過期(expirationTime)朋譬,會(huì)先把所有已經(jīng)超時(shí)的任務(wù)執(zhí)行掉盐茎,直到遇到非超時(shí)的任務(wù)時(shí),如果當(dāng)前時(shí)間分片 deadline 還沒到點(diǎn)徙赢,則繼續(xù)執(zhí)行字柠,如果已經(jīng)到點(diǎn)了,則控制權(quán)交給瀏覽器狡赐。
  • 采用了 deadline 分片保證異步更新任務(wù)不會(huì)阻塞瀏覽器 GUI 的渲染募谎、如動(dòng)畫等能在 30FPS 以上。

TODO:Scheduler 圖阴汇,可能要自己手繪

2. scheduleWork

在上一篇 “React 中的更新” 提到数冬,ReactDOM.render/setState/forceUpdate 最終都會(huì)進(jìn)入 scheduleWork 即調(diào)度工作。

  • 找到更新所對應(yīng)的 FiberRoot 節(jié)點(diǎn)搀庶。
  • 如果符合條件則重置 stack
  • 如果符合條件就請求工作調(diào)度

TODO: Fiber 樹圖
**
點(diǎn)擊 button拐纱,是 List 組件實(shí)例調(diào)用了 setState,創(chuàng)建 update 后開始調(diào)度時(shí)哥倔,是將 List 所在的 RootFiber 這個(gè) fiber 根節(jié)點(diǎn)加入到調(diào)度隊(duì)列中(而并不是直接把 List Fiber 節(jié)點(diǎn)加入調(diào)度中)秸架,正如每次更新時(shí)也是從 RootFiber 開始更新。

一些全局變量:

  • isWorking: 用來標(biāo)志是否當(dāng)前有更新正在進(jìn)行咆蒿,不區(qū)分階段东抹,包含了 commit 和 render 階段
  • isCommitting: 是否處于 commit 階段

React在16版本之后處理任務(wù)分為兩個(gè)階段

  • render 階段: 判斷哪些變更需要被處理成 DOM,也就是比較上一次渲染的結(jié)果和新的更新沃测,打標(biāo)記缭黔。
  • commit 階段: 處理從 js 對象到 DOM 的更新,不會(huì)被打斷蒂破,并且會(huì)調(diào)用 componentDidMount 和 componentDidUpdate 這些生命周期方法馏谨。
function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
  const root = scheduleWorkToRoot(fiber, expirationTime);
  if (root === null) {
    return;
  }

  // 異步任務(wù)由于時(shí)間片不夠被中斷,執(zhí)行權(quán)了交給瀏覽器附迷,
  // 此時(shí)產(chǎn)生了更新任務(wù)惧互,其優(yōu)先級(jí)更高哎媚,則會(huì)打斷老的任務(wù)。
  if (
    !isWorking &&
    nextRenderExpirationTime !== NoWork &&
    expirationTime < nextRenderExpirationTime
  ) {
    // This is an interruption. (Used for performance tracking.)
    interruptedBy = fiber; // 給開發(fā)工具用的喊儡,用來展示被哪個(gè)節(jié)點(diǎn)打斷了異步任務(wù)
    resetStack(); // 重置之前的中斷的任務(wù)已經(jīng)產(chǎn)生的部分節(jié)點(diǎn)更新
  }
  // 暫時(shí)先忽略
  markPendingPriorityLevel(root, expirationTime);
  
  // 沒有正在進(jìn)行的工作拨与,或者是上一次render階段已經(jīng)結(jié)束(更新結(jié)束),已經(jīng)到了commit階段艾猜,
  // 那么可以繼續(xù)請求一次調(diào)度工作截珍。
  if (
    // If we're in the render phase, we don't need to schedule this root
    // for an update, because we'll do it before we exit...
    !isWorking ||
    isCommitting ||
    // ...unless this is a different root than the one we're rendering.
    nextRoot !== root // 單個(gè)FiberRoot入口時(shí)nextRoot永遠(yuǎn)等于root,所以基本不需要考慮這個(gè)條件箩朴,因?yàn)榛径际菃稳肟趹?yīng)用
  ) {
    const rootExpirationTime = root.expirationTime;
    requestWork(root, rootExpirationTime); // 請看下一節(jié)
  }
  
  // 檢測死循環(huán)更新
  if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
    // Reset this back to zero so subsequent updates don't throw.
    nestedUpdateCount = 0;
    invariant(
      false,
      'Maximum update depth exceeded. This can happen when a ' +
      'component repeatedly calls setState inside ' +
      'componentWillUpdate or componentDidUpdate. React limits ' +
      'the number of nested updates to prevent infinite loops.',
    );
  }
}

function resetStack() {
  // 用于記錄render階段Fiber樹遍歷過程中下一個(gè)需要執(zhí)行的節(jié)點(diǎn)岗喉,
  // 不為null說明之前存在更新任務(wù),只是時(shí)間片不夠了炸庞,被中斷了
  // 于是就要向上遍歷父節(jié)點(diǎn)钱床,將已經(jīng)更新了的狀態(tài)回退到更新之前,
  // 回退是為了避免狀態(tài)混亂埠居,不能把搞了一半的攤子直接丟給新的更新任務(wù),
  // 因?yàn)樾碌母邇?yōu)先級(jí)任務(wù)也要從根節(jié)點(diǎn)RootFiber開始更新查牌。
  if (nextUnitOfWork !== null) {
    let interruptedWork = nextUnitOfWork.return;
    while (interruptedWork !== null) {
      unwindInterruptedWork(interruptedWork);
      interruptedWork = interruptedWork.return;
    }
  }

  if (__DEV__) {
    ReactStrictModeWarnings.discardPendingWarnings();
    checkThatStackIsEmpty();
  }

  // 回退一些全局變量
  nextRoot = null;
  nextRenderExpirationTime = NoWork;
  nextLatestAbsoluteTimeoutMs = -1;
  nextRenderDidError = false;
  nextUnitOfWork = null;
}

scheduleWorkToRoot() 根據(jù)傳入的 Fiber 節(jié)點(diǎn),找到對應(yīng)的 FiberRoot(也就是最初調(diào)用 ReactDOM.render 時(shí)創(chuàng)建的 FiberRoot)滥壕。對于 ReactDOM.render 產(chǎn)生的調(diào)用 scheduleWork() 纸颜,其傳入的是 RootFiber 節(jié)點(diǎn), RootFiber.stateNode 就找到了 FiberRoot绎橘;而 setState/forceUpdate 傳入的是自身組件所對應(yīng)的 Fiber 子節(jié)點(diǎn)胁孙,會(huì)復(fù)雜一些:

function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
  // 記錄調(diào)度時(shí)間,
  recordScheduleUpdate();

  // 如果尚未更新過称鳞,沒有存儲(chǔ)過期時(shí)間涮较,或者最新計(jì)算出的過期時(shí)間更短,意味著優(yōu)先級(jí)更高冈止,
  // 那么就更新目標(biāo)Fiber上的過期時(shí)間 (即產(chǎn)生更新的那個(gè)Fiber狂票,如剛才的List組件對應(yīng)的Fiber)
  // Update the source fiber's expiration time
  if (
    fiber.expirationTime === NoWork ||
    fiber.expirationTime > expirationTime
  ) {
    fiber.expirationTime = expirationTime;
  }
  let alternate = fiber.alternate;
  // 同理如果alternate fiber(之前說過的雙緩存、雙buff機(jī)制)存在熙暴,也要嘗試更新過期時(shí)間
  if (
    alternate !== null &&
    (alternate.expirationTime === NoWork ||
      alternate.expirationTime > expirationTime)
  ) {
    alternate.expirationTime = expirationTime;
  }
  
  
  // Walk the parent path to the root and update the child expiration time.
  let node = fiber.return; // fiber.return指向的就是父fiber節(jié)點(diǎn)闺属。
  let root = null;
  // 只有RootFiber.return會(huì)是null,說明傳入的Fiber就是RootFiber周霉,其tag就是3掂器,也就是HostRoot 
  if (node === null && fiber.tag === HostRoot) { 
    root = fiber.stateNode; // 找到了 FiberRoot
  } else {
    // 向上遍歷,尋找FiberRoot
    while (node !== null) {
      alternate = node.alternate;
      if (
        node.childExpirationTime === NoWork ||
        node.childExpirationTime > expirationTime
      ) {
        // childExpirationTime: 父節(jié)點(diǎn)記錄了子樹中優(yōu)先級(jí)最高的過期時(shí)間诗眨,即最先的過期時(shí)間
        // 如果當(dāng)前傳入節(jié)點(diǎn)的過期時(shí)間優(yōu)先級(jí)更高唉匾,則更新父節(jié)點(diǎn)的childExpirationTime,
        // 同理還要更新父節(jié)點(diǎn)的alternate節(jié)點(diǎn)匠楚。
        node.childExpirationTime = expirationTime;
        if (
          alternate !== null &&
          (alternate.childExpirationTime === NoWork ||
            alternate.childExpirationTime > expirationTime)
        ) {
          alternate.childExpirationTime = expirationTime;
        }
      // node.childExpirationTime不需要更新巍膘,node.alternate.childExpirationTime也要嘗試更新
      } else if (
        alternate !== null &&
        (alternate.childExpirationTime === NoWork ||
          alternate.childExpirationTime > expirationTime)
      ) {
        alternate.childExpirationTime = expirationTime;
      }
      // 找到了 FiberRoot 就跳出
      if (node.return === null && node.tag === HostRoot) {
        root = node.stateNode;
        break;
      }
      // 沒找到則指針上溯,繼續(xù)下次while循環(huán)
      node = node.return;
    }
  }

  // 提醒當(dāng)前傳入的fiber沒有找到FiberRoot節(jié)點(diǎn)
  if (root === null) {
    if (__DEV__ && fiber.tag === ClassComponent) {
      warnAboutUpdateOnUnmounted(fiber);
    }
    return null;
  }

  // 跟蹤應(yīng)用更新的相關(guān)代碼芋簿,精力和水平有限峡懈,汪洋大海不再深究。
  if (enableSchedulerTracing) {
    const interactions = __interactionsRef.current;
    if (interactions.size > 0) {
      const pendingInteractionMap = root.pendingInteractionMap;
      const pendingInteractions = pendingInteractionMap.get(expirationTime);
      if (pendingInteractions != null) {
        interactions.forEach(interaction => {
          if (!pendingInteractions.has(interaction)) {
            // Update the pending async work count for previously unscheduled interaction.
            interaction.__count++;
          }

          pendingInteractions.add(interaction);
        });
      } else {
        pendingInteractionMap.set(expirationTime, new Set(interactions));

        // Update the pending async work count for the current interactions.
        interactions.forEach(interaction => {
          interaction.__count++;
        });
      }

      const subscriber = __subscriberRef.current;
      if (subscriber !== null) {
        const threadID = computeThreadID(
          expirationTime,
          root.interactionThreadID,
        );
        subscriber.onWorkScheduled(interactions, threadID);
      }
    }
  }

  return root;
}

3. requestWork

  • 加入到 root 調(diào)度隊(duì)列
  • 判斷是否批量更新
  • 根據(jù) expirationTime 判斷調(diào)度類型

requestWork:

// requestWork is called by the scheduler whenever a root receives an update.
// It's up to the renderer to call renderRoot at some point in the future.
function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
  addRootToSchedule(root, expirationTime);
  if (isRendering) {
    // Prevent reentrancy. Remaining work will be scheduled at the end of
    // the currently rendering batch.
    return;
  }
    // 關(guān)于批處理与斤,下一小節(jié)來探討
  if (isBatchingUpdates) {
    // Flush work at the end of the batch.
    if (isUnbatchingUpdates) {
      // ...unless we're inside unbatchedUpdates, in which case we should
      // flush it now.
      nextFlushedRoot = root;
      nextFlushedExpirationTime = Sync;
      performWorkOnRoot(root, Sync, true);
    }
    return;
  }

  // TODO: Get rid of Sync and use current time?
  if (expirationTime === Sync) { 
    // 同步調(diào)用js代碼直到結(jié)束為止肪康,不會(huì)被打斷。
    performSyncWork();
  } else { 
    // 否則進(jìn)行異步調(diào)度撩穿,進(jìn)入到了獨(dú)立的scheduler包中, 即react的requestIdleCallback polyfill
    // 等待有在deadline時(shí)間片內(nèi)才能得到執(zhí)行磷支,deadline到點(diǎn)后則控制權(quán)交給瀏覽器,等待下一個(gè)時(shí)間片食寡。
    scheduleCallbackWithExpirationTime(root, expirationTime);
  }
}

addRootToSchedule:

  • 檢查 root 是否已參與調(diào)度雾狈,沒有則加入到 root 調(diào)度隊(duì)列,實(shí)際就是單向鏈表添加節(jié)點(diǎn)的操作抵皱。當(dāng)然絕大多數(shù)時(shí)候善榛,我們的應(yīng)用只有一個(gè) root。
  • 如果 root 已經(jīng)參與調(diào)度呻畸,但可能需要提升優(yōu)先級(jí)移盆,使用 expirationTime 保存最高優(yōu)先級(jí)的任務(wù)。
function addRootToSchedule(root: FiberRoot, expirationTime: ExpirationTime) {
  // Add the root to the schedule.
  // Check if this root is already part of the schedule.
  if (root.nextScheduledRoot === null) {
    // This root is not already scheduled. Add it.
    root.expirationTime = expirationTime;
    if (lastScheduledRoot === null) {
      firstScheduledRoot = lastScheduledRoot = root;
      root.nextScheduledRoot = root;
    } else {
      lastScheduledRoot.nextScheduledRoot = root;
      lastScheduledRoot = root;
      lastScheduledRoot.nextScheduledRoot = firstScheduledRoot;
    }
  } else {
    // This root is already scheduled, but its priority may have increased.
    const remainingExpirationTime = root.expirationTime;
    if (
      remainingExpirationTime === NoWork ||
      expirationTime < remainingExpirationTime
    ) {
      // Update the priority.
      root.expirationTime = expirationTime;
    }
  }
}

4. batchUpdates

顧名思義伤为,批量更新咒循,可以避免短期內(nèi)的多次渲染,攢為一次性更新绞愚。

在后面提供的 demo 中的 handleClick 中有三種方式調(diào)用 this.countNumber() 剑鞍。
**
第1種:
批量更新,會(huì)打印 0 0 0爽醋,然后按鈕文本顯示為3蚁署。每次 setState 雖然都會(huì)經(jīng)過 enqueueUpdate (創(chuàng)建update 并加入隊(duì)列)-> scheduleWork(尋找對應(yīng)的 FiberRoot 節(jié)點(diǎn))-> requestWork (把 FiberRoot 加入到調(diào)度隊(duì)列),可惜上下文變量 isBatchingUpdates 在外部某個(gè)地方被標(biāo)記為了 true ,因此本次 setState 一路走來蚂四,尚未到達(dá)接下來的 performSyncWork 或者 scheduleCakkbackWithExpirationTime 就開始一路 return 出棧:

image

isBatchingUpdates 變量在早前的調(diào)用棧中(我們?yōu)?onClick 綁定的事件處理函數(shù)會(huì)被 react 包裹多層)光戈,被標(biāo)記為了 true ,然后 fn(a, b) 內(nèi)部經(jīng)過了3次 setState 系列操作遂赠,然后 finally 中 isBatchingUpdates 恢復(fù)為之前的 false久妆,此時(shí)執(zhí)行同步更新工作 performSyncWork

image

第2種:
handleClick 中使用 setTimeoutthis.countNumber 包裹了一層 setTimeout(() => { this.countNumber()}, 0) ,同樣要調(diào)用 handleClick 也是先經(jīng)過 interactiveUpdates$1 上下文跷睦,也會(huì)執(zhí)行 setTimeout 筷弦,然后 fn(a, b) 就執(zhí)行完了,因?yàn)樽罱K是瀏覽器來調(diào)用 setTimeout 的回調(diào) 然后執(zhí)行里面的 this.countNumber ,而對于 interactiveUpdates$1 來說繼續(xù)把自己的 performSyncWork 執(zhí)行完烂琴,就算結(jié)束了爹殊。顯然不管 performSyncWork 做了什么同步更新,我們的 setState 目前為止都還沒得到執(zhí)行奸绷。然后等到 setTimeout 的回調(diào)函數(shù)等到空閑被執(zhí)行的時(shí)候梗夸,才會(huì)執(zhí)行 setState ,此時(shí)沒有了批量更新之上下文号醉,所以每個(gè) setState 都會(huì)單獨(dú)執(zhí)行一遍 requestWork 中的 performSyncWork 直到渲染結(jié)束反症,且不會(huì)被打斷,3次 setState 就會(huì)整個(gè)更新渲染 3 遍(這樣性能不好畔派,所以一般不會(huì)這樣寫 react)铅碍。
什么叫不會(huì)被打斷的同步更新渲染?看一下 demo 中的輸出线椰,每次都同步打印出了最新的 button dom 的 innerText 胞谈。

第3種:
已經(jīng)可以猜到,無非就是因?yàn)槭褂?setTimeout 而“錯(cuò)過了”第一次的批量更新上下文士嚎,那等到 setTimeout 的回調(diào)執(zhí)行的時(shí)候呜魄,專門再創(chuàng)建一個(gè)批量更新上下文即可:

image.png
image.png

**
所以,setState 是同步還是異步莱衩?
**
setState 方法本身是被同步調(diào)用爵嗅,但并不代表 react 的 state 就會(huì)被立馬同步地更新,而是要根據(jù)當(dāng)前執(zhí)行上下文來判斷笨蚁。
如果處于批量更新的情況下睹晒,state 不會(huì)立馬被更新,而是批量更新括细。
如果非批量更新的情況下伪很,那么就“有可能”是立馬同步更新的。為什么不是“一定”奋单?因?yàn)槿绻?React 開啟了 Concurrent Mode锉试,非批量更新會(huì)進(jìn)入之前介紹過的異步調(diào)度中(時(shí)間分片)。

批量更新演示 demo:

import React from 'react'
import { unstable_batchedUpdates as batchedUpdates } from 'react-dom'

export default class BatchedDemo extends React.Component {
  state = {
    number: 0,
  }

  handleClick = () => {
    // 事件處理函數(shù)自帶batchedUpdates
    // this.countNumber()

    // setTimeout中沒有batchedUpdates
    setTimeout(() => {
      this.countNumber()
    }, 0)

    // 主動(dòng)batchedUpdates
    // setTimeout(() => {
    //   batchedUpdates(() => this.countNumber())
    // }, 0)
  }

  countNumber() {
    const button = document.getElementById('myButton')
    const num = this.state.number
    this.setState({
      number: num + 1,
    })
    console.log(this.state.number)
    console.log(button.innerText)
    this.setState({
      number: num + 2,
    })
    console.log(this.state.number)
    console.log(button.innerText)
    this.setState({
      number: num + 3,
    })
    console.log(this.state.number)
    console.log(button.innerText)
  }

  render() {
    return <button id="myButton" onClick={this.handleClick}>Num: {this.state.number}</button>
  }
}

5. Scheduler 調(diào)度器

  • 維護(hù)時(shí)間片
  • 模擬 requestIdleCallback
  • 調(diào)度列表和超時(shí)判斷

如果1秒30幀览濒,那么需要是平均的30幀呆盖,而不是前0.5秒1幀,后0.5秒29幀贷笛,這樣也會(huì)感覺卡頓的应又。
Scheduler 目的就是保證 React 執(zhí)行更新的時(shí)間,在瀏覽器的每一幀里不超過一定值乏苦。
不要過多占用瀏覽器用來渲染動(dòng)畫或者響應(yīng)用戶輸入的處理時(shí)間株扛。

image

image

**
繼續(xù)之前的源碼,requestWork 的最后,如果不是同步的更新任務(wù)洞就,那么就要參與 Scheduler 時(shí)間分片調(diào)度了:

  // TODO: Get rid of Sync and use current time?
  if (expirationTime === Sync) {
    performSyncWork();
  } else {
    scheduleCallbackWithExpirationTime(root, expirationTime);
  }

scheduleCallbackWithExpirationTime:

function scheduleCallbackWithExpirationTime(
  root: FiberRoot,
  expirationTime: ExpirationTime,
) {
  
  // 如果已經(jīng)有在調(diào)度的任務(wù)盆繁,那么調(diào)度操作本身就是在循環(huán)遍歷任務(wù),等待即可奖磁。
  if (callbackExpirationTime !== NoWork) {
    // A callback is already scheduled. Check its expiration time (timeout).
    // 因此改基,如果傳入的任務(wù)比已經(jīng)在調(diào)度的任務(wù)優(yōu)先級(jí)低繁疤,則返回
    if (expirationTime > callbackExpirationTime) {
      // Existing callback has sufficient timeout. Exit.
      return;
    } else {
      // 但是咖为!如果傳入的任務(wù)優(yōu)先級(jí)更高,則要打斷已經(jīng)在調(diào)度的任務(wù)
      if (callbackID !== null) {
        // Existing callback has insufficient timeout. Cancel and schedule a
        // new one.
        cancelDeferredCallback(callbackID);
      }
    }
    // The request callback timer is already running. Don't start a new one.
  } else {
    startRequestCallbackTimer(); // 涉及到開發(fā)工具和polyfill稠腊,略過
  }
    
  // 如果是取消了老的調(diào)度任務(wù)躁染,或者是尚未有調(diào)度任務(wù),則接下來會(huì)安排調(diào)度
  callbackExpirationTime = expirationTime;
  // 計(jì)算出任務(wù)的timeout架忌,也就是距離此刻還有多久過期
  const currentMs = now() - originalStartTimeMs; // originalStartTimeMs 代表react應(yīng)用最初被加載的那一刻
  const expirationTimeMs = expirationTimeToMs(expirationTime);
  const timeout = expirationTimeMs - currentMs;
  // 類似于 setTimeout 返回的 ID吞彤,可以用來延期回調(diào)
  callbackID = scheduleDeferredCallback(performAsyncWork, {timeout});
}

6. unstable_scheduleCallback

前面都還在 packages/react-reconciler/ReactFiberScheduler.js 中,下面就要跟著剛才的 **scheduleDeferredCallback **輾轉(zhuǎn)進(jìn)入到單獨(dú)的 packages/scheduler 包中:

  • 根據(jù)不同優(yōu)先級(jí)等級(jí)計(jì)算不同的 callbackNode 上的過期時(shí)間叹放。
  • 存儲(chǔ)以過期時(shí)間為優(yōu)先級(jí)的環(huán)形鏈表饰恕,用時(shí)可借助首節(jié)點(diǎn) firstCallbackNode 可對鏈表進(jìn)行遍歷讀取。
  • firstCallbackNode 變了后要調(diào)用 ensureHostCallbackIsScheduled 重新遍歷鏈表進(jìn)行調(diào)度井仰。
image

unstable_scheduleCallback:

function unstable_scheduleCallback(callback, deprecated_options) {
  var startTime =
    currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();

  var expirationTime;
  if (
    typeof deprecated_options === 'object' &&
    deprecated_options !== null &&
    typeof deprecated_options.timeout === 'number'
  ) {
    // FIXME: Remove this branch once we lift expiration times out of React.
    expirationTime = startTime + deprecated_options.timeout;
  } else {
    switch (currentPriorityLevel) {
      case ImmediatePriority:
        expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
        break;
      case UserBlockingPriority:
        expirationTime = startTime + USER_BLOCKING_PRIORITY;
        break;
      case IdlePriority:
        expirationTime = startTime + IDLE_PRIORITY;
        break;
      case NormalPriority:
      default:
        expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
    }
  }

  var newNode = {
    callback,
    priorityLevel: currentPriorityLevel,
    expirationTime,
    next: null,
    previous: null,
  };

  // Insert the new callback into the list, ordered first by expiration, then
  // by insertion. So the new callback is inserted any other callback with
  // equal expiration.
  if (firstCallbackNode === null) {
    // This is the first callback in the list.
    firstCallbackNode = newNode.next = newNode.previous = newNode;
    ensureHostCallbackIsScheduled();
  } else {
    var next = null;
    var node = firstCallbackNode;
    do {
      if (node.expirationTime > expirationTime) {
        // The new callback expires before this one.
        next = node;
        break;
      }
      node = node.next;
    } while (node !== firstCallbackNode);

    if (next === null) {
      // No callback with a later expiration was found, which means the new
      // callback has the latest expiration in the list.
      next = firstCallbackNode;
    } else if (next === firstCallbackNode) {
      // The new callback has the earliest expiration in the entire list.
      firstCallbackNode = newNode;
      ensureHostCallbackIsScheduled();
    }

    var previous = next.previous;
    previous.next = next.previous = newNode;
    newNode.next = next;
    newNode.previous = previous;
  }

  return newNode;
}

7. ensureHostCallbackIsScheduled

  • 該方法名字就說明了目的是保證 callback 會(huì)被調(diào)度埋嵌,故若已經(jīng)有 callbackNode 在被調(diào)度,自會(huì)自動(dòng)循環(huán)俱恶。
  • 從頭結(jié)點(diǎn)雹嗦,也就是最先過期的 callbackNode 開始請求調(diào)用,順表如果有已存在的調(diào)用要取消合是。這就是之前說過的參與調(diào)用的任務(wù)有兩種被打斷的可能:1. 時(shí)間片到點(diǎn)了了罪,2. 有更高優(yōu)先級(jí)的任務(wù)參與了調(diào)度
function ensureHostCallbackIsScheduled() {
  if (isExecutingCallback) {
    // Don't schedule work yet; wait until the next time we yield.
    return;
  }
  // Schedule the host callback using the earliest expiration in the list.
  var expirationTime = firstCallbackNode.expirationTime;
  if (!isHostCallbackScheduled) {
    isHostCallbackScheduled = true;
  } else {
    // Cancel the existing host callback.
    cancelHostCallback();
  }
  requestHostCallback(flushWork, expirationTime);
}
requestHostCallback = function(callback, absoluteTimeout) {
  scheduledHostCallback = callback;
  timeoutTime = absoluteTimeout;
  
  // 超時(shí)了要立即安排調(diào)用
  if (isFlushingHostCallback || absoluteTimeout < 0) {
    // Don't wait for the next frame. Continue working ASAP, in a new event.
    window.postMessage(messageKey, '*');
  } else if (!isAnimationFrameScheduled) {
    // 沒有超時(shí),就常規(guī)安排聪全,等待時(shí)間片
    isAnimationFrameScheduled = true;
    requestAnimationFrameWithTimeout(animationTick);
  }
};

// 取消之前安排的任務(wù)回調(diào)泊藕,就是重置一些變量
cancelHostCallback = function() {
  scheduledHostCallback = null;
  isMessageEventScheduled = false;
  timeoutTime = -1;
};

為了模擬 requestIdleCallback API:
傳給 window.requestanimationframe 的回調(diào)函數(shù)會(huì)在瀏覽器下一次重繪之前執(zhí)行,也就是執(zhí)行該回調(diào)后瀏覽器下面會(huì)立即進(jìn)入重繪难礼。使用 window.postMessage 技巧將空閑工作推遲到重新繪制之后娃圆。
具體太過復(fù)雜,就大概聽個(gè)響吧鹤竭,若要深究則深究:

  • animationTick
  • idleTick
// 僅供示意
requestAnimationFrameWithTimeout(animationTick);
var animationTick = function(rafTime) {
  requestAnimationFrameWithTimeout(animationTick);
}
window.addEventListener('message', idleTick, false);
window.postMessage(messageKey, '*');

react 這里還能統(tǒng)計(jì)判斷出平臺(tái)刷新頻率踊餐,來動(dòng)態(tài)減少 react 自身運(yùn)行所占用的時(shí)間片,支持的上限是 120hz 的刷新率臀稚,即每幀總共的時(shí)間不能低于 8ms吝岭。
此間如果一幀的時(shí)間在執(zhí)行 react js 之前就已經(jīng)被瀏覽器用完,那么對于非過期任務(wù),等待下次時(shí)間片窜管;而對于過期任務(wù)散劫,會(huì)強(qiáng)制執(zhí)行。

8. flushWork

ensureHostCallbackIsScheduled 中的 requestHostCallback(flushWork, expirationTime) 參與時(shí)間片調(diào)度:
flushWork:

  • 即使當(dāng)前時(shí)間片已超時(shí)幕帆,也要把 callbackNode 鏈表中所有已經(jīng)過期的任務(wù)先強(qiáng)制執(zhí)行掉
  • 若當(dāng)前幀還有時(shí)間片获搏,則常規(guī)處理任務(wù)
function flushWork(didTimeout) {
  isExecutingCallback = true;
  deadlineObject.didTimeout = didTimeout;
  try {
    // 把callbackNode鏈表中所有已經(jīng)過期的任務(wù)先強(qiáng)制執(zhí)行掉
    if (didTimeout) {
      // Flush all the expired callbacks without yielding.
      while (firstCallbackNode !== null) {
        // Read the current time. Flush all the callbacks that expire at or
        // earlier than that time. Then read the current time again and repeat.
        // This optimizes for as few performance.now calls as possible.
        var currentTime = getCurrentTime();
        if (firstCallbackNode.expirationTime <= currentTime) {
          do {
            flushFirstCallback();
          } while (
            firstCallbackNode !== null &&
            firstCallbackNode.expirationTime <= currentTime
          );
          continue;
        }
        break;
      }
    } else {
      // 當(dāng)前幀還有時(shí)間片,則繼續(xù)處理任務(wù)
      // Keep flushing callbacks until we run out of time in the frame.
      if (firstCallbackNode !== null) {
        do {
          flushFirstCallback();
        } while (
          firstCallbackNode !== null &&
          getFrameDeadline() - getCurrentTime() > 0
        );
      }
    }
  } finally {
    isExecutingCallback = false;
    if (firstCallbackNode !== null) {
      // There's still work remaining. Request another callback.
      ensureHostCallbackIsScheduled();
    } else {
      isHostCallbackScheduled = false;
    }
    // Before exiting, flush all the immediate work that was scheduled.
    flushImmediateWork();
  }
}

flushFirstCallback 負(fù)責(zé)處理鏈表節(jié)點(diǎn)失乾,然后執(zhí)行 flushedNode.callback常熙。

9. performWork

  • 是否有 deadline 的區(qū)分
  • 循環(huán)渲染 Root 的條件
  • 超過時(shí)間片的處理

performSyncWork 不會(huì)傳 deadline。
沒有deadline時(shí)碱茁,會(huì)循環(huán)執(zhí)行 root 上的同步任務(wù)裸卫,或者任務(wù)過期了,也會(huì)立馬執(zhí)行任務(wù)纽竣。

performAsyncWork:

function performAsyncWork(dl) {
  if (dl.didTimeout) { // 是否過期
    if (firstScheduledRoot !== null) {
      recomputeCurrentRendererTime();
      let root: FiberRoot = firstScheduledRoot;
      do {
        didExpireAtExpirationTime(root, currentRendererTime);
        // The root schedule is circular, so this is never null.
        root = (root.nextScheduledRoot: any);
      } while (root !== firstScheduledRoot);
    }
  }
  performWork(NoWork, dl);
}

performSyncWork:

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

performWork:

function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
  deadline = dl;

  // Keep working on roots until there's no more work, or until we reach
  // the deadline.
  findHighestPriorityRoot();

  if (deadline !== null) {
    recomputeCurrentRendererTime();
    currentSchedulerTime = currentRendererTime;

    if (enableUserTimingAPI) {
      const didExpire = nextFlushedExpirationTime < currentRendererTime;
      const timeout = expirationTimeToMs(nextFlushedExpirationTime);
      stopRequestCallbackTimer(didExpire, timeout);
    }

    while (
      nextFlushedRoot !== null &&
      nextFlushedExpirationTime !== NoWork &&
      (minExpirationTime === NoWork ||
        minExpirationTime >= nextFlushedExpirationTime) &&
      (!deadlineDidExpire || currentRendererTime >= nextFlushedExpirationTime)
    ) {
      performWorkOnRoot(
        nextFlushedRoot,
        nextFlushedExpirationTime,
        currentRendererTime >= nextFlushedExpirationTime,
      );
      findHighestPriorityRoot();
      recomputeCurrentRendererTime();
      currentSchedulerTime = currentRendererTime;
    }
  } else {
    while (
      nextFlushedRoot !== null &&
      nextFlushedExpirationTime !== NoWork &&
      (minExpirationTime === NoWork ||
        minExpirationTime >= nextFlushedExpirationTime)
    ) {
      performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, true);
      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 = null;
  }
  // If there's work left over, schedule a new callback.
  if (nextFlushedExpirationTime !== NoWork) {
    scheduleCallbackWithExpirationTime(
      ((nextFlushedRoot: any): FiberRoot),
      nextFlushedExpirationTime,
    );
  }

  // Clean-up.
  deadline = null;
  deadlineDidExpire = false;

  finishRendering();
}

performWorkOnRoot:

  • isRendering 標(biāo)記現(xiàn)在開始渲染了
  • 判斷 finishedWork:是:調(diào)用 completeRoot 進(jìn)入下一章的 commit 階段墓贿;否:調(diào)用 renderRoot 遍歷 Fiber 樹。
function performWorkOnRoot(
  root: FiberRoot,
  expirationTime: ExpirationTime,
  isExpired: boolean,
) {
    
  isRendering = true;

  // Check if this is async work or sync/expired work.
  if (deadline === null || isExpired) {
    // Flush work without yielding.
    // TODO: Non-yieldy work does not necessarily imply expired work. A renderer
    // may want to perform some work without yielding, but also without
    // requiring the root to complete (by triggering placeholders).

    let finishedWork = root.finishedWork;
    if (finishedWork !== null) {
      // This root is already complete. We can commit it.
      completeRoot(root, finishedWork, expirationTime);
    } else {
      root.finishedWork = null;
      // If this root previously suspended, clear its existing timeout, since
      // we're about to try rendering again.
      const timeoutHandle = root.timeoutHandle;
      if (timeoutHandle !== noTimeout) {
        root.timeoutHandle = noTimeout;
        // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
        cancelTimeout(timeoutHandle);
      }
      const isYieldy = false;
      renderRoot(root, isYieldy, isExpired);
      finishedWork = root.finishedWork;
      if (finishedWork !== null) {
        // We've completed the root. Commit it.
        completeRoot(root, finishedWork, expirationTime);
      }
    }
  } else {
    // Flush async work.
    let finishedWork = root.finishedWork;
    if (finishedWork !== null) {
      // This root is already complete. We can commit it.
      completeRoot(root, finishedWork, expirationTime);
    } else {
      root.finishedWork = null;
      // If this root previously suspended, clear its existing timeout, since
      // we're about to try rendering again.
      const timeoutHandle = root.timeoutHandle;
      if (timeoutHandle !== noTimeout) {
        root.timeoutHandle = noTimeout;
        // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
        cancelTimeout(timeoutHandle);
      }
      const isYieldy = true;
      renderRoot(root, isYieldy, isExpired);
      finishedWork = root.finishedWork;
      if (finishedWork !== null) {
        // We've completed the root. Check the deadline one more time
        // before committing.
        if (!shouldYield()) {
          // Still time left. Commit the root.
          completeRoot(root, finishedWork, expirationTime);
        } else {
          // There's no time left. Mark this root as complete. We'll come
          // back and commit it later.
          root.finishedWork = finishedWork;
        }
      }
    }
  }

  isRendering = false;
}

10. renderRoot

  • 調(diào)用 workLoop 進(jìn)行循環(huán)單元更新
  • 捕獲錯(cuò)誤并進(jìn)行處理
  • 走完流程之后善后

**renderRoot **流程:

  • 遍歷 Fiber 樹的每個(gè)節(jié)點(diǎn)蜓氨。

根據(jù) Fiber 上的 updateQueue 是否有內(nèi)容聋袋,決定是否要更新那個(gè) Fiber 節(jié)點(diǎn),并且計(jì)算出新的 state穴吹,
對于異步任務(wù)幽勒,更新每個(gè) Fiber 節(jié)點(diǎn)時(shí)都要判斷時(shí)間片是否過期,如果一個(gè) Fiber 更新時(shí)出錯(cuò)刀荒,則其子節(jié)點(diǎn)就不用再更新了代嗤。最終整個(gè) Fiber 樹遍歷完之后,根據(jù)捕獲到的問題不同缠借,再進(jìn)行相應(yīng)處理干毅。

  • createWorkInProgress:renderRoot 中,調(diào)用 createWorkInProgress 創(chuàng)建 “workInProgress” 樹泼返,在其上進(jìn)行更新操作硝逢。在 renderRoot 開始之后,所有的操作都在 “workInProgress” 樹上進(jìn)行绅喉,而非直接操作 “current” 樹渠鸽。(雙buff機(jī)制)
  • workLoop:開始更新一顆 Fiber 樹上的每個(gè)節(jié)點(diǎn), isYieldy 指示是否可以中斷柴罐,對于 sync 任務(wù)和已經(jīng)超時(shí)的任務(wù)都是不可中斷的徽缚,于是 while 循環(huán)更新即可;對于可中斷的革屠,則每次 while 循環(huán)條件中還要判斷是否時(shí)間片到點(diǎn)需先退出凿试。
function workLoop(isYieldy) {
  if (!isYieldy) {
    // Flush work without yielding
    while (nextUnitOfWork !== null) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  } else {
    // Flush asynchronous work until the deadline runs out of time.
    while (nextUnitOfWork !== null && !shouldYield()) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  }
}
  • performUnitOfWork:更新子樹:
function performUnitOfWork(workInProgress: Fiber): Fiber | null {
  const current = workInProgress.alternate;
  // See if beginning this work spawns more work.
  startWorkTimer(workInProgress);
  let next;
    if (enableProfilerTimer) {
    if (workInProgress.mode & ProfileMode) {
      startProfilerTimer(workInProgress);
    }

    next = beginWork(current, workInProgress, nextRenderExpirationTime);
    workInProgress.memoizedProps = workInProgress.pendingProps;

    if (workInProgress.mode & ProfileMode) {
      // Record the render duration assuming we didn't bailout (or error).
      stopProfilerTimerIfRunningAndRecordDelta(workInProgress, true);
    }
  } else {
    next = beginWork(current, workInProgress, nextRenderExpirationTime);
    workInProgress.memoizedProps = workInProgress.pendingProps;
  }
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    next = completeUnitOfWork(workInProgress);
  }
  ReactCurrentOwner.current = null;
  return next;
}
  • beginWork:開始具體的節(jié)點(diǎn)更新排宰,下一章再說。

Root 節(jié)點(diǎn)具體怎么遍歷更新那婉,以及不同類型組件的更新板甘,將在下一篇探討。


image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末详炬,一起剝皮案震驚了整個(gè)濱河市盐类,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌呛谜,老刑警劉巖在跳,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件呻率,死亡現(xiàn)場離奇詭異硬毕,居然都是意外死亡礼仗,警方通過查閱死者的電腦和手機(jī)元践,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進(jìn)店門饥伊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來象浑,“玉大人愉豺,你說我怎么就攤上這事∶R颍” “怎么了蚪拦?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長冻押。 經(jīng)常有香客問我驰贷,道長,這世上最難降的妖魔是什么洛巢? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任括袒,我火速辦了婚禮,結(jié)果婚禮上稿茉,老公的妹妹穿的比我還像新娘锹锰。我一直安慰自己类垦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布城须。 她就那樣靜靜地躺著蚤认,像睡著了一般。 火紅的嫁衣襯著肌膚如雪糕伐。 梳的紋絲不亂的頭發(fā)上砰琢,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天,我揣著相機(jī)與錄音良瞧,去河邊找鬼陪汽。 笑死,一個(gè)胖子當(dāng)著我的面吹牛褥蚯,可吹牛的內(nèi)容都是我干的挚冤。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼赞庶,長吁一口氣:“原來是場噩夢啊……” “哼训挡!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起歧强,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤澜薄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后摊册,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肤京,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年茅特,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了忘分。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,599評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡白修,死狀恐怖妒峦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情熬荆,我是刑警寧澤舟山,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站卤恳,受9級(jí)特大地震影響累盗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜突琳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一若债、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧拆融,春花似錦蠢琳、人聲如沸啊终。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蓝牲。三九已至,卻和暖如春泰讽,著一層夾襖步出監(jiān)牢的瞬間例衍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工已卸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留佛玄,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓累澡,卻偏偏與公主長得像梦抢,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子愧哟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評論 2 348