js非同步發展歷史與Promise原理分析
關於非同步
所謂 “非同步”,簡單說就是一個任務不是連續完成的,可以理解成該任務被人為分成兩段,先執行第一段,然後轉而執行其他任務,等做好了準備,再回過頭執行第二段。
比如,有一個任務是讀取檔案進行處理,任務的第一段是向作業系統發出請求,要求讀取檔案。然後,程式執行其他任務,等到作業系統返回檔案,再接著執行任務的第二段(處理檔案)。這種不連續的執行,就叫做非同步。
相應地,連續的執行就叫做同步。由於是連續執行,不能插入其他任務,所以作業系統從硬碟讀取檔案的這段時間,程式只能乾等著。
簡單的說同步就是大家排隊工作,非同步就是大家同時工作。如果你還不太明白非同步與同步,多看看JS基礎的文章。
非同步的發展歷史
1.CallBack寫法
回撥意為“回撥函式”,即非同步操作執行完後觸發執行的函式,例如:
$.get("http://api.xxxx.com/xxx",callback);
當請求完成時就會觸發回撥函式。
回撥可以完成非同步操作,但是經歷過JQuery的時代的人應該都對某一種需求折磨過,舉個例子:專案要求前端AJAX請求後端介面列表型別名稱,然後在用型別名稱AJAX請求列表ID,在用ID請求列表具體內容,最後程式碼大概是這樣的
$.ajax({
url: "type",
data:1,
success: function (a) {
$.ajax({
url: "list" ,
data:a,
success: function (b) {
$.ajax({
url: "content",
data:b,
success: function (c) {
console.log(c)
}
})
}
})
}
})
這是是單純的巢狀程式碼,如若再加上業務程式碼,程式碼可讀性可想而知,如果是開發起來還好,但是後期的維護和修改的難度足以讓人瘋掉。這就是那個JQuery的時代的“回撥地獄”問題。
2.Promise
為了解決“回撥地獄”問題,提出了無極物件,並且後來加入了ES6標準,無極物件簡單說就是一個容器,裡面儲存著某個未來才會結束的事件(通常是一個非同步操作)的結果。從語法上說,Promise是一個物件,從它可以獲取非同步操作的訊息。提供統一的API,各種非同步操作都可以用同樣的方法進行處理。
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 非同步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise建構函式接受一個函式作為引數,該函式的兩個引數分別是resolve和reject。它們是兩個函式,由JavaScript引擎提供,不用自己部署。
解決函式的作用是,將Promise物件的狀態從“未完成”變為“成功”(即從待定變為已解決),在非同步操作成功時呼叫,並將非同步操作的結果,作為引數傳遞出去;函式的作用是,將Promise物件的狀態從“未完成”變為“失敗”(即從待決變為被拒絕),在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。
無極例項生成以後,可以用那麼方法分別指定解決狀態和拒收狀態的回撥函式。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
那麼方法可以接受兩個回撥函式作為引數。第一個回撥函式是無極物件的狀態變為解決時呼叫,第二個回撥函式是無極物件的狀態變為拒絕時呼叫。其中,第二個函式是可選的,不一定要提供。這兩個函式都接受無極物件傳出的值作為引數。
這樣採用Promise,解決“回撥地獄”問題,比如連續讀取多個檔案,寫法如下。
var readFile = require('fs-readfile-promise');
readFile(fileA)
.then(function (data) {
console.log(data.toString());
})
.then(function () {
return readFile(fileB);
})
.then(function (data) {
console.log(data.toString());
})
.catch(function (err) {
console.log(err);
});
可見這種寫法要比回撥寫法直觀的多。但是,有沒有更好的寫法呢?
3.Generator函式
Genrator函式要用*來比標識,yield關鍵字表示暫停。將函式分割出好多個部分,呼叫一次next就會繼續向下執行。返回結果是一個迭代器,迭代器有一個下一個方法。
function* read() {
console.log(1);
let a = yield '123';
console.log(a);
let b = yield 9
console.log(b);
return b;
}
let it = read();
console.log(it.next('213')); // {value:'123',done:false}
console.log(it.next('100')); // {value:9,done:false}
console.log(it.next('200')); // {value:200,done:true}
console.log(it.next('200')); // {value:200,done:true}
收率後面跟著的是值的值,產率等號前面的是我們當前呼叫下一個傳進來的值,並且第一次下傳值是無效的。
處理非同步的時候發生器和無極搭配使用
let bluebird = require('bluebird');
let fs = require('fs');
let read = bluebird.promisify(fs.readFile);//將readFile轉為Promise物件的例項
function* r() {
let content1 = yield read('./2.promise/1.txt', 'utf8');
let content2 = yield read(content1, 'utf8');
return content2;
}
這樣看起來是我們想要的樣子,但是隻寫成這樣還不行,想得到R()的結果還要對函式進行處理
function co(it) {
return new Promise(function (resolve, reject) {
function next(d) {
let { value, done } = it.next(d);
if (!done) {
value.then(function (data) { // 2,txt
next(data)
}, reject)
} else {
resolve(value);
}
}
next();
});
}
co(r()).then(function (data) {
console.log(data)//得到r()的執行結果
})
這樣的處理方式顯然很麻煩,並不是我們想要,我們想要直觀的寫起來就就像同步函式,而且簡便的方式處理非同步。有這樣的方法嗎?
4.async,等待函式
ES2017標準引入了非同步函式,使得非同步操作變得更加方便。
async函式是什麼?一句話,它就是Generator函式的語法糖。
let bluebird = require('bluebird');
let fs = require('fs');
let read = bluebird.promisify(fs.readFile);
async function r(){
try{
let content1 = await read('./2.promise/100.txt','utf8');
let content2 = await read(content1,'utf8');
return content2;
}catch(e){ // 如果出錯會catch
console.log('err',e)
}
}
一比較就會發現,非同步函式就是將發生函式的星號(*)替換成非同步,將產生替換成await,僅此而已。
非同步函式返回的是承諾
r().then(function(data){
console.log(data);
},function(err){
})
至此,非同步的await函式已經可以我們滿意,以後會不會出現更優秀的方案?以我們廣大程式群體的創造力,相信一定會有的。
無極原理分析
非同步-AWAIT函式其實只是發生器函式的語法糖,而發電機函式的實現方式也是要基於無極,所以我們隊無極的實現原理進行分析。
無極物件有以下幾種狀態:
- pending:初始狀態,既不是fulfilled也不是拒絕。
- 履行:成功的操作。
- 被拒絕:失敗的操作。
在上面瞭解了承諾的基本用法後,我們先將承諾的框架搭起來
function Promise(executor) { // executor是一個執行函式
let self = this;
self.status = 'pending';
self.value = undefined; // 預設成功的值
self.reason = undefined; // 預設失敗的原因
self.onResolvedCallbacks = []; // 存放then成功的回撥
self.onRejectedCallbacks = []; // 存放then失敗的回撥
function resolve(value) { // 成功狀態
}
function reject(reason) { // 失敗狀態
}
try {
executor(resolve, reject)
} catch (e) { // 捕獲的時候發生異常,就直接失敗了
reject(e);
}
}
Promise.prototype.then = function (onFulfilled, onRjected) {
//then方法
})
接下來當呼叫成功狀態的決心的時候,會改變狀態,執行回撥函式:
function resolve(value) { // 成功狀態
if (self.status === 'pending') {
self.status = 'resolved';
self.value = value;
self.onResolvedCallbacks.forEach(function (fn) {
fn();
});
}
}
拒絕函式同理
function reject(reason) { // 失敗狀態
if (self.status === 'pending') {
self.status = 'rejected';
self.reason = reason;
self.onRejectedCallbacks.forEach(function (fn) {
fn();
})
}
}
接下來我們完成然後函式
Promise.prototype.then = function (onFulfilled, onRjected) {
let self = this;
let promise2; //返回的promise
if (self.status === 'resolved') {
promise2 = new Promise(function (resolve, reject) {
})
}
if (self.status === 'rejected') {
promise2 = new Promise(function (resolve, reject) {
})
}
// 當呼叫then時可能沒成功 也沒失敗
if (self.status === 'pending') {
promise2 = new Promise(function (resolve, reject) {
})
}
return promise2;
}
承諾允許鏈式呼叫,所以要返回一個新的承諾物件promise2
Promise.prototype.then = function (onFulfilled, onRjected) {
//成功和失敗預設不穿給一個函式
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function (value) {
return value;
}
onRjected = typeof onRjected === 'function' ? onRjected : function (err) {
throw err;
}
let self = this;
let promise2; //返回的promise
if (self.status === 'resolved') {
promise2 = new Promise(function (resolve, reject) {
setTimeout(function () {
try {
let x = onFulfilled(self.value);
// x可能是別人promise,寫一個方法統一處理
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
})
})
}
if (self.status === 'rejected') {
promise2 = new Promise(function (resolve, reject) {
setTimeout(function () {
try {
let x = onRjected(self.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
})
})
}
// 當呼叫then時可能沒成功 也沒失敗
if (self.status === 'pending') {
promise2 = new Promise(function (resolve, reject) {
// 此時沒有resolve 也沒有reject
self.onResolvedCallbacks.push(function () {
setTimeout(function () {
try {
let x = onFulfilled(self.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e)
}
})
});
self.onRejectedCallbacks.push(function () {
setTimeout(function () {
try {
let x = onRjected(self.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
})
});
})
}
return promise2;
}
在promise2內部定義一個變數X為回撥函式的返回值,由於返回值可能會有多種可能的情況,所以我們定義一個resolvePromise函式統一處理
返回值可以分為
- 諾言回報自己(報錯迴圈引用)
- 返回承諾物件(根據承諾物件呼叫成功或失敗回撥函式)
- 返回普通值(呼叫成功回撥函式傳入返回值)
- 報錯(呼叫失敗回到傳入錯誤)
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('迴圈引用了'))
}
// 判定x是不是一個promise,promise應該是一個物件
let called; // 表示是否呼叫過成功或者失敗
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
try { // {then:1}
let then = x.then;
if (typeof then === 'function') {
// 成功
then.call(x, function (y) {
if (called) return
called = true
// y可能還是一個promise,在去解析直到返回的是一個普通值
resolvePromise(promise2, y, resolve, reject)
}, function (err) { //失敗
if (called) return
called = true
reject(err);
})
} else {
resolve(x)
}
} catch (e) {
if (called) return
called = true;
reject(e);
}
} else { // 說明是一個普通值
resolve(x); // 呼叫成功回撥
}
}
如果返回值為物件或函式,且有那麼方法,那我們就認為是一個承諾物件,去呼叫這個承諾進行遞迴,直到返回普通值呼叫成功回撥。
最後,再加上一個捕方法,很簡單
Promise.prototype.catch = function (callback) {
return this.then(null, callback)
}
這些就是承諾的主要功能的原理,附上完整程式碼
function Promise(executor) { // executor是一個執行函式
let self = this;
self.status = 'pending';
self.value = undefined; // 預設成功的值
self.reason = undefined; // 預設失敗的原因
self.onResolvedCallbacks = []; // 存放then成功的回撥
self.onRejectedCallbacks = []; // 存放then失敗的回撥
function resolve(value) { // 成功狀態
if (self.status === 'pending') {
self.status = 'resolved';
self.value = value;
self.onResolvedCallbacks.forEach(function (fn) {
fn();
});
}
}
function reject(reason) { // 失敗狀態
if (self.status === 'pending') {
self.status = 'rejected';
self.reason = reason;
self.onRejectedCallbacks.forEach(function (fn) {
fn();
})
}
}
try {
executor(resolve, reject)
} catch (e) {
reject(e);
}
}
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('迴圈引用了'))
}
let called;
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
try {
let then = x.then;
if (typeof then === 'function') {
then.call(x, function (y) {
if (called) return
called = true
resolvePromise(promise2, y, resolve, reject)
}, function (err) { //失敗
if (called) return
called = true
reject(err);
})
} else {
resolve(x)
}
} catch (e) {
if (called) return
called = true;
reject(e);
}
} else {
resolve(x);
}
}
Promise.prototype.then = function (onFulfilled, onRjected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function (value) {
return value;
}
onRjected = typeof onRjected === 'function' ? onRjected : function (err) {
throw err;
}
let self = this;
let promise2;
if (self.status === 'resolved') {
promise2 = new Promise(function (resolve, reject) {
setTimeout(function () {
try {
let x = onFulfilled(self.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
})
})
}
if (self.status === 'rejected') {
promise2 = new Promise(function (resolve, reject) {
setTimeout(function () {
try {
let x = onRjected(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 () {
setTimeout(function () {
try {
let x = onFulfilled(self.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e)
}
})
});
self.onRejectedCallbacks.push(function () {
setTimeout(function () {
try {
let x = onRjected(self.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
})
});
})
}
return promise2;
}
Promise.prototype.catch = function (callback) {
return this.then(null, callback)
}