所謂"非同步",簡單說就是一個任務分成兩段,先執行第一段,然後轉而執行其他任務,等做好了準備,再回過頭執行第二段,比如,在我們燒水時可以幹很多事情,當水燒開後在用水洗臉。這種不連續的執行,就叫做非同步。相應地,連續的執行,就叫做同步。例如在燒水的過程中我們一直等待水燒開而不去幹別的事情。
1.高階函式
函式作為一等公民,可以作為引數和返回值
高階函式至少滿足以兩個條件
- 接受一個或多個函式作為輸入
- 輸出一個函式
1.1 預置引數
let isType = function(type,obj){
return Object.prototype.toString.call(obj) ===`[object ${type}]`
}
console.log(isType('Object',{}));
console.log(isType('Object',{}));
console.log(isType('String','hello'));
console.log(isType('String','hello'));
// 我們發現每次呼叫isType都需要傳入型別,所以我們可以先批量產出可供呼叫的函式
let isType = function(type){
return function(obj){
return Object.prototype.toString.call(obj) ===`[object ${type}]`
}
}
let isObject = isType('Object');
let isString = isType('String');
console.log(isObject({}));
console.log(isObject({}));
複製程式碼
1.2 預置函式
function after(times,cb){
return function(){
if(--times === 0){
cb();
}
}
}
let eat = after(3,function(){console.log('吃完了')});
eat();
eat();
eat();
// 當呼叫次數達到我們預置的次數時,執行我們預置的函式
複製程式碼
到此我們對函式又有了進一步的認識,下面我們就來開始進入非同步的解決方案。
2.回撥函式
所謂回撥函式,就是把任務的第二段單獨寫在一個函式裡面,等到重新執行這個任務的時候,就直接呼叫這個函式。我們介紹一個常見的node中的非同步方法readFile可以用來讀取檔案。
fs.readFile(filename, function (err, data) {
if (err) throw err;
console.log(data);
});
複製程式碼
在node中回撥函式的第一個引數是錯誤物件(error-first callbacks)
2.1 回撥函式的應用
function read(callback){
setTimeout(function(){
let result = 'zpfx';
callback(result);
})
}
read(function(data){
console.log(data);
});
複製程式碼
我們可以利用回撥函式來解決非同步問題
3.回撥的問題
- 非同步不支援try/catch,回撥函式是在下一事件環中取出,所以一般在回撥函式的第一個引數預置錯誤物件
- 回撥地獄問題,非同步多級依賴的情況下巢狀非常深,程式碼難以閱讀的維護
- 多個非同步在某一時刻獲取所有非同步的結果
- 結果不能通過return返回
4.Promise
Promise本意是承諾,在程式中的意思就是承諾我過一段時間後會給你一個結果。 什麼時候會用到過一段時間?答案是非同步操作,非同步是指可能比較長時間才有結果的才做,例如網路請求、讀取本地檔案等
4.1 Promise的三種狀態
例如媳婦說想買個包,這時候他就要"等待"我的回覆,我可以過兩天買,如果買了表示"成功",如果我 最後拒絕表示"失敗",當然我也有可能一直拖一輩子
- Pending Promise物件例項建立時候的初始狀態
- Fulfilled 可以理解為成功的狀態
- Rejected 可以理解為失敗的狀態
then 方法就是用來指定Promise 物件的狀態改變時確定執行的操作,resolve 時執行第一個函式 (onFulfilled),reject 時執行第二個函式(onRejected)
4.2 構造Promise
promise的方法會立刻執行
let promise = new Promise(() => {
console.log('hello');
});
console.log('world');
// hello
// world
複製程式碼
4.3 promise也可以代表一個未來的值
const fs = require('fs');
let promise = new Promise((resolve, reject) => {
fs.readFile('./content.txt', 'utf8', function (err, data) {
if (err) return reject(err);
resolve(data);
})
});
promise.then(data => {
console.log(data);
});
promise.then(data => {
console.log(data);
});
// 一個promise例項可以多次呼叫then當成功後會將結果依次執行
複製程式碼
4.4 代表一個用於不會返回的值
const fs = require('fs');
let promise = new Promise((resolve, reject) => { });
promise.then(data => {
console.log(data); //代表一個用於不會返回的值
});
複製程式碼
4.5 實現買包的案例
function buyPack() {
return new Promise((resolve, reject) => {
setTimeout(function () {
var random = Math.random();
if (random > 0.5) {
resolve('買');
} else {
resolve('不買');
}
}, 2000)
})
}
buyPack().then(data => {
console.log(data);
}, data => {
console.log(data);
});
複製程式碼
4.6 Error會導致觸發Reject
可以採用then的第二個引數捕獲失敗,也可以通過catch函式進行
function buyPack() {
return new Promise((resolve, reject) => {
throw new Error('沒錢')
})
}
buyPack().then(data => {
console.log(data);
}, data => {
console.log(data);
});
複製程式碼
5.解決回撥地獄
迴歸正題,先用promise解決第一個問題"回撥地獄"
// 1.txt => 2.txt
// 2.txt => 我很帥
let fs = require('fs');
function read(){
fs.readFile('./1.txt','utf8',function(err,data){
if(err) return console.log(err);
fs.readFile(data,'utf8',function(err,data){
if(err) return console.log(err);
console.log(data); // 我很帥
})
})
}
read();
複製程式碼
改寫Promise形式
let fs = require('fs');
function read(file){
return new Promise(function(resolve,reject){
fs.readFile(file,'utf8',function(err,data){
if(err) return reject(err);
resolve(data);
})
})
}
read('./1.txt').then(function(data){
return read(data);
}).then(function(data){
console.log(data)
}).catch(function(err){
console.log(err)
});
複製程式碼
當第一個then中返回一個promise,會將返回的promise的結果,傳遞到下一個then中。這就是比較著名的鏈式呼叫了。
6.同步非同步的結果
我們將多個非同步請求的結果在同一時間進行彙總
// 1.txt => template
// 2.txt => data
let fs = require('fs');
let result = {}
function out(key,data) {
result[key] = data;
if(Object.keys(result).length === 2){
console.log(result)
}
}
fs.readFile('./1.txt', 'utf8', function (err, data) {
if (err) return console.log(err);
out('template',data);
})
fs.readFile('./2.txt', 'utf8', function (err, data) {
if (err) return console.log(err);
out('data',data);
});
// 這種方式並不好,要宣告全域性的物件,並且成功的數量也是寫死的,我們可以使用偏函式來進行改寫
複製程式碼
let fs = require('fs');
let result = {}
function after(times,cb) {
let result = {}
return function(key,data){
result[key] = data;
if(Object.keys(result).length === times){
cb(result)
}
}
}
let out = after(2,function(data){
console.log(data)
})
fs.readFile('./1.txt', 'utf8', function (err, data) {
if (err) return console.log(err);
out('template',data);
})
fs.readFile('./2.txt', 'utf8', function (err, data) {
if (err) return console.log(err);
out('data',data);
});
複製程式碼
最後我們可以採用Promise.all方法來進行簡化
let fs = require('fs');
function read(file){
return new Promise(function(resolve,reject){
fs.readFile(file,'utf8',function(err,data){
if(err) return reject(err);
resolve(data);
})
})
}
Promise.all([read('1.txt'),read('2.txt')]).then(([template,data])=>{
console.log({template,data})
});
// 不管兩個promise誰先完成,Promise.all 方法會按照陣列裡面的順序將結果返回
複製程式碼
7.Promise.race
接受一個陣列,陣列內都是Promise例項,返回一個Promise例項,這個Promise例項的狀態轉移取決於引數的Promise例項的狀態變化。當引數中任何一個例項處於resolve狀態時,返回的Promise例項會變為resolve狀態。如果引數中任意一個例項處於reject狀態,返回的Promise例項變為reject狀態。
Promise.race([read('1.txt'),read('2.txt')]).then(data=>{
console.log({template,data})
},(err)=>{
console.log(err)
});
複製程式碼
8.Promise.resolve
返回一個Promise例項,這個例項處於resolve狀態
Promise.resolve('成功').then(data=>{
console.log(data);
});
複製程式碼
9.Promise.reject
返回一個Promise例項,這個例項處於reject狀態
Promise.reject('失敗').then(data=>{
console.log(data);
},err=>{
console.log(err);
})
複製程式碼
下一節我們會繼續介紹generator和async/await的用法,以及Promise的實現原理,喜歡的點個贊吧^_^!
支援我的可以給我打賞哈