深刻JavaScript 之異步編程

近日,整理了學習筆記,而後分享給你們,共同窗習,共同進步,篇幅過長建議收藏。css

js異步編程咱們從如下幾個部分來看一看html

  1. 理解異步
  2. EventLoop
  3. 異步編程方法-發佈訂閱
  4. 深刻理解promise
  5. Generator函數
  6. 深刻理解async/await

1. 理解異步

同步與異步

首先咱們得清楚的知道,什麼是同步,什麼是異步?前端

通俗的來說,同步指的是 調用以後獲得的結果,才能夠去作別的任務node

異步指的是 調用以後先無論結果,繼續再幹別的任務git

在深一步瞭解異步相關內容以前咱們先來了解兩個重要的概念。github

  • 進程

進程是程序運行的實例,同一個程序能夠產生多個進程,一個進程有能夠包含一個或者多個線程web

  • 線程

線程是操做系統可以運算調度的最小單位,一次只能執行一個任務,有本身的調用棧,寄存器環境,同一進程的線程共享進程資源。ajax

JavaScript單線程

那麼,問題就又來了,咱們知道JS是單線程的,那麼單線程的JS又是如何實現一步的呢?chrome

答案呢,其實就是單線程的JS是經過瀏覽器內核多線程實現異步編程

咱們來深刻的瞭解一下瀏覽器相關工做原理吧。

以開源的Chromium爲例

瀏覽器的進程

  • 瀏覽器進程
  • 渲染進程(重點展開介紹)
  • GPU進程
  • 網絡進程
  • 插件進程

渲染進程又包含

  • GUI線程

渲染布局(頁面的html,css,js,構建DOM樹和渲染樹就是GUI線程的工做)

  • JS引擎線程

解析、執行JS程序(chrome v8引擎),JS引擎線程只有一個,這也是爲何咱們所說的js是單線程的緣由,其實呢語言是沒有單線程多線程之說的,由於解釋這個語言的引擎是單線程的,因此咱們說js是單線程的

JS引擎線程與GUI線程互斥,由於JS引擎線程也能夠操做DOM,容易形成混亂

儘可能控制js文件的大小,不要讓js執行時間太長

  • 定時觸發器線程

setTimeout

setInterval

  • 事件觸發線程

將知足觸發條件的事件放入任務隊列(異步事件放入任務隊列)

  • 異步HTTP請求線程

處理ajax請求的線程

XHR所在線程

若是請求完成時有回調函數,它就會通知事件觸發線程往任務隊列裏面添加事件

知道了瀏覽器是如何進行工做的以後,咱們回顧一下前端常見的有哪些異步場景?

  • 定時器
  • 網絡請求
  • 事件綁定
  • ES6 Promise

定時器

定時器的執行過程

Event Loop

  1. 主線程--執行棧(代碼是在執行棧中執行)
  2. web APIs--setTimeout(webAPi能夠理解成瀏覽器提供的一種能力好比setTimeout)
  3. 定時器線程--計時2s
  4. 事件觸發線程(事件觸發線程將定時器事件放入任務隊列)
  5. 任務隊列--異步任務
順序以下:
1.調用webApi(setTimeout)
2.定時器線程計數2s
3.事件觸發線程將定時器事件放入任務隊列(往執行棧中加任務)
4.主線程經過EventLoop遍歷任務隊列(往執行棧中出任務)
複製代碼
定時器應用場景
  • 防抖
  • 節流
  • 倒計時
  • 動畫(會存在丟幀的問題)
定時器存在的問題
  1. 定時任務可能不會按時執行(若是同步任務好事好久的話,定時器任務不必定會及時執行)
  2. 定時器嵌套5次以後最小間隔不能低於4ms
看一個定時器的常見示例
// for 循環是同步任務,因此等for 執行完以後纔會去執行定時器這個異步任務, var做用域是全局的,沒有塊級做用域
for (var i = 1; i <= 10; i ++) {
    setTimeout(function() {
        console.log(i)
    }, 1000 * i)
}
複製代碼
// 利用函數閉包構建做用域
for (var i = 1; i <= 10; i ++) {
    (function(i) {
        setTimeout(function() {
            console.log(i)
        }, 1000 * i)
    })(i)
}
複製代碼
// ES6新增的let 塊級做用域
for (let i = 1; i <= 10; i ++) {
    setTimeout(function() {
        console.log(i)
    }, 1000 * i)
}
複製代碼

2. Event Loop機制(微觀角度看異步)

主要根據在瀏覽器環境下的Event Loop來講明,node.js的EventLoop 後續node部分會講到。

首先,咱們要明白異步是怎麼去實現的:

  1. 宏觀角度:是因爲瀏覽器多線程
  2. 微觀角度:Event Loop

異步的任務有兩類

  • 宏任務(普通任務 task)
    • script
    • setTimeout/setInterval(定時器)
    • setImmediate(Node.js的一個方法)
    • I/O操做
    • UI rendering
  • 微任務(microtask)
    • Promise
    • Object.observe 監聽對象變化的一個方法
    • MutationObserver 是一個類,能夠監聽DOM結構變化
    • postMessage Window對象之間用來通訊

Event Loop執行順序

  1. 首先執行script,script被稱爲全局任務,也屬於macrotask(宏任務)
  2. 當script這個macrotask(宏任務)執行完之後,再去執行全部的微任務
  3. 而後再去取任務隊列中取宏任務一個一個執行

注意

  • 一個Event Loop 有一個或多個task queue

  • 每一個Event Loop 有一個 Microtask queue

3. 異步編程方法-發佈/訂閱

首先呢,咱們要先去知道,異步編程的方法有哪些?

  • 回調函數
  • 事件發佈/訂閱
  • Promise
  • generator函數
  • async函數

咱們一個一個的來說述,回調函數你們很經常使用,直接開始事件發佈/訂閱這個異步編程方法。

那麼如何理解發布/訂閱呢?

有三個核心概念咱們得了解一下

  • publish(發佈者)
  • Event Center(事件中心)
  • SubScriber(訂閱者)
1. 首先發布者 發佈消息到 事件中心
2. 而後訂閱者 從事件中心 訂閱消息
3. 訂閱者能夠有多個
複製代碼

如何實現一個事件的發佈/訂閱呢?

class PubSub {
    constructor() {
        // 用對象來存儲,是由於事件的名字和事件的處理函數用對象能夠很方便的對應起來
        this.events = {}
    }
    publish(eventName, data) {
        if (this.events[eventName]) {
            this.events[eventName].forEach(cb => {
                cb.apply(this, data)
            })
        }
    }
    subscribe(eventName, cb) {
        if (this.events[eventName]) {
            this.events[eventName].push(cb)
        } else {
            this.events[eventName] = [cb]
        }
    }
    // 取消訂閱
    unSubscribe(eventName, cb) {
        if (this.events[eventName]) {
            this.events[eventName] = this.events[eventName].filter(item => item !== cb)
        } 
    }
}
複製代碼

優勢

  • 鬆耦合
  • 靈活

缺點

  • 沒法確保消息被觸發或者觸發幾回

4. promise

咱們先來看一看promise/A+ 規範

術語

  • promise

一個有then方法的對象或者函數,行爲符合本規範

  • thenable

一個定義了then方法的對象或函數

  • 值(value)

任何JavaScript的合法值

  • 異常(exception)

throw語句拋出的值

  • 拒絕緣由(reason)

表示一個promise的拒絕緣由

promise的狀態

  • pending--等待
  • fulfilled--完成
  • rejected--拒絕
state: pending
1.	value --> state: fulfilled
2.	reason --> state: rejected
複製代碼

then方法

一個promise必須提供一個then方法來訪問其當前值、終值和拒因

promise的then方法接受兩個參數

const promise2 = promise1.then(onFulfilled, onRejected)
複製代碼

1. 參數

onFulfilled 和 onRejected都是可選參數

若是onFulfilled不是函數,其必須被忽略

若是onRejected不是函數,其必須被忽略

onFulfilled 不是函數,promise1的狀態是fulfilled
state:fulfilled
value:同promise1

onFulfilled 不是函數,promise1的狀態是rejected
state:rejected
value:同promise1

onFulfilled或者onRejected 是一個函數
return x
進入解析過程
複製代碼

2. onFulfilled特性

若是onFulfilled是函數:

當promise執行結束後其必須被調用,其第一個參數爲promise的終值

在promise執行結束前其不可被調用

其調用次數不可超過一次

3. onRejected特性

若是onRejected是函數:

當promise被拒絕執行後其必須被調用,其第一個參數爲promise的拒因

在promise被拒絕執行前其不可被調用

其調用次數不可超過一次

4. 屢次調用

then方法能夠被同一個promise調用屢次

  • promise 成功執行時,全部 onFulfilled 需按照其註冊順序依次回調
  • promise 被拒絕執行時,全部的 onRejected 需按照其註冊順序依次回調

5. 注意事項

  • onFulfilled和onRejected必須被做爲函數調用
  • onFulfilled在promise完成後被調用,onRejected在promise被拒絕執行後調用
  • onFulfilled和onRejected只被調用一次
  • 若是參數不是一個函數,直接被忽略掉(Promise.resolve(1).then(Promise.resolve(3))

6. 返回

const promise2 = promise1.then(onFulfilled, onRejected)
複製代碼
onFulfilled 不是函數,promise1的狀態是fulfilled
state:fulfilled
value:同promise1

onFulfilled 不是函數,promise1的狀態是rejected
state:rejected
value:同promise1

onFulfilled或者onRejected 是一個函數
return x
進入解析過程
複製代碼

promise的解析過程

先抽象出一個模型resolove(promise, x)

1. x 與 promise 相等

若是 promisex 指向同一對象,以 TypeError 爲據因拒絕執行 promise

2. x 爲 promise

若是 x 爲 Promise ,則使 promise 接受 x 的狀態

  • 若是 x 處於等待態, promise 需保持爲等待態直至 x 被執行或拒絕
  • 若是 x 處於執行態,用相同的值執行 promise
  • 若是 x 處於拒絕態,用相同的據因拒絕 promise
3. x 爲對象或者函數

若是 x 爲對象或者函數:

x.then 賦值給 then

  • 若是取 x.then 的值時拋出錯誤 e ,則以 e 爲據因拒絕 promise

  • 若是then是函數,將x做爲函數的做用域this調用之。傳遞兩個回調函數做爲參數,第一個參數叫作resolvePromise,第二個參數叫作rejectPromise:

    • 若是 resolvePromise 以值 y 爲參數被調用,則運行 [[Resolve]](promise, y)
    • 若是 rejectPromise 以據因 r 爲參數被調用,則以據因 r 拒絕 promise
    • 若是 resolvePromiserejectPromise 均被調用,或者被同一參數調用了屢次,則優先採用首次調用並忽略剩下的調用
  • 若是調用then方法方法拋出了異常e

    • 若是 resolvePromiserejectPromise 已經被調用,則忽略之
    • 不然以 e 爲據因拒絕 promise
  • 若是 then 不是函數,以 x 爲參數執行 promise

4. x 不爲對象或函數

若是 x 不爲對象或者函數,以 x 爲參數執行 promise

那麼咱們來實現如下這個解析過程吧

import { isObject, isFunction } from "util"
//! promise 解析過程
function resolve(promise, x) {
    if (x === promise) {
        return reject(promise, new TypeError('cant be the same'))
    }
    if (isPromise(x)) {
        if (x.state === 'pending') {
            return x.then(() => {
                resolve(promise, x.value)
            }, () => {
                reject(promise, x.value)
            })
        }
        if (x.state === 'fulfilled') {
            return fulfill(promise, x.value)
        }
        if (x.state === 'rejected') {
            return reject(promise, x.value)
        }
    } else if (isObject(x) || isFunction(x)) {
        let then;
        try {
            then = x.then
        } catch (e) {
            return reject(promise, e)
        }
        if (isFunction(then)) {
            let isCalled = false;
            try {
                then.call(x, function resolvePromise(y) {
                    if (isCalled) {
                        return 
                    }
                    isCalled = true
                    resolve(promise, y)
                }, function rejectPromise(r) {
                    if (isCalled) {
                        return
                    }
                    isCalled = true
                    reject(promise, r)
                })
            } catch (e) {
                if (!isCalled) {
                    reject(promise, e)
                }
            }
        } else {
            return fulfill(promise, x)
        }
    
    } else {
        return fulfill(promise, x)
    }
}
複製代碼

接下來咱們來看一下ES6 Promise API

ES6 Promise API

Promise構造函數

構造函數 說明
new Promise(function(resolve, reject) { }) 函數做爲參數
resolve函數將promise狀態從pending變成resolved(fulfilled)
reject函數將promise狀態從pending變成rejected

靜態方法

方法 說明
Promise.resolve(param) 等同於 new Promise(function(resolve, reject){resolve(param)})
Promise.reject(reason) 等同於 new Promise(function(resolve, reject){reject(reason)})
Promise.all([p1,...,pn]) 輸入一組promise返回一個新的promise,所有promise都是fulfilled結果纔是fulfilled狀態
Promise.allSettled([p1,...,pn]) 輸入一組promise返回一個新的promise,全部的promise都是fulfilled結果纔是fulfilled狀態
Promise.race([p1,...,pn]) 輸入一組promise返回一個新的promise,結果promise的狀態根據第一個變化的promise狀態

Promise實例方法

方法 說明
promise.then(onFulfilled,onRejected) promise 狀態改變以後的回調,返回新的promise對象
promise.catch(function(reason) {}) 同promise.then(null, onRejected),promise狀態爲rejected的回調
promise.finally(function(reason) { // test}) 同promise.then(function(){ // test}, function(){ // test}),無論promise狀態如何都會執行

注意點

  • then、catch返回的promise是新的promise,不是原來的promise。
  • Promise對象的錯誤會「冒泡」,直到捕獲爲止,錯誤會被下一個catch語句捕獲。

因此說寫catch的時候只須要在鏈式調用的最後面加一個catch語句去捕獲就能夠。

最佳實踐

  • 不要忘記catch捕捉錯誤
  • then方法中使用return
  • 傳遞函數給then方法
  • 不要把promise寫成嵌套

來實戰一下

**題目:**3秒以後亮一次紅燈,再過兩秒亮一次綠燈,再過一秒亮一次黃燈,用promise 實現屢次交替亮燈的效果,console.log 模擬亮燈

// 思路拆解:
// 1.多少秒後亮某個顏色的燈
// 2.順序亮一批燈
// 3.循環順序亮一批燈

function light(color, second) {
    return new Promise(function(resolve, reject) {
        setTimeout(() => {
            console.log(color)
            resolve()
        }, second * 1000)
    })
}

function orderLights(list) {
    let promise = Promise.resolve()
    list.forEach(item => {
        promise = promise.then(() => {
            return light(item.color, item.second)
        })
    })
    promise.then(function() {
        return orderLights(list)
    })
}

const list = [
    {color: 'red', second: 3},
    {color: 'green', second: 2},
    {color: 'yellow', second: 1},
]

orderLights(list)
複製代碼

5. Generator函數及其異步應用

Generator函數

首先先來看兩個概念

迭代器
  • 有next方法,執行返回結果對象
  • 結果對象
    • value
    • done
function createIterator(items) {
    var i = 0;
    return {
        next: function() {
            var done = i >= items.length
            var value = !done ? items[i++] : undefined
            return {
                done, 
                value
            }
        }
    }
}
複製代碼
可迭代協議
  • [Symbol.iterator]屬性
  • 內置可迭代對象
    • String Array Map Set 等
迭代器協議
  • next方法
    • done
    • value
生成器

Generator函數(生成器)

  • ES6異步編程解決方案
  • 聲明:經過function*聲明
  • 返回值:符合可迭代協議和迭代器協議的生成器對象
  • 特色:在執行時能暫停,又能從暫停處繼續執行

執行Generator函數生成一個生成器對象

生成器對象的原型上有三個方法
  • next(param)
  • return(param)
  • throw(param)
yield
  • 只能出如今Generator函數裏

  • 用來暫停和恢復生成器函數

  • next執行

    • 遇到yield暫停,將緊跟yield表達式的值做爲返回的對象的value
    • 沒有yield,一直執行到return,將return的值做爲返回的對象的value
    • 沒有return,將undefined做爲返回的對象的value
  • next參數

    • next方法能夠帶一個參數,該參數會被當作上一個yield表達式的返回值
generator函數運行流程以下:
function* createGenerator() {
    let first = yield 1
    let second = yield first + 2
    yield second + 3
}

let gen = createGenerator();

let g1 = gen.next()  // {value: 1, done: false}
let g2 = gen.next(4) // {value: 6, done: false}
let g3 = gen.next(5) // {value: 8, done: false}
let g4 = gen.next()  // {value: undefined, done: true}
複製代碼

yield生成器函數 / 可迭代對象

  • 委託給其餘可迭代對象
  • 做用:複用生成器(多個生成器同時使用)

舉個栗子

function* generator1() {
    yield 1;
    yield 2;
}

function* generator2() {
    yield 100;
    yield* generator1();
    yield 200;
}

let g = generator2()
g.next() //? { value: 100, done: false }
g.next() //? { value: 1, done: false }
g.next() //? { value: 2, done: false }
g.next() //? { value: 200, done: false }
g.next() //? { value: undefined, done: true }
複製代碼

return(param)

  • 給定param值終結遍歷器,param可缺省。
//! return(param)
function* createIterator() {
    yield 1;
    yield 2;
    yield 3;
}
let iterator = createIterator();

iterator.next(); // { value: 1, done: false }
iterator.return();// { value: undefined, done: true }
iterator.next();// { value: undefined, done: true }
複製代碼

throw(param)

  • 讓生成器對象內部拋出錯誤,走到try,catch語句中
//! throw(param)
function* createIterator() {
    let first = yield 1;
    let second;
    try{
        second = yield first + 2;
    } catch (e) {
        second = 6;
    }
    yield second + 3
}
let iterator = createIterator();

console.log(iterator.next());   // { value: 1, done: false }
console.log(iterator.next(10));  // { value: 12, done: false }
console.log(iterator.throw(new Error('error'))); // { value: 9, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
複製代碼

接下來咱們來看一下generator函數的實現原理

首先先得了解找一個概念

協程

介紹協程:cnodejs.org/topic/58ddd…

  • 一個線程存在多個協程,但同時只能執行一個
  • Generator函數式協程在ES6的實現
  • yield掛起x 協程(交給其餘協程),next喚醒 x協程
Generator函數應用

耦合程度仍是挺高的

Thunk函數

  • 求值策略 傳值調用,傳名調用 sum(x+1, x+2)
  • thunk函數是傳名調用的實現方式之一
  • 能夠實現自動執行Generator函數

舉個栗子

const fs = require('fs');
const Thunk = function(fn) {
    return function(...args) {
        return function(callback) {
            return fn.call(this, ...args, callback)
        }
    }
}

const readFileThunk = Thunk(fs.readFile);

function run(fn) {
    var gen = fn();
    function next(err, data) {
        var result =gen.next(data);
        if (result.done) return;
        result.value(next);
    }
    next()
}

const g = function*() {
    const s1 = yield readFileThunk('./g1.json')
    console.log(s1.toString());
    const s2 = yield readFileThunk('./g2.json')
    console.log(s2.toString());
    const s3 = yield readFileThunk('./g3.json')
    console.log(s3.toString());
}

run(g);
複製代碼

co模塊

co模塊是generator函數的自動執行器,功能相似與Thunk函數做用

co源碼:github.com/tj/co

6. 深刻理解async/await

async函數

  • 是一個語法糖,使異步操做更簡單
  • 返回值 是一個 promise對象
    • return的值是promise resolved時候的value
    • throw的值是promise rejected時候的reason
async function test() {
    return 1;
}
const p = test();
console.log(p);  // Promise { 1 }
p.then(function (data) {
    console.log(data) // 1
})

async function test() {
    throw new Error('error')
}
const p = test();
console.log(p); 
p.catch(function (data) {
    console.log(data) 
})
複製代碼

await

  • 只能出如今async函數內或者最外層
  • 等待一個promise對象的值
  • await的promise狀態爲rejected,後續執行中斷
await
1. promise
	1> resolved 返回promise的值
	2> rejected 拋出promise的拒因
	
2. 非promise
	1> 返回對應的值  await 1
複製代碼

async函數實現原理

Generator + 自動執行器

// async函數實現原理
async function example(params) {
    // ...
}

function example(params) {
    return spawn(function*() {
        // ...
    })
}

function spawn(genF) {
    return new Promise(function(resolve, reject) {
        const gen = genF(); // 生成器對象
        function step(nextF) {
            let next;
            try {
                next = nextF(); //執行gen.next()
            } catch (e) {
                return reject(e)
            }
            if (next.done) {
                return resolve(next.value)
            }
            //* next.done 爲 false時,繼續step;
            Promise.resolve(next.value).then(
                function(v) {
                    step(function() {
                        return gen.next(v)
                    })
                },
                function(e) {
                    stop(function() {
                        return gen.throw(e)
                    })
                }
            )
        }
        step(function() {
            return gen.next(undefined)
        })
    })
}
複製代碼

整理的過程當中,不免會有疏漏,如果看到有誤或者須要補充的知識點,歡迎留言小編

相關文章
相關標籤/搜索