小哥哥、小姐姐,你們好,請把手伸出來,我給你們點東西。
1、JavaScript非同步程式設計
- 同步與非同步
- 回撥函式
- promise
- generator
- async+await
2、寫一個符合規範的promise庫
1、JavaScript非同步程式設計
1-1、同步與非同步
我們都知道js是單執行緒語言,這就導致了會有同步非同步的概念。所謂同步,就是指指令碼直譯器在解析程式碼時,從上往下一行一行解釋,第一行解釋不完,就不去解釋第二行。所謂非同步,就是指,當解釋到某一行中,發現有非同步方法(比如settimeout、ajax、DOM點選事件等等),直譯器不會去等待非同步方法執行完,再往下解釋。而是,將非同步任務放到任務佇列,當所有的同步程式碼全部執行完,也就是主執行緒沒有可執行的程式碼了,就回去任務佇列拿出之前遇到的非同步任務,執行,執行完畢後再去任務佇列調取下一個任務,如此迴圈。
一圖勝千言
一次只能服務一個人,請排好隊。誰事兒多也沒辦法,後面只能等著。 三個人同時吃飯,這是非同步。如果是同步的話,只能一個人吃完下一個人再吃。1-2、回撥函式
相信大家對回撥函式已經不陌生了,函式A作為引數被傳遞到函式B裡,那麼函式A就是回撥函式。有什麼用呢?請看程式碼
let doSomething = () => { console.log('do something') }
setTimeout(doSomething, 500);
console.log('a');
複製程式碼
宣告瞭一個
doSomething
函式,並作為第一個引數傳遞給了setTimeout
函式,setTimeout
函式會在合適的時機執行它。達到了非同步程式設計的目的。這種方式用處有很多,node.js
有大部分api
都是通過回撥來實現非同步程式設計的。
1-3、promise
寫法
回撥函式這種形式有一個缺點,那就是如果非同步任務比較多的話,並且多工執行有先後順序,那麼回撥函式很容易就形成多層巢狀。如下:
function doA() { }
function doB() { }
function doC() { }
function doD() { }
doA(function () {
doB(function () {
doC(function () {
doD(function () {
})
})
})
})
複製程式碼
當改用promise後,瞬間清爽了許多。
new Promise(function(resolve,reject){
resolve();//在合適的時機出發resolve
})
.then(doA,null)
.then(doB,null)
.then(doC,null)
.then(doD,null)
複製程式碼
這也是這篇文章的重點講解內容,一會我會一步一步按照規範編寫一個promise庫。徹底搞懂promise。
1-4、generator
寫法
function* gen() {
let a = yield doA();
let b = yield doB();
let c = yield doC();
let d = yield doD();
}
let it = gen();//it是一個迭代器
it.next();//{ value: undefined, done: false }
it.next();{ value: undefined, done: false }
it.next();{ value: undefined, done: false }
it.next();{ value: undefined, done: false }
it.next();{ value: undefined, done: true }
複製程式碼
可以看到,generator
函式有另外一個功能,那就是可以暫停,不像普通函式,只要一執行,那就會一口氣執行完。
1-5、async
+ await
寫法
async function doSomething() {
await doA();
await doB();
await doC();
await doD();
}
doSomething();
複製程式碼
是不是發現,這種寫法更加簡潔,就像在寫同步程式碼一樣。
2、寫一個符合規範的promise庫
2-1、實現最簡單的一個Promise類
先來看Promise
的用法,然後根據用法一步步編寫Promise
類
let p1 = new Promise(function (resolve, reject) {
})
p1.then(function (data) {
console.log(data)
}, function (err) {
console.log(err)
})
複製程式碼
在例項化一個Promise
時,傳入一個函式作為引數,該函式接受兩個引數,分別為resolve
,reject
,然後按照Promise/A+規範一個Promise類應該包含如下狀態
status
value
reason
onResolvedCallbacks
onRejectedCallbacks
我們很容易就寫出了Promise
的原型。程式碼如下:
function Promise(executor) {
let self = this;
self.status = 'pending';
self.value = undefined;
self.reason = undefined;
self.onResolvedCallbacks = [];
self.onRejectedCallbacks = [];
function resolve() { }
function reject() { }
executor(resolve, reject);
}
Promise.prototype.then = function (onFulfilled, onRejected) {
}
複製程式碼
接下來我們一個一個方法去攻破。
2-2、 實現resolve
、 reject
方法
resolve
方法需要完成的事情是:
- 將Promise的狀態置為
resolved
- 更改
self.value
的值 - 通知
self.onResolvedCallbacks
,並一一執行。
reject
方法需要完成的事情是
- 將Promise的狀態置為
rejectd
- 更改
self.reason
的值 - 通知
self.onRejectedCallbacks
,並一一執行。 程式碼如下:
function resolve(value) {
self.status = 'resolved';
self.value = value;
self.onResolvedCallbacks.forEach(item => item(value))
}
function reject(reason) {
self.status = 'rejected';
self.reason = reason;
self.onRejectedCallbacks.forEach(item => item(reason))
}
複製程式碼
2-3、 實現then方法
then
方法的作用是收集到成功、失敗的回撥函式,將他們分別新增到成功和失敗的陣列中。也就是程式碼中,我們需要將onFulfilled
新增到self.onResolvedCallbacks
裡,將onRejected
新增到self.onRejectedCallbacks
裡。
Promise.prototype.then = function (onFulfilled, onRejected) {
this.onResolvedCallbacks.push(onFulfilled);
this.onRejectedCallbacks.push(onRejected);
}
複製程式碼
寫到這裡,這個Promise
其實已經可以用了,不過還有一個潛在的問題。那就是,當resolve
方法被同步呼叫時,通過then
方法加入到佇列的函式沒有被執行。只有resolve
被非同步呼叫時才會被執行,為什麼呢。因為這裡的then
是同步的,resolve
也被同步呼叫的話,那肯定是,先執行resolve
後執行then
,換句話說就是,先執行回撥,後新增回撥,這不是我們想看到的,要達到先新增回撥,後執行回撥的效果,我們稍作修改。
function resolve(value) {
setTimeout(() => {
if (self.status === 'pending') {
self.status = 'resolved';
self.value = value;
self.onResolvedCallbacks.forEach(item => item(value))
}
});
}
function reject(reason) {
setTimeout(function () {
if (self.status == 'pending') {
self.value = value;
self.status = 'rejected';
self.onRejectedCallbacks.forEach(item => item(value));
}
});
}
複製程式碼
這裡加入了狀態判斷,因為當Promise
的狀態一旦確定,就不能更改,所以狀態只能是pending
時,resolve
和reject
才生效。
2-4、實現鏈式呼叫
先看一下Promise/A+規範中對then
方法的返回值描述。
then
方法必須返回一個promise
,以實現鏈式呼叫。現在我們需要關注的是,呼叫then
方法時,傳入的第一個引數(onFulfilled
)的返回值問題。如果是一個普通值,那我們就把它繼續傳遞下去,傳遞給then方法返回的promise
裡;如果是一個新的promise
的話,那就需要將新的promise
和then
方法返回的promise
關聯起來。
具體如何關聯,我們們慢慢來,這裡有點繞,我先上張圖看圖說話。
圖中有部分程式碼只需要注意三個promise,p1
、x
、p2
,為了不造成混淆,這三個東西我一次標到了圖的左部分,他們是對應的。請大家先明白一句話,然後我們開始說。
呼叫promise的resolve方法,會執行該promise的then函式的第一個引數
(這裡我那resolve舉例,reject道理一樣,就不贅述了。)
看圖,請看圖。
呼叫p1的resolve方法,那麼a就會被執行。
呼叫p2的resolve方法,那麼c就會被執行。
也就是說,你想執行a或者b或者c或者d,那麼你得找到它屬於哪個promise,例如:圖中,a b 屬於p1,c d屬於p2.這個關係必須要明確。
假設你已經理解了上面的話,現在我們面臨的問題來了。
c d 本來是屬於p2的,執行還是不執行也得看p2調不呼叫resolve、reject。 現在要讓c d執不執行不看p2了,得看x。為什麼要看x,因為x這個回撥函式是使用者傳遞的,使用者的意思是:我讓這個回撥返回一個promise,然後繼續使用then方法新增成功或失敗的回撥,而且這兩個回撥啥時候執行,得看我返回的那個promise。 反應到圖中就是這個意思:c d何時執行,看x何時呼叫resolve、reject。
希望你理解了。...繼續
破解方法:將p2的resolve、reject放入到x的then方法裡。 解釋一下:x的resolve、reject是暴露給使用者的,也就是說,這兩個方法的執行權在使用者手裡,當使用者執行resolve時,其實就執行了x的then方法的第一個引數,而x的then方法的第一個引數正好是p2的resolve,p2的resolve就被執行了,p2的resolve一執行,那麼c就被執行了。就實現了x的resolve、reject控制著c d的執行與否。
說了這麼多,上程式碼吧還是,改造後的then方法如下,加入了狀態判斷,錯誤捕獲
Promise.prototype.then = function (onFulfilled, onRejected) {
let promise2;
let self = this;
if (self.status === 'resolve') {
promise2 = Promise(function (resolve, reject) {
setTimeout(() => {
try {
let x = onFulfilled(self.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
})
}
if (self.satus === 'rejected') {
promise2 = new Promise(function (resolve, reject) {
setTimeout(function () {
try {
let x = onRejected(self.reason);
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e);
}
})
})
}
if (self.status === 'pending') {
promise2 = new Promise(function (resolve, reject) {
self.onResolvedCallbacks.push(function (value) {
try {
let x = onFulfilled(value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
self.onRejectedCallbacks.push(function (reason) {
try {
let x = onRejected(reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
})
})
}
return promise2;
}
複製程式碼
再把resolvePromise
方法寫一下,因為多個地方用到了,所以就單獨封裝了。
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('迴圈引用'));
}
let then, called;
if (x != null && ((typeof x == 'object' || typeof x == 'function'))) {
try {
then = x.then;
if (typeof then == 'function') {
then.call(x, function (y) {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
}, function (r) {
if (called) return;
called = true;
reject(r);
});
} else {
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
複製程式碼
到這裡,我們寫的promise已經支援鏈式呼叫了。我希望閱讀本文的你,先去讀懂那張圖,一定要看懂,知道自己在幹什麼,就是思路要清晰,然後再去寫程式碼。我剛接觸的時候,就是一步步去捋思路,然後輔助畫圖去理解。用了好幾天才弄懂。
2-5、實現catch
、all
、race
、Promise.resolve()
、Promise.reject()
相比較then方法,這幾個方法就輕鬆許多了。直接上程式碼了,我會把註釋寫到程式碼裡邊 catch
Promise.prototype.catch = function (onRejected) {
return this.then(null, onRejected);//原來這麼簡單
}
複製程式碼
all
Promise.all = function (promises) {
return new Promise(function (resolve, reject) {
let result = [];//結果集
let count = 0;//計數器,用來記錄promise有沒有執行完
for (let i = 0; i < promises.length; i++) {
promises[i].then(function (data) {
result[i] = data;
if (++count == promises.length) {
resolve(result);//計數器滿足條件時,觸發resolve
}
}, function (err) {
reject(err);
});
}
});
}
複製程式碼
race
// 只要有一個promise成功了 就算成功。如果第一個失敗了就失敗了
Promise.race = function (promises) {
return new Promise(function (resolve, reject) {
for (var i = 0; i < promises.length; i++) {
promises[i].then(resolve,reject)
}
})
}
複製程式碼
Promise.resolve()、Promise.reject()
// 生成一個成功的promise
Promise.resolve = function (value) {
return new Promise(function (resolve, reject) {
resolve(value);
})
}
// 生成一個失敗的promise
Promise.reject = function (reason) {
return new Promise(function (resolve, reject) {
reject(reason);
})
}
複製程式碼
2-6、Promise
的語法糖
Promise.deferred = Promise.defer = function () {
var defer = {};
defer.promise = new Promise(function (resolve, reject) {
defer.resolve = resolve;
defer.reject = reject;
})
return defer;
}
複製程式碼
看一個例子
let fs = require('fs');
let Promise = require('./promise');
function read() {
// 好處就是解決巢狀問題
// 壞處錯誤處理不方便了
let defer = Promise.defer();
fs.readFile('./2.promise.js/a.txt','utf8',(err,data)=>{
if(err)defer.reject(err);
defer.resolve(data)
});
return defer.promise;
}
read().then(data=>{
console.log(data);
});
複製程式碼
說了很多,希望你們理解了,如果文中有錯誤或者大家有不懂的地方,歡迎留言。