自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

從簡單中窺見高端,徹底搞懂任務(wù)可中斷機(jī)制與任務(wù)插隊(duì)機(jī)制

開發(fā) 前端
我們要明確的一個(gè)前提,是一個(gè)完整的函數(shù)執(zhí)行是不可以中斷的。因此如果你把一整個(gè)任務(wù)全部都放到一個(gè)函數(shù)中來執(zhí)行,那么想要做到任務(wù)可中斷是不可能的。

今天我就用最基礎(chǔ)的方式重新跟大家分享一下什么是任務(wù)可中斷。

一、任務(wù)拆分

首先,我們要明確的一個(gè)前提,是一個(gè)完整的函數(shù)執(zhí)行是不可以中斷的。因此如果你把一整個(gè)任務(wù)全部都放到一個(gè)函數(shù)中來執(zhí)行,那么想要做到任務(wù)可中斷是不可能的。

例如,現(xiàn)在我有一個(gè)任務(wù),往父級(jí)元素中插入 10 萬個(gè)子節(jié)點(diǎn) <span>1</span>,然后我們可以隨便寫這樣一個(gè)函數(shù)來完成這個(gè)邏輯。

btn.onclick = () => {
  let i = 0
  for (; i < 100000; i++) {
    let span = document.createElement('span')
    span.innerText = 1
    container.appendChild(span)
  }
}

然后這個(gè)時(shí)候,我們就發(fā)現(xiàn)一個(gè)問題,當(dāng)我們點(diǎn)擊之后,頁面上并不會(huì)立即顯示插入的內(nèi)容,而是會(huì)卡頓一會(huì)兒,才會(huì)顯示。

原因是因?yàn)?10 萬個(gè)節(jié)點(diǎn)的插入邏輯是一個(gè)同步的過程,JS 邏輯的執(zhí)行時(shí)間過長導(dǎo)致了瀏覽器遲遲無法執(zhí)行渲染。

那么為了優(yōu)化這種情況,我們可以考慮將渲染 10 萬個(gè)節(jié)點(diǎn)這個(gè)大的任務(wù),拆分成 10 萬個(gè)渲染 1 個(gè)節(jié)點(diǎn)的小任務(wù)。

function task() {
  let span = document.createElement('span')
  span.innerText = 1
  container.appendChild(span)
}

并將這 10 萬個(gè)任務(wù),放進(jìn)一個(gè)數(shù)組中。

const taskQueue = Array.from({ length: 100000 }, () => task)

執(zhí)行這 100 萬個(gè)任務(wù),就通過遍歷 taskQueue 的方式來執(zhí)行,這樣,我們就可以通過中斷隊(duì)列遍歷的方式,來中斷任務(wù)的執(zhí)行。

二、需要中斷的原因

在瀏覽器中,渲染引擎在每一幀都有機(jī)會(huì)渲染頁面,那么頁面的表現(xiàn)就不會(huì)卡頓。但是剛才我們的情況是,JS 執(zhí)行時(shí)間過長,導(dǎo)致渲染引擎一直沒有機(jī)會(huì)渲染,所以用戶感受到的就是卡頓。

那么解決這個(gè)問題的原理,就是根據(jù)瀏覽器渲染頻率,對(duì) JS 要執(zhí)行的任務(wù)進(jìn)行拆分,JS 執(zhí)行一部分,然后渲染引擎渲染一部分,完成之后,JS 再繼續(xù)執(zhí)行,渲染引擎再渲染。

通過這樣間隔執(zhí)行的方式,讓用戶感知不到卡頓的存在。

三、中斷的判斷條件

如果你的顯示器是 60 Hz,那么瀏覽器一幀的渲染間隔時(shí)間大約就是 16.7ms,因此,我們可以利用瀏覽器渲染任務(wù)完成之后的空余時(shí)間來執(zhí)行被拆分的 JS 任務(wù),瀏覽器給我們提供了一個(gè)鉤子函數(shù) requestIdleCallback 在空余時(shí)間執(zhí)行我們想要的邏輯。

需要注意的是,許多朋友對(duì) 1ms 沒什么概念,對(duì)于計(jì)算機(jī)來說,16.7ms 時(shí)間其實(shí)非常的長,簡單的函數(shù) 1 ms 可以執(zhí)行非常多次。

例如,如果只是簡單的遞增。

var k = 0
let startTime = performance.now()

while (performance.now() - startTime <= 1) {
  // console.log('xx')
  k += 1
}
console.log(k)

在我的電腦上,1ms k 值最高可以遞增到 6500+,如果我要執(zhí)行 console.log 函數(shù),最高可以執(zhí)行 100+ 次。

我們來學(xué)習(xí)一下 requestIdleCallback 的語法。

requestIdleCallback(callback[, options])

callback 是需要執(zhí)行的任務(wù),接收一個(gè) IdleDeadline 對(duì)象作為參數(shù)。IdleDeadline 包含 2 個(gè)重要字段。

  • didTimeout,布爾值,表示任務(wù)是否超時(shí)。
  • timeRemaining() ,用于獲取當(dāng)前幀的剩余時(shí)間。

options 是一個(gè)可選參數(shù),目前只有一個(gè)值 timeout,表示如果超過這個(gè)時(shí)間,任務(wù)還沒有執(zhí)行,則強(qiáng)制執(zhí)行任務(wù),不需要等待空閑時(shí)間。

因此當(dāng)我們通過上面的 deadline 發(fā)現(xiàn)沒有剩余時(shí)間執(zhí)行更多的任務(wù)了,那我們就中斷遍歷過程。

四、代碼實(shí)現(xiàn)

實(shí)現(xiàn)起來非常簡單,我們用 while 循環(huán)來遍歷 queueTask,然后根據(jù) deadline 的情況來中斷遍歷過程,代碼如下:

btn.onclick = () => {
  btn.innerText = '已點(diǎn)擊,插入中'

  requestIdleCallback((deadline) => {
    let task;
    while ((task = taskQueue.pop()) && !deadline.didTimeout && deadline.timeRemaining() > 0) {
      task()
    }
  })
}

此時(shí)因?yàn)闆]有加入遞歸邏輯去連續(xù)觸發(fā) requestIdleCallback,但是我們可以通過連續(xù)點(diǎn)擊的方式查看執(zhí)行效果。

然后我們加入遞歸邏輯讓他們自動(dòng)把剩余的任務(wù)全部執(zhí)行完,定義一個(gè) performWorkUnit。

function performWorkUnit() {
  // 任務(wù)執(zhí)行完畢后結(jié)束遞歸
  if (taskQueue.length === 0) {
    btn.innerText = '執(zhí)行'
    return
  }

  requestIdleCallback(deadline => {
    let task;
    while ((task = taskQueue.pop()) && !deadline.didTimeout && deadline.timeRemaining() > 0) {
      task()
    }
    performWorkUnit()
  })
}

然后在點(diǎn)擊事件中調(diào)用即可。

btn.onclick = () => {
  btn.innerText = '已點(diǎn)擊,插入中'
  requestIdleCallback(performWorkUnit)
}

執(zhí)行效果如圖所示,我們會(huì)發(fā)現(xiàn)卡頓消失了。

此時(shí)我們?yōu)榱烁玫挠^察效果,讓每一個(gè)小任務(wù)的執(zhí)行都阻塞 1ms。

function task() {
  const startTime = performance.now()
  let span = document.createElement('span')
  span.innerText = 1

  while (performance.now() - startTime < 1) {
    // 阻塞 1 ms
  }
  container.appendChild(span)
}

然后把任務(wù)數(shù)量改成 1000。

const taskQueue = Array.from({ length: 1000 }, () => task)

執(zhí)行效果如下:

五、插隊(duì)

我們另外起一個(gè)按鈕,專門用于執(zhí)行一些插隊(duì)任務(wù)。插隊(duì)的邏輯非常簡單,只需要往 taskQueue 中添加任務(wù)即可。不過插隊(duì)任務(wù)的優(yōu)先級(jí)更高一些,因此要通過 push 來添加,以確保任務(wù)能夠更早的執(zhí)行。

首先聲明一個(gè) highPriorityTask 函數(shù)用于創(chuàng)建優(yōu)先級(jí)更高的任務(wù)。

function highPriorityTask() {
  const startTime = performance.now()
  let span = document.createElement('span')
  span.style.color = 'red'
  span.innerHTML = '<strong>插隊(duì)任務(wù)</strong>'

  while (performance.now() - startTime < 1) {
    // 阻塞 1 ms
  }
  container.appendChild(span)
}

新增一個(gè)按鈕,用于觸發(fā)插隊(duì)任務(wù)的執(zhí)行。

pushBtn.onclick = function () {
  taskQueue.push(highPriorityTask)
}

我們來看一下執(zhí)行效果,每當(dāng)我點(diǎn)擊插隊(duì)任務(wù)按鈕,就會(huì)執(zhí)行一個(gè)優(yōu)先級(jí)更高的任務(wù)。

代碼非常的簡單,不過理解可能需要稍微思考一下。因?yàn)?nbsp;performWorkUnit 中遞歸在遍歷隊(duì)列 taskQueue,并且這個(gè)遞歸過程是一直處于中斷 -> 恢復(fù)的過程中,因此,當(dāng)遍歷被中斷后,在它恢復(fù)之前,我們可以往 taskQueue 中插入新的任務(wù)到隊(duì)列頭部,當(dāng)它重新開始遍歷時(shí),新加入的任務(wù)就會(huì)被執(zhí)行。

這里一個(gè)小的細(xì)節(jié)是,在事件循環(huán)的運(yùn)行規(guī)則中,點(diǎn)擊事件的回調(diào)會(huì)比 requestIdleCallback 更早執(zhí)行。

六、總結(jié)

這個(gè)邏輯就是 React 并發(fā)模式的底層原理。只不過在 React 中,同時(shí)兼容了同步更新與異步更新,并且設(shè)計(jì)了更加復(fù)雜的優(yōu)先級(jí)機(jī)制,增加了更多場景的條件判斷,導(dǎo)致源碼看上去變得更加復(fù)雜了。

當(dāng)然,React 由于為了兼容更多的場景,改寫了任務(wù)中斷的判斷條件。因?yàn)樵趧e的環(huán)境里,例如 node/React Native 等,不支持 requestIdleCallback,在這些場景之下,React 把中斷策略改為 5ms 中斷一次,然后利用 performance.now 或者 Date.now 來記錄時(shí)間。

/* eslint-disable no-var */
var getCurrentTime;
var hasPerformanceNow = typeof performance === 'object' && typeof performance.now === 'function';

if (hasPerformanceNow) {
  var localPerformance = performance;

  getCurrentTime = function () {
    return localPerformance.now();
  };
} else {
  var localDate = Date;
  var initialTime = localDate.now();

  getCurrentTime = function () {
    return localDate.now() - initialTime;
  };
function shouldYieldToHost() {
  var timeElapsed = getCurrentTime() - startTime;

  if (timeElapsed < frameInterval) { // 5ms
    // 主線程只被阻塞了很短時(shí)間;
    // smaller than a single frame. Don't yield yet.
    return false;
  } 
  // 主線程被阻塞的時(shí)間不可忽視
  return true;
}

并使用別的方式來替代 requestIdleCallback。

  • node/old IE:setImmediate
  • DOM/worker:MessageChannel
  • 兜底方案:setTimeout
let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === 'function') {
  // Node.js and old IE.
  schedulePerformWorkUntilDeadline = () => {
    localSetImmediate(performWorkUntilDeadline);
  };
} else if (typeof MessageChannel !== 'undefined') {
  // DOM and Worker environments.
  // We prefer MessageChannel because of the 4ms setTimeout clamping.
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  schedulePerformWorkUntilDeadline = () => {
    port.postMessage(null);
  };
} else {
  // We should only fallback here in non-browser environments.
  schedulePerformWorkUntilDeadline = () => {
    // $FlowFixMe[not-a-function] nullable value
    // @ts-ignore
    localSetTimeout(performWorkUntilDeadline, 0);
  };
}
責(zé)任編輯:姜華 來源: 這波能反殺
相關(guān)推薦

2021-02-02 14:55:48

React前端高優(yōu)先

2020-11-02 11:40:24

Node.jsRequire前端

2025-03-21 09:01:34

Swift任務(wù)取消機(jī)制協(xié)作式取消

2021-12-11 19:00:54

Java中斷機(jī)制

2013-12-12 16:44:25

Lua協(xié)程

2017-07-20 16:55:56

Android事件響應(yīng)View源碼分析

2021-11-02 06:58:55

FlinkWindow機(jī)制

2025-01-14 10:09:43

硬中斷Linux系統(tǒng)

2014-06-18 10:41:31

Android多任務(wù)機(jī)制

2020-03-03 15:40:51

開發(fā)技能代碼

2021-02-01 11:30:13

React前端調(diào)度

2011-08-02 14:48:04

IOS 多任務(wù)

2009-07-23 14:08:46

Windows Emb

2021-07-24 11:15:19

開發(fā)技能代碼

2022-04-25 09:03:16

JavaScript代碼

2022-06-08 06:38:00

iPadOS 16SafariiCloud

2024-08-27 09:34:24

2012-07-31 09:44:27

Windows Pho

2017-05-02 22:38:44

前端開發(fā)JS事件循環(huán)機(jī)制

2015-08-03 09:54:26

Java線程Java
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)