JS非同步解決方案的發展流程(一)

凌晨夏沫發表於2018-02-18

所謂"非同步",簡單說就是一個任務分成兩段,先執行第一段,然後轉而執行其他任務,等做好了準備,再回過頭執行第二段,比如,在我們燒水時可以幹很多事情,當水燒開後在用水洗臉。這種不連續的執行,就叫做非同步。相應地,連續的執行,就叫做同步。例如在燒水的過程中我們一直等待水燒開而不去幹別的事情。

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的實現原理,喜歡的點個贊吧^_^!
支援我的可以給我打賞

打賞

相關文章