promise 浏览器实现的源码_【大前端01-01】函数式编程与JS异步编程、手写Promise...
【簡答題】一、談?wù)勀闶侨绾卫斫釰S異步編程的,EventLoop、消息隊列都是做什么的,什么是宏任務(wù)、什么是微任務(wù)?
如何理解JS異步編程
眾所周知JavaScript語言執(zhí)行環(huán)境是“單線程”(單線程,就是指一次只能完成一件任務(wù),如果有多個任務(wù)就必須排隊等候,前面一個任務(wù)完成,再執(zhí)行后面一個任務(wù))。這種“單線程”模式執(zhí)行效率較低,任務(wù)耗時長。 為了解決這個問題,提出了“異步模式”(異步模式,是指后一個任務(wù)不等前一個任務(wù)執(zhí)行完就執(zhí)行,每個任務(wù)有一個或多個回調(diào)函數(shù))。 異步模式使得JavaScript在處理事務(wù)時非常高效,但也帶來很多問題,如異常處理困難、嵌套過深。
EventLoop是做什么的event loop是一個執(zhí)行模型,在不同的地方有不同的實現(xiàn)。瀏覽器和NodeJS基于不同的技術(shù)實現(xiàn)了各自的Event Loop。瀏覽器的Event Loop是在html5的規(guī)范中明確定義。
NodeJS的Event Loop是基于libuv實現(xiàn)的。
libuv已經(jīng)對Event Loop做出了實現(xiàn),而HTML5規(guī)范中只是定義了瀏覽器中Event Loop的模型,具體的實現(xiàn)留給了瀏覽器廠商。
瀏覽器的Event Loop
這張圖將瀏覽器的Event Loop完整的描述了出來,我來講執(zhí)行一個JavaScript代碼的具體流程: 1. 執(zhí)行全局Script同步代碼,這些同步代碼有一些是同步語句,有一些是異步語句(比如setTimeout等); 2. 全局Script代碼執(zhí)行完畢后,調(diào)用棧Stack會清空; 3. 從微隊列microtask queue中取出位于隊首的回調(diào)任務(wù),放入調(diào)用棧Stack中執(zhí)行,執(zhí)行完后microtask queue長度減1; 4. 繼續(xù)取出位于隊首的任務(wù),放入調(diào)用棧Stack中執(zhí)行,以此類推,直到直到把microtask queue中的所有任務(wù)都執(zhí)行完畢。注意,如果在執(zhí)行microtask的過程中,又產(chǎn)生了microtask,那么會加入到隊列的末尾,也會在這個周期被調(diào)用執(zhí)行; 5. microtask queue中的所有任務(wù)都執(zhí)行完畢,此時microtask queue為空隊列,調(diào)用棧Stack也為空; 6. 取出宏隊列macrotask queue中位于隊首的任務(wù),放入Stack中執(zhí)行; 7. 執(zhí)行完畢后,調(diào)用棧Stack為空; 8. 重復(fù)第3-7個步驟; 9. ......
可以看到,這就是瀏覽器的事件循環(huán)Event Loop
這里歸納3個重點: 1. 宏隊列macrotask一次只從隊列中取一個任務(wù)執(zhí)行,執(zhí)行完后就去執(zhí)行微任務(wù)隊列中的任務(wù); 2. 微任務(wù)隊列中所有的任務(wù)都會被依次取出來執(zhí)行,直到microtask queue為空; 3. 圖中沒有畫UI rendering的節(jié)點,因為這個是由瀏覽器自行判斷決定的,但是只要執(zhí)行UI rendering,它的節(jié)點是在執(zhí)行完所有的microtask之后,下一個macrotask之前,緊跟著執(zhí)行UI render。
在執(zhí)行微隊列microtask queue中任務(wù)的時候,如果又產(chǎn)生了microtask,那么會繼續(xù)添加到隊列的末尾,也會在這個周期執(zhí)行,直到microtask queue為空停止。
注:當(dāng)然如果你在microtask中不斷的產(chǎn)生microtask,那么其他宏任務(wù)macrotask就無法執(zhí)行了,但是這個操作也不是無限的,拿NodeJS中的微任務(wù)process.nextTick()來說,它的上限是1000個,這里不再詳細(xì)講。總結(jié):
瀏覽器的Event Loop和NodeJS的Event Loop是不同的,實現(xiàn)機制也不一樣,不要混為一談,今天我們只介紹瀏覽器里面的Event Loop。
瀏覽器可以理解成只有1個宏任務(wù)隊列和1個微任務(wù)隊列,先執(zhí)行全局Script代碼,執(zhí)行完同步代碼調(diào)用棧清空后,從微任務(wù)隊列中依次取出所有的任務(wù)放入調(diào)用棧執(zhí)行,微任務(wù)隊列清空后,從宏任務(wù)隊列中只取位于隊首的任務(wù)放入調(diào)用棧執(zhí)行,注意這里和Node的區(qū)別,只取一個,然后繼續(xù)執(zhí)行微隊列中的所有任務(wù),再去宏隊列取一個,以此構(gòu)成事件循環(huán)。
消息隊列是做什么的
消息隊列:也稱為任務(wù)隊列,是一個先進(jìn)先出的隊列,它里面存放著各種消息,即異步操作的回調(diào)函數(shù),異步操作會將相關(guān)回調(diào)添加到任務(wù)隊列中,而不同的異步操作添加到任務(wù)隊列的時機也不同,如onclick,setTimeout,ajax處理的方式都不同,這些異步操作都是由瀏覽器內(nèi)核的不同模塊來執(zhí)行的:onclick由瀏覽器內(nèi)核的DOM Binding模塊來處理,當(dāng)事件觸發(fā)的時候,回調(diào)函數(shù)會立即添加到任務(wù)隊列中;
setTimeout會由瀏覽器內(nèi)核的timer模塊來進(jìn)行延時處理,當(dāng)時間到達(dá)的時候,才會將回調(diào)函數(shù)添加到任務(wù)隊列中;
ajax會由瀏覽器內(nèi)核的network模塊來處理,在網(wǎng)絡(luò)請求完成返回之后,才將回調(diào)添加到任務(wù)隊列中;
什么是宏任務(wù)宏任務(wù)/宏隊列,macrotask,也叫tasks。 一些異步任務(wù)的回調(diào)會依次進(jìn)入macro task queue,等待后續(xù)被調(diào)用,這些異步任務(wù)包括:setTimeout
setInterval
setImmediate (Node獨有)
requestAnimationFrame (瀏覽器獨有)
I/O
UI rendering (瀏覽器獨有)
什么是微任務(wù)微任務(wù)/微隊列,microtask,也叫jobs。 另一些異步任務(wù)的回調(diào)會依次進(jìn)入micro task queue,等待后續(xù)被調(diào)用,這些異步任務(wù)包括:process.nextTick (Node獨有)
Promise
Object.observe
MutationObserver
queueMicroTask
宏任務(wù)與微任務(wù)的區(qū)別
這個就像去銀行辦業(yè)務(wù)一樣,先要取號進(jìn)行排號。 一般上邊都會印著類似:“您的號碼為XX,前邊還有XX人。”之類的字樣。
因為柜員同時只能處理一個來辦理業(yè)務(wù)的客戶,這時每一個來辦理業(yè)務(wù)的人就可以認(rèn)為是銀行柜員的一個宏任務(wù)來存在的,當(dāng)柜員處理完當(dāng)前客戶的問題以后,選擇接待下一位,廣播報號,也就是下一個宏任務(wù)的開始。 所以多個宏任務(wù)合在一起就可以認(rèn)為說有一個任務(wù)隊列在這,里邊是當(dāng)前銀行中所有排號的客戶。 任務(wù)隊列中的都是已經(jīng)完成的異步操作,而不是說注冊一個異步任務(wù)就會被放在這個任務(wù)隊列中,就像在銀行中排號,如果叫到你的時候你不在,那么你當(dāng)前的號牌就作廢了,柜員會選擇直接跳過進(jìn)行下一個客戶的業(yè)務(wù)處理,等你回來以后還需要重新取號。
而且一個宏任務(wù)在執(zhí)行的過程中,是可以添加一些微任務(wù)的,就像在柜臺辦理業(yè)務(wù),你前邊的一位老大爺可能在存款,在存款這個業(yè)務(wù)辦理完以后,柜員會問老大爺還有沒有其他需要辦理的業(yè)務(wù),這時老大爺想了一下:“最近P2P爆雷有點兒多,是不是要選擇穩(wěn)一些的理財呢”,然后告訴柜員說,要辦一些理財?shù)臉I(yè)務(wù),這時候柜員肯定不能告訴老大爺說:“您再上后邊取個號去,重新排隊”。 所以本來快輪到你來辦理業(yè)務(wù),會因為老大爺臨時添加的“理財業(yè)務(wù)”而往后推。 也許老大爺在辦完理財以后還想 再辦一個信用卡?或者 再買點兒紀(jì)念幣? 無論是什么需求,只要是柜員能夠幫她辦理的,都會在處理你的業(yè)務(wù)之前來做這些事情,這些都可以認(rèn)為是微任務(wù)。
在當(dāng)前的微任務(wù)沒有執(zhí)行完成時,是不會執(zhí)行下一個宏任務(wù)的。
【代碼題】一、將下面異步代碼使用Promise的方式改進(jìn)
setTimeout(function(){
var a = 'hello'
setTimeout(function(){
var b = 'lagou'
setTimeout(function(){
var c = 'I ? U'
console.log(a + b + c)
},10)
},10)
},10)
解答:
function fn(parp) {
const promise = new Promise((resolved, rejected) => {
setTimeout(()=>resolved(parp), 10)
});
return promise;
}
fn()
.then(() => fn("hello"))
.then(value => fn(value+"lagou"))
.then(value => fn(value+"I ? U"))
.then(value => console.log(value))
【代碼題】二、基于以下代碼完成下面的四個練習(xí)
const fp = require('lodash/fp')
// 數(shù)據(jù)// horsepower 馬力, dollar_value 價格, in_stock 庫存const cars = [
{ name: 'Ferrari FF', horsepower: 660,
dollar_value: 700000, in_stock: true },
{ name: 'Spyker C12 Zagato', horsepower: 650,
dollar_value: 648000, in_stock: false },
{ name: 'Jaguar XKR-S', horsepower: 550,
dollar_value: 132000, in_stock: false },
{ name: 'Audi R8', horsepower: 525,
dollar_value: 114200, in_stock: false },
{ name: 'Aston Martin One-77', horsepower: 750,
dollar_value: 1850000, in_stock: true },
{ name: 'Pagani Huayra', horsepower: 700,
dollar_value: 1300000, in_stock: false },
]
練習(xí)1:使用函數(shù)組合fp.flowRight()重新實現(xiàn)下面這個函數(shù)
let isLastInStock = function (cars) {
// 獲取最后一條數(shù)據(jù) let last_car = fp.last(cars)
// 獲取最后一條數(shù)據(jù)的 in_stock 屬性值 return fp.prop('in_stock', last_car)
}
解答:
let isLastInStock = fp.flowRight(fp.prop('in_stock'), fp.last)
//fp.prop可以替換為fp.get
console.log(isLastInStock(cars))
練習(xí)2:使用fp.flowRight()、fp.prop()和fp.first()獲取第一個car的name
解答:
let isFirstCarName = fp.flowRight(fp.prop('name'), fp.first)
//fp.first可替換為fp.head
console.log(isFirstCarName(cars))
練習(xí)3:使用幫助函數(shù)_average重構(gòu)averageDollarValue,使用組合函數(shù)的方式實現(xiàn)
let _average = function (xs) {
return fp.reduce(fp.add, 0, xs)/xs.length
}//
let dollar_values = fp.map(function(car){
return car.dollar_value
}, cars)
return _average(dollar_values)
}
解答:
//第一種let averageDollarValue = fp.flowRight(_average, fp.map(car=>car.dollar_value))
//第二種let averageDollorValue = fp.flowRight(_average, fp.map(fp.curry(fp.prop)('dollar_value')))
練習(xí)4:使用flowRight寫一個sanitizeNames()函數(shù)
返回一個下劃線連接的小寫字符串,把數(shù)組中的name轉(zhuǎn)換為這種形式:例如:sanitizeNames(["Hello World"])=>["hello_world"]
let _underscore = fp.replace(/\w+/g, '_') //
解答:
//let _underscore = fp.replace(/\w+/g, '_') // 不符合題意 應(yīng)該為\slet _underscore = fp.replace(/\s+/g, '_')
//答案let sanitizeNames = fp.flowRight(fp.map(_underscore), fp.map(fp.toLower), fp.map(car => car.name))
【代碼題】三、基于下面提供的代碼,完成后續(xù)的四個練習(xí)
// support.jsclass Container {
static of(value) {
return new Container(value)
}
constructor(value) {
this.value = value
}
map(fn) {
return Container.of(fn(this.value))
}
}
class Maybe {
static of(x) {
return new Maybe(x)
}
isNothing() {
return this._value === null || this._value === undefined
}
constructor(x) {
this._value = x
}
map(fn) {
return this.isNothing() ? this : Maybe.of(fn(this._value))
}
}
module.exports = { Maybe, Container }
練習(xí)1:使用fp.add(x,y)和fp.map(f,x)創(chuàng)建一個能讓functor里的值增加的函數(shù)ex1
// app.jsconst fp = require('lodash/fp')
const { Maybe, Container } = require('./support')
let maybe = Maybe.of([5, 6, 1])
let ex1 = () => {
// 你需要實現(xiàn)的函數(shù)。。。}
解答:
return maybe.map(function(arr){
return fp.map(function(v){
return fp.add(v,1) // 數(shù)組每一項加1 },arr)
})
//ES6return maybe.map(arr => fp.map(v => fp.add(v, 1), arr))
練習(xí)2:實現(xiàn)一個函數(shù)ex2,能夠使用fp.first獲取列表的第一個元素
// app.jsconst fp = require('lodash/fp')
const { Maybe, Container } = require('./support')
let xs = Container.of(['do', 'ray',
'me', 'fa', 'so', 'la', 'ti', 'do'])
let ex2 = () => {
// 你需要實現(xiàn)的函數(shù)。。。}
解答:
return xs.map(fp.first).value
練習(xí)3:實現(xiàn)一個函數(shù)ex3,使用safeProp和fp.first找到user的名字的首字母
// app.jsconst fp = require('lodash/fp')
const { Maybe, Container } = require('./support')
let safeProp = fp.curry(function (x, o){
return Maybe.of(o[x])
})
let user = { id:2, name: 'Albert' }
let ex3 = () => {
// 你需要實現(xiàn)的函數(shù)。。。}
解答:
return safeProp('name', user).map(fp.first)._value
練習(xí)4:使用Maybe重寫ex4,不要有if語句
// app.jsconst fp = require('lodash/fp')
const { Maybe, Container } = require('./support')
let ex4 = function (n) {
if(n) {
return parseInt(n)
}
}
解答:
let ex4 = n => Maybe.of(n).map(parseInt)._value;
【代碼題】四、手寫實現(xiàn)MyPromise源碼
要求:盡可能還原Promise中的每一個API,并通過注釋的方式描述思路和原理
解答:
/*1. Promise 就是一個類 在執(zhí)行這個類的時候 需要傳遞一個執(zhí)行器進(jìn)去 執(zhí)行器會立即執(zhí)行2. Promise 中有三種狀態(tài) 分別為 成功 fulfilled 失敗 rejected 等待 pendingpending -> fulfilledpending -> rejected一旦狀態(tài)確定就不可更改3. resolve和reject函數(shù)是用來更改狀態(tài)的resolve: fulfilledreject: rejected4. then方法內(nèi)部做的事情就判斷狀態(tài) 如果狀態(tài)是成功 調(diào)用成功的回調(diào)函數(shù) 如果狀態(tài)是失敗 調(diào)用失敗回調(diào)函數(shù) then方法是被定義在原型對象中的5. then成功回調(diào)有一個參數(shù) 表示成功之后的值 then失敗回調(diào)有一個參數(shù) 表示失敗后的原因6. 同一個promise對象下面的then方法是可以被調(diào)用多次的7. then方法是可以被鏈?zhǔn)秸{(diào)用的, 后面then方法的回調(diào)函數(shù)拿到值的是上一個then方法的回調(diào)函數(shù)的返回值*/
//我們將狀態(tài)定義為常量,因為我們需要頻繁使用它//當(dāng)我們?nèi)ナ褂眠@個常量的時候,編輯器是有代碼提示的const PENDING = 'pending'; // 等待const FULFILLED = 'fulfilled'; // 成功const REJECTED = 'rejected'; // 失敗
class MyPromise {
constructor (executor) { // 代表執(zhí)行器 try { // 捕獲執(zhí)行器的錯誤 executor error // 這個執(zhí)行器executor是立即執(zhí)行的 //當(dāng)前我們在一個類的里面,我們需要通過this訪問 executor(this.resolve, this.reject)
} catch (e) {
this.reject(e);
}
}
// promsie 狀態(tài) 默認(rèn)為 等待 status = PENDING;
// 成功之后的值 value = undefined;
// 失敗后的原因 reason = undefined;
// 成功回調(diào) 數(shù)組的原因是同一個promise對象下面的then方法是可以被多次調(diào)用的 為了同時存儲多個回調(diào)函數(shù) successCallback = [];
// 失敗回調(diào) 數(shù)組的原因是同一個promise對象下面的then方法是可以被多次調(diào)用的 為了同時存儲多個回調(diào)函數(shù) failCallback = [];
// 這里的resolve和reject之所以使用箭頭函數(shù)是因為this指向的問題 // 我們希望this指向promise對象而不是window resolve = value => {
// 如果狀態(tài)不是等待 阻止程序向下執(zhí)行 if (this.status !== PENDING) return;
// 將狀態(tài)更改為成功 this.status = FULFILLED;
// 保存成功之后的值 this.value = value;
// 判斷成功回調(diào)是否存在 如果存在 調(diào)用 // this.successCallback && this.successCallback(this.value); //處理異步情況 由于回調(diào)為數(shù)組 故失效 while(this.successCallback.length) this.successCallback.shift()() //從前往后執(zhí)行 彈出回調(diào)函數(shù) }
reject = reason => {
// 如果狀態(tài)不是等待 阻止程序向下執(zhí)行 if (this.status !== PENDING) return;
// 將狀態(tài)更改為失敗 this.status = REJECTED;
// 保存失敗后的原因 this.reason = reason;
// 判斷失敗回調(diào)是否存在 如果存在 調(diào)用 // this.failCallback && this.failCallback(this.reason); //處理異步情況 while(this.failCallback.length) this.failCallback.shift()() //從前往后執(zhí)行 彈出回調(diào)函數(shù) }
then (successCallback, failCallback) {
//then()方法可以不傳遞參數(shù) // 參數(shù)可選 successCallback = successCallback ? successCallback : value => value;
// 參數(shù)可選 failCallback = failCallback ? failCallback: reason => { throw reason };
// 為了能夠被鏈?zhǔn)秸{(diào)用 then方法必須返回一個promise對象 let promsie2 = new MyPromise((resolve, reject) => { //立即執(zhí)行 resolve和reject是為了傳遞給下一個then方法的回調(diào)函數(shù) // 判斷狀態(tài) if (this.status === FULFILLED) { // 成功 setTimeout(() => { // 正常來講內(nèi)部無法直接獲取到 promise2 因為其本身還未執(zhí)行 所以將其變?yōu)楫惒酱a try { // then回調(diào)函數(shù)錯誤 并在下一次promise的下一次錯誤處理函數(shù)中輸出 // 方便then的鏈?zhǔn)秸{(diào)用 我們需要獲取到then的返回值 以便傳遞給下一個then的成功回調(diào)函數(shù) let x = successCallback(this.value);
// 判斷 x 的值是普通值還是promise對象 // 如果是普通值 直接調(diào)用resolve // 如果是promise對象 查看promsie對象返回的結(jié)果 // 再根據(jù)promise對象返回的結(jié)果 決定調(diào)用resolve 還是調(diào)用reject resolvePromise(promsie2, x, resolve, reject) // 調(diào)用 解析promise }catch (e) {
reject(e);
}
}, 0)
}else if (this.status === REJECTED) { // 失敗 setTimeout(() => { // 正常來講內(nèi)部無法直接獲取到 promise2 因為其本身還未執(zhí)行 所以將其變?yōu)楫惒酱a try { // then回調(diào)函數(shù)錯誤 并在下一個promise的錯誤處理函數(shù)中輸出 // 方便then的鏈?zhǔn)秸{(diào)用 我們需要獲取到then的返回值 以便傳遞給下一個then的失敗回調(diào)函數(shù) let x = failCallback(this.reason);
// 判斷 x 的值是普通值還是promise對象 // 如果是普通值 直接調(diào)用resolve // 如果是promise對象 查看promsie對象返回的結(jié)果 // 再根據(jù)promise對象返回的結(jié)果 決定調(diào)用resolve 還是調(diào)用reject resolvePromise(promsie2, x, resolve, reject) // 調(diào)用 解析promise }catch (e) {
reject(e);
}
}, 0)
} else { // 等待 // 將成功回調(diào)和失敗回調(diào)存儲起來 this.successCallback.push(() => { // 存儲成功回調(diào)到數(shù)組 setTimeout(() => {
try {
let x = successCallback(this.value);
// 判斷 x 的值是普通值還是promise對象 // 如果是普通值 直接調(diào)用resolve // 如果是promise對象 查看promsie對象返回的結(jié)果 // 再根據(jù)promise對象返回的結(jié)果 決定調(diào)用resolve 還是調(diào)用reject resolvePromise(promsie2, x, resolve, reject)
}catch (e) {
reject(e);
}
}, 0)
});
this.failCallback.push(() => { // 存儲失敗回調(diào)到數(shù)組 setTimeout(() => {
try {
let x = failCallback(this.reason);
// 判斷 x 的值是普通值還是promise對象 // 如果是普通值 直接調(diào)用resolve // 如果是promise對象 查看promsie對象返回的結(jié)果 // 再根據(jù)promise對象返回的結(jié)果 決定調(diào)用resolve 還是調(diào)用reject resolvePromise(promsie2, x, resolve, reject)
}catch (e) {
reject(e);
}
}, 0)
});
}
});
return promsie2;
}
finally (callback) { // 接收一個回調(diào)函數(shù) 可以返回一個函數(shù) return this.then(value => { //通過then得到當(dāng)前promise的狀態(tài) 并返回以便再次調(diào)用 return MyPromise.resolve(callback()).then(() => value);
}, reason => {
return MyPromise.resolve(callback()).then(() => { throw reason })
})
}
catch (failCallback) {
return this.then(undefined, failCallback)
}
static all (array) { // 接收數(shù)組 參數(shù)順序一定為結(jié)果順序 也是一個promise對象 Promise.all 故為靜態(tài)方法 let result = []; // 結(jié)果數(shù)組 let index = 0; // 解決for循環(huán)的異步操作問題 return new MyPromise((resolve, reject) => {
function addData (key, value) {
result[key] = value;
index++;
if (index === array.length) {
resolve(result);
}
}
for (let i = 0; i < array.length; i++) { // 執(zhí)行for循環(huán)的過程中可能有異步操作 let current = array[i]; // 當(dāng)前值 if (current instanceof MyPromise) {
// promise 對象 current.then(value => addData(i, value), reason => reject(reason))
}else {
// 普通值 addData(i, array[i]);
}
}
})
}
static resolve (value) { // 判斷給定的是不是promise 是 直接返回; 不是 創(chuàng)建promise 并返回 if (value instanceof MyPromise) return value;
return new MyPromise(resolve => resolve(value));
}
}
function resolvePromise (promsie2, x, resolve, reject) {
// 解析promise 判斷then回調(diào)的返回值的類型 為promise對象還是普通值 if (promsie2 === x) { // 判斷 then是否被循環(huán)調(diào)用 并阻止其運行 return reject(new TypeError('Chaining cycle detected for promise #'))
}
if (x instanceof MyPromise) {
// promise 對象 // x.then(value => resolve(value), reason => reject(reason)); // 簡化為 ↓ x.then(resolve, reject);
} else {
// 普通值 resolve(x);
}
}
module.exports = MyPromise;
總結(jié)
以上是生活随笔為你收集整理的promise 浏览器实现的源码_【大前端01-01】函数式编程与JS异步编程、手写Promise...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 首发199元!Ticwatch GTK智
- 下一篇: HTML5中常见的列表标签包括,介绍几个