關於非同步
所謂”非同步”,簡單說就是一個任務不是連續完成的,可以理解成該任務被人為分成兩段,先執行第一段,然後轉而執行其他任務,等做好了準備,再回過頭執行第二段。
比如,有一個任務是讀取檔案進行處理,任務的第一段是向作業系統發出請求,要求讀取檔案。然後,程式執行其他任務,等到作業系統返回檔案,再接著執行任務的第二段(處理檔案)。這種不連續的執行,就叫做非同步。
相應地,連續的執行就叫做同步。由於是連續執行,不能插入其他任務,所以作業系統從硬碟讀取檔案的這段時間,程式只能乾等著。
簡單的說同步就是大家排隊工作,非同步就是大家同時工作。如果你還不太明白非同步與同步,多看看JS基礎的文章。
非同步的發展歷史
1.CallBack寫法
CallBack意為“回撥函式”,即非同步操作執行完後觸發執行的函式,例如:
$.get("http://api.xxxx.com/xxx",callback);
當請求完成時就會觸發callback函式。
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
為了解決“回撥地獄”問題,提出了Promise物件,並且後來加入了ES6標準,Promise物件簡單說就是一個容器,裡面儲存著某個未來才會結束的事件(通常是一個非同步操作)的結果。從語法上說,Promise 是一個物件,從它可以獲取非同步操作的訊息。Promise 提供統一的 API,各種非同步操作都可以用同樣的方法進行處理。
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 非同步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise建構函式接受一個函式作為引數,該函式的兩個引數分別是resolve和reject。它們是兩個函式,由 JavaScript 引擎提供,不用自己部署。
resolve函式的作用是,將Promise物件的狀態從“未完成”變為“成功”(即從 pending 變為 resolved),在非同步操作成功時呼叫,並將非同步操作的結果,作為引數傳遞出去;reject函式的作用是,將Promise物件的狀態從“未完成”變為“失敗”(即從 pending 變為 rejected),在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。
Promise例項生成以後,可以用then方法分別指定resolved狀態和rejected狀態的回撥函式。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
then方法可以接受兩個回撥函式作為引數。第一個回撥函式是Promise物件的狀態變為resolved時呼叫,第二個回撥函式是Promise物件的狀態變為rejected時呼叫。其中,第二個函式是可選的,不一定要提供。這兩個函式都接受Promise物件傳出的值作為引數。
這樣採用 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);
});
可見這種寫法要比CallBack寫法直觀的多。但是,有沒有更好的寫法呢?
3.Generator 函式
Genrator 函式要用* 來比標識,yield關鍵字表示暫停。將函式分割出好多個部分,呼叫一次next就會繼續向下執行。返回結果是一個迭代器,迭代器有一個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}
yield後面跟著的是value的值,yield等號前面的是我們當前呼叫next傳進來的值,並且第一次next傳值是無效的。
處理非同步的時候Generator和Promise搭配使用
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-await函式
ES2017 標準引入了 async 函式,使得非同步操作變得更加方便。
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)
}
}
一比較就會發現,async函式就是將 Generator 函式的星號(*)替換成async,將yield替換成await,僅此而已。
async函式返回的是promise
r().then(function(data){
console.log(data);
},function(err){
})
至此,async-await函式已經可以我們滿意,以後會不會出現更優秀的方案?以我們廣大程式群體的創造力,相信一定會有的。
Promise原理分析
async-await函式其實只是Generator函式的語法糖,而Generator函式的實現方式也是要基於Promise,所以我們隊Promise的實現原理進行分析。
Promise物件有以下幾種狀態:
- pending: 初始狀態, 既不是 fulfilled 也不是 rejected.
- fulfilled: 成功的操作.
- rejected: 失敗的操作.
在上面瞭解了Promise的基本用法後,我們先將Promise的框架搭起來
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方法
})
接下來當呼叫成功狀態resolve的時候,會改變狀態,執行回撥函式:
function resolve(value) { // 成功狀態
if (self.status === `pending`) {
self.status = `resolved`;
self.value = value;
self.onResolvedCallbacks.forEach(function (fn) {
fn();
});
}
}
reject函式同理
function reject(reason) { // 失敗狀態
if (self.status === `pending`) {
self.status = `rejected`;
self.reason = reason;
self.onRejectedCallbacks.forEach(function (fn) {
fn();
})
}
}
接下來我們完成then函式
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;
}
Promise允許鏈式呼叫,所以要返回一個新的Promise物件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函式統一處理
返回值可以分為
- promise返回自己 (報錯迴圈引用)
- 返回promise物件 (根據promise物件呼叫成功或失敗回撥函式)
- 返回普通值 (呼叫成功回撥函式傳入返回值)
- 報錯 (呼叫失敗回到傳入錯誤)
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); // 呼叫成功回撥
}
}
如果返回值為物件或函式,且有then方法,那我們就認為是一個promise物件,去呼叫這個promise進行遞迴,直到返回普通值呼叫成功回撥。
最後,再加上一個catch方法,很簡單
Promise.prototype.catch = function (callback) {
return this.then(null, callback)
}
這些就是promise的主要功能的原理,附上完整程式碼
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)
}