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>

        深入"時(shí)間管理大師" —— React Scheduler

        共 24572字,需瀏覽 50分鐘

         ·

        2021-06-05 19:12

        眾所周知 React 的愿景就是快速響應(yīng)用戶(hù), 讓用戶(hù)覺(jué)得夠快, 不能阻塞用戶(hù)的交互. 而 Scheduler 作為 React 的調(diào)度中樞, 通過(guò)劃分優(yōu)先級(jí), 時(shí)間切片, 可中斷、可恢復(fù)任務(wù)等策略來(lái)保證高優(yōu)任務(wù)先被執(zhí)行, 以提高性能. 可謂"時(shí)間管理大師", 羅志祥本祥了.

        什么是 Scheduler

        Scheduler[1] 是內(nèi)置于 React 項(xiàng)目下的一個(gè)包, 你只需要將任務(wù)以及任務(wù)的優(yōu)先級(jí)交給它, 它就可以幫你進(jìn)行任務(wù)的協(xié)調(diào)調(diào)度. 目前 Scheduler 只被用于 React, 但團(tuán)隊(duì)的愿景是希望它能夠更通用化.

        Scheduler 用來(lái)做什么

        Scheduler 從宏觀(guān)和微觀(guān)對(duì)任務(wù)進(jìn)行管控. 宏觀(guān)上, 也就是對(duì)于多個(gè)任務(wù), Scheduler 根據(jù)優(yōu)先級(jí)來(lái)安排執(zhí)行順序; 而對(duì)于單個(gè)任務(wù)(微觀(guān)上), 需要"有節(jié)制"的執(zhí)行. 什么是"有節(jié)制"呢? 我們知道 JavaScript 是單線(xiàn)程的, 如果一個(gè)同步任務(wù)占用時(shí)間很長(zhǎng), 就會(huì)導(dǎo)致掉幀和卡頓. 因此需要把一個(gè)耗時(shí)的任務(wù)及時(shí)中斷掉, 去執(zhí)行更重要的任務(wù)(比如用戶(hù)交互), 后續(xù)再執(zhí)行該耗時(shí)任務(wù), 如此往復(fù). Scheduler 就是用這樣的模式, 將任務(wù)細(xì)粒度切分, 來(lái)避免一直占用有限的資源執(zhí)行耗時(shí)較長(zhǎng)的任務(wù), 實(shí)現(xiàn)更快的響應(yīng).

        原理綜述

        為了實(shí)現(xiàn)多個(gè)任務(wù)的管理 和 單個(gè)任務(wù)的控制, Scheduler 引入了兩個(gè)概念: 任務(wù)優(yōu)先級(jí)時(shí)間片. 任務(wù)優(yōu)先級(jí)讓任務(wù)按照自身的緊急程度排序, 這樣可以讓優(yōu)先級(jí)最高的任務(wù)最先被執(zhí)行到. 時(shí)間片規(guī)定的是單個(gè)任務(wù)在這一幀內(nèi)最大的執(zhí)行時(shí)間(yieldInterval = 5ms), 任務(wù)一旦執(zhí)行時(shí)間超過(guò)時(shí)間片, 則會(huì)被打斷, 轉(zhuǎn)而去執(zhí)行更高優(yōu)的任務(wù), 這樣可以保證頁(yè)面不會(huì)因?yàn)槿蝿?wù)執(zhí)行時(shí)間過(guò)長(zhǎng)而產(chǎn)生掉幀或者影響用戶(hù)交互.

        多個(gè)任務(wù)的管理

        在 Scheduler 中, 任務(wù)被分成了兩種: 未過(guò)期的任務(wù)已過(guò)期的任務(wù), 分別存儲(chǔ)在 timerQueue 和 taskQueue 兩個(gè)隊(duì)列中.

        如何區(qū)分兩種任務(wù)

        通過(guò)任務(wù)的開(kāi)始時(shí)間(startTime) 和 當(dāng)前時(shí)間(currentTime) 比較:

        ?當(dāng) startTime > currentTime, 說(shuō)明未過(guò)期, 存到 timerQueue?當(dāng) startTime <= currentTime, 說(shuō)明已過(guò)期, 存到 taskQueue

        入隊(duì)的任務(wù)如何排序

        即便是區(qū)分了 timerQueue 和 taskQueue, 但每個(gè)隊(duì)列中的任務(wù)也是有不同優(yōu)先級(jí)的, 因此在入隊(duì)時(shí)需要根據(jù)緊急程度將緊急的任務(wù)排在前面. 老版本的 React Scheduler 使用循環(huán)鏈表來(lái)串聯(lián), 代碼比較難懂, 這里不展開(kāi).

        目前源碼中使用小頂堆[2]這個(gè)數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn), 堆是優(yōu)先隊(duì)列[3]的底層實(shí)現(xiàn), 它在插入或者刪除元素的時(shí)候, 通過(guò)"上浮"和"下沉"操作來(lái)使元素自動(dòng)排序(優(yōu)先隊(duì)列經(jīng)常用來(lái)解決算法中 topK[4] 問(wèn)題). 需要注意的是, 堆的元素存儲(chǔ)在數(shù)組中, 而非鏈?zhǔn)浇Y(jié)構(gòu). 關(guān)于二叉堆相關(guān)的邏輯本文不去展開(kāi), 有興趣可以參考我學(xué)習(xí)數(shù)據(jù)結(jié)構(gòu)與算法[5]的倉(cāng)庫(kù).


        回到源碼, 當(dāng)我們插入任務(wù)時(shí), timerQueue 和 taskQueue 能保證元素是從小到大排序的. 那排序的依據(jù)是什么呢?

        ?timerQueue 中, 依據(jù)任務(wù)的開(kāi)始時(shí)間(startTime)排序, 開(kāi)始時(shí)間越早, 說(shuō)明會(huì)越早開(kāi)始, 開(kāi)始時(shí)間小的排在前面. 任務(wù)進(jìn)來(lái)的時(shí)候, 開(kāi)始時(shí)間默認(rèn)是當(dāng)前時(shí)間, 如果進(jìn)入調(diào)度的時(shí)候傳了延遲時(shí)間, 開(kāi)始時(shí)間則是當(dāng)前時(shí)間與延遲時(shí)間的和.?taskQueue 中, 依據(jù)任務(wù)的過(guò)期時(shí)間(expirationTime)排序, 過(guò)期時(shí)間越早, 說(shuō)明越緊急, 過(guò)期時(shí)間小的排在前面. 過(guò)期時(shí)間根據(jù)任務(wù)優(yōu)先級(jí)計(jì)算得出, 優(yōu)先級(jí)越高, 過(guò)期時(shí)間越早.

        任務(wù)的執(zhí)行

        ?對(duì)于 taskQueue, 因?yàn)槔锩娴娜蝿?wù)已經(jīng)過(guò)期了, 需要在 workLoop 中循環(huán)執(zhí)行完這些任務(wù)?對(duì)于 timerQueue, 它里面的任務(wù)都不會(huì)立即執(zhí)行, 但在 workLoop 方法中會(huì)通過(guò) advanceTimers 方法來(lái)檢測(cè)第一個(gè)任務(wù)是否過(guò)期, 如果過(guò)期了, 就放到 taskQueue 中.

        相較于單個(gè)任務(wù)的執(zhí)行(馬上會(huì)說(shuō)到), 任務(wù)隊(duì)列的管理屬于宏觀(guān)層面的范疇. 從 react-reconciler 計(jì)算的 Lane, 會(huì)被轉(zhuǎn)化成 Scheduler 可識(shí)別的任務(wù)優(yōu)先級(jí), 然后通過(guò)它去管理任務(wù)隊(duì)列中的任務(wù)順序. 總之來(lái)講, 就是越緊急的任務(wù), 它就需要被優(yōu)先處理.

        單個(gè)任務(wù)的中斷及恢復(fù)

        在循環(huán) taskQueue 執(zhí)行每一個(gè)任務(wù)時(shí), 如果某個(gè)任務(wù)執(zhí)行時(shí)間過(guò)長(zhǎng), 達(dá)到了時(shí)間片限制的時(shí)間, 那么該任務(wù)必須中斷, 以便于讓位給更重要的事情(如瀏覽器繪制), 等高優(yōu)過(guò)期任務(wù)完成了, 再恢復(fù)執(zhí)行該任務(wù). Scheduler 要實(shí)現(xiàn)這樣的調(diào)度效果需要兩個(gè)角色: 任務(wù)的調(diào)度者任務(wù)的執(zhí)行者. 調(diào)度者調(diào)度一個(gè)執(zhí)行者, 執(zhí)行者去循環(huán) taskQueue, 逐個(gè)執(zhí)行任務(wù). 當(dāng)某個(gè)任務(wù)的執(zhí)行時(shí)間比較長(zhǎng), 執(zhí)行者會(huì)根據(jù)時(shí)間片中斷任務(wù)執(zhí)行, 然后告訴調(diào)度者: 我現(xiàn)在正執(zhí)行的這個(gè)任務(wù)被中斷了, 還有一部分沒(méi)完成, 但現(xiàn)在必須讓位給更重要的事情, 你再調(diào)度一個(gè)執(zhí)行者吧, 好讓這個(gè)任務(wù)能在之后被繼續(xù)執(zhí)行完(任務(wù)的恢復(fù)). 于是, 調(diào)度者知道了任務(wù)還沒(méi)完成, 需要繼續(xù)做, 它會(huì)再調(diào)度一個(gè)執(zhí)行者去繼續(xù)完成這個(gè)任務(wù). 通過(guò)執(zhí)行者和調(diào)度者的配合, 可以實(shí)現(xiàn)任務(wù)的中斷和恢復(fù). 其實(shí)將任務(wù)掛起與恢復(fù)并不是一個(gè)新潮的概念, 它有一個(gè)名詞叫做協(xié)程[6], ES6 之后的生成器, 就可以用 yield 關(guān)鍵字來(lái)模擬協(xié)程的概念.

        源碼解析

        以上就是 Scheduler 的核心原理, talk is cheap, 想要真正搞懂, 還是得深入源碼才行. 我切了個(gè)分支專(zhuān)門(mén)來(lái)讀React 源碼[7], 看完下面的內(nèi)容可以再去 GayHub 上整體復(fù)習(xí)下.

        React 和 Scheduler 優(yōu)先級(jí)的轉(zhuǎn)換

        我們知道 React 的優(yōu)先級(jí)采用的是 Lane 模型, 而 Scheduler 是一個(gè)獨(dú)立的包, 有自己的一套優(yōu)先級(jí)機(jī)制, 因此需要做一個(gè)轉(zhuǎn)換. 這里摘錄 react-reconciler/src/ReactFiberWorkLoop.old(new).js 中的一部分.

        let newCallbackNode;
        // 同步
        if (newCallbackPriority === SyncLane) {
        // 執(zhí)行 scheduleSyncCallback 方法
        // 只不過(guò)要區(qū)分下 legacy 模式還是 concurrent 模式
        // scheduleSyncCallback 自己有個(gè) syncQueue, 用來(lái)承載同步任務(wù)
        // 并交由 flushSyncCallbacks 處理這些同步任務(wù)后, 再交由下面 scheduleCallback
        // 以最高優(yōu)先級(jí)讓 Scheduler 調(diào)度
        if (root.tag === LegacyRoot) {
        scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
        } else {
        scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
        }

        // 這里我們只談 scheduleCallback, 即以最高優(yōu)先級(jí)
        // ImmediateSchedulerPriority 來(lái)執(zhí)行同步任務(wù)
        if (supportsMicrotasks) {
        scheduleMicrotask(flushSyncCallbacks);
        } else {
        scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
        }
        newCallbackNode = null;
        } else {
        // 異步
        let schedulerPriorityLevel;
        // 需要將 lane 轉(zhuǎn)換為 Scheduler 可識(shí)別的優(yōu)先級(jí)
        switch (lanesToEventPriority(nextLanes)) {
        case DiscreteEventPriority:
        schedulerPriorityLevel = ImmediateSchedulerPriority;
        break;
        case ContinuousEventPriority:
        schedulerPriorityLevel = UserBlockingSchedulerPriority;
        break;
        case DefaultEventPriority:
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
        case IdleEventPriority:
        schedulerPriorityLevel = IdleSchedulerPriority;
        break;
        default:
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
        }
        // 通過(guò) scheduleCallback 將任務(wù)及其優(yōu)先級(jí)傳入到 Scheduler 中
        newCallbackNode = scheduleCallback(
        schedulerPriorityLevel,
        performConcurrentWorkOnRoot.bind(null, root)
        );
        }

        Scheduler 中的優(yōu)先級(jí)

        Scheduler 自身維護(hù) 6 種優(yōu)先級(jí), 不過(guò)翻了一遍源碼 NoPriority 沒(méi)被用過(guò). 它們是計(jì)算 expirationTime 的重要依據(jù), 而我們知道 expirationTime 事關(guān) taskQueue 的排序. 該文件位于 scheduler/src/SchedulerPriorities.js.

        export const NoPriority = 0; // 沒(méi)有任何優(yōu)先級(jí)
        export const ImmediatePriority = 1; // 立即執(zhí)行的優(yōu)先級(jí), 級(jí)別最高
        export const UserBlockingPriority = 2; // 用戶(hù)阻塞級(jí)別的優(yōu)先級(jí), 比如用戶(hù)輸入, 拖拽這些
        export const NormalPriority = 3; // 正常的優(yōu)先級(jí)
        export const LowPriority = 4; // 低優(yōu)先級(jí)
        export const IdlePriority = 5; // 最低階的優(yōu)先級(jí), 可以被閑置的那種

        scheduleCallback

        通過(guò)上面的介紹, 我們知道 Scheduler 的主入口是 scheduleCallback, 它負(fù)責(zé)生成調(diào)度任務(wù), 根據(jù)任務(wù)是否過(guò)期將任務(wù)放入 timerQueue 或 taskQueue, 然后觸發(fā)調(diào)度行為, 讓任務(wù)進(jìn)入調(diào)度. 注意: enableProfiling 用來(lái)做一些審計(jì)和 debugger, 本文不去涉及.

        1.首先計(jì)算 startTime, 它被用作 timerQueue 排序的依據(jù), getCurrentTime() 用來(lái)獲取當(dāng)前時(shí)間, 下面會(huì)講到.2.接著計(jì)算 expirationTime, 它被用作 taskQueue 排序的依據(jù), 過(guò)期時(shí)間通過(guò)傳入的優(yōu)先級(jí)確定.3.newTask 是 Scheduler 中任務(wù)單元的數(shù)據(jù)結(jié)構(gòu), 注釋寫(xiě)的很清楚, 其中 sortIndex 是優(yōu)先隊(duì)列(小頂堆)中排序的依據(jù).4.根據(jù)上面三步的鋪墊, 這一步就是根據(jù) startTime 和 currentTime 的關(guān)系將任務(wù)放到 timerQueue 或 taskQueue 之中, 然后觸發(fā)調(diào)度行為.

        function unstable_scheduleCallback(priorityLevel, callback, options) {
        /*
        * (1
        */
        var currentTime = getCurrentTime();

        // timerQueue 根據(jù) startTime 排序
        // 任務(wù)進(jìn)來(lái)的時(shí)候, 開(kāi)始時(shí)間默認(rèn)是當(dāng)前時(shí)間, 如果進(jìn)入調(diào)度的時(shí)候傳了延遲時(shí)間
        // 開(kāi)始時(shí)間則是當(dāng)前時(shí)間與延遲時(shí)間的和
        var startTime;
        if (typeof options === "object" && options !== null) {
        var delay = options.delay;
        if (typeof delay === "number" && delay > 0) {
        startTime = currentTime + delay;
        } else {
        startTime = currentTime;
        }
        } else {
        startTime = currentTime;
        }

        /*
        * (2
        */
        // taskQueue 根據(jù) expirationTime 排序
        var timeout;
        switch (priorityLevel) {
        case ImmediatePriority:
        timeout = IMMEDIATE_PRIORITY_TIMEOUT; // -1
        break;
        case UserBlockingPriority:
        timeout = USER_BLOCKING_PRIORITY_TIMEOUT; // 250
        break;
        case IdlePriority:
        timeout = IDLE_PRIORITY_TIMEOUT; // 1073741823 (2^30 - 1)
        break;
        case LowPriority:
        timeout = LOW_PRIORITY_TIMEOUT; // 10000
        break;
        case NormalPriority:
        default:
        timeout = NORMAL_PRIORITY_TIMEOUT; // 5000
        break;
        }

        // 計(jì)算任務(wù)的過(guò)期時(shí)間, 任務(wù)開(kāi)始時(shí)間 + timeout
        // 若是立即執(zhí)行的優(yōu)先級(jí)(IMMEDIATE_PRIORITY_TIMEOUT(-1))
        // 它的過(guò)期時(shí)間是 startTime - 1, 意味著立刻就過(guò)期
        var expirationTime = startTime + timeout;

        /*
        * (3
        */
        // 創(chuàng)建調(diào)度任務(wù)
        var newTask = {
        id: taskIdCounter++,
        callback, // 調(diào)度的任務(wù)
        priorityLevel, // 任務(wù)優(yōu)先級(jí)
        startTime, // 任務(wù)開(kāi)始的時(shí)間, 表示任務(wù)何時(shí)才能執(zhí)行
        expirationTime, // 任務(wù)的過(guò)期時(shí)間
        sortIndex: -1, // 在小頂堆隊(duì)列中排序的依據(jù)
        };

        if (enableProfiling) {
        newTask.isQueued = false;
        }

        /*
        * (4
        */
        // startTime > currentTime 說(shuō)明任務(wù)無(wú)需立刻執(zhí)行
        // 故放到 timerQueue 中
        if (startTime > currentTime) {
        // timerQueue 是通過(guò) startTime 判斷優(yōu)先級(jí)的,
        // 故將 startTime 設(shè)為 sortIndex 作為優(yōu)先級(jí)依據(jù)
        newTask.sortIndex = startTime;
        push(timerQueue, newTask);

        // 如果 taskQueue 是空的, 并且當(dāng)前任務(wù)優(yōu)先級(jí)最高
        // 那么這個(gè)任務(wù)就應(yīng)該優(yōu)先被設(shè)為 isHostTimeoutScheduled
        if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
        // 如果超時(shí)調(diào)度已經(jīng)在執(zhí)行了, 就取消掉
        // 因?yàn)楫?dāng)前這個(gè)任務(wù)是最高優(yōu)的, 需要先處理當(dāng)前這個(gè)任務(wù)
        if (isHostTimeoutScheduled) {
        cancelHostTimeout();
        } else {
        isHostTimeoutScheduled = true;
        }
        // Schedule a timeout.
        requestHostTimeout(handleTimeout, startTime - currentTime);
        }
        } else {
        // startTime <= currentTime 說(shuō)明任務(wù)已過(guò)期
        // 需將任務(wù)放到 taskQueue
        newTask.sortIndex = expirationTime;
        push(taskQueue, newTask);

        if (enableProfiling) {
        markTaskStart(newTask, currentTime);
        newTask.isQueued = true;
        }

        // 如果目前正在對(duì)某個(gè)過(guò)期任務(wù)進(jìn)行調(diào)度,
        // 當(dāng)前任務(wù)需要等待下次時(shí)間片讓出時(shí)才能執(zhí)行
        if (!isHostCallbackScheduled && !isPerformingWork) {
        isHostCallbackScheduled = true;
        requestHostCallback(flushWork);
        }
        }

        return newTask;
        }

        getCurrentTime

        顧名思義, getCurrentTime 用來(lái)獲取當(dāng)前時(shí)間, 它優(yōu)先使用 performance.now()[8], 否則使用 Date.now(). 提起 performance 我們并不陌生, 它主要被用來(lái)收集性能指標(biāo). performance.now() 返回一個(gè)精確到毫秒的 DOMHighResTimeStamp(emmm, 一看到 HighRes 就想起大法).

        let getCurrentTime;
        const hasPerformanceNow =
        typeof performance === "object" && typeof performance.now === "function";

        if (hasPerformanceNow) {
        const localPerformance = performance;
        getCurrentTime = () => localPerformance.now();
        } else {
        const localDate = Date;
        const initialTime = localDate.now();
        getCurrentTime = () => localDate.now() - initialTime;
        }

        稍微看了下 chromium 源碼[9](反正也看不懂啦), 大抵就是說(shuō) performance.now() 是個(gè)單調(diào)遞增的時(shí)間(monotonic_time), 這保證了兩個(gè)調(diào)用之間的差永遠(yuǎn)不會(huì)是負(fù)的; 此外還看到通過(guò) time_lower_digits 和 time_upper_digits 來(lái)做了一些降噪處理, 保證計(jì)算結(jié)果不會(huì)太突兀. 此外還有什么粗化時(shí)間算法(coarsen time algorithm)[10]就更尼瑪看不懂了.

        DOMHighResTimeStamp Performance::now() const {
        return MonotonicTimeToDOMHighResTimeStamp(tick_clock_->NowTicks());
        }

        requestHostTimeout 和 cancelHostTimeout

        顯然這是一對(duì)相愛(ài)相殺的好基友. 為了讓一個(gè)未過(guò)期的任務(wù)能夠到達(dá)恰好過(guò)期的狀態(tài), 那么需要延遲 startTime - currentTime 毫秒就可以了(其實(shí)它倆的差就是 XXX_PRIORITY_TIMEOUT), requestHostTimeout 就是來(lái)做這件事的, 而 cancelHostTimeout 就是用來(lái)取消這個(gè)超時(shí)函數(shù)的.

        function requestHostTimeout(callback, ms) {
        taskTimeoutID = setTimeout(() => {
        callback(getCurrentTime());
        }, ms);
        }

        function cancelHostTimeout() {
        clearTimeout(taskTimeoutID);
        taskTimeoutID = -1;
        }

        handleTimeout

        requestHostTimeout 的第一個(gè)參數(shù)是 handleTimeout, 讓我們來(lái)看看它是來(lái)做什么的. 首先調(diào)用了 advanceTimers 方法, 這個(gè)方法下面具體說(shuō), 它主要是用來(lái)更新 timerQueue 和 taskQueue 兩個(gè)序列, 如果發(fā)現(xiàn) timerQueue 有過(guò)期的, 就放到 taskQueue 中. 接下來(lái)如果沒(méi)有正在調(diào)度任務(wù), 就看看 taskQueue 中是否存在任務(wù), 如果有的話(huà)就先 flush 掉; 否則就遞歸執(zhí)行 requestHostTimeout(handleTimeout, ...). 總之來(lái)講, 這個(gè)方法就是要把 timerQueue 中的任務(wù)轉(zhuǎn)移到 taskQueue 中.

        function handleTimeout(currentTime) {
        isHostTimeoutScheduled = false;
        // 更新 timerQueue 和 taskQueue 兩個(gè)序列
        // 如果發(fā)現(xiàn) timerQueue 有過(guò)期的, 就放到 taskQueue 中
        advanceTimers(currentTime);

        // 檢查是否已經(jīng)開(kāi)始調(diào)度
        // 如果正在調(diào)度, 就什么都不做
        if (!isHostCallbackScheduled) {
        // 如果 taskQueue 中有任務(wù), 那就先去執(zhí)行已過(guò)期的任務(wù)
        if (peek(taskQueue) !== null) {
        isHostCallbackScheduled = true;
        requestHostCallback(flushWork);
        } else {
        // 如果沒(méi)有過(guò)期任務(wù), 那就接著對(duì)最高優(yōu)的第一個(gè)未過(guò)期的任務(wù)
        // 繼續(xù)重復(fù)這個(gè)過(guò)程, 直到它可以被放置到 taskQueue
        const firstTimer = peek(timerQueue);
        if (firstTimer !== null) {
        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
        }
        }
        }
        }

        advanceTimers

        這個(gè)方法就是用來(lái)檢查 timerQueue 中的過(guò)期任務(wù), 放到 taskQueue. 主要是對(duì)小頂堆的各種操作, 直接看注釋即可.

        function advanceTimers(currentTime) {
        let timer = peek(timerQueue);
        while (timer !== null) {
        if (timer.callback === null) {
        // Timer was cancelled.
        pop(timerQueue);

        // 開(kāi)始時(shí)間小于等于當(dāng)前時(shí)間, 說(shuō)明已過(guò)期,
        // 從 taskQueue 移走, 放到 taskQueue
        } else if (timer.startTime <= currentTime) {
        pop(timerQueue);
        // taskQueue 是通過(guò) expirationTime 判斷優(yōu)先級(jí)的,
        // expirationTime 越小, 說(shuō)明越緊急, 它就應(yīng)該放在 taskQueue 的最前面
        timer.sortIndex = timer.expirationTime;
        push(taskQueue, timer);

        if (enableProfiling) {
        markTaskStart(timer, currentTime);
        timer.isQueued = true;
        }
        } else {
        // 開(kāi)始時(shí)間大于當(dāng)前時(shí)間, 說(shuō)明未過(guò)期, 任務(wù)仍然保留在 timerQueue
        // 任務(wù)進(jìn)來(lái)的時(shí)候, 開(kāi)始時(shí)間默認(rèn)是當(dāng)前時(shí)間, 如果進(jìn)入調(diào)度的時(shí)候傳了延遲時(shí)間, 開(kāi)始時(shí)間則是當(dāng)前時(shí)間與延遲時(shí)間的和
        // 開(kāi)始時(shí)間越早, 說(shuō)明會(huì)越早開(kāi)始, 排在最小堆的前面
        // Remaining timers are pending.
        return;
        }
        timer = peek(timerQueue);
        }
        }

        requestHostCallback

        不管你接沒(méi)接觸過(guò) React 源碼, 想必也聽(tīng)到過(guò)時(shí)間切片, 任務(wù)中斷可恢復(fù)這些概念. requestHostCallback 這個(gè)方法就是用來(lái)調(diào)度任務(wù)的. 既然是"調(diào)度", 那勢(shì)必得有指揮的和干活的.

        舊的 React 版通過(guò) requestAnimationFrame 和 requestIdleCallback 進(jìn)行任務(wù)調(diào)度與幀對(duì)齊, 但在 [scheduler] Yield many times per frame, no rAF #16214[11] 這個(gè) pr 中, 這種方式被廢棄了. 如果你看過(guò)我以前的一篇文章 剖析 requestAnimationFrame[12], 就會(huì)發(fā)現(xiàn) rAF 是會(huì)受到用戶(hù)行為的干擾的, 比如切換選項(xiàng)卡, 滾動(dòng)頁(yè)面等. 看下面這張圖, 前面一部分的斜率大抵就是 16.7, 也就是 1 / 60, 但我切換了選項(xiàng)卡之后, 幀刷新率立馬不穩(wěn)定了.

        此外, rAF 畢竟仰仗顯示器的刷新頻率, 而市面上的刷新頻率層次不齊, 有 60Hz 的, 像蘋(píng)果的 ProMotion 就到了 120Hz, 再加上好的顯卡都被拿去挖礦了, 兼容起來(lái)實(shí)在麻煩. 簡(jiǎn)言之, 這種方式會(huì)受到外界因素影響, 無(wú)法使 Scheduler 做到百分百掌控.

        requestIdleCallback 就不詳細(xì)說(shuō)了, 它可用在瀏覽器空閑階段去執(zhí)行一些低優(yōu)先級(jí)任務(wù), 而不會(huì)影響延遲關(guān)鍵事件, 如動(dòng)畫(huà)和輸入響應(yīng). 具體使用方法可自行去看 MDN[13] 上的介紹.

        目前最新的代碼中, Scheduler 通過(guò) MessageChannel 來(lái)人為的控制調(diào)度頻率, 默認(rèn)的時(shí)間切片是 5ms, 可見(jiàn)這個(gè)粒度比 ProMotion 還要高. 如果你以前沒(méi)聽(tīng)說(shuō)過(guò) MessageChannel, 但一定得聽(tīng)說(shuō)過(guò) postMessage 這家伙, 它經(jīng)常被用做宿主跟 iframe 之間的通信. 此外它兼容性上也是好到?jīng)]朋友.

        鋪墊的都說(shuō)完了, 直接看源碼. 它做了一波兼容, 如果是 Node.js 或者低端 IE, 就使用 setImmediate, 這塊不展開(kāi)說(shuō). 在正經(jīng)的瀏覽器環(huán)境下(IE: 你直接念我身份證好了), 我們通過(guò) MessageChannel 創(chuàng)建一個(gè)實(shí)例 channel, 該實(shí)例有兩個(gè) port, 用來(lái)互相通信. Scheduler 通過(guò) port2 發(fā)送消息(port.postMessage), 通過(guò) port1 來(lái)接收消息(port1.onmessage). 因此, port2 就是那個(gè)調(diào)度者, port1 是那個(gè)收到調(diào)度信號(hào)真正干活的.

        let schedulePerformWorkUntilDeadline;

        if (typeof setImmediate === "function") {
        schedulePerformWorkUntilDeadline = () => {
        setImmediate(performWorkUntilDeadline);
        };
        } else {
        const channel = new MessageChannel();
        const port = channel.port2;

        // port1 接收調(diào)度信號(hào), 來(lái)執(zhí)行 performWorkUntilDeadline(受)
        channel.port1.onmessage = performWorkUntilDeadline;

        // port 是調(diào)度者(攻)
        schedulePerformWorkUntilDeadline = () => {
        port.postMessage(null);
        };
        }

        requestHostCallback 將傳進(jìn)來(lái)的 callback 賦值給全局變量 scheduledHostCallback, 如果當(dāng)前 isMessageLoopRunning 是 false, 即沒(méi)有任務(wù)調(diào)度, 就把它開(kāi)啟, 然后發(fā)送調(diào)度信號(hào)給 port1 進(jìn)行調(diào)度.

        function requestHostCallback(callback) {
        scheduledHostCallback = callback;
        if (!isMessageLoopRunning) {
        isMessageLoopRunning = true;

        // postMessage, 告訴 port1 來(lái)執(zhí)行 performWorkUntilDeadline 方法
        schedulePerformWorkUntilDeadline();
        }
        }

        performWorkUntilDeadline

        performWorkUntilDeadline 是任務(wù)的執(zhí)行者, 也就是 port1 接收到信號(hào)后需要執(zhí)行的函數(shù), 它用來(lái)在時(shí)間片內(nèi)執(zhí)行任務(wù), 如果沒(méi)執(zhí)行完, 用一個(gè)新的調(diào)度者繼續(xù)調(diào)度. 首先判斷是否有 scheduledHostCallback, 如果存在說(shuō)明存在需要被調(diào)度的任務(wù). 計(jì)算 deadline 為當(dāng)前時(shí)間加上 yieldInterval(也就是那 5ms). 看到這里相必你就恍然大悟了, deadline 其實(shí)就來(lái)做時(shí)間切片! 接下來(lái)設(shè)置了一個(gè)常量 hasTimeRemaining 為 true, 看到這倆名字你是不是想起了 requestIdleCallback 的用法了呢. 至于為什么 hasTimeRemaining 為 true, 因?yàn)椴还苣愕恼麄€(gè)任務(wù)是否執(zhí)行完, 給你的時(shí)間就是 5ms, 要么超時(shí)就中斷, 要么不超時(shí)就恰好執(zhí)行完了, 總之時(shí)間切片內(nèi)一定是有剩余時(shí)間的.

        后面的邏輯直接看代碼注釋即可, 總結(jié)來(lái)講就是任務(wù)在時(shí)間切片內(nèi)沒(méi)有被執(zhí)行完, 就需要讓調(diào)度者再次調(diào)度一個(gè)執(zhí)行者繼續(xù)執(zhí)行任務(wù), 否則這個(gè)任務(wù)就算執(zhí)行完了. 判斷一個(gè)任務(wù)執(zhí)行完成的標(biāo)記是 hasMoreWork 字段, 下面 workLoop 會(huì)講到.

        //
        const performWorkUntilDeadline = () => {
        if (scheduledHostCallback !== null) {
        const currentTime = getCurrentTime();
        // 時(shí)間分片
        deadline = currentTime + yieldInterval;
        const hasTimeRemaining = true;

        let hasMoreWork = true;
        try {
        // scheduledHostCallback 去執(zhí)行真正的任務(wù)
        // 如果返回 true, 說(shuō)明當(dāng)前任務(wù)被中斷了
        // 會(huì)再讓調(diào)度者調(diào)度一個(gè)執(zhí)行者繼續(xù)執(zhí)行任務(wù)
        // 下面講 workLoop 方法時(shí)會(huì)說(shuō)到中斷恢復(fù)的邏輯, 先留個(gè)坑
        hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
        } finally {
        if (hasMoreWork) {
        // 如果任務(wù)中斷了(沒(méi)執(zhí)行完), 就說(shuō)明 hasMoreWork 為 true
        // 這塊類(lèi)似于遞歸, 就再申請(qǐng)一個(gè)調(diào)度者來(lái)繼續(xù)執(zhí)行該任務(wù)
        schedulePerformWorkUntilDeadline();
        } else {
        // 否則當(dāng)前任務(wù)就執(zhí)行完了
        // 關(guān)閉 isMessageLoopRunning
        // 并將 scheduledHostCallback 置為 null
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
        }
        }
        } else {
        isMessageLoopRunning = false;
        }
        // Yielding to the browser will give it a chance to paint, so we can
        // reset this.
        needsPaint = false;
        };

        flushWork

        我們?cè)缭?nbsp;requestHostCallback 就將 flushWork 作為參數(shù)賦值給了全局變量 scheduledHostCallback, 在上面 performWorkUntilDeadline 也調(diào)用了該方法, 讓我們看看 flushWork 用來(lái)做什么. 顧名思義, flushWork 就是把任務(wù)"沖刷"掉, 就好比 taskQueue 是馬桶, 里面的任務(wù)是那啥, flushWork 就是沖水那套機(jī)制. 當(dāng)然剖絲抽繭, 該方法的核心就是 return 了 workLoop.

        function flushWork(hasTimeRemaining, initialTime) {
        if (enableProfiling) {
        markSchedulerUnsuspended(initialTime);
        }

        // 由于 requestHostCallback 并不一定立即執(zhí)行傳入的回調(diào)函數(shù)
        // 所以 isHostCallbackScheduled 狀態(tài)可能會(huì)維持一段時(shí)間
        // 等到 flushWork 開(kāi)始處理任務(wù)時(shí), 則需要釋放該狀態(tài)以支持其他的任務(wù)被 schedule 進(jìn)來(lái)
        isHostCallbackScheduled = false;

        // 因?yàn)橐呀?jīng)在執(zhí)行 taskQueue 的任務(wù)了
        // 所以不需要等 timerQueue 中的任務(wù)過(guò)期了
        if (isHostTimeoutScheduled) {
        isHostTimeoutScheduled = false;
        cancelHostTimeout();
        }

        isPerformingWork = true;
        const previousPriorityLevel = currentPriorityLevel;
        try {
        if (enableProfiling) {
        try {
        return workLoop(hasTimeRemaining, initialTime);
        } catch (error) {
        if (currentTask !== null) {
        const currentTime = getCurrentTime();
        markTaskErrored(currentTask, currentTime);
        currentTask.isQueued = false;
        }
        throw error;
        }
        } else {
        // No catch in prod code path.
        return workLoop(hasTimeRemaining, initialTime);
        }
        } finally {
        // 執(zhí)行完任務(wù)后還原這些全局狀態(tài)
        currentTask = null;
        currentPriorityLevel = previousPriorityLevel;
        isPerformingWork = false;
        if (enableProfiling) {
        const currentTime = getCurrentTime();
        markSchedulerSuspended(currentTime);
        }
        }
        }

        任務(wù)中斷與恢復(fù) —— workLoop

        終于到了尾聲, workLoop 可謂是集大成者, 承載了任務(wù)中斷, 任務(wù)恢復(fù), 判斷任務(wù)完成等功能.

        ?循環(huán) taskQueue 執(zhí)行任務(wù)?任務(wù)狀態(tài)的判斷

        ?如果 taskQueue 執(zhí)行完成了, 就返回 false, 并從 timerQueue 中拿出最高優(yōu)的來(lái)做超時(shí)調(diào)度?如果未執(zhí)行完, 說(shuō)明當(dāng)前調(diào)度發(fā)生了中斷, 就返回 true, 下次接著調(diào)度(這個(gè) Boolean 類(lèi)型的返回值, 其實(shí)就對(duì)應(yīng)著 performWorkUntilDeadline 中的 hasMoreWork)


        function workLoop(hasTimeRemaining, initialTime) {
        let currentTime = initialTime;

        // 因?yàn)槭莻€(gè)異步的, 需要再次調(diào)整一下 timerQueue 跟 taskQueue
        advanceTimers(currentTime);

        // 最緊急的過(guò)期任務(wù)
        currentTask = peek(taskQueue);
        while (
        currentTask !== null &&
        !(enableSchedulerDebugging && isSchedulerPaused) // 用于 debugger, 不管
        ) {
        // 任務(wù)中斷!!!
        // 時(shí)間片到了, 但 currentTask 未過(guò)期, 跳出循環(huán)
        // 當(dāng)前任務(wù)就被中斷了, 需要放到下次 workLoop 中執(zhí)行
        if (
        currentTask.expirationTime > currentTime &&
        (!hasTimeRemaining || shouldYieldToHost())
        ) {
        // This currentTask hasn't expired, and we've reached the deadline.
        break;
        }

        const callback = currentTask.callback;
        if (typeof callback === "function") {
        // 清除掉 currentTask.callback
        // 如果下次迭代 callback 為空, 說(shuō)明任務(wù)執(zhí)行完了
        currentTask.callback = null;

        currentPriorityLevel = currentTask.priorityLevel;

        // 已過(guò)期
        const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;

        if (enableProfiling) {
        markTaskRun(currentTask, currentTime);
        }

        // 執(zhí)行任務(wù)
        const continuationCallback = callback(didUserCallbackTimeout);
        currentTime = getCurrentTime();

        // 如果產(chǎn)生了連續(xù)回調(diào), 說(shuō)明出現(xiàn)了中斷
        // 故將新的 continuationCallback 賦值 currentTask.callback
        // 這樣下次恢復(fù)任務(wù)時(shí), callback 就接上趟了
        if (typeof continuationCallback === "function") {
        currentTask.callback = continuationCallback;

        if (enableProfiling) {
        markTaskYield(currentTask, currentTime);
        }
        } else {
        if (enableProfiling) {
        markTaskCompleted(currentTask, currentTime);
        currentTask.isQueued = false;
        }
        // 如果 continuationCallback 不是 Function 類(lèi)型, 說(shuō)明任務(wù)完成!!!
        // 否則, 說(shuō)明這個(gè)任務(wù)執(zhí)行完了, 可以被彈出了
        if (currentTask === peek(taskQueue)) {
        pop(taskQueue);
        }
        }

        // 上面執(zhí)行任務(wù)會(huì)消耗一些時(shí)間, 再次重新更新兩個(gè)隊(duì)列
        advanceTimers(currentTime);
        } else {
        // 上面的 if 清空了 currentTask.callback, 所以
        // 如果 callback 為空, 說(shuō)明這個(gè)任務(wù)就執(zhí)行完了, 可以被彈出了
        pop(taskQueue);
        }

        // 如果當(dāng)前任務(wù)執(zhí)行完了, 那么就把下一個(gè)最高優(yōu)的任務(wù)拿出來(lái)執(zhí)行, 直到清空了 taskQueue
        // 如果當(dāng)前任務(wù)沒(méi)執(zhí)行完, currentTask 實(shí)際還是當(dāng)前的任務(wù), 只不過(guò) callback 變成了 continuationCallback
        currentTask = peek(taskQueue);
        }

        // 任務(wù)恢復(fù)!!!
        // 上面說(shuō)到 ddl 到了, 但 taskQueue 還沒(méi)執(zhí)行完(也就是任務(wù)被中斷了)
        // 就返回 true, 這就是恢復(fù)任務(wù)的標(biāo)志
        if (currentTask !== null) {
        return true;
        } else {
        // 若任務(wù)完成!!!, 去 timerQueue 中找需要最早開(kāi)始執(zhí)行的那個(gè)任務(wù)
        // 進(jìn)行 requestHostTimeout 調(diào)度那一套
        const firstTimer = peek(timerQueue);
        if (firstTimer !== null) {
        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
        }
        return false;
        }
        }

        shouldYieldToHost

        這個(gè)方法沒(méi)啥可說(shuō)的, 就是判斷是否要讓出主線(xiàn)程. 不過(guò)它引申出一個(gè)比較新潮的 API 即 navigator.scheduling.isInputPending, 它用來(lái)再不讓出主線(xiàn)程的情況下提高響應(yīng)能力, 不過(guò) Chrome 90 還沒(méi)有該 API, 想必這是個(gè)面向未來(lái)的. Better JS scheduling with isInputPending()[14] 講得不錯(cuò), 可以看看.

        function shouldYieldToHost() {
        if (
        enableIsInputPending &&
        navigator !== undefined &&
        navigator.scheduling !== undefined &&
        navigator.scheduling.isInputPending !== undefined
        ) {
        const scheduling = navigator.scheduling;
        const currentTime = getCurrentTime();
        if (currentTime >= deadline) {
        // There's no time left. We may want to yield control of the main
        // thread, so the browser can perform high priority tasks. The main ones
        // are painting and user input. If there's a pending paint or a pending
        // input, then we should yield. But if there's neither, then we can
        // yield less often while remaining responsive. We'll eventually yield
        // regardless, since there could be a pending paint that wasn't
        // accompanied by a call to `requestPaint`, or other main thread tasks
        // like network events.
        // 需要繪制或者有高優(yōu)先級(jí)的 I/O, 必須得讓出主線(xiàn)程
        if (needsPaint || scheduling.isInputPending()) {
        // There is either a pending paint or a pending input.
        return true;
        }
        // There's no pending input. Only yield if we've reached the max
        // yield interval.
        return currentTime >= maxYieldInterval;
        } else {
        // There's still time left in the frame.
        return false;
        }
        } else {
        // `isInputPending` is not available. Since we have no way of knowing if
        // there's pending input, always yield at the end of the frame.

        // task 執(zhí)行超過(guò)了 ddl 就應(yīng)該讓出主進(jìn)程了
        return getCurrentTime() >= deadline;
        }
        }

        取消調(diào)度

        在 workLoop 的代碼中有一段是 currentTask.callback = null;, 也就是 Scheduler 以 callback 是否為 null 來(lái)判斷任務(wù)被取消(或者完成了).

        function unstable_cancelCallback(task) {
        if (enableProfiling) {
        if (task.isQueued) {
        const currentTime = getCurrentTime();
        markTaskCanceled(task, currentTime);
        task.isQueued = false;
        }
        }

        // Null out the callback to indicate the task has been canceled. (Can't
        // remove from the queue because you can't remove arbitrary nodes from an
        // array based heap, only the first one.)
        task.callback = null;
        }

        自定義的時(shí)間切片頻率

        為了后續(xù) Scheduler 獨(dú)立成包, 它開(kāi)放了設(shè)置時(shí)間切片的大小, 默認(rèn)為 5ms, 你可以根據(jù)實(shí)際情況調(diào)整到 0 ~ 125 之間. 不過(guò)怎么把握這個(gè)度, 咱也不知道咱也不敢問(wèn).

        function forceFrameRate(fps) {
        if (fps < 0 || fps > 125) {
        // Using console['error'] to evade Babel and ESLint
        console["error"](
        "forceFrameRate takes a positive int between 0 and 125, " +
        "forcing frame rates higher than 125 fps is not supported"
        );
        return;
        }
        if (fps > 0) {
        yieldInterval = Math.floor(1000 / fps);
        } else {
        // reset the framerate
        yieldInterval = 5;
        }
        }

        最后

        以上全部就是 Scheduler 的源碼解析了, 洋洋灑灑兩萬(wàn)余字, 一大半都是代碼... 除此之外源碼中還有一些通用邏輯的封裝, 以及一些面向未來(lái)的特性文中沒(méi)有涉及, 有興趣可以去 GayHub 上翻源碼看看. 本文基于 v17.0.2, 未來(lái)誰(shuí)也沒(méi)法保證它的代碼會(huì)變成啥樣, 先看先享受, 且行且珍惜. 后面如有大的更新, 我會(huì)盡力更新文章, 以保證和 master 對(duì)齊. 讀源碼這事兒, 不是一朝一夕的事兒, 也不能只一家之言, 歡迎大家拍磚提意見(jiàn). 實(shí)在是畫(huà)圖苦手, 盜用 shockw4ver 大佬的一張流程圖收尾.

        參考

        ?React 中的優(yōu)先級(jí)管理[15]?React 調(diào)度原理(scheduler)[16]?探索 React 的內(nèi)在 —— postMessage & Scheduler[17]?一篇長(zhǎng)文幫你徹底搞懂 React 的調(diào)度機(jī)制原理[18]?這可能是最通俗的 React Fiber(時(shí)間分片) 打開(kāi)方式[19]

        References

        [1] Scheduler: https://github.com/facebook/react/tree/master/packages/scheduler
        [2] 小頂堆https://algorithm.yanceyleo.com/data-structure/tree/binary-heap
        [3] 優(yōu)先隊(duì)列: https://algorithm.yanceyleo.com/data-structure/queue/priority-queue
        [4] topK: https://algorithm.yanceyleo.com/leetcode/lcof/40-get-least-numbers
        [5] 數(shù)據(jù)結(jié)構(gòu)與算法: https://algorithm.yanceyleo.com
        [6] 協(xié)程https://en.wikipedia.org/wiki/Coroutine
        [7] React 源碼: https://github.com/learn-frame/react/blob/feature/learn-react/packages/scheduler/src/forks/SchedulerDOM.js
        [8] performance.now()https://developer.mozilla.org/en-US/docs/Web/API/Performance/now
        [9] chromium 源碼: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/timing/performance.cc;l=1107;drc=28684b63b1e1ece5396dc0e4c03118855710a75f;bpv=1;bpt=1
        [10] 粗化時(shí)間算法(coarsen time algorithm): https://w3c.github.io/hr-time/#dfn-coarsen-time
        [11] [scheduler] Yield many times per frame, no rAF #16214: https://github.com/facebook/react/pull/16214/commits
        [12] 剖析 requestAnimationFrame: https://www.yanceyleo.com/post/20506b75-0a04-450d-aeec-6ea08ef25116
        [13] MDN: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
        [14] Better JS scheduling with isInputPending(): https://web.dev/isinputpending/
        [15] React 中的優(yōu)先級(jí)管理: https://github.com/7kms/react-illustration-series/blob/master/docs/main/priority.md
        [16] React 調(diào)度原理(scheduler): https://github.com/7kms/react-illustration-series/blob/master/docs/main/scheduler.md
        [17] 探索 React 的內(nèi)在 —— postMessage & Scheduler: https://segmentfault.com/a/1190000022942008
        [18] 一篇長(zhǎng)文幫你徹底搞懂 React 的調(diào)度機(jī)制原理: https://segmentfault.com/a/1190000039101758
        [19] 這可能是最通俗的 React Fiber(時(shí)間分片) 打開(kāi)方式: https://juejin.cn/post/6844903975112671239


        瀏覽 60
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        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>
            大香蕉老师| 黄色在线免费观看网站| 国产凹凸视频在线观看| 亚洲成人精品少妇| 男人的天堂黄色| 国产成人精品av| 韩国无码一区二区| 91av在线免费播放| 草比网| 在线第一页| 黄色视频网站在线免费观看| 少妇bbb搡bbbb搡bbbb| 免费污视频在线观看| 国产丝袜AV| 午夜无码视频| 奇米色色色| 中文字幕在线免费看线人| 四虎91| 无码毛片在线观看| 高清无码1区| 日本黄色电影网址| 婷婷五月在线观看| 91农村站街老熟女露脸| 亚洲视频无码在线| av影音先锋在线| 国产激情123区| 狼人综合网| 亚洲国产高清无码| av无码电影| 麻豆精品在线观看| 亚洲色图网站| 青娱乐国产av| 成人黄网站在线观看| 色第一页| 91中文字幕| 大香蕉视频网| 欧美性爱-熊猫成人网| 美女自慰网站免费| 天堂网亚洲| 黄色一级小说| 欧洲精品在线免费观看| 蜜桃导航-精品导航| 中文字幕免费观看| 一区二线视频| 色色热| 五月涩| 超碰成人97| 97人人干人人| 久久视频在线| 男人午夜网站| av天天av无码av天天爽| 天天视频狠狠狠狠| 久久无码电影| 插逼综合网| 91成人视频在线播放| 男人天堂色男人| 俺来也在线视频| 成人毛片18女人毛片真水| 91亚洲国产成人精品一区二区三| 91视频在线免费观看app| 热无码av| 免费精品黄色网页| 老师机性爱视频在线播放| 亚洲日韩电影| 婷婷色在线| www香蕉成人片com| 天天综合国产| 欧美一级婬片AAAA毛片| 狠狠综合网| 狠狠干伊人| 国产精品theporn| 亚洲视频www| 午夜视频网站| 日韩午夜在线观看| 台湾色综合| 国产精品H| 欧美精品18videosex性欧美| 熟妇私拍| XXXX操| 日韩AAA在线| 亚洲色色频| 国产色片| 性爱免费视频| yw在线观看| 天天爽日日澡AAAA片| 亚洲精品乱码| 色五月综合| 北条麻妃被操| 大鸡巴久久| 嫩BBB搡BBB槡BBB小号| 日韩黄频| 91亚洲国产成人精品一区| 日韩免费在线播放| www.激情五月天| 婷婷五月天综合| 中文无码播放| 小泬BBBBBB免费看| 依人综合网| 热久久这里只有精品| 懂色av懂色av粉嫩av无码| 日韩无码精品一区二区三区| 777777国产7777777| 少妇4p| 婷婷狠狠爱| 日本牲交| 视色视频在线观看18| 制服无码| 日韩成人网站在线观看| 成年人在线播放| 自拍视频网| 在线观看视频免费无码免费视频 | 99免费小视频| 亚洲伦乱| 国产成人综合在线| 五月婷在线观看| 一级黄色视频免费观看| 国产精品自拍三级| 草草视频在线观看| 久久久91精品国产一区苍井空| 午夜福利影片| 无码看片| 成人三级视频在线观看| 色色看片| 欧美一级黄色电影| 亚洲午夜AV久久乱码| 高清一区二区三区| 亚洲午夜福利| 中文字幕在线观看a| 亚洲激情性爱| 99在线视频播放| 欧美久色| 亚洲精品福利视频导航| 手机AV在线观看| 国产性猛交╳XXX乱大交| 伊人综合影院| 囯产精品99久久久久久WWW| 中文区中文字幕免费看| 人人干干| 黄色亚洲视频| 欧美综合色| 18禁91| 91丨露脸丨熟女| 亚洲无码天堂| 性做久久久久久久久| 77q视频| 色婷婷电影网| 伊人大香焦网| 精品视频999| 3d动漫精品一区二区三区在线观看 | 偷窥丶亚洲丶熟女| 8090操逼网| 91蝌蚪在线视频| 一级黄色免费视频| 精品白浆| 成人片无码| 一曲二曲三曲在线观看中文字| 成片免费观看视频大全| 久视频在线| 91精品国产综合久久久久久| 欧美日本成人网站入口| 中文无码熟妇一区二区| 日韩精品网址| 久久不射网站| 日韩在线视频中文字幕| 无码在线免费视频| 一区二区三区电影高清电影免费观看 | 91人妻无码精品蜜桃| 欧美亚洲成人在线| 日日干日日操| 亚洲福利影院| 能看的操逼网站| 香蕉福利网| 亚洲青青| 神马午夜秋霞不卡| 久久久偷拍| 超碰成人福利| 丁香婷婷色五月激情综合三级三级片欧美日韩国 | 青青草成人在线观看| 成人四区| 17c.白丝喷水自慰| 国产乱子伦-区二区三区| 一区二区三区四区五区六区高清无吗视频 | 波多野结衣av一区| 欧美日韩一道本| 3d动漫精品H区XXXXX区| 男人天堂社区| 成人超碰在线| 熟妇人妻久久中文字幕| 亚洲高清毛片一区二区| 精品国内自产拍在线观看视频| 国产黄色片视频| 婷婷AV在线| 国产成人三级| 欧美激情视频在线| 中文字幕在线视频无码| 人妻少妇精品无码| 麻豆视频一区| 亚洲日本在线观看| 秋霞久久日| 男女做爱无码| 久久婷婷在线| 东京热小视频| 特一级黄色片| 搞搞网日本9| 最新亚洲无码在线观看| 欧美性爱导航| 国产又色又爽又黄又免费| 男女av在线| 午夜黄色操逼视频| 免费看污网站| 91人妻人人澡人人爽人妻| 久热精品视频| 久操视频一区二区三区| 欧美日韩中文在线观看| 国产无遮挡又黄又爽又色视频| 日韩在线视频91| 日韩人妻无码一区二区| 国产精品天天AVJ精麻传媒| 夜夜撸天天日| 一区二区三区电影网| 欧美日韩国产在线播放| 大香蕉手机视频| 在线观看免费完整版中文字幕视频| 中日韩中文字幕一区二区区别| 国产熟睡乱子伦午夜视频_第1集| 18岁成人毛片| 亚洲91在线| 五月六月丁香激情视频| 欧美日韩高清丝袜| 人人看人人摸人人搞| 欧美另类激情| 99视频精品视频| av東熱激情东京热| 高潮AV在线观看| 久操视频免费观看| 日本三级片网站在线观看| 中文字幕免费毛片| 家庭乱伦AV| 亚洲AV第二区国产精品| 欧美狼友| 嫩草嫩草69| 国产原创精品| 天堂视频在线| 未满十八18禁止免费无码网站| 91逼站| 人人搞人人摸| 亚欧无码| 青春草在线视频免费观看| 中文字幕人妻系列| 搞搞视频| 黄色免费高清视频| 日韩在线一区二区三区四区| 亚洲三级视频在线观看| 成人在线乱码视频| 操逼黄视频| 国产成人免费做爰视频| av天天干| 1024手机在线视频| 中文不卡在线| www.狠狠撸| 欧美日韩不卡在线| 国产精品日韩高清北条麻衣| 久久成人综合| 国产欧美综合在线三区| 91精品丝袜久久久久久| 开心深爱激情网| 亚洲成人一区| 成人在线日韩| 91精品国产aⅴ一区二区| 色情五月| 日本一区二区三区四区在线观看| 免费观看黄色视频网站| 乱伦性爱视频| 欧美九九九九| 毛片导航| 国产黄色影院| 起碰在线视频| 国产尤物视频| 青青草视频在线观看| 伊人免费视频在线观看| 性生活黄色视频| 国产www| 中文字幕+乱码+中文字幕一区| 欧美一级特黄AAAAAA片| 久久久精品电影| 大香蕉伊人导航| 俺来也俺去也www色官网| 殴美老妇BBBBBBBBB| 成人一区二区三区四区五区| 亚洲在线高清| 日韩欧美一级片| 国产精品一区二区三区不卡| 91网站免费在线观看| 日韩不卡| 亚洲无码av中文字幕| 欧美激情亚洲| 国产黄色视频观看| 欧美大骚逼| 亚洲成人AV在线| 四川性BBB搡BBB爽爽爽小说 | 97超碰人人操| 人人草人人看人人摸| 国产丝袜在线| 色综合99| 亚洲午夜激情电影| 夜夜骑夜夜| 欧美性猛交一区二区三区精品 | 欧美群交在线观看| 亚洲精品一区二区三区蜜桃| 国产老骚逼| 欧美在线一区二区| 91麻豆电影| 69看片| 国产一级a毛一级a做免费的视频l 精品国产免费观看久久久_久久天天 | 久久午夜无码鲁丝片主演是谁| 五十路在线视频| 性欧美日韩| 五月丁香性爱| 特黄特色免费大片| 亚洲欧美日韩另类| 九九热精品在线| 欧美午夜影院| 国产理伦| 女人一区二区| 少妇搡BBBB搡BBB搡造水多 | 91探花秘入囗| 久久99精品久久久水蜜桃| 国产又黄又大又粗| 性天堂| 肏亚洲美女| 中文字幕一区二区三区人妻在线视频| 日日免费视频| 免费国产成人看片在线| 久久精品成人电影| 在线观看黄色小电影| 男人天堂视频网| 东北成人毛片| 人妻无码不卡| 最新版本日本亚洲色| 久久天堂AV综合合色蜜桃网| 黄片伊人| 阿v视频在线观看| 安徽妇搡BBB搡BBBB户外老太太| 日韩在线小电影| 无卡无码| 豆花精品视频| 久久毛片视频| 亚洲精品成人av| 日本一区二区三区在线观看网站| 人人摸人人| 丁香激情五月少妇| 成年人视频网站| 羽月希奶水饱胀在线播放| 无码国产精品一区二区免费96| 国产高清无码免费在线观看 | 狠狠肏| 蜜桃传媒入口| 18禁在线| 九色91PORNY国产| 一级A片亲子乱| 久久久91人妻无码精品蜜桃ID| 国产A片视频| 俺也去网av| 日韩成人一区| 狠狠色av| jizz在线免费观看| 亚洲不卡一区二区三区| 中日韩一级片| 国产在线拍揄自揄拍无码男男| 人妻少妇无码| 毛片大香蕉| 亚洲天堂无码视频| 日韩无码视频一区二区| 无码免费高清| 亚洲AV秘无码不卡在线观看| 人人色人人爱| 老太老熟女城中层露脸60| 久久ww| 欧美日韩一道本| 97无码| 国产精品天天狠天天看| 波多野结衣毛片| 国产三级麻豆| 日韩视频免费观看高清完整版在线观| 尤物精品在线| 国色天香一区二区| 99热在线观看免费| 日本成人激情视频| 91探花视频在线观看| gogogo免费高清在线偷拍| 欧美一级网| av天堂小说网| 人妻视频网站| 青青操b| 亚洲女人被黑人巨大的原因| 日本狠狠干| S28AV| 一区二区三区四区在线播放| 欧美亚洲成人网站| 潮喷av| h成人在线| 日韩精品人妻中文字幕| 人妻在线免费视频| 中文字幕亚洲在线| 婷婷精品在线视频| 北条麻妃一区二区三区在线| 日本中文字幕在线播放| 日日夜夜天天综合| 亚洲无码高清视频在线观看| www免费视频| 99热1| 色三区| 国产精品欧美综合亚洲| 麻豆成人无码精品视频| 日本在线免费视频| 成人免费毛片AAAAAA片| 12——13女人毛片毛片| 成人福利影视| 亚洲图片在线观看| 伊人成人网视频| 黄色一级录像| 狠狠狠狠狠狠狠狠| 日韩视频免费观看高清完整版在线观 | 成人黄色视频网| 成人无码区免费AV片| www.199麻豆在线观看网站| 国产免费黄色视频| 无码人妻精品一区二区三区蜜桃91 | 国产精品秘麻豆免费版现看视频| 美女网站视频黄| AV中文在线| 一级成人毛片| 激情无码av| 中文字幕不卡在线观看| 日韩免费视频一区| 中文字幕东京热加勒比| 国产精品无码天天爽视频| 亚洲无码AV免费观看| 日韩一区二区视频| 日韩在线视频中文字幕| 一级操逼大片| 青青草97国产精品麻豆| 欧美熟妇BBB搡BBB| 夜夜嗨av无码一区二区三区| 国产一区二区视频在线观看| 日本免费色视频| 91在线观看| 高清无码激情| 亚洲深夜福利| 日韩性爱网站| 国产日韩欧美综合在线| 人妻一区二区在线| 国产黄片视频| 波多野结衣91| 日逼高清无码| 久久久三级片| 91老熟女视频| 欧美一区二区三区系列电影| 亚洲无码二区| 国产小电影在线| 91成人视频免费观看| 亚洲视频免费观看| 日韩欧美视频一区| 中国精品77777777| 蜜桃精品一区二区三区美女| 色婷婷一级A片AAA毛片| 亚洲黄色视频免费观看| 女孩自慰在线观看| 大鸡巴久久久| 噜噜影院| 美国高清无码| 午夜福利片| 山西真实国产乱子伦| 特级西西444www高清大胆免费看 | 亚洲激情图| 国产伦精品一区二区三区色大师 | 天天插综合| 亚洲国产精品视频| 撸久久| 亚洲毛片亚洲毛片亚洲毛片| 丁香婷婷男人天堂| 91日逼视频| 在线啊啊啊| 亚洲中文视频免费| JUY-579被丈夫的上司侵犯后的第7天,我| 综合久久亚洲| 国产91嫩草乱婬A片2蜜臀 | 99久久精品国产一区二区成人| 中文无码毛片| 日韩AAA| 大鸡吧网| 欧美一级成人| 日韩高清无码成人| 国产免费一区二区三区最新不卡| 97人妻天天摸天天爽天天| 天天做天天日| 99色逼| 高清视频一区二区| 天堂va欧美va亚洲va在线| 人人澡av| 超碰在线| 亚洲免费成人网站| 7799综合| 日韩AV网站在线观看| 操B网站| 亚洲有码在线| 99reav| 精品国产AV色一区二区深夜久久| 69av在线观看| 亚洲最大的成人网站| 青青草原成人在线视频| 91一区二区在线播放精品| 无码一二区| av先锋资源| 成人AV一区二区三区| 成人黄色在线看| 黄片在线免费观看| 午夜无码AV| 四个熟妇搡BBBB搡BBBB| 色播视频在线观看| 国产精品久久久久久亚洲毛片| 欧美A视频| 久操视频网站| 久草视频这里只有精品| 天天操夜夜操视频免费高清| 日韩A级片| 中文字幕一区二区三区四区在线视频 | 无码不卡在线| 日本AⅤ电影| 成人无码日本动漫电影| 青青在线| 亚洲天堂无码在线观看| 俺去俺来WWW色官方| 视频一区中文字幕| 69av天堂| www.操逼| 色九九综合| 性爱av天堂| 操B影院| 色九月婷婷| 久久久国产探花视频| 91免费视频在线| 91久久综合| 中文字幕av免费在线观看| 日本绿色精品视频| 操逼逼一区二区三区| 日韩一区二区三区在线| 亚洲中文字幕免费视频| 精品白浆| 成人在线黄片| 高清无码在线免费观看| 久久免费视频观看| 精品无码一区二区三区的天堂| 国产xxxx视频| 一区二区三区三级片| 亚洲品久久久蜜| 悠悠色影院| 在线视频免费观看| 国产精品婷婷久久久| 变态另类av| 男女啪啪啪| 亚洲AV无码一区二区三竹菊| 色婷婷综合久久久中文字幕| 91人妻无码精品蜜桃| 91精品久久久久久久久久| 7x7x7x人成免费观学生视频 | 欧美日韩不卡在线| 爱爱打炮影院| sesese999| 日韩欧美视频在线| 日韩久久网站| 黄片网站免费在线观看| 久热精品在线| 在线观看国产区| 大香蕉伊人在线视频| 一区二区三区四区无码视频| 日本毛片视频| 精品一区电影| 日本免费不卡视频| 欧美视频自拍| 久久久精品999| 激情性爱婷婷色五月| 久热超碰| 人人操天天操| 欧美特黄AAAAAAAAA片 | 国产精品一区二区在线观看| 天天撸天天日| 伊人久久久影视大全| 美女插插| 狠狠干狠狠操| 婷婷色色五月天| 少妇嫩搡BBBB搡BBBB| 深爱五月激情| 日韩在线视频中文字幕码无| 91熟女偷情| 中文字幕精品一级A片| 超碰观看| 国模一区二区三区| 国产av探花| 欧美一区二区三区婷婷五月| 怡红院av| 翔田千里av| 欧美黄色大片| 神马午夜秋霞不卡| 亚洲高清成人| 亚洲精品18禁| 2018中文字幕第一页| 国精品无码一区二区三区在线秋菊| 欧美h网站| 欧美激情无码炮击| 亚洲免费清高| 黄色成人视频在线观看| 91乱子伦国产乱子伦| 国产精品成人午夜福利| 999免费视频| 成人电影一区二区| 日韩爱爱免费视频| 综合激情网站| 麻豆成人精品| 在线观看18s| 91亚洲影院| 色99在线| 欧洲无码一区二区三区| www.伊人大香蕉| 色天使色天堂| 一本到在线观看午夜剧场| 黄片视频在线观看| 国产激情自拍| 久久影音先锋| 強姧伦一区二区三区在线播放| 激情综合婷婷| 中文字幕AV播放| 大香蕉久热| 羞羞视频com.入口| 天天色AV| 丁香五月五月婷婷| 三级无码电影| 自拍偷拍15p| 柠檬AV导航| 国产主播中文字幕| 日本乱伦网站| 亚洲男女免费视频| 国产精品一色哟哟哟| 精品一区三区| a免费在线| 亚洲高清无码视频大全| 午夜福利无码电影| 大肉大捧一进一出免费阅读| 最新亚洲中文字幕| 三级日韩视频| 激情五月天亚洲| 亚洲视频在线观| 韩国成人免费无码免费视频| aaa国产精品| 亚洲精品视频在线观看免费| 亚洲色成人网站www永久四虎| 茄子av| 久久综合站| 高清无码中文字| 国产激情精品视频| 特级西西| 国产嫩草影院| 一区二区三区国产精品| 久久影院av| 国产91高跟丝袜| 综合色五月| 日韩午夜片| 中文字幕无码播放| 成人影视在线免费观看| 欧美日屄视频| 抽插影院| 狠狠狠狠狠狠狠狠| 日日日日日干| 精品伊人| 久热草| 青草福利在线| 中文字幕第69页| 婷婷色色婷婷| 狠狠视频| 婷婷中文在线| 精品国产久久| 91无码精品一区二区| 亚洲日韩中文字幕在线观看| 成人超碰在线| 日韩精品在线观看视频| 色色加勒比综合| 精品中文在线视频| 人人操人人爽人人爱| 又大又长又粗91| 五月天深爱激情网| 婷婷国产| 操b视频在线免费观看| 国产成人精品无码片子的价格| 首屈一指视频在线观看| 亚洲XXXXX| 熟女人妻在线观看| 超碰在线无码| 色五月av| 北京熟妇槡BBBB槡BBBB| 人人妻人人爽人人精品| 一级女婬片A片AAAA片| 免费看操逼视频| 亚洲成人黄色电影| 丝袜制服中文字幕无码专区| 免费无码A片在线观看全| 国产AV网| 国产欧美一区二区三区特黄手机版| 中文字幕无码一区二区三区一本久| 国产av综合网| 天天天天操| 77久久| 亚洲日韩欧美一区二区| 东京热av在线| 小處女末发育嫩苞AV| 亚洲中文在线播放| 欧亚无码| 成人黄色大香蕉| 女女女女女女BBBBBB手| 亚洲AV无码成人精品区h麻豆| 午夜亚洲无码| 日本a在线免费观看| 一级操逼毛片| 国产一区二区成人久久919色| 国产一二三四区| 日韩性爱网| 亚洲天堂在线播放| 在线观看免费黄| 97精品人妻一区| 中字AV| 国产美女在线播放| 亚洲国产熟妇无码日韩| 日韩AV免费在线| 91麻豆国产| 操逼精品| 亚洲高清人妻| 婷婷丁香五月花| 精品色播| 六月婷婷网| aaa久久| 国产成人小视频| 欧美操逼逼| 欧美男女日逼视频| 91大熟女91大腚女人| 国精品无码人妻一区二区三区 | 色欲av伊人久久大香线蕉影院| www亚洲无码A片贴吧| 亚洲色图偷拍| 偷拍92| 国产波霸爆乳一区二区| 香蕉综合在线| 色三区| 97香蕉久久国产超碰青草专区| 久久麻豆| 黄色操逼网站?| 日韩欧美中文在线观看| 欧美69成人| 成人无码免费毛片| 日韩精品久久久久久久酒店| 婷婷成人电影| 蜜桃91精品秘入口| 黄色18禁| 国产99久久| 国产老熟女久久久| 中文字幕国产av| www黄色视频| 精品91美女| 伊人色色| 久热re| 亚洲国产成人91PORN| 五月天久久久| 亚洲五月婷婷| 五月天AV网站| 国产小黄片在线| 九九久久99| 成人小说在线观看| 91日韩在线| 中文字幕网站在线观看| 亚洲日韩字幕| 初尝人妻滑进去了莹莹视频| 日韩一级电影在线观看| 五月天福利导航| 97热| 红桃91人妻爽人妻爽| AV自拍偷拍| 欧一美一婬一伦一区二区三区黑人 | 成人午夜无码| 丁香五月六月| 日韩黄色在线观看| 欧美不卡在线播放| 又大又粗AV| 国产农村妇女精品一二区| 台湾成人视频| 91精品久久久久久久| 91蜜桃传媒| 天天干欧美| 永久免费无码中文字幕| 亚洲精品视频在线播放| 最近日韩中文字幕中文翻译歌词| 国产欧美综合一区| 日本中文无码视频| 亚洲1区2区| 99热视| 狠狠色狠狠操| 狠狠干2024| jizzjizz欧美| 91精品人妻一区二区三区| 午夜国产精品AV| 国产小福利| 一起草在线视频| 国产黄色精品| 日韩无码人妻一区二区三区| 六十路老熟女码视频| AV网站在线免费观看| 日本色色色| 亚洲免费在线婷婷| 99久久大香蕉| 三上悠亚无码破解69XXX| 黄页网站在线免费观看| 少妇三区| 亚洲黄色视频在线观看网站| 超碰人人在线观看| 色婷婷综合久久久中文字幕| 国产AV久| 免费日韩黄色电影| 日本精品中文字幕| 日韩一区二区视频| 思思操在线视频| 91看片看婬黄大片女跟女| 日韩性爱视频在线观看| 黄色成人视频在线观看| 亚洲激情综合视频| 蜜桃AV无码一区二区三区| 无码AV一区二区| 亚洲综合免费观看高清完整版在线| 99久久人妻无码中文字幕系列| 可以免费观看的av| 成人黄色免费网站| 人妻人人干| 亚洲视频五区| 香蕉久久网| 亚洲AV五月天在线| 国产成人秘在线观看免费网站 | 日韩黄色电影网站| 男人操女人网站| aaa国产| 国产欧美日韩在线观看| 亚洲乱码国产乱码精品天美传媒 | h片在线观看| 久久久久无码国产精品一区| www.亚洲| www.婷婷| 日韩AV在线天堂| 日韩91在线| 女生被操网站| 婷婷二区| 大香蕉三级| 中文字幕二区| 成年人免费黄色视频| 四虎麻豆| 河南少妇搡BBBB搡BBBB| 天天插天天| 亚洲在线第一页| 成人无码一区二区| 国产,亚洲91| 久久久久性| jizz在线观看视频| 欧美性爱动态| 国产无码AV在线| 国产成人aV| 二区三区在线观看| A片黄色电影网站| 亚洲综合色网| 国产成人三级在线播放| 日一日射一射| 黄色在线网站| 在线免费观看黄色网址| 强开小嫩苞一区二区电影| 成人网站在线免费观看| 啪啪91| 天天撸天天射| 一级国产黄色视频| 免费国产在线视频| 五月婷婷激情网| 人人澡人人爽欧一区| 色综合加勒比| 蜜桃视频一区| 最近中文字幕av| 欧美黄片免费在线观看| 日日射人妻| 日本黄色视频大全| 国产在线导航| 无码人妻丰满熟妇区毛片蜜桃麻豆| 日本男人天堂| 日韩乱伦视频| 性爱福利导航| 亚洲V国产v欧美v久久久久久| 韩剧《邻居的妻子》电视剧| 尤物网站在线观看| 中文字幕亚洲综合| 97中文在线|