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