JavaScript知識總結 非同步編程篇

来源:https://www.cnblogs.com/smileZAZ/archive/2022/05/06/16227921.html
-Advertisement-
Play Games

這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 1. 非同步編程的實現方式? JavaScript中的非同步機制可以分為以下幾種: 回調函數 的方式,使用回調函數的方式有一個缺點是,多個回調函數嵌套的時候會造成回調函數地獄,上下兩層的回調函數間的代碼耦合度太高,不利於代碼的可維護。 Pro ...


這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助

1. 非同步編程的實現方式?

JavaScript中的非同步機制可以分為以下幾種:

  • 回調函數 的方式,使用回調函數的方式有一個缺點是,多個回調函數嵌套的時候會造成回調函數地獄,上下兩層的回調函數間的代碼耦合度太高,不利於代碼的可維護。
  • Promise 的方式,使用 Promise 的方式可以將嵌套的回調函數作為鏈式調用。但是使用這種方法,有時會造成多個 then 的鏈式調用,可能會造成代碼的語義不夠明確。
  • generator 的方式,它可以在函數的執行過程中,將函數的執行權轉移出去,在函數外部還可以將執行權轉移回來。當遇到非同步函數執行的時候,將函數執行權轉移出去,當非同步函數執行完畢時再將執行權給轉移回來。因此在 generator 內部對於非同步操作的方式,可以以同步的順序來書寫。使用這種方式需要考慮的問題是何時將函數的控制權轉移回來,因此需要有一個自動執行 generator 的機制,比如說 co 模塊等方式來實現 generator 的自動執行。
  • async 函數 的方式,async 函數是 generator 和 promise 實現的一個自動執行的語法糖,它內部自帶執行器,當函數內部執行到一個 await 語句的時候,如果語句返回一個 promise 對象,那麼函數將會等待 promise 對象的狀態變為 resolve 後再繼續向下執行。因此可以將非同步邏輯,轉化為同步的順序來書寫,並且這個函數可以自動執行。

2. setTimeout、Promise、Async/Await 的區別

(1)setTimeout

console.log('script start')	//1. 列印 script start
setTimeout(function(){
    console.log('settimeout')	// 4. 列印 settimeout
})	// 2. 調用 setTimeout 函數,並定義其完成後執行的回調函數
console.log('script end')	//3. 列印 script start
// 輸出順序:script start->script end->settimeout

(2)Promise

Promise本身是同步的立即執行函數, 當在executor中執行resolve或者reject的時候, 此時是非同步操作, 會先執行then/catch等,當主棧完成後,才會去調用resolve/reject中存放的方法執行,列印p的時候,是列印的返回結果,一個Promise實例。

console.log('script start')
let promise1 = new Promise(function (resolve) {
    console.log('promise1')
    resolve()
    console.log('promise1 end')
}).then(function () {
    console.log('promise2')
})
setTimeout(function(){
    console.log('settimeout')
})
console.log('script end')
// 輸出順序: script start->promise1->promise1 end->script end->promise2->settimeout

當JS主線程執行到Promise對象時:

  • promise1.then() 的回調就是一個 task
  • promise1 是 resolved或rejected: 那這個 task 就會放入當前事件迴圈回合的 microtask queue
  • promise1 是 pending: 這個 task 就會放入 事件迴圈的未來的某個(可能下一個)回合的 microtask queue 中
  • setTimeout 的回調也是個 task ,它會被放入 macrotask queue 即使是 0ms 的情況

(3)async/await

async function async1(){
   console.log('async1 start');
    await async2();
    console.log('async1 end')
}
async function async2(){
    console.log('async2')
}
console.log('script start');
async1();
console.log('script end')
// 輸出順序:script start->async1 start->async2->script end->async1 end

async 函數返回一個 Promise 對象,當函數執行的時候,一旦遇到 await 就會先返回,等到觸發的非同步操作完成,再執行函數體內後面的語句。可以理解為,是讓出了線程,跳出了 async 函數體。

例如:

async function func1() {
    return 1
}
console.log(func1())

func1的運行結果其實就是一個Promise對象。因此也可以使用then來處理後續邏輯。

func1().then(res => {
    console.log(res);  // 30
})

await的含義為等待,也就是 async 函數需要等待await後的函數執行完成並且有了返回結果(Promise對象)之後,才能繼續執行下麵的代碼。await通過返回一個Promise對象來實現同步的效果。

3. 對Promise的理解

Promise是非同步編程的一種解決方案,它是一個對象,可以獲取非同步操作的消息,他的出現大大改善了非同步編程的困境,避免了地獄回調,它比傳統的解決方案回調函數和事件更合理和更強大。

所謂Promise,簡單說就是一個容器,裡面保存著某個未來才會結束的事件(通常是一個非同步操作)的結果。從語法上說,Promise 是一個對象,從它可以獲取非同步操作的消息。Promise 提供統一的 API,各種非同步操作都可以用同樣的方法進行處理。

(1)Promise的實例有三個狀態:

  • Pending(進行中)
  • Resolved(已完成)
  • Rejected(已拒絕)

當把一件事情交給promise時,它的狀態就是Pending,任務完成了狀態就變成了Resolved、沒有完成失敗了就變成了Rejected。

(2)Promise的實例有兩個過程

  • pending -> fulfilled : Resolved(已完成)
  • pending -> rejected:Rejected(已拒絕)

註意:一旦從進行狀態變成為其他狀態就永遠不能更改狀態了。

Promise的特點:

  • 對象的狀態不受外界影響。promise對象代表一個非同步操作,有三種狀態,pending(進行中)、fulfilled(已成功)、rejected(已失敗)。只有非同步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態,這也是promise這個名字的由來——“承諾”;
  • 一旦狀態改變就不會再變,任何時候都可以得到這個結果。promise對象的狀態改變,只有兩種可能:從pending變為fulfilled,從pending變為rejected。這時就稱為resolved(已定型)。如果改變已經發生了,你再對promise對象添加回調函數,也會立即得到這個結果。這與事件(event)完全不同,事件的特點是:如果你錯過了它,再去監聽是得不到結果的。

Promise的缺點:

  • 無法取消Promise,一旦新建它就會立即執行,無法中途取消。
  • 如果不設置回調函數,Promise內部拋出的錯誤,不會反應到外部。
  • 當處於pending狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。

總結:

Promise 對象是非同步編程的一種解決方案,最早由社區提出。Promise 是一個構造函數,接收一個函數作為參數,返回一個 Promise 實例。一個 Promise 實例有三種狀態,分別是pending、resolved 和 rejected,分別代表了進行中、已成功和已失敗。實例的狀態只能由 pending 轉變 resolved 或者rejected 狀態,並且狀態一經改變,就凝固了,無法再被改變了。

狀態的改變是通過 resolve() 和 reject() 函數來實現的,可以在非同步操作結束後調用這兩個函數改變 Promise 實例的狀態,它的原型上定義了一個 then 方法,使用這個 then 方法可以為兩個狀態的改變註冊回調函數。這個回調函數屬於微任務,會在本輪事件迴圈的末尾執行。

註意:在構造 Promise 的時候,構造函數內部的代碼是立即執行的

4. Promise的基本用法

(1)創建Promise對象

Promise對象代表一個非同步操作,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。

Promise構造函數接受一個函數作為參數,該函數的兩個參數分別是resolvereject

const promise = new Promise(function(resolve, reject) {
  // ... some code
  if (/* 非同步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

一般情況下都會使用new Promise()來創建promise對象,但是也可以使用promise.resolvepromise.reject這兩個方法:

  • Promise.resolve

Promise.resolve(value)的返回值也是一個promise對象,可以對返回值進行.then調用,代碼如下:

Promise.resolve(11).then(function(value){
  console.log(value); // 列印出11
});

resolve(11)代碼中,會讓promise對象進入確定(resolve狀態),並將參數11傳遞給後面的then所指定的onFulfilled 函數;

創建promise對象可以使用new Promise的形式創建對象,也可以使用Promise.resolve(value)的形式創建promise對象;

  • Promise.reject

Promise.reject 也是new Promise的快捷形式,也創建一個promise對象。代碼如下

Promise.reject(new Error(“我錯了,請原諒俺!!”));

就是下麵的代碼new Promise的簡單形式:

new Promise(function(resolve,reject){
   reject(new Error("我錯了,請原諒俺!!"));
});

下麵是使用resolve方法和reject方法:

function testPromise(ready) {
  return new Promise(function(resolve,reject){
    if(ready) {
      resolve("hello world");
    }else {
      reject("No thanks");
    }
  });
};
// 方法調用
testPromise(true).then(function(msg){
  console.log(msg);
},function(error){
  console.log(error);
});

上面的代碼的含義是給testPromise方法傳遞一個參數,返回一個promise對象,如果為true的話,那麼調用promise對象中的resolve()方法,並且把其中的參數傳遞給後面的then第一個函數內,因此列印出 “hello world”, 如果為false的話,會調用promise對象中的reject()方法,則會進入then的第二個函數內,會列印No thanks

(2)Promise方法

Promise有五個常用的方法:then()、catch()、all()、race()、finally。下麵就來看一下這些方法。

  1. then()

當Promise執行的內容符合成功條件時,調用resolve函數,失敗就調用reject函數。Promise創建完了,那該如何調用呢?

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

then方法可以接受兩個回調函數作為參數。第一個回調函數是Promise對象的狀態變為resolved時調用,第二個回調函數是Promise對象的狀態變為rejected時調用。其中第二個參數可以省略。

then方法返回的是一個新的Promise實例(不是原來那個Promise實例)。因此可以採用鏈式寫法,即then方法後面再調用另一個then方法。

當要寫有順序的非同步事件時,需要串列時,可以這樣寫:

let promise = new Promise((resolve,reject)=>{
    ajax('first').success(function(res){
        resolve(res);
    })
})
promise.then(res=>{
    return new Promise((resovle,reject)=>{
        ajax('second').success(function(res){
            resolve(res)
        })
    })
}).then(res=>{
    return new Promise((resovle,reject)=>{
        ajax('second').success(function(res){
            resolve(res)
        })
    })
}).then(res=>{
    
})

那當要寫的事件沒有順序或者關係時,還如何寫呢?可以使用all 方法來解決。

2. catch()

Promise對象除了有then方法,還有一個catch方法,該方法相當於then方法的第二個參數,指向reject的回調函數。不過catch方法還有一個作用,就是在執行resolve回調函數時,如果出現錯誤,拋出異常,不會停止運行,而是進入catch方法中。

p.then((data) => {
     console.log('resolved',data);
},(err) => {
     console.log('rejected',err);
     }
); 
p.then((data) => {
    console.log('resolved',data);
}).catch((err) => {
    console.log('rejected',err);
});

3. all()

all方法可以完成並行任務, 它接收一個數組,數組的每一項都是一個promise對象。當數組中所有的promise的狀態都達到resolved的時候,all方法的狀態就會變成resolved,如果有一個狀態變成了rejected,那麼all方法的狀態就會變成rejected

javascript
let promise1 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(1);
	},2000)
});
let promise2 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(2);
	},1000)
});
let promise3 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(3);
	},3000)
});
Promise.all([promise1,promise2,promise3]).then(res=>{
    console.log(res);
    //結果為:[1,2,3] 
})

調用all方法時的結果成功的時候是回調函數的參數也是一個數組,這個數組按順序保存著每一個promise對象resolve執行時的值。

(4)race()

race方法和all一樣,接受的參數是一個每項都是promise的數組,但是與all不同的是,當最先執行完的事件執行完之後,就直接返回該promise對象的值。如果第一個promise對象狀態變成resolved,那自身的狀態變成了resolved;反之第一個promise變成rejected,那自身狀態就會變成rejected

let promise1 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       reject(1);
	},2000)
});
let promise2 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(2);
	},1000)
});
let promise3 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(3);
	},3000)
});
Promise.race([promise1,promise2,promise3]).then(res=>{
	console.log(res);
	//結果:2
},rej=>{
    console.log(rej)};
)

那麼race方法有什麼實際作用呢?當要做一件事,超過多長時間就不做了,可以用這個方法來解決:

Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})

5. finally()

finally方法用於指定不管 Promise 對象最後狀態如何,都會執行的操作。該方法是 ES2018 引入標準的。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代碼中,不管promise最後的狀態,在執行完thencatch指定的回調函數以後,都會執行finally方法指定的回調函數。

下麵是一個例子,伺服器使用 Promise 處理請求,然後使用finally方法關掉伺服器。

server.listen(port)
  .then(function () {
    // ...
  })
  .finally(server.stop);

finally方法的回調函數不接受任何參數,這意味著沒有辦法知道,前面的 Promise 狀態到底是fulfilled還是rejected。這表明,finally方法裡面的操作,應該是與狀態無關的,不依賴於 Promise 的執行結果。finally本質上是then方法的特例:

promise
.finally(() => {
  // 語句
});
// 等同於
promise
.then(
  result => {
    // 語句
    return result;
  },
  error => {
    // 語句
    throw error;
  }
);

上面代碼中,如果不使用finally方法,同樣的語句需要為成功和失敗兩種情況各寫一次。有了finally方法,則只需要寫一次。

5. Promise解決了什麼問題

在工作中經常會碰到這樣一個需求,比如我使用ajax發一個A請求後,成功後拿到數據,需要把數據傳給B請求;那麼需要如下編寫代碼:

let fs = require('fs')
fs.readFile('./a.txt','utf8',function(err,data){
  fs.readFile(data,'utf8',function(err,data){
    fs.readFile(data,'utf8',function(err,data){
      console.log(data)
    })
  })
})

上面的代碼有如下缺點:

  • 後一個請求需要依賴於前一個請求成功後,將數據往下傳遞,會導致多個ajax請求嵌套的情況,代碼不夠直觀。
  • 如果前後兩個請求不需要傳遞參數的情況下,那麼後一個請求也需要前一個請求成功後再執行下一步操作,這種情況下,那麼也需要如上編寫代碼,導致代碼不夠直觀。

Promise出現之後,代碼變成這樣:

let fs = require('fs')
function read(url){
  return new Promise((resolve,reject)=>{
    fs.readFile(url,'utf8',function(error,data){
      error && reject(error)
      resolve(data)
    })
  })
}
read('./a.txt').then(data=>{
  return read(data) 
}).then(data=>{
  return read(data)  
}).then(data=>{
  console.log(data)
})

這樣代碼看起了就簡潔了很多,解決了地獄回調的問題。

6. Promise.all和Promise.race的區別的使用場景

(1)Promise.all

Promise.all可以將多個Promise實例包裝成一個新的Promise實例。同時,成功和失敗的返回值是不同的,成功的時候返回的是一個結果數組,而失敗的時候則返回最先被reject失敗狀態的值

Promise.all中傳入的是數組,返回的也是是數組,並且會將進行映射,傳入的promise對象返回的值是按照順序在數組中排列的,但是註意的是他們執行的順序並不是按照順序的,除非可迭代對象為空。

需要註意,Promise.all獲得的成功結果的數組裡面的數據順序和Promise.all接收到的數組順序是一致的,這樣當遇到發送多個請求並根據請求順序獲取和使用數據的場景,就可以使用Promise.all來解決。

(2)Promise.race

顧名思義,Promse.race就是賽跑的意思,意思就是說,Promise.race([p1, p2, p3])裡面哪個結果獲得的快,就返回那個結果,不管結果本身是成功狀態還是失敗狀態。當要做一件事,超過多長時間就不做了,可以用這個方法來解決:

Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})

7. 對async/await 的理解

async/await其實是Generator 的語法糖,它能實現的效果都能用then鏈來實現,它是為優化then鏈而開發出來的。從字面上來看,async是“非同步”的簡寫,await則為等待,所以很好理解async 用於申明一個 function 是非同步的,而 await 用於等待一個非同步方法執行完成。當然語法上強制規定await只能出現在asnyc函數中,先來看看async函數返回了什麼: 

async function testAsy(){
   return 'hello world';
}
let result = testAsy(); 
console.log(result)

所以,async 函數返回的是一個 Promise 對象。async 函數(包含函數語句、函數表達式、Lambda表達式)會返回一個 Promise 對象,如果在函數中 return 一個直接量,async 會把這個直接量通過 Promise.resolve() 封裝成 Promise 對象。

async 函數返回的是一個 Promise 對象,所以在最外層不能用 await 獲取其返回值的情況下,當然應該用原來的方式:then() 鏈來處理這個 Promise 對象,就像這樣:

async function testAsy(){
   return 'hello world'
}
let result = testAsy() 
console.log(result)
result.then(v=>{
    console.log(v)   // hello world
})

那如果 async 函數沒有返回值,又該如何?很容易想到,它會返回 Promise.resolve(undefined)

聯想一下 Promise 的特點——無等待,所以在沒有 await 的情況下執行 async 函數,它會立即執行,返回一個 Promise 對象,並且,絕不會阻塞後面的語句。這和普通返回 Promise 對象的函數並無二致。

註意:Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的簡寫,可以用於快速封裝字面量對象或其他對象,將其封裝成 Promise 實例。

8. await 到底在等啥?

await 在等待什麼呢?一般來說,都認為 await 是在等待一個 async 函數完成。不過按語法說明,await 等待的是一個表達式,這個表達式的計算結果是 Promise 對象或者其它值(換句話說,就是沒有特殊限定)。

因為 async 函數返回一個 Promise 對象,所以 await 可以用於等待一個 async 函數的返回值——這也可以說是 await 在等 async 函數,但要清楚,它等的實際是一個返回值。註意到 await 不僅僅用於等 Promise 對象,它可以等任意表達式的結果,所以,await 後面實際是可以接普通函數調用或者直接量的。所以下麵這個示例完全可以正確運行:

function getSomething() {
    return "something";
}
async function testAsync() {
    return Promise.resolve("hello async");
}
async function test() {
    const v1 = await getSomething();
    const v2 = await testAsync();
    console.log(v1, v2);
}
test();

await 表達式的運算結果取決於它等的是什麼。

  • 如果它等到的不是一個 Promise 對象,那 await 表達式的運算結果就是它等到的東西。
  • 如果它等到的是一個 Promise 對象,await 就忙起來了,它會阻塞後面的代碼,等著 Promise 對象 resolve,然後得到 resolve 的值,作為 await 表達式的運算結果。

來看一個例子:

function testAsy(x){
   return new Promise(resolve=>{setTimeout(() => {
       resolve(x);
     }, 3000)
    }
   )
}
async function testAwt(){    
  let result =  await testAsy('hello world');
  console.log(result);    // 3秒鐘之後出現hello world
  console.log('cuger')   // 3秒鐘之後出現cug
}
testAwt();
console.log('cug')  //立即輸出cug

這就是 await 必須用在 async 函數中的原因。async 函數調用不會造成阻塞,它內部所有的阻塞都被封裝在一個 Promise 對象中非同步執行。await暫停當前async的執行,所以'cug''最先輸出,hello world'和‘cuger’是3秒鐘後同時出現的。

9. async/await的優勢

單一的 Promise 鏈並不能發現 async/await 的優勢,但是,如果需要處理由多個 Promise 組成的 then 鏈的時候,優勢就能體現出來了(很有意思,Promise 通過 then 鏈來解決多層回調的問題,現在又用 async/await 來進一步優化它)。

假設一個業務,分多個步驟完成,每個步驟都是非同步的,而且依賴於上一個步驟的結果。仍然用 setTimeout 來模擬非同步操作:

/**
 * 傳入參數 n,表示這個函數執行的時間(毫秒)
 * 執行的結果是 n + 200,這個值將用於下一步驟
 */
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}
function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}
function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}
function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

現在用 Promise 方式來實現這三個步驟的處理:

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}
doIt();
// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms

輸出結果 resultstep3() 的參數 700 + 200 = 900doIt() 順序執行了三個步驟,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 計算的結果一致。

如果用 async/await 來實現呢,會是這樣:

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}
doIt();

結果和之前的 Promise 實現是一樣的,但是這個代碼看起來是不是清晰得多,幾乎跟同步代碼一樣

10. async/await對比Promise的優勢

  • 代碼讀起來更加同步,Promise雖然擺脫了回調地獄,但是then的鏈式調⽤也會帶來額外的閱讀負擔
  • Promise傳遞中間值⾮常麻煩,⽽async/await⼏乎是同步的寫法,⾮常優雅
  • 錯誤處理友好,async/await可以⽤成熟的try/catchPromise的錯誤捕獲⾮常冗餘
  • 調試友好,Promise的調試很差,由於沒有代碼塊,你不能在⼀個返回表達式的箭頭函數中設置斷點,如果你在⼀個.then代碼塊中使⽤調試器的步進(step-over)功能,調試器並不會進⼊後續的.then代碼塊,因為調試器只能跟蹤同步代碼的每⼀步。

11. async/await 如何捕獲異常

async function fn(){
    try{
        let a = await Promise.reject('error')
    }catch(error){
        console.log(error)
    }
}

12. 併發與並行的區別?

  • 併發是巨集觀概念,我分別有任務 A 和任務 B,在一段時間內通過任務間的切換完成了這兩個任務,這種情況就可以稱之為併發。
  • 並行是微觀概念,假設 CPU 中存在兩個核心,那麼我就可以同時完成任務 A、B。同時完成多個任務的情況就可以稱之為並行。

13. 什麼是回調函數?回調函數有什麼缺點?如何解決回調地獄問題?

以下代碼就是一個回調函數的例子:

ajax(url, () => {
    // 處理邏輯
})

回調函數有一個致命的弱點,就是容易寫出回調地獄(Callback hell)。假設多個請求存在依賴性,可能會有如下代碼:

ajax(url, () => {
    // 處理邏輯
    ajax(url1, () => {
        // 處理邏輯
        ajax(url2, () => {
            // 處理邏輯
        })
    })
})

以上代碼看起來不利於閱讀和維護,當然,也可以把函數分開來寫:

function firstAjax() {
  ajax(url1, () => {
    // 處理邏輯
    secondAjax()
  })
}
function secondAjax() {
  ajax(url2, () => {
    // 處理邏輯
  })
}
ajax(url, () => {
  // 處理邏輯
  firstAjax()
})

以上的代碼雖然看上去利於閱讀了,但是還是沒有解決根本問題。回調地獄的根本問題就是:

  1. 嵌套函數存在耦合性,一旦有所改動,就會牽一發而動全身
  2. 嵌套函數一多,就很難處理錯誤

當然,回調函數還存在著別的幾個缺點,比如不能使用 try catch 捕獲錯誤,不能直接 return

14. setTimeout、setInterval、requestAnimationFrame 各有什麼特點?

非同步編程當然少不了定時器了,常見的定時器函數有 setTimeoutsetIntervalrequestAnimationFrame。最常用的是setTimeout,很多人認為 setTimeout 是延時多久,那就應該是多久後執行。

其實這個觀點是錯誤的,因為 JS 是單線程執行的,如果前面的代碼影響了性能,就會導致 setTimeout 不會按期執行。當然了,可以通過代碼去修正 setTimeout,從而使定時器相對準確:

let period = 60 * 1000 * 60 * 2
let startTime = new Date().getTime()
let count = 0
let end = new Date().getTime() + period
let interval = 1000
let currentInterval = interval
function loop() {
  count++
  // 代碼執行所消耗的時間
  let offset = new Date().getTime() - (startTime + count * interval);
  let diff = end - new Date().getTime()
  let h = Math.floor(diff / (60 * 1000 * 60))
  let hdiff = diff % (60 * 1000 * 60)
  let m = Math.floor(hdiff / (60 * 1000))
  let mdiff = hdiff % (60 * 1000)
  let s = mdiff / (1000)
  let sCeil = Math.ceil(s)
  let sFloor = Math.floor(s)
  // 得到下一次迴圈所消耗的時間
  currentInterval = interval - offset 
  console.log('時:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代碼執行時間:'+offset, '下次迴圈間隔'+currentInterval) 
  setTimeout(loop, currentInterval)
}
setTimeout(loop, currentInterval)

接下來看 setInterval,其實這個函數作用和 setTimeout 基本一致,只是該函數是每隔一段時間執行一次回調函數。

通常來說不建議使用 setInterval。第一,它和 setTimeout 一樣,不能保證在預期的時間執行任務。第二,它存在執行累積的問題,請看以下偽代碼

function demo() {
  setInterval(function(){
    console.log(2)
  },1000)
  sleep(2000)
}
demo()

以上代碼在瀏覽器環境中,如果定時器執行過程中出現了耗時操作,多個回調函數會在耗時操作結束以後同時執行,這樣可能就會帶來性能上的問題。

如果有迴圈定時器的需求,其實完全可以通過 requestAnimationFrame 來實現:

function setInterval(callback, interval) {
  let timer
  const now = Date.now
  let startTime = now()
  let endTime = startTime
  const loop = () => {
    timer = window.requestAnimationFrame(loop)
    endTime = now()
    if (endTime - startTime >= interval) {
      startTime = endTime = now()
      callback(timer)
    }
  }
  timer = window.requestAnimationFrame(loop)
  return timer
}
let a = 0
setInterval(timer => {
  console.log(1)
  a++
  if (a === 3) cancelAnimationFrame(timer)
}, 1000)

首先 requestAnimationFrame 自帶函數節流功能,基本可以保證在 16.6 毫秒內只執行一次(不掉幀的情況下),並且該函數的延時效果是精確的,沒有其他定時器時間不准的問題,當然你也可以通過該函數來實現 setTimeout

如果對您有所幫助,歡迎您點個關註,我會定時更新技術文檔,大家一起討論學習,一起進步。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 線上服務的MongoDB中有一個很大的表,我查詢時使用了sort()根據某個欄位進行排序,結果報了下麵這個錯誤: [Error] Executor error during find command :: caused by :: Sort operation used more than the ...
  • 基於 OpenAtom OpenHarmony(以下簡稱“OpenHarmony”)源碼寫點內容,幫助大家瞭解下協議開發領域,儘可能將 3gpp 協議內容與 OpenHarmony 電話子系統模塊進行結合講解。 ...
  • 變聲是直播類、聊天類應用中用戶經常使用的功能。例如:很多主播選擇使用變聲器來實現帶動直播間氣氛;和朋友語音聊天時選擇變成蘿莉音讓聊天更有趣。HMS Core音頻編輯服務提供變聲能力,幫助開發者在應用中構建變聲功能。用戶可以通過預置的變聲風格進行變聲,提升音頻可玩性的同時有效保護用戶隱私,讓你隨心所欲 ...
  • 技術大咖們從開源實戰項目總結經驗,利用真實場景的應用案例分享前沿技術,引導開發者從零參與 OpenHarmony 開源貢獻,提升代碼效率,培養開發者成為開源社區的貢獻者。 ...
  • 1. 準備階段 關於該功能的實現我們需要學習以下的資料: 1.1 【ARKUI】ets怎麼實現文件操作 1.2 文件管理 1.3 Ability上下文 2. demo 實現 2.1 文件路徑讀取 參考 context.getFilesDir 來進行獲取文件路徑,代碼如下 private getCac ...
  • 今天做了一個案例,可以好好做做能夠將之前的內容結合起來,最主要的是能對組件化編碼流程有一個大概的清晰認知,這一套做下來,明天自己再做一遍複習一下,其實組件化流程倒是基本上沒什麼問題了,主要是很多vue的方法需要多熟悉一下,畢竟打破了之前的一些對於傳統js的認知,還需要多熟悉一下。 這兩天可能內容不是 ...
  • 大家好,我是半夏👴,一個剛剛開始寫文的沙雕程式員.如果喜歡我的文章,可以關註➕ 點贊 👍 加我微信:frontendpicker,一起學習交流前端,成為更優秀的工程師~關註公眾號:搞前端的半夏,瞭解更多前端知識! 點我探索新世界! 原文鏈接 ==>http://sylblog.xin/archi ...
  • 一、主要區別 1、{} 和 new Object() 除了本身創建的對象,都繼承了 Object 原型鏈上(Object.prototype)的屬性或者方法,eg:toString();當創建的對象相同時,可以說 {} 等價於 new Object() 。2、Object.create() 是將創建 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...