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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...