React 源碼解析 - React 創(chuàng)建更新回顧和 React 的批量更新

回顧 React 更新

創(chuàng)建更新

  • ReactDOM.render 初始渲染
    每次調(diào)用都通過傳入的 <App />, getElementById('app') 構(gòu)建 root 節(jié)點亿鲜,每個 rootFiber 都有獨立的 updateQueue 和 fiberTree芽世,最后調(diào)用 ReactRoot.prototypye.render 來創(chuàng)建更新容客。

  • setState & forceUpdate 更新渲染
    都是 Component 構(gòu)造函數(shù)的原型方法剥哑,目的都是給節(jié)點的 fiber 對象上創(chuàng)建更新,區(qū)別在于更新的類型不同铃慷。
    創(chuàng)建更新 update考抄,記錄當(dāng)前時間,計算 expirationTime沐绒,設(shè)置當(dāng)前更新的 payload俩莽,再把 update 推入 fiber 對象的 updateQueue 屬性上, 之后進(jìn)入調(diào)度流程。

expirationTime

由 ReactFiberReconciler.js 包乔遮,updateContainer 中的 const expirationTime = computeExpirationForFiber(currentTime, current); 計算出

function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
  let expirationTime;
// ...
    if (fiber.mode & ConcurrentMode) {
      if (isBatchingInteractiveUpdates) {
        // This is an interactive update 高優(yōu)先級
        expirationTime = computeInteractiveExpiration(currentTime);
      } else {
        // This is an async update 低優(yōu)先級
        expirationTime = computeAsyncExpiration(currentTime);
      }
    } else {
      // This is a sync update
      expirationTime = Sync;
    }
}
  • 簡化 computeExpirationForFiber 函數(shù)
  • 發(fā)現(xiàn) expirationTime 正常情況是 Sync = 1 同步的
  • 只有在 fiber.mode 存在并且使用 ConcurrentMode 新版本的異步更新模式時才會真正的計算 expirationTime
  • ConcurrentMode 模式下還會根據(jù) isBatchingInteractiveUpdates 全局變量判斷當(dāng)前更新的上下文環(huán)境來決定 expirationTime 是高優(yōu)先級還是低優(yōu)先級的運(yùn)算結(jié)果扮超。(isBatchingInteractiveUpdates 在 batchedUpdates 中講解)

scheduleWork 開始調(diào)度

核心功能

  • 找到更新對應(yīng)的 FiberRoot 節(jié)點
    setState 時傳入的都是組件的 Fiber 節(jié)點而不是 FiberRoot 節(jié)點
  • 符合條件時 - 重置 stack
    具有公共變量,用于調(diào)度和更新
  • 符合條件時 - 請求工作調(diào)度

回顧 FiberTree

  • FiberTree 屬性
    1 child 為第一個子節(jié)點
    2 sibling 為兄弟節(jié)點
    3 return 為父節(jié)點,只有 RootFiber 對象 renturn 為 null
    4 FiberRoot.current 和 RootFiber.stateNode 互相引用

  • 執(zhí)行操作時的 Fiber 對象
    1 點擊組件上的元素
    2 執(zhí)行組件的原型方法調(diào)用 setState
    3 把 RootFiber 加入到調(diào)度中

scheduleWork 進(jìn)入調(diào)度隊列

每一次進(jìn)入調(diào)度隊列的只有 FiberRoot 對象, 更新也是從 FiberRoot 對象上開始的出刷。

function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
  // 找到 root 更新 FiberTree 上的所有 expirationTime
  const root = scheduleWorkToRoot(fiber, expirationTime);
  if (root === null) { // 沒有 FiberRoot 暫停
    return;
  }

  if (
    !isWorking && // 沒有執(zhí)行渲染
    nextRenderExpirationTime !== NoWork && // 任務(wù)是個異步的璧疗,執(zhí)行到一半了,交還給瀏覽器執(zhí)行
    expirationTime < nextRenderExpirationTime // 新的任務(wù)優(yōu)先級高于現(xiàn)在的任務(wù)
  ) {
    // This is an interruption. (Used for performance tracking.)
    interruptedBy = fiber; // 記錄
    resetStack(); // 優(yōu)先執(zhí)行高優(yōu)先級任務(wù)
  }
  markPendingPriorityLevel(root, expirationTime);
  if (
    !isWorking || // 沒有正在工作
    isCommitting || // 或者正在提交馁龟,也就是更新dom 樹的渲染階段
    nextRoot !== root // 不同的 root 一般不存在不同
  ) {
    const rootExpirationTime = root.expirationTime;
    requestWork(root, rootExpirationTime); // 請求工作
  }
  if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
    // Reset this back to zero so subsequent updates don't throw.
    nestedUpdateCount = 0;
    invariant( false, 超出最大更新深度崩侠。 當(dāng)組件在componentWillUpdate或componentDidUpdate中重復(fù)調(diào)用setState時,可能會發(fā)生這種情況屁柏。 React限制嵌套更新的數(shù)量以防止無限循環(huán)啦膜。  );
  }
}

scheduleWorkToRoot 通過 Fiber 對象找到 RootFiber 對象進(jìn)行調(diào)度

  • 根據(jù)傳入的 Fiber 對象向上尋找到 RootFiber 對象
  • 同時更新所有子樹上面的 expirationTime
function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
  // ...
  if (  // 更新 fiber 對象上 expirationTime
    fiber.expirationTime === NoWork || // 沒有任何更新操作的
    fiber.expirationTime > expirationTime // 有更新產(chǎn)生,但是優(yōu)先級低于新計算的 expirationTime
  ) { // 設(shè)置成最新的 expirationTime
    fiber.expirationTime = expirationTime;
  }
  let alternate = fiber.alternate;
  if (
    alternate !== null &&
    (alternate.expirationTime === NoWork ||
      alternate.expirationTime > expirationTime)
  ) {
    // 邏輯和上面一樣淌喻,更新 alternate 的expirationTime
    alternate.expirationTime = expirationTime;
  }
 // 通過 FiberTree 的屬性向上尋找 FiberRoot 并更新每個子 fiber 對象的 expirationTime
  let node = fiber.return; // renturn 父節(jié)點僧家,
  let root = null;
  // node === null 就是 FiberRoot 對象
  if (node === null && fiber.tag === HostRoot) {
    root = fiber.stateNode;
  } else {
    // 循環(huán)查找 FiberRoot
    while (node !== null) {
      alternate = node.alternate;
      if ( // 更新 expirationTime
        node.childExpirationTime === NoWork ||
        node.childExpirationTime > expirationTime
      ) {
        node.childExpirationTime = expirationTime;
        if (
          alternate !== null &&
          (alternate.childExpirationTime === NoWork ||
            alternate.childExpirationTime > expirationTime)
        ) {
          alternate.childExpirationTime = expirationTime;
        }
      } else if (
        alternate !== null &&
        (alternate.childExpirationTime === NoWork ||
          alternate.childExpirationTime > expirationTime)
      ) {
        alternate.childExpirationTime = expirationTime;
      }
      // 找到 FiberRoot 結(jié)束循環(huán)
      if (node.return === null && node.tag === HostRoot) {
        root = node.stateNode;
        break;
      }
      // 繼續(xù)向父節(jié)點查找
      node = node.return;
    }
  }

resetStack

  • 當(dāng)發(fā)現(xiàn)當(dāng)前任務(wù)的優(yōu)先級大于下一個任務(wù)的優(yōu)先級時,把下個任務(wù)的優(yōu)先級重置執(zhí)行當(dāng)前任務(wù)
  • resetStack 在重置下個任務(wù)時裸删,會先記錄這個任務(wù)八拱,等待以后執(zhí)行,并且使用 unwindInterruptedWork 來重置這個任務(wù) fiber 上級的狀態(tài)
  if (
    !isWorking && // 沒有執(zhí)行渲染
    nextRenderExpirationTime !== NoWork && // 任務(wù)是個異步的涯塔,執(zhí)行到一半了肌稻,交還給瀏覽器執(zhí)行
    expirationTime < nextRenderExpirationTime // 新的任務(wù)優(yōu)先級高于現(xiàn)在的任務(wù)
  ) {
    // This is an interruption. (Used for performance tracking.)
    interruptedBy = fiber; // 記錄
    resetStack(); // 優(yōu)先執(zhí)行高優(yōu)先級任務(wù)
  }
function resetStack() {
  // nextUnitOfWork 被打斷的任務(wù)
  if (nextUnitOfWork !== null) {
    // 記錄,等待以后執(zhí)行
    let interruptedWork = nextUnitOfWork.return;
    while (interruptedWork !== null) {
      // 退回任務(wù)
      unwindInterruptedWork(interruptedWork);
      interruptedWork = interruptedWork.return;
    }
  }

  // 變回初始值匕荸,進(jìn)行新任務(wù)更新
  nextRoot = null;
  nextRenderExpirationTime = NoWork;
  nextLatestAbsoluteTimeoutMs = -1;
  nextRenderDidError = false;
  nextUnitOfWork = null;
}

何時執(zhí)行 requestWork

isWorking, isCommitting 是 react 渲染的兩個不同階段爹谭,

  • isWorking
    working 包含 committing(不可打斷)
  • isCommitting
    fiberTree 的更新已經(jīng)結(jié)束,正在提交也就是更新dom 樹的渲染階段, 不可打斷
  if (
    !isWorking || // 沒有正在工作
    isCommitting || // 或者正在提交榛搔,也就是更新dom 樹的渲染階段
    nextRoot !== root // 不同的 root 一般不存在不同
  ) {
    const rootExpirationTime = root.expirationTime; // 重新查找 root expirationTime诺凡,因為可能會改變
    requestWork(root, rootExpirationTime); // 請求工作
  }

requestWork

核心功能

  • 將 root 節(jié)點加入到 root調(diào)度隊列中
  • 判斷是否是批量更新
  • 最后根據(jù) expirationTime 的類型判斷調(diào)度的類型

requestWork 流程

function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
  addRootToSchedule(root, expirationTime); // 把當(dāng)前 root設(shè)置為最高優(yōu)先級
  // isRendering 調(diào)度已經(jīng)在執(zhí)行了, 循環(huán)已經(jīng)開始了
  if (isRendering) {
    return;
  }

  // 批量處理相關(guān)
  // 調(diào)用 setState 時在 enqueueUpdates 前 batchedUpdates 會把 isBatchingUpdates 設(shè)置成 true
  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; // isBatchingUpdates true // 普通的 setState 在進(jìn)入 enqueueUpdates 時在這里直接不執(zhí)行了,下面其實沒進(jìn)入調(diào)度
  }

  // TODO: Get rid of Sync and use current time?
  if (expirationTime === Sync) { // 同步的調(diào)用 js 代碼
    performSyncWork();
  } else { // 異步調(diào)度 獨立的 react 模塊包践惑,利用瀏覽器有空閑的時候進(jìn)行執(zhí)行腹泌,設(shè)置 deadline 在此之前執(zhí)行
    scheduleCallbackWithExpirationTime(root, expirationTime); // 在 secheduler 文件夾下的單獨模塊
  }
}

addRootToSchedule

  • 判斷當(dāng)前 root 是否調(diào)度過, 單個或多個 root 構(gòu)建成單向鏈表結(jié)構(gòu)
  • 如果調(diào)度過,設(shè)置當(dāng)前任務(wù)優(yōu)先級最高
function addRootToSchedule(root: FiberRoot, expirationTime: ExpirationTime) {
  // root.nextScheduledRoot 用來判斷是否有異步任務(wù)正在調(diào)度, 為 null 時會增加 nextScheduledRoot
  // 這個 root 還沒有進(jìn)入過調(diào)度
  if (root.nextScheduledRoot === null) {
    root.expirationTime = expirationTime;
    // lastScheduledRoot firstScheduledRoot 是單向鏈表結(jié)構(gòu)尔觉,表示多個 root 更新
    // 這里只有一個 root 只會在這里執(zhí)行
    if (lastScheduledRoot === null) {
      firstScheduledRoot = lastScheduledRoot = root;
      root.nextScheduledRoot = root;
    } else { // 有個多個root 時進(jìn)行單向鏈表的插入操作
      lastScheduledRoot.nextScheduledRoot = root;
      lastScheduledRoot = root;
      lastScheduledRoot.nextScheduledRoot = firstScheduledRoot;
    }
  } else { 
    // 傳入的 root 已經(jīng)進(jìn)入過調(diào)度, 把 root 的優(yōu)先級設(shè)置最高
    const remainingExpirationTime = root.expirationTime;
    // 如果 root 的 expirationTime 是同步或者優(yōu)先級低凉袱,增加為計算出的最高優(yōu)先級
    if (
      remainingExpirationTime === NoWork ||
      expirationTime < remainingExpirationTime
    ) {
      root.expirationTime = expirationTime; // 把當(dāng)前 root 的優(yōu)先級設(shè)置為當(dāng)前優(yōu)先級最高的
    }
  }
}

batchedUpdates 批量更新

  • 每次 react 創(chuàng)建更新都會執(zhí)行 requestWork。如: setState
  • 在 requestWork 中決定 react 的更新是異步調(diào)度還是同步執(zhí)行

setState 的調(diào)用

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() // 執(zhí)行的結(jié)果是 0, 0, 0

    // 方法二
    // 主動`batchedUpdates`
   setTimeout(() => {
      this.countNumber() // 執(zhí)行的結(jié)果是 1侦铜,2专甩,3
    }, 0)
    // 方法三
    // setTimeout中沒有`batchedUpdates`
    // setTimeout(() => {
    //   batchedUpdates(() => this.countNumber())  // 執(zhí)行的結(jié)果是 0, 0, 0
    // }, 0)

  }

  countNumber() {
    const num = this.state.number
    this.setState({
      number: num + 1,
    })
    console.log(this.state.number)
    this.setState({
      number: num + 2,
    })
    console.log(this.state.number)
    this.setState({
      number: num + 3,
    })
    console.log(this.state.number)
  }

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

requestWork

  • 當(dāng) setState 創(chuàng)建更新后進(jìn)入調(diào)度,執(zhí)行到 requestWork 里時會判斷一個 isBatchingUpdates 的全局變量钉稍。
  • 在 requestWork 中斷點
function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
  debugger
  // ...
  if (isRendering) {
    return;
  }

  // 批量處理相關(guān)
  // 調(diào)用 setState 時在 enqueueUpdates 前 batchedUpdates 會把 isBatchingUpdates 設(shè)置成 true
  if (isBatchingUpdates) {
    if (isUnbatchingUpdates) {
      nextFlushedRoot = root;
      nextFlushedExpirationTime = Sync;
      performWorkOnRoot(root, Sync, true);
    }
    return; // isBatchingUpdates true // 普通的 setState 在進(jìn)入 enqueueUpdates 時在這里直接不執(zhí)行了配深,下面其實沒進(jìn)入調(diào)度
  }
  // 只有異步模式任務(wù)時才會執(zhí)行
  }
  • 在 requestWork 中斷點,發(fā)現(xiàn)在判斷 isBatchingUpdates 變量時就直接返回了嫁盲,雖然 expirationTime 是 Sync 但是下面的 performSyncWork() 并不會執(zhí)行。
  • setState 時先執(zhí)行了一個 batchedUpdates 的函數(shù)。


  • 多次的 setState 在 enqueueUpdates 函數(shù)中羞秤,fiber 對象的 baseState 仍然是 0, 但是 fiber 對象上的 updateQueue 更新隊列上已經(jīng)記錄好了多次 update 對象將要更新 state 的 payload缸托。


batchedUpdates 的源碼

  • setState 在 batchedUpdates 中先把 isBatchingUpdates 暫存為 previousIsBatchingUpdates, 再設(shè)置為 true 防止在 requestWork 中執(zhí)行。
  • 在 try 代碼塊中執(zhí)行組件的方法 fn瘾蛋,fn 不論執(zhí)行多少次 setState 執(zhí)行完了都會通過 finally 進(jìn)入把 isBatchingUpdates 再設(shè)置回 false俐镐。
  • 最后通過執(zhí)行 performSyncWork() 方法,而不是在 requestWork 中調(diào)用哺哼。
function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
  const previousIsBatchingUpdates = isBatchingUpdates; // 初始為 false
  isBatchingUpdates = true;
  try {
    return fn(a); // 執(zhí)行組件綁定的方法, 走到 requestWork 里
  } finally {
    // setState 最終 enqueueUpdates 全部走到 requestWork 后變回 false 再一同 performSyncWork 才真正的執(zhí)行并改變 state
    isBatchingUpdates = previousIsBatchingUpdates; // 變回 false
    // 如果是 setTimeout(() => { this.setState }) setTimeout 走到這里后才執(zhí)行 this.setState 這時上下文環(huán)境是 window isBatchingUpdates 已經(jīng) false佩抹,setState 就是同步的
    if (!isBatchingUpdates && !isRendering) {
      performSyncWork(); // 當(dāng)所有 setState 執(zhí)行完全部enqueueUpdates 后代替 requestWork 來調(diào)度
    }
  }
}

方法二 setTimout 執(zhí)行方式

 setTimeout(() => {
   batchedUpdates(() => this.countNumber())
 }, 0)
  • setTimeout 等瀏覽器 API 執(zhí)行的方式結(jié)果都會把三次 setState 結(jié)算的結(jié)果打印出來,像是一種同步的執(zhí)行方式
  • 再次 debugger 取董,這時在 batchedUpdates 函數(shù)中的 fn 的執(zhí)行內(nèi)容只是 setTimeout棍苹。
  • 當(dāng) setTimeout 執(zhí)行完后直接進(jìn)入了 finally 代碼塊中,isBatchingUpdates 變回了 false
  • 當(dāng) setTimeout 結(jié)束執(zhí)行回調(diào)中的 setState 進(jìn)入 requestWork 時 isBatchingUpdates 已經(jīng)變?yōu)?false茵汰,requestWork 將會執(zhí)行下去枢里,最終執(zhí)行自己 performSyncWork()
  • 三次 setState 都會通過 requestWork 執(zhí)行 performSyncWork(),而不是之前通過 batchedUpdates 執(zhí)行一次蹂午,所以每次 setState 的 update 都會立刻改變 state栏豺,結(jié)果也是同步的輸出。

方法三 使用 batchedUpdates API

batchedUpdates 讓 setState 的更新仍然為批量更新

 setTimeout(() => {
   batchedUpdates(() => this.countNumber())
 }, 0)
  • batchedUpdates API 其實就是 batchedUpdates 函數(shù)
  • setTimeout 執(zhí)行回調(diào)時 batchedUpdates API 又把 isBatchingUpdates 設(shè)置為 true豆胸,讓 多次的 setState 又能進(jìn)行批量更新奥洼。
function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
  const previousIsBatchingUpdates = isBatchingUpdates; // 初始為 false
  isBatchingUpdates = true;
  try {
    return fn(a); // 執(zhí)行組件綁定的方法, 走到 requestWork 里
  } finally {
    isBatchingUpdates = previousIsBatchingUpdates; // 變回 false
    if (!isBatchingUpdates && !isRendering) {
      performSyncWork(); // 當(dāng)所有 setState 執(zhí)行完全部enqueueUpdates 后代替 requestWork 來調(diào)度
    }
  }
}

總結(jié) setState 是同步還是異步

  • setState 本身的方法調(diào)用時同步的,但是調(diào)用 setState 不表示 state 立即更新的晚胡,state 的更新是根據(jù)我們執(zhí)行環(huán)境的上下文來判斷的灵奖。
  • 如果處于批量更新的情況下 state 就不是立即更新的,如果不處于批量更新情況下有可能立即更新.
  • 現(xiàn)在有 asyncMode 異步渲染的情況搬泥,state 也不是立即更新的桑寨,需要進(jìn)入異步調(diào)度的過程。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末忿檩,一起剝皮案震驚了整個濱河市尉尾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌燥透,老刑警劉巖沙咏,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異班套,居然都是意外死亡肢藐,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門吱韭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吆豹,“玉大人,你說我怎么就攤上這事《幻海” “怎么了凑阶?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長衷快。 經(jīng)常有香客問我宙橱,道長,這世上最難降的妖魔是什么蘸拔? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任师郑,我火速辦了婚禮,結(jié)果婚禮上调窍,老公的妹妹穿的比我還像新娘宝冕。我一直安慰自己,他們只是感情好陨晶,可當(dāng)我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布猬仁。 她就那樣靜靜地躺著,像睡著了一般先誉。 火紅的嫁衣襯著肌膚如雪湿刽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天褐耳,我揣著相機(jī)與錄音诈闺,去河邊找鬼。 笑死铃芦,一個胖子當(dāng)著我的面吹牛雅镊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播刃滓,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼仁烹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了咧虎?” 一聲冷哼從身側(cè)響起卓缰,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎砰诵,沒想到半個月后征唬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡茁彭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年总寒,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片理肺。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡摄闸,死狀恐怖善镰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情贪薪,我是刑警寧澤媳禁,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站画切,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏囱怕。R本人自食惡果不足惜霍弹,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望娃弓。 院中可真熱鬧典格,春花似錦、人聲如沸台丛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽挽霉。三九已至防嗡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間侠坎,已是汗流浹背蚁趁。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留实胸,地道東北人他嫡。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像庐完,于是被迫代替她去往敵國和親钢属。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,033評論 2 355

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