此篇是JavaScript的工做原理的第四篇,其它三篇能夠看這裏:javascript
此次咱們將經過回顧在單線程環境中編程的缺點以及如何克服它們來構建使人驚歎的JavaScript UI來擴展咱們的第一篇文章。按照傳統,在文章的最後,咱們將分享有關如何使用async / await編寫更清晰代碼的5個技巧。前端
在第一篇文章中,咱們提到過一個問題:當調用棧中含有須要長時間運行的函數調用的時候會發生什麼。
想象一下,例如,當瀏覽器中運行着一個複雜的圖片轉換算法。
在這個時候,堆棧中正好有函數在執行,瀏覽器此時不能作任何事情。此時,他被阻塞了。這意味着它不能渲染,不能運行其餘代碼,他被卡住了,沒有任何響應。這就帶來了一個問題,你的程序再也不是高效的了。
你的程序沒有相應了。
在某些狀況下,這沒有什麼大不了的,可是這可能會形成更加嚴重的問題。一旦瀏覽器在調用棧中同時運行太多的任務的時候,瀏覽器會很長時間中止響應。在那個時候,大多數瀏覽器會拋出一個錯誤,詢問是否終止網頁。java
這很醜陋且它徹底摧毀了程序的用戶體驗。c++
你可能會在單一的 .js 文件中書寫 JavaScript 程序,可是程序是由多個代碼塊組成的,當前,只有一個代碼塊在運行,其它代碼塊將在隨後運行。最多見的塊狀單元是函數。
許多 JavaScript 新的開發者可能須要理解的問題是以後運行表示的是並非必須當即在如今以後就執行。換句話說即,根據定義,如今不可以運行完畢的任務將會異步完成,這樣你就不會不經意間遇到以上說起的 UI 阻塞。
看下面的代碼:web
// ajax 爲一個庫提供的任意 ajax 函數
var response = ajax('https://example.com/api');
console.log(response);
// `response` 將不會有數據返回
複製代碼
可能你已經知道標準的 ajax 請求不會徹底同步執行完畢,意即在代碼運行階段,ajax(..) 函數不會返回任何值給 response 變量ajax
得到異步函數返回值的一個簡單方法是使用回調函數。算法
ajax('https://example.com/api', function(response) {
console.log(response); // `response` 如今有值
});
複製代碼
只是要注意一點:即便能夠也永遠不要發起同步 ajax 請求。若是發起同步 ajax 請求,JavaScript 程序的 UI 將會被阻塞-用戶不可以點擊,輸入數據,跳轉或者滾動。任何用戶交互都會被阻塞。這是很是糟糕。編程
如下示例代碼,但請別這樣作,這會毀掉網頁:api
// 假設你使用 jQuery
jQuery.ajax({
url: 'https://api.example.com/endpoint',
success: function(response) {
// 成功回調.
},
async: false // 同步
});
複製代碼
咱們以 Ajax 請求爲例。你能夠異步執行任意代碼。數組
你可使用 setTimeout(callback, milliseconds) 函數來異步執行代碼。setTimeout 函數會在以後的某個時刻觸發事件(定時器)。以下代碼:
function first() {
console.log('first');
}
function second() {
console.log('second');
}
function third() {
console.log('third');
}
first();
setTimeout(second, 1000); // 1 秒後調用 second 函數
third();
複製代碼
控制檯輸出以下:
first
third
second
複製代碼
咱們這兒從一個奇怪的聲明開始——儘管容許異步 JavaScript 代碼(就像上例討論的setTimeout),但在ES6以前,JavaScript自己實際上歷來沒有任何內置異步的概念,JavaScript引擎在任何給定時刻只執行一個塊。
對於更多的JavaScript引擎怎麼工做的,能夠看系列文章的第一篇
那麼,是誰告訴JS引擎執行程序的代碼塊呢?實際上,JS引擎並非單獨運行的——它是在一個宿主環境中運行的,對於大多數開發人員來講,宿主環境就是典型的web瀏覽器或Node.js。實際上,如今JavaScript被嵌入到各類各樣的設備中,從機器人到燈泡,每一個設備表明 JS 引擎的不一樣類型的託管環境。
全部環境中的共同點是一個稱爲事件循環的內置機制,它每次調用JS引擎時都會處理程序的多個塊的執行。
這意味着JS引擎只是任意JS代碼的按需執行環境,是宿主環境處理事件運行及結果。
例如,當 JavaScript 程序發出 Ajax 請求從服務器獲取一些數據時,在函數(「回調」)中設置「response」代碼,JS引擎告訴宿主環境:"我如今要推遲執行,但當完成那個網絡請求時,會返回一些數據,請回調這個函數並給數據傳給它"。
而後瀏覽器將偵聽來自網絡的響應,當監聽到網絡請求返回內容時,瀏覽器經過將回調函數插入事件循環來調度要執行的回調函數。如下是示意圖:
您能夠在咱們以前的文章中閱讀有關內存堆和調用堆棧的更多信息。
這些Web api是什麼?從本質上說,它們是沒法訪問的線程,只能調用它們。它們是瀏覽器的併發部分。若是你是一個Node.js開發者,這些就是 c++ 的 Api。
那麼事件循環到底是什麼呢?
事件循環有一個簡單的工做——監視調用堆棧和回調隊列。若是調用堆棧爲空,它將從隊列中獲取第一個事件,並將其推送到調用堆棧,這將有效地運行它。
這樣的迭代在事件循環中稱爲 (tick標記),每一個事件只是一個函數回調。
console.log('Hi');
setTimeout(function cb1() {
console.log('cb1');
}, 5000);
console.log('Bye');
複製代碼
讓咱們執行這份代碼看看發生了什麼:
console.log('Hi')
被添加到調用堆棧中。console.log(Hi)
被執行。console.log('Hi')
從調用堆棧中移除。setTimeout(function cb1() { ... })
被添加到調用堆棧當中setTimeout(function cb1() { ... })
被執行,瀏覽器經過它的Web APIS建立了一個計時器,爲你的代碼計時。setTimeout(function cb1() { ... })
調用計時器它自己的函數是已經執行完成,從調用堆棧中移除。console.log('Bye')
被添加到調用堆棧中。console.log('Bye')
被執行。console.log('Bye')
從調用堆棧中移除。cb1
回調函數添加到回調隊列裏面。cb1
從回調隊列中取出,添加到調用堆棧中。cb1
被執行,把console.log('cb1')
添加調用堆棧中。console.log('cb1')
被執行。console.log('cb1')
從調用堆棧中移除。cb1
從調用堆棧中移除。總體過程回顧:
比較值得注意的是,ES6指定了事件循環應該怎麼運行。這意味着在技術範圍內,他是屬於JS引擎的職責範圍內,再也不僅僅扮演宿主環境的角色。這種變化的一個主要緣由是ES6中引入了Promises,由於後者須要對事件循環隊列上的調度操做更直接,控制更細粒度(稍後咱們將更詳細地討論它們)須要注意的是,setTimeout(…)不會自動將回調放到事件循環隊列中。它設置了一個計時器。當計時器過時時,環境將回調放到回調中,以便未來某個標記(tick)將接收並執行它。請看下面的代碼:
setTimeout(myCallback, 1000);
複製代碼
這不是意味着myCallback
將在1000ms後執行,而是在1000ms後myCallback
將被添加到回調隊列裏面去,這個隊列可能也有其餘比較早被添加的事件正在等待,這個時候,你的回調就必需要等待。
有很多文章和教程說在JavaScript中開始使用異步編程的時候,都建議使用setTimeout(callback,0)
,那麼如今你知道了事件循環的機制和setTimeout怎麼運行的,調用setTimeout 0毫秒做爲第二個參數只是推遲迴調將它放到回調隊列中,直到調用堆棧是空的。
看下下面的代碼:
console.log('Hi');
setTimeout(function() {
console.log('callback');
}, 0);
console.log('Bye');
複製代碼
儘管等待時間設置成了0ms,這個瀏覽器打印的結果以下:
Hi
Bye
callback
複製代碼
在ES6的介紹中有一個新的叫作「任務隊列」的概念,它是事件循環隊列上面的一層,最多見的是在promise
處理異步方式的時候。 如今只討論這個概念,以便在討論帶有Promises的異步行爲時,可以瞭解 Promises 是如何調度和處理。
想象一下:這個任務隊列是附加到事件循環隊列中每一個標記(一次從回調隊裏裏面取到數據後,放到調用堆棧執行的過程)末尾的隊列,某些異步操做可能發生在事件循環的一個標記期間,不會致使一個全新的事件被添加到事件循環隊列中,而是將一個項目(即任務)添加到當前標記的任務隊列的末尾。
這意味着能夠放心添加另外一個功能以便稍後執行,它將在其餘任何事情以前當即執行。
一個任務還可能建立更多任務添加到同一隊列的末尾。理論上,任務「循環」(不斷添加其餘任務的任等等)能夠無限運行,從而使程序沒法得到轉移到下一個事件循環標記的必要資源。從概念上講,這相似於在代碼中表示長時間運行或無限循環(如while (true) ..)。
任務有點像 setTimeout(callback, 0) 「hack」,但其實現方式是引入一個定義更明確、更有保證的順序:稍後執行,但越快越好。
正如你已經知道的,回調是到目前爲止JavaScript程序中表達和管理異步最多見的方法。實際上,回調是JavaScript語言中最基本的異步模式。無數的JS程序,甚至是很是複雜的程序,除了一些基本都是在回調異步基礎上編寫的。
可是回調函數仍是有一些缺點,開發者們試圖探索更好的異步模式。可是,若是不瞭解底層的過程,就不可能有效地使用任何抽象出來的異步模式。
在下一章中,咱們將深刻探討這些抽象,以說明爲何更復雜的異步模式(將在後續文章中討論)是必要的,甚至是值得推薦的。
看下下面的代碼:
listen('click', function (e){
setTimeout(function(){
ajax('https://api.example.com/endpoint', function (text){
if (text == "hello") {
doSomething();
}
else if (text == "world") {
doSomethingElse();
}
});
}, 500);
});
複製代碼
咱們組成了三個函數內嵌到一塊兒的鏈式嵌套,每個函數表明在異步系列裏面的一步。
這種代碼一般被稱爲「回調地獄」。可是「回調地獄」實際上與嵌套/縮進幾乎沒有任何關係,這是一個更深層次的問題。
首先,咱們等待「單擊」事件,而後等待計時器觸發,而後等待Ajax響應返回,此時可能會再次重複全部操做。
乍一看,這段代碼彷佛能夠將其異步過程對應到如下多個函數順序執行的步驟:
listen('click', function (e) {
// ..
});
複製代碼
而後:
setTimeout(function(){
// ..
}, 500);
複製代碼
再而後:
ajax('https://api.example.com/endpoint', function (text){
// ..
});
複製代碼
最後:
if (text == "hello") {
doSomething();
}
else if (text == "world") {
doSomethingElse();
}
複製代碼
因此這種同步的方式去表達你的異步嵌套代碼,是否是更天然一些?必定有這樣的方法,對吧?
看下下面的代碼:
var x = 1;
var y = 2;
console.log(x + y);
複製代碼
很是的直觀,這個x
和y
相加之和經過console.log
打印出來。若是,x
和y
的值尚未賦上,仍然須要求值,怎麼辦?
例如,須要從服務器取回x和y的值,而後才能在表達式中使用它們。假設咱們有一個函數loadX和loadY,它們分別從服務器加載x
和y
y的值。而後,一旦x
和y
都被加載,咱們有一個函數sum,它對x
和y
的值進行求和。 它多是這樣的:
function sum(getX, getY, callback) {
var x, y;
getX(function(result) {
x = result;
if (y !== undefined) {
callback(x + y);
}
});
getY(function(result) {
y = result;
if (x !== undefined) {
callback(x + y);
}
});
}
// A sync or async function that retrieves the value of `x`
//獲取到x值得方法
function fetchX() {
// ..
}
// A sync or async function that retrieves the value of `y`
//獲取到y值得方法
function fetchY() {
// ..
}
//調用
sum(fetchX, fetchY, function(result) {
console.log(result);
});
複製代碼
這段代碼中有一些很是重要的東西,咱們將x和y做爲異步獲取的值,而且執行了一個函數sum(…)(從外部),它不關心x或y,也不關心它們是否當即可用。
固然,這種基於回調的粗略方法還有不少不足之處。 這只是一個咱們沒必要判斷對於異步請求的值的處理方式一個小步驟而已。
簡單的看一下,咱們怎麼用promise表達x+y
:
function sum(xPromise, yPromise) {
// `Promise.all([ .. ])` takes an array of promises,
// and returns a new promise that waits on them
// all to finish
//`Promise.all([ .. ])` 傳入一個promise數組,
//經過返回一個新的promise,這個promise將等待全部的返回
return Promise.all([xPromise, yPromise])
// when that promise is resolved, let's take the
// received `X` and `Y` values and add them together.
//當promise是被resolved了,就返回這個x和y的值,執行加法
.then(function(values){
// `values` is an array of the messages from the
// previously resolved promises
//`values` 是上一個promise.all執行結果的數組
return values[0] + values[1];
} );
}
// `fetchX()` and `fetchY()` return promises for
// their respective values, which may be ready
// *now* or *later*.
//`fetchX()` 和 `fetchY()`返回各自的promise
sum(fetchX(), fetchY())
// we get a promise back for the sum of those
// two numbers.
// now we chain-call `then(...)` to wait for the
// resolution of that returned promise.
//咱們獲得兩個promise之和的值,等待這個promise執行成功
.then(function(sum){
console.log(sum);
});
複製代碼
在這個代碼中有兩層promise。
fetchX()
和 fetchY()
直接被調用,他們返回的值(promise)傳入到了sum(...)
。這個promise所表明的基礎值不管是如今
或者未來
均可以準備就緒。但每一個promise都會將其行爲規範化,咱們以與時間無關的方式推理x
和y
的值。某一段時間內,他們是一個未來的值。
這第二層promise是sum(...)
創造的(經過 Promise.all([ ... ])
),而後返回promise。經過調用then(…)
來等待。當 sum(…)
操做完成時,sum 傳入的兩個 Promise 都執行完後,能夠打印出來了。這裏隱藏了在sum(…)
中等待x和y將來值的邏輯。
注意: 在這個
sum(...)
裏面,這個Promise.all([...])
調用建立一個 promise(等待 promiseX 和 promiseY 它們resolve)。而後鏈式調用.then(...)
方法裏再的建立了另外一個 Promise,而後把(values[0] + values[1])
進行求和並返回。
所以,咱們在sum(...
)末尾調用then(...)
方法——其實是在返回的第二個 Promise 上的運行,而不是由Promise.all([ ... ])
建立的Promise。此外,雖然沒有在第二個 Promise 結束時再調用 then方法 ,其時這裏也建立一個 Promise。
Promise.then(…)
實際上可使用兩個函數,第一個函數用於執行成功的操做,第二個函數用於處理失敗的操做:
若是在獲取x或y時出現錯誤,或者在添加過程當中出現某種失敗,sum(…)
返回的 Promise將被拒絕,傳遞給then(…)
的第二個回調錯誤處理程序將從 Promise 接收失敗的信息。
從外部看,因爲 Promise 封裝了依賴於時間的狀態(等待底層值的完成或拒絕,Promise 自己是與時間無關的),它能夠按照可預測的方式組成,不須要開發者關心時序或底層的結果。 Promise一旦resolve,此刻在外部他就成了不可變的值——而後就能夠根據需求屢次觀察。
鏈式調用對於你來講是真的有用:
function delay(time) {
return new Promise(function(resolve, reject){
setTimeout(resolve, time);
});
}
delay(1000)
.then(function(){
console.log("after 1000ms");
return delay(2000);
})
.then(function(){
console.log("after another 2000ms");
})
.then(function(){
console.log("step 4 (next Job)");
return delay(5000);
})
複製代碼
調用delay(2000)
建立一個2000ms後將被實現(fulfill)的promise,而後經過第一個then(...)
來接收回調信號,在這裏面也返回一個promise,經過第二個then(...)
的promise來等待2000ms的promise。
注意: 由於一個Promise一旦被resolved,在外面看來就成了不可變了,因此如今能夠把它安全的傳遞到程序的任何地方。由於它不能被意外地或惡意地修改,這一點在多個地方觀察一個promise時尤爲正確。一方不可能影響另外一方觀察promise結果的能力,不變性聽起來像是一個學術話題,但它其實是promise設計最基本和最重要的方面之一,不該該被隨意忽略。
關於 Promise 的一個重要細節是要肯定某個值是不是一個實際的Promise。換句話說,它是否具備像Promise同樣行爲?
咱們知道 Promise 是由new Promise(…)
語法構造的,你可能認爲p instanceof Promise
是一個足夠能夠判斷的類型,嗯,不徹底是!
這主要是由於能夠從另外一個瀏覽器窗口(例如iframe)接收Promise值,而該窗口或框架具備本身的Promise值,與當前窗口或框架中的Promise 值不一樣,因此該檢查將沒法識別 Promise 實例。
此外,庫或框架能夠選擇性的封裝本身的Promise,而不使用原生 ES6 的Promise 來實現。事實上,極可能在老瀏覽器的庫中沒有 Promise。
若是在 Promise 建立中,出現了一個javascript異常錯誤(TypeError或者ReferenceError),這個異常會被捕捉,而且使這個 promise 被拒絕。
好比:
var p = new Promise(function(resolve, reject){
foo.bar(); // `foo` is not defined, so error!'foo'沒有定義
resolve(374); // never gets here :( 不會到達這兒
});
p.then(
function fulfilled(){
// never gets here :(不會到達這兒
},
function rejected(err){
// `err` will be a `TypeError` exception object
// from the `foo.bar()` line.
}
);
複製代碼
可是,若是在調用 then(…)
方法中出現了JS異常錯誤,那麼會發生什麼狀況呢?即便它不會丟失,你可能會發現它們的處理方式有點使人吃驚,直到你挖得更深一點:
var p = new Promise( function(resolve,reject){
resolve(374);
});
p.then(function fulfilled(message){
foo.bar();
console.log(message); // never reached不會到達這兒
},
function rejected(err){
// never reached 不會到達這兒
}
);
複製代碼
看起來foo.bar()
中的異常確實被吞噬了,不過,它不是。然而,還有一些更深層次的問題,咱們沒有注意到。 p.then(…) 調用自己返回另外一個 Promise,該 Promise 將被 TypeError 異常拒絕。
許多人會說,還有其餘更好的方法。
一個常見的建議是,Promise 應該添加一個 done(…)
,這其實是將 Promise 鏈標記爲 「done」。done(…)
不會建立並返回 Promise ,所以傳遞給 done(..)
的回調顯然不會將問題報告給不存在的連接 Promise 。
Promise 對象的回調鏈,無論以then方法或catch方法結尾,要是最後一個方法拋出錯誤,都有可能沒法捕捉到(由於Promise內部的錯誤不會冒泡到全局)。所以,咱們能夠提供一個 done 方法,老是處於回調鏈的尾端,保證拋出任何可能出現的錯誤。
var p = Promise.resolve(374);
p.then(function fulfilled(msg){
// numbers don't have string functions,
// so will throw an error
console.log(msg.toLowerCase());
})
.done(null, function() {
// If an exception is caused here, it will be thrown globally
});
複製代碼
JavaScript ES8引入了async/await,這使得使用Promise的工做更容易。這裏將簡要介紹async/await 提供的可能性以及如何利用它們編寫異步代碼。
使用 async 聲明異步函數。這個函數返回一個AsyncFunction 對象。AsyncFunction 對象表示該函數中包含的代碼是異步函數。
調用使用 async 聲明函數時,它返回一個Promise。當這個函數返回一個值時,這個值只是一個普通值而已,這個函數內部將自動建立一個promise,並使用函數返回的值進行解析。當這個函數拋出異常時,Promise 將被拋出的值拒絕。
使用 async 聲明函數時能夠包含一個await符號,await暫停這個函數的執行並等待傳遞的 Promise 的解析完成,而後恢復這個函數的執行並返回解析後的值。
async/wait 的目的是簡化使用promise的行爲
看下下面的列子:
// Just a standard JavaScript function
//標準的js寫法
function getNumber1() {
return Promise.resolve('374');
}
// This function does the same as getNumber1
//這個函數作了相同的事情,返回一個promise
async function getNumber2() {
return 374;
}
複製代碼
相似地,函數拋出異常至關於函數返回的promise被reject了:
//這兩個函數同樣
function f1() {
return Promise.reject('Some error');
}
async function f2() {
throw 'Some error';
}
複製代碼
await
關鍵詞只能使用在async
函數中,容許去同步等待一個promise執行。若是在async
外面使用promise,仍然須要使用then
回調。
async function loadData() {
// `rp` is a request-promise function.
//`rp` 是一個請求promise函數
var promise1 = rp('https://api.example.com/endpoint1');
var promise2 = rp('https://api.example.com/endpoint2');
// Currently, both requests are fired, concurrently and
// now we'll have to wait for them to finish
//如今,兩個請求都被執行,必須等到他們執行完成
var response1 = await promise1;
var response2 = await promise2;
return response1 + ' ' + response2;
}
// Since, we're not in an `async function` anymore
// we have to use `then`.
//因爲再也不異步函數當中,咱們必須使用`then`
loadData().then(() => console.log('Done'));
複製代碼
還可使用「異步函數表達式」定義異步函數。異步函數表達式與異步函數語句很是類似,語法也幾乎相同。異步函數表達式和異步函數語句之間的主要區別是函數名,能夠在異步函數表達式中省略函數名來建立匿名函數。異步函數表達式能夠用做聲明(當即調用的函數表達式),一旦定義它就會運行。
就像這樣:
var loadData = async function() {
// `rp` is a request-promise function.
var promise1 = rp('https://api.example.com/endpoint1');
var promise2 = rp('https://api.example.com/endpoint2');
// Currently, both requests are fired, concurrently and
// now we'll have to wait for them to finish
var response1 = await promise1;
var response2 = await promise2;
return response1 + ' ' + response2;
}
複製代碼
更重要的是,在全部主流的瀏覽器都支持 async/await:
最後,重要的是不要盲目選擇編寫異步代碼的「最新」方法。理解異步 JavaScript 的內部結構很是重要,瞭解爲何異步JavaScript如此關鍵,並深刻理解所選擇的方法的內部結構。與編程中的其餘方法同樣,每種方法都有優勢和缺點。使用 async/await 能夠編寫更少的代碼。每次使用async/await時,都會跳過一些沒必要·要的步驟:使用.then
,建立一個匿名函數來處理響應:
// `rp` is a request-promise function.
rp('https://api.example.com/endpoint1').then(function(data) {
// …
});
複製代碼
與:
// `rp` is a request-promise function.
var response = await rp(‘https://api.example.com/endpoint1');
複製代碼
Async/wait 可使用相同的代碼結構(衆所周知的try/catch語句)處理同步和異步錯誤。看看它是如何與 Promise 結合的:
function loadData() {
try { // Catches synchronous errors.
getJSON().then(function(response) {
var parsed = JSON.parse(response);
console.log(parsed);
}).catch(function(e) { // Catches asynchronous errors
console.log(e);
});
} catch(e) {
console.log(e);
}
}
view raw
複製代碼
與:
async function loadData() {
try {
var data = JSON.parse(await getJSON());
console.log(data);
} catch(e) {
console.log(e);
}
}
複製代碼
用async/ wait編寫條件代碼要簡單得多:
function loadData() {
return getJSON()
.then(function(response) {
if (response.needsAnotherRequest) {
return makeAnotherRequest(response)
.then(function(anotherResponse) {
console.log(anotherResponse)
return anotherResponse
})
} else {
console.log(response)
return response
}
})
}
複製代碼
與:
async function loadData() {
var response = await getJSON();
if (response.needsAnotherRequest) {
var anotherResponse = await makeAnotherRequest(response);
console.log(anotherResponse)
return anotherResponse
} else {
console.log(response);
return response;
}
}
複製代碼
與 async/await不一樣,從 Promise 鏈返回的錯誤堆棧不提供錯誤發生在哪裏。看看下面這些:
function loadData() {
return callAPromise()
.then(callback1)
.then(callback2)
.then(callback3)
.then(() => {
throw new Error("boom");
})
}
loadData()
.catch(function(e) {
console.log(err);
// Error: boom at callAPromise.then.then.then.then (index.js:8:13)
});
複製代碼
與:
async function loadData() {
await callAPromise1()
await callAPromise2()
await callAPromise3()
await callAPromise4()
await callAPromise5()
throw new Error("boom");
}
loadData()
.catch(function(e) {
console.log(err);
// output
// Error: boom at loadData (index.js:7:9)
});
複製代碼
若是你使用過 Promise,那麼你知道調試它們是一場噩夢。例如,若是在一個程序中設置了一個斷點,而後阻塞並使用調試快捷方式(如「中止」),調試器將不會移動到下面,由於它只「逐步」執行同步代碼。使用async/wait,您能夠逐步完成wait調用,就像它們是正常的同步函數同樣。
後續文檔翻譯會陸續跟進!!
歡迎關注玄說前端公衆號: