非同步的發展,順手學會怎麼處理多請求

顏醬發表於2019-03-02

TL;DR

  • 非同步:先幹一件事,中間去幹其他的事,最終再回來幹這件事
  • 高階函式是函式作為引數或者函式作為返回值 ,作用是批量生成函式和預置函式做為引數(可以快取函式,當達到條件時執行該函式)
  • 非同步的發展流程:callback -> promise -> generator + co -> async+await(語法糖)
  • 回撥函式金字塔處理多請求 -> 哨兵函式處理多請求 -> promise處理多請求 -> co處理多請求 -> async處理多請求
  • 手寫實現co、bluebird的promisify和promisifyAll

非同步的發展流程

  • 非同步:先幹一件事 中間去幹其他的事,最終在回來幹這件事

  • 同步:同步連續執行

  • 非同步的發展流程:callback -> promise -> generator + co -> async+await(語法糖)

  • 非同步發展的最終結果就是,像同步一樣的寫法,簡單優雅易懂

回撥函式的金字塔地獄版本1.0

普通的讀到2個檔案之後才能進行某件事,可能最開始的手段:

// 本地寫3檔案,index.js寫以下程式碼 template.txt寫些html的程式碼,data.txt寫些json資料,然後命令列執行 node index.js
let fs = require(`fs`)
fs.readFile(`template.txt`,`utf8`,function(err,template){ // error-first
    fs.readFile(`data.txt`,`utf8`,function(err,data){ // error-first
        console.log({template:template,data:data});
    });
});
複製程式碼

理解高階函式

先介紹高階函式的含義和用法。

  • 含義:函式作為引數或者函式作為返回值

  • 用法:批量生成函式和預置函式做為引數

批量生成函式

比如判斷變數是不是物件或者陣列

function isObject(content){
    return Object.prototype.toString.call(content) === `[object Object]`;
}
function isArray(content){
    return Object.prototype.toString.call(content) === `[object Array]`;
}

複製程式碼

但這樣一個個寫很麻煩,可以寫一個函式生成這些函式,這樣簡單粗暴。在平時你發現函式裡有重複程式碼的時候,可以考慮封裝一個高階函式生成函式~

function isType(type){
    return function(content){
        return Object.prototype.toString.call(content) === `[object ${type}]`;
    }
}

const isObject = isType(`Object`)
const isArray = isType(`Array`)

複製程式碼

預置函式做為引數

lodash裡面有個after的函式,功能是函式呼叫幾次之後才真正執行函式,很神奇是吧,走一個~

function after(times,fn){
    return function(){
            if(--times===0){
            fn()
        }
    }
}
let eat = after(3,function(){
    console.log(`飽了`)
})
eat();
eat();
eat(); // 這次才會執行


複製程式碼

舉一反三,換句話說這樣可以快取函式,當達到條件時執行該函式。這就超級厲害了~

高階函式的哨兵變數版2.0

由上面例子得到的啟發,再看前面的例子

// 本地寫3檔案,index.js寫以下程式碼 template.txt寫些html的程式碼,data.txt寫些json資料,然後命令列執行 node index.js

function after(requestCounts,fn){
    let dataSet = {} // 資料收集,請求跟結果一一對應,所以存為物件,這個變數通常稱為哨兵變數
    // return的函式就是單個讀取到結果之後在其回撥函式裡執行的函式,所以可以拿到資料
    return function(key,data){
        dataSet[key] = data
        // 所有請求都拿到結果之後
        if(Object.keys(dataSet).length ===requestCounts){
            fn(dataSet)
        }
    }
}

let out = after(2,function(res){
    console.log(res);
})
let fs = require(`fs`)
fs.readFile(`template.txt`,`utf8`,function(err,data){out(`template`,data)})
fs.readFile(`data.txt`,`utf8`,function(err,data){out(`data`,data)});

複製程式碼

這樣很方便處理併發請求,請求的數量傳入即可。

理解promise

可以對照promise的網站,自己試著實現promise。

// 大概用法
var y = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        let x = Math.random()
        if(x >0.5){
            resolve(x)
        }else{
            reject(x)
        }
    },100)
})
console.log(y)
var yThen = y.then((data)=>{
    console.log(`then`,data)
},(data)=>{
    console.log(`catch`,data)
})
複製程式碼

promise版本3.0

let fs = require(`fs`)
function readFilePro(filename){
    return new Promise((resolve,reject)=>{
        fs.readFile(filename,`utf8`,function(err,data){err?reject(err):resolve(data)});
    })
}

Promise.all([readFilePro(`template.txt`),readFilePro(`data.txt`)]).then(res=>{
    console.log({template:res[0],data:res[1]})
})
複製程式碼

理解生成器generator

生成器函式雖然是一個函式,但和普通函式不一樣,普通函式一旦呼叫就會執行完。

  • 生成器函式用* 來標識
  • 呼叫的結果是一個迭代器 迭代器有一個next方法
  • 遇到暫停點yield就停下來,直到執行迭代器的next,最後才能返回這個函式的return
  • yield後面跟著的是value的值
  • yield等號前面的是我們當前呼叫next傳進來的值
  • 第一次next傳值是無效的
  • 當done為true的時候就是value就是生成器return的值
// 生成器函式有個特點需要加個*
function *go(a){
    console.log(1)
    // 此處b是外界輸入,這行程式碼實現輸入輸出
    let b = yield a
    console.log(2)
    let c = yield b
    console.log(3)
    return `o`
}
// 生成器函式和普通函式不一樣呼叫他函式不會立刻執行
// 返回生成器的迭代器,迭代器是一個物件
let it = go(`a`)
// next第一次執行不需要傳引數,想想也是,沒有意義
let r1 = it.next()
console.log(r1) // {value:`a`,done:false}
let r2 = it.next(`B`)
console.log(r2) // {value:`B`,done:false}
let r3 = it.next(`C`)
// 當done為true的時候就是return的值
console.log(r3) // {value:`o`,done:true}
複製程式碼

理解co,讓生成器自動執行

co是大神tj寫出來的,超棒的小夥子啊,才23歲好像,再次感慨人與人之間的差距簡直比人與狗之間的差距還大,麵條淚~
co讓生成器自動執行的原理其實想想就是讓next執行到結束為止。
!!!!必須特別強調: co 有個使用條件,generator 函式的 yield 命令後面,只能是 Thunk 函式或 Promise 物件。

// gen是生成器generator的簡寫
function co(gen){
    let it = gen()
    return new Promise((resolve,reject)=>{
        !function next(lastValue){
            let {value,done} = it.next(lastValue)
            if(done){
               resolve(value)
            }else{
                // 遞迴,這裡也看出來,這也是為啥yield後面必須是promise型別
                value.then(next)
            }
        }()
    })
}
co(go)
複製程式碼

promise和co版本的4.0

// 本地寫3檔案,index.js寫以下程式碼 template.txt寫些html的程式碼,data.txt寫些json資料,然後命令列執行 node index.js

let fs = require(`fs`)
function readFilePro(filename){
    return new Promise((resolve,reject)=>{
        fs.readFile(filename,`utf8`,function(err,data){err?reject(err):resolve(data)});
    })
}
function *gen(){
    // let res = {}
    let template = yield readFilePro(`template.txt`)
    let data = yield readFilePro(`data.txt`)
    return {template,data}
}
// 也可以直接引入 co的庫 npm i co  let co = require(`co`)
function co(gen){
    let it = gen()
    return new Promise((resolve,reject)=>{
        !function next(lastValue){
            let {value,done} = it.next(lastValue)
            if(done){
               resolve(value)
            }else{
                // 遞迴,這裡也看出來,這也是為啥yield後面必須是promise型別
                value.then(next)
            }
        }()
    })
}
co(gen).then(res=>console.log(res))
複製程式碼

async和await版本5.0

async和await是promise和generator的語法糖。其實go函式就是gen函式裡面的yield變成await~
因為async函式其實有點co的感覺,await後面必須是promise~

// 本地寫3檔案,index.js寫以下程式碼 template.txt寫些html的程式碼,data.txt寫些json資料,然後命令列執行 node index.js
let fs = require(`fs`)
// readFilePro也可以用bluebird生成
function readFilePro(filename){
    return new Promise((resolve,reject)=>{
        fs.readFile(filename,`utf8`,function(err,data){err?reject(err):resolve(data)});
    })
}
async function go(){
    let template = await readFilePro(`template.txt`)
    let data = await readFilePro(`data.txt`)
    // 這裡的return必須用then才能拿到值,因為是語法糖啊~
    return {template,data}
}

go().then(res=>console.log(res))
複製程式碼

這也是最終版啦,非同步寫成同步的感覺~

bluebird

再叨叨點bluebird,它能把任意通過回撥函式實現的非同步API換成promiseApi。
常用的方法兩個:promisify和promisifyAll。
promisify將回撥函式實現的非同步API換成promiseApi。
promisifyAll遍歷物件上所有的方法 然後對每個方法新增一個新的方法 Async。

let fs = require(`fs`)
// npm i bluebird
let Promise = require(`bluebird`)
let readFilePro = Promise.promisify(fs.readFile)
// 好像很眼熟是不是 哈哈哈哈
readFilePro(`template.txt`,`utf8`).then((template)=>{console.log(template)})
Promise.promisifyAll(fs)
// console.log(fs) // 發現fs的方法多了
fs.readFileAsync(`template.txt`,`utf8`).then((template)=>{console.log(template)})
複製程式碼

其實感覺可以手寫實現的有木有,來走一個~

let fs = require(`fs`)
// 先看簡單版的
function readFilePro(filename,encode){
    return new Promise((resolve,reject)=>{
        fs.readFile(filename,encode,function(err,data){err?reject(err):resolve(data)});
    })
}

// 高階函式生成上面的函式
function promisify(fn){
    // 這裡生成readFilePro類似的函式,這裡因為引數不一定,所以用args
    return function(...args){
        return new Promise((resolve,reject)=>{
            // 因為回撥函式在最後一個,所以用拼接的方式,call的用法知道哈~
            fn.call(null,...args,function(err,data){err?reject(err):resolve(data)})
        })
    }
}
function promisifyAll(object){
    for (const key in object) {
        if (object.hasOwnProperty(key) && typeof object[key]===`function`) {
            object[`${key}Async`] = promisify(object[key])
        }
    }
    return object
}
let readFilePro = promisify(fs.readFile)
// 好像很眼熟是不是 哈哈哈哈
readFilePro(`template.txt`,`utf8`).then((template)=>{console.log(template)})

promisifyAll(fs)
// console.log(fs)
fs.readFileAsync(`template.txt`,`utf8`).then((template)=>{console.log(template)})

複製程式碼

多請求

其實如果看懂到這裡,對於多請求的實現也就不是大難事了。請求能用fetch用fetch哈。
多請求分為併發請求(請求之間沒有關係,但需要拿到所有請求結果)和串發請求(後面請求必須要拿到前面請求的結果)。
對於併發請求,感覺Promise.all處理更簡單,串發請求那就用await吧~

相關文章