1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

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

        共 7159字,需瀏覽 15分鐘

         ·

        2021-05-09 03:22

        點擊上方 前端瓶子君,關注公眾號

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

        來源:MoonBall

        https://juejin.cn/post/6953804914715803678


        本文包括:

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

        Scheduler 簡介 —— 時間分片

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

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

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

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

        React 和 Scheduler 交互

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

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

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

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

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

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

        該過程可用如下偽代碼表達:

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

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

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

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

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

          scheduler.pushTask(task)

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

        Scheduler 是一種通用設計,不僅僅應用于 React

        上一節(jié)是 React 與 Scheduler 的交互過程。實際上 Scheduler 是一種通用設計,它可以應用于任何任務調(diào)度中。

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

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

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

        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()
        }
        復制代碼

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

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

        與 MessageChannel 的關系

        那 Scheduler 和 MessageChannel 有啥關系呢?

        關鍵點就在于當 scheduler.shouldYield() 返回 true 后,Scheduler 需要滿足以下功能點:

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

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

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

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

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

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

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

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

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

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

        原因是遞歸執(zhí)行 setTimeout(fn, 0) 時,最后間隔時間會變成 4 毫秒,而不是最初的 1 毫秒。可在瀏覽器中執(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()
        復制代碼

        運行結(jié)果為:

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

        有興趣的同學可以試試 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)
        復制代碼

        為什么不選擇 requestAnimationFrame(fn)

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

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

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

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

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

        總結(jié)

        React Scheduler 使用 MessageChannel 的原因為:生成宏任務,實現(xiàn):

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

        為什么不使用微任務呢?

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

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

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

        為什么不使用 rAF() 呢?

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

        其他 React 好文推薦

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

        招賢納士

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

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

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

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

        最后

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


        瀏覽 64
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            开心激情站 | 一级婬片A片AAA毛片艳谭 | 日韩欧美一区二区三区在线观看 | 成人啪啪视频在线 | 美女的大逼 | 九九久久久久 | 丁香五月婷婷五月天 | 3p两男一女被两根一起进漫画 | 久久这里只有 | 新五月激情 |