<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          React Scheduler 時間分片為什么選擇使用 MessageChannel 實(shí)現(xiàn)

          共 7159字,需瀏覽 15分鐘

           ·

          2021-05-09 03:22

          點(diǎn)擊上方 前端瓶子君,關(guān)注公眾號

          回復(fù)算法,加入前端編程面試算法每日一題群

          來源:MoonBall

          https://juejin.cn/post/6953804914715803678


          本文包括:

          1. Scheduler 簡介 —— 時間分片
          2. 時間分片應(yīng)選擇微任務(wù)還是宏任務(wù)
          3. 為什么不選擇 setTimeout(fn, 0)
          4. 為什么不選擇 requestAnimationFrame(fn)

          Scheduler 簡介 —— 時間分片

          如果「組件 Render 過程耗時」或「參與調(diào)和階段的虛擬 DOM 節(jié)點(diǎn)很多」時,那么一次性完成所有組件的調(diào)和階段就會花費(fèi)較長時間。

          為了避免長時間執(zhí)行調(diào)和階段而引起頁面卡頓,React 團(tuán)隊(duì)提出了 Fiber 架構(gòu)和 Scheduler 任務(wù)調(diào)度。

          Fiber 架構(gòu)的目的是「能獨(dú)立執(zhí)行每個虛擬 DOM 的調(diào)和階段」,而不是每次執(zhí)行整個虛擬 DOM 樹的調(diào)和階段。

          Scheduler 的主要功能是時間分片,每隔一段時間就把主線程還給瀏覽器,避免長時間占用主線程。

          React 和 Scheduler 交互

          如果只考慮 React 和 Scheduler 的交互,則組件更新的流程如下:

          1. React 組件狀態(tài)更新,向 Scheduler 中存入一個任務(wù),該任務(wù)為 React 更新算法。
          2. Scheduler 調(diào)度該任務(wù),執(zhí)行 React 更新算法。
          3. React 在調(diào)和階段更新一個 Fiber 之后,會詢問 Scheduler 是否需要暫停。如果不需要暫停,則重復(fù)步驟 3,繼續(xù)更新下一個 Fiber。
          4. 如果 Scheduler 表示需要暫停,則 React 將返回一個函數(shù),該函數(shù)用于告訴 Scheduler 任務(wù)還沒有完成。Scheduler 將在未來某時刻調(diào)度該任務(wù)。

          在第一步中,Scheduler 需要暴露 pushTask() 方法,React 通過該方法存入任務(wù)。

          在第二步中,Scheduler 需要暴露 scheduleTask() 方法,用于調(diào)度任務(wù)。

          在第三步中,Scheduler 需要暴露 shouldYield() 方法,React 通過該方法決定是否需要暫停執(zhí)行該任務(wù)。

          在第四步中,Scheduler 判斷任務(wù)執(zhí)行后的返回值是否是一個函數(shù),如果是則說明任務(wù)未完成,將來還需要調(diào)度它。

          該過程可用如下偽代碼表達(dá):

          const scheduler = {
            pushTask() {
              // 1. 存入任務(wù)
            },

            scheduleTask() {
              // 2. 挑選一個任務(wù)并執(zhí)行
              const task = pickTask()
              const hasMoreTask = task()

              if (hasMoreTask) {
                // 4. 未來繼續(xù)調(diào)度
              }
            },

            shouldYield() {
              // 3. 由調(diào)用方調(diào)用,調(diào)用方判斷是否需要暫停
            },
          }

          // 當(dāng)用戶點(diǎn)擊時修改了組件狀態(tài),則偽代碼如下
          const handleClick = () => {
            // React 組件更新時,產(chǎn)生任務(wù)
            const task = () => {
              const fiber = root
              while (!scheduler.shouldYield() && fiber) {
                // reconciliation() 對當(dāng)前的 fiber 執(zhí)行調(diào)和階段
                // 并返回下一個 fiber
                fiber = reconciliation(fiber)
              }
            }

            scheduler.pushTask(task)

            // React 會在將來某個時間執(zhí)行 scheduler.scheduleTask()
            // 這里假設(shè)立即執(zhí)行 scheduler.scheduleTask()
            scheduler.scheduleTask()
          }
          復(fù)制代碼

          Scheduler 是一種通用設(shè)計(jì),不僅僅應(yīng)用于 React

          上一節(jié)是 React 與 Scheduler 的交互過程。實(shí)際上 Scheduler 是一種通用設(shè)計(jì),它可以應(yīng)用于任何任務(wù)調(diào)度中。

          舉個例子(為了舉例的例子),假設(shè)我們要計(jì)算 1000 個整數(shù)的和,一次性遍歷的代碼如下:

          let sum = 0
          for (let i = 0; i < 1000; ++i) {
            sum += arr[i]
          }
          復(fù)制代碼

          假設(shè)執(zhí)行一次加法操作需要一毫秒,那么整個過程就需要一秒鐘,進(jìn)而導(dǎo)致頁面卡頓一秒。如果將該過程改為 Scheduler 調(diào)度的任務(wù),則代碼如下:

          const task = () => {
            let pos = 0
            let sum = 0
            const continuousExec = () => {
              for (; !scheduler.shouldYield() && pos < 1000; ++pos) {
                sum += arr[i]
              }

              if (pos === 1000) {
                return
              }

              return continuousExec
            }

            return continuousExec()
          }
          復(fù)制代碼

          當(dāng) scheduler.shouldYield() 返回 true 時,就暫停執(zhí)行任務(wù),此時瀏覽器便能更新頁面,避免頁面卡頓。

          可以將 Scheduler 這種調(diào)度方式理解為:當(dāng)前執(zhí)行函數(shù)返回執(zhí)行權(quán)給調(diào)用方,調(diào)用方可以在將來繼續(xù)執(zhí)行該函數(shù)。這種調(diào)度方式與生成器函數(shù)(Generator Function)的功能一模一樣,所以如果使用生成器函數(shù)來實(shí)現(xiàn) Scheduler 將變得更簡單。但 React 團(tuán)隊(duì)并沒有使用生成器函數(shù)實(shí)現(xiàn),主要原因是生成器函數(shù)是有狀態(tài)的,而 React 希望無狀態(tài)重新執(zhí)行該任務(wù)??蓞⒖脊俜浇忉尅?/p>

          與 MessageChannel 的關(guān)系

          那 Scheduler 和 MessageChannel 有啥關(guān)系呢?

          關(guān)鍵點(diǎn)就在于當(dāng) scheduler.shouldYield() 返回 true 后,Scheduler 需要滿足以下功能點(diǎn):

          1. 暫停 JS 執(zhí)行,將主線程還給瀏覽器,讓瀏覽器有機(jī)會更新頁面
          2. 在未來某個時刻繼續(xù)調(diào)度任務(wù),執(zhí)行上次還沒有完成的任務(wù)

          要滿足這兩點(diǎn)就需要調(diào)度一個宏任務(wù),因?yàn)楹耆蝿?wù)是在下次事件循環(huán)中執(zhí)行,不會阻塞本次頁面更新。而微任務(wù)是在本次頁面更新前執(zhí)行,與同步執(zhí)行無異,不會讓出主線程。事件循環(huán)可參考下圖,圖片來源于事件循環(huán)的進(jìn)一步探索。

          事件循環(huán)代碼.png

          使用 MessageChannel 的目的就是為了產(chǎn)生宏任務(wù)。在 Scheduler 中使用 MessageChannel 的代碼如下:

          const channel = new MessageChannel()
          const port = channel.port2

          // 每次 port.postMessage() 調(diào)用就會添加一個宏任務(wù)
          // 該宏任務(wù)為調(diào)用 scheduler.scheduleTask 方法
          channel.port1.onmessage = scheduler.scheduleTask

          const scheduler = {
            scheduleTask() {
              // 挑選一個任務(wù)并執(zhí)行
              const task = pickTask()
              const continuousTask = task()

              // 如果當(dāng)前任務(wù)未完成,則在下個宏任務(wù)繼續(xù)執(zhí)行
              if (continuousTask) {
                port.postMessage(null)
              }
            },
          }
          復(fù)制代碼

          為什么不選擇 setTimeout(fn, 0)

          setTimeout(fn, 0) 是我們最常用的創(chuàng)建宏任務(wù)的手段,為什么 React 沒選擇用它實(shí)現(xiàn) Scheduler 呢?

          原因是遞歸執(zhí)行 setTimeout(fn, 0) 時,最后間隔時間會變成 4 毫秒,而不是最初的 1 毫秒??稍跒g覽器中執(zhí)行以下代碼:

          var count = 0

          var startVal = +new Date()
          console.log("start time"00)
          function func({
            setTimeout(() => {
              console.log("exec time", ++count, +new Date() - startVal)
              if (count === 50) {
                return
              }
              func()
            }, 0)
          }

          func()
          復(fù)制代碼

          運(yùn)行結(jié)果為:

          如果使用 setTimeout(fn, 0) 實(shí)現(xiàn) Scheduler,就會浪費(fèi) 4 毫秒。因?yàn)?60 FPS 要求每幀間隔不超過 16.66 ms,所以 4ms 是不容忽視的浪費(fèi)。

          有興趣的同學(xué)可以試試 setInterval(fn, 0) 的效果,其結(jié)果與 setTimeout 相同。

          // setInterval 0ms 試試
          var count = 0
          var startVal = +new Date()
          var timer = setInterval(() => {
            console.log("exec time", ++count, +new Date() - startVal)
            if (count >= 50) {
              clearInterval(timer)
            }
          }, 0)
          復(fù)制代碼

          為什么不選擇 requestAnimationFrame(fn)

          我們知道 rAF() 是在頁面更新之前被調(diào)用。

          如果第一次任務(wù)調(diào)度不是由 rAF() 觸發(fā)的,例如直接執(zhí)行 scheduler.scheduleTask(),那么在本次頁面更新前會執(zhí)行一次 rAF() 回調(diào),該回調(diào)就是第二次任務(wù)調(diào)度。所以使用 rAF() 實(shí)現(xiàn)會導(dǎo)致在本次頁面更新前執(zhí)行了兩次任務(wù)。

          為什么是兩次,而不是三次、四次?因?yàn)樵?rAF() 的回調(diào)中再次調(diào)用 rAF(),會將第二次 rAF() 的回調(diào)放到下一幀前執(zhí)行,而不是在當(dāng)前幀前執(zhí)行。

          另一個原因是 rAF() 的觸發(fā)間隔時間不確定,如果瀏覽器間隔了 10ms 才更新頁面,那么這 10ms 就浪費(fèi)了。

          現(xiàn)有 WEB 技術(shù)中并沒有規(guī)定瀏覽器應(yīng)該什么何時更新頁面,所以通常認(rèn)為是在一次宏任務(wù)完成之后,瀏覽器自行判斷當(dāng)前是否應(yīng)該更新頁面。如果需要更新頁面,則執(zhí)行 rAF() 的回調(diào)并更新頁面。否則,就執(zhí)行下一個宏任務(wù)。

          總結(jié)

          React Scheduler 使用 MessageChannel 的原因?yàn)椋?strong style="color: black;">生成宏任務(wù),實(shí)現(xiàn):

          1. 將主線程還給瀏覽器,以便瀏覽器更新頁面。
          2. 瀏覽器更新頁面后繼續(xù)執(zhí)行未完成的任務(wù)。

          為什么不使用微任務(wù)呢?

          1. 微任務(wù)將在頁面更新前全部執(zhí)行完,所以達(dá)不到「將主線程還給瀏覽器」的目的。

          為什么不使用 setTimeout(fn, 0) 呢?

          1. 遞歸的 setTimeout() 調(diào)用會使調(diào)用間隔變?yōu)?4ms,導(dǎo)致浪費(fèi)了 4ms。

          為什么不使用 rAF() 呢?

          1. 如果上次任務(wù)調(diào)度不是 rAF() 觸發(fā)的,將導(dǎo)致在當(dāng)前幀更新前進(jìn)行兩次任務(wù)調(diào)度。
          2. 頁面更新的時間不確定,如果瀏覽器間隔了 10ms 才更新頁面,那么這 10ms 就浪費(fèi)了。

          其他 React 好文推薦

          1. React 性能優(yōu)化 | 包括原理、技巧、Demo、工具使用
          2. 聊聊 useSWR,為開發(fā)提效 - 包括 useSWR 設(shè)計(jì)思想、優(yōu)缺點(diǎn)和最佳實(shí)踐
          3. React 為什么使用 Lane 技術(shù)方案

          招賢納士

          筆者在成都-字節(jié)跳動-私有云方向,主要技術(shù)棧為 React + Node.js。團(tuán)隊(duì)擴(kuò)張速度快,組內(nèi)技術(shù)氛圍活躍。公有云私有云剛剛起步,有很多技術(shù)挑戰(zhàn),未來可期。

          有意愿者可通過該鏈接投遞簡歷:job.toutiao.com/s/e69g1rQ

          也可以添加我的微信 moonball_cxy,一起聊聊,交個朋友。

          原創(chuàng)不易,別忘了點(diǎn)贊鼓勵哦 ??

          最后

          歡迎關(guān)注【前端瓶子君】??ヽ(°▽°)ノ?
          回復(fù)「算法」,加入前端編程源碼算法群,每日一道面試題(工作日),第二天瓶子君都會很認(rèn)真的解答喲!
          回復(fù)「交流」,吹吹水、聊聊技術(shù)、吐吐槽!
          回復(fù)「閱讀」,每日刷刷高質(zhì)量好文!
          如果這篇文章對你有幫助,在看」是最大的支持
          》》面試官也在看的算法資料《《
          “在看和轉(zhuǎn)發(fā)”就是最大的支持


          瀏覽 64
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          為什么很多公司選擇使用考勤系統(tǒng)?
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产精视频| 一区二区三区国产乱伦 | 欧美日韩国产VA在线观看免费 | 国产成人在线综合豆花 | 国产熟妇乱伦 |