Javascript 異步編程

什麼是異步

所謂"異步",簡單說就是一個任務分紅兩段,先執行第一段,而後轉而執行其餘任務,當第一段有了執行結果以後,再回過頭執行第二段。JavaScript採用異步編程緣由有兩點,一是JavaScript是單線程,二是爲了提升CPU的利用率。在提升CPU的利用率的同時也提升了開發難度,尤爲是在代碼的可讀性上。javascript

console.log(1);

setTimeout(function () {
  console.log(2);
});

console.log(3);
複製代碼

JavaScript異步執行示意圖

callback

最開始咱們在處理異步的時候,採用的是callback回調函數的方式java

asyncFunction(function(value){
	// todo
})
複製代碼

在通常簡單的狀況下,這種方式是徹底夠用的,可是若是碰到稍微複雜的場景,就有些力不從心,例如當異步嵌套過多的時候。node

回調金字塔

可是當咱們的異步操做比較多,並且都依賴於上一步的異步的執行結果,那麼咱們就會產生回調金字塔,難於閱讀git

step1(function (value1) {
    step2(function(value2) {
        step3(function(value3) {
            step4(function(value4) {
                // Do something with value4
            });
        });
    });
});
複製代碼

固然爲了改進這種層層嵌套的寫法,咱們有幾種方式 1 命名函數github

function fun1 (params) {
  // todo
  asyncFunction(fun2);
}

function fun2 (params) {
  // todo
  asyncFunction(fun3)
}

function fun3 (params) {
  // todo
  asyncFunction(fun4)
}

function fun4 (params) {
  // todo
}

asyncFunction(fun1)
複製代碼

2 基於事件消息機制的寫法編程

eventbus.on("init", function(){
    operationA(function(err,result){
        eventbus.dispatch("ACompleted");
    });
});
 
eventbus.on("ACompleted", function(){
    operationB(function(err,result){
        eventbus.dispatch("BCompleted");
    });
});
 
eventbus.on("BCompleted", function(){
    operationC(function(err,result){
        eventbus.dispatch("CCompleted");
    });
});
 
eventbus.on("CCompleted", function(){
    // do something when all operation completed
});
複製代碼

固然也能夠利用模塊化來處理,使得代碼易於閱讀。以上這三種方式都只是在代碼的可讀性上面作了改進,可是並無解決另一個問題就是異常捕獲。json

錯誤棧

function a () {
    b();
}

function b () {
    c();
}

function c () {
    d();
}

function d () {
    throw new Error('出錯啦');
}

a();
複製代碼

Node錯誤打印

從上面的圖咱們能夠看到有一個比較清晰的錯誤棧信息,a調用b - b調用c - c調用d ,在d中拋出了一個異常。也就是說在JavaScript中在執行一個函數的時候首先會壓入執行棧中,執行完畢後會移除執行棧,FILO的結構。咱們能夠很方便的從錯誤信息中定位到出錯的地方。windows

function a() {
    b();
}

function b() {
    c(cb);
}

function c(callback) {
    setTimeout(callback, 0)
}

function cb() {
    throw new Error('出錯啦');
}

a();
複製代碼

包含異步的錯誤棧

從上圖咱們能夠看到只打印出了是在一個setTimeout中的回調函數中出現了異常,執行順序是跟蹤不到的。promise

異常捕獲

回調函數中的異常是不可以捕捉到的,由於是異步的,咱們只能在回調函數中使用try catch捕獲,也就是我註釋的部分。瀏覽器

function a() {
    setTimeout(function () {
        // try{
            throw new Error('出錯啦');
        // } catch (e) {
        
        // }
        
    }, 0);
}

try {
    a();
} catch (e) {
    console.log('捕捉到異常啦,好高興哦');
}
複製代碼

可是try catch只能捕捉到同步的錯誤,不過在回調中也有一些比較好的錯誤處理模式,例如error-first的代碼風格約定,這種風格在node.js中普遍被使用 。

function foo(cb) {
  setTimeout(() => {
    try {
      func();
      cb(null, params);
    } catch (error) {
      cb(error);
    }
    
  }, 0);
}

foo(function(error, value){
	if(error){
		// todo
	}
	// todo
});
複製代碼

可是這麼作也很容易陷入惡魔金字塔中。

Promise

規範簡述

  • promise 是一個擁有 then 方法的對象或函數。
  • 一個promise有三種狀態 pending, rejected, resolved 狀態一旦肯定就不能改變,且只可以由pending狀態變成rejected或者resolved狀態,reject和resolved狀態不能相互轉換。
  • 當promise執行成功時,調用then方法的第一個回調函數,失敗時調用第二個回調函數。
  • promise實例會有一個then方法,這個then方法必須返回一個新的promise。

基本用法

// 異步操做放在Promise構造器中
const promise1 = new Promise((resolve) => {
    setTimeout(() => {
        resolve('hello');
    }, 1000);
});

// 獲得異步結果以後的操做
promise1.then(value => {
  console.log(value, 'world');
}, error =>{
  console.log(error, 'unhappy')
});
複製代碼

異步代碼,同步寫法

asyncFun()
	.then(cb)
	.then(cb)
	.then(cb)
複製代碼

promise以這種鏈式寫法,解決了回調函數處理多重異步嵌套帶來的回調地獄問題,使代碼更加利於閱讀,固然本質仍是使用回調函數。

異常捕獲

前面說過若是在異步的callback函數中也有一個異常,那麼是捕獲不到的,緣由就是回調函數是異步執行的。咱們看看promise是怎麼解決這個問題的。

asyncFun(1).then(function (value) {
    throw new Error('出錯啦');
}, function (value) {
    console.error(value);
}).then(function (value) {

}, function (result) {
  console.log('有錯誤', result);
});
複製代碼

實際上是promise的then方法中,已經自動幫咱們try catch了這個回調函數,實現大體以下。

Promise.prototype.then = function(cb) {
	try {
		cb()
	} catch (e) {
       // todo
       reject(e)
	}
}
複製代碼

then方法中拋出的異常會被下一個級聯的then方法的第二個參數捕獲到(前提是有),那麼若是最後一個then中也有異常怎麼辦。

Promise.prototype.done = function (resolve, reject) {
    this.then(resolve, reject).catch(function (reason) {
        setTimeout(() => {
           throw reason;
        }, 0);
    });
};
複製代碼
asyncFun(1).then(function (value) {
     throw new Error('then resolve回調出錯啦');
 }).catch(function (error) {
     console.error(error);
     throw new Error('catch回調出錯啦');
 }).done((reslove, reject) => {});
複製代碼

咱們能夠加一個done方法,這個方法並不會返回promise對象,因此在此以後並不能級聯,done方法最後會把異常拋到全局,這樣就能夠被全局的異常處理函數捕獲或者中斷線程。這也是promise的一種最佳實踐策略,固然這個done方法並無被ES6實現,因此咱們在不適用第三方Promise開源庫的狀況下就只能本身來實現了。爲何須要這個done方法。

const asyncFun = function (value) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(value);
    }, 0);
  })
};


asyncFun(1).then(function (value) {
  throw new Error('then resolve回調出錯啦');
});
複製代碼

(node:6312) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: then resolve回調出錯啦 (node:6312) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code 咱們能夠看到JavaScript線程只是報了一個警告,並無停止線程,若是是一個嚴重錯誤若是不及時停止線程,可能會形成損失。

侷限

promise有一個侷限就是不可以停止promise鏈,例如當promise鏈中某一個環節出現錯誤以後,已經沒有了繼續往下執行的必要性,可是promise並無提供原生的取消的方式,咱們能夠看到即便在前面已經拋出異常,可是promise鏈並不會中止。雖然咱們能夠利用返回一個處於pending狀態的promise來停止promise鏈。

const promise1 = new Promise((resolve) => {
    setTimeout(() => {
        resolve('hello');
    }, 1000);
});

promise1.then((value) => {
    throw new Error('出錯啦!');
}).then(value => {
    console.log(value);
}, error=> {
    console.log(error.message);
    return result;
}).then(function () {
    console.log('DJL簫氏');
});
複製代碼

特殊場景

  • 當咱們的一個任務依賴於多個異步任務,那麼咱們可使用Promise.all
  • 當咱們的任務依賴於多個異步任務中的任意一個,至因而誰無所謂,Promise.race

上面所說的都是ES6的promise實現,實際上功能是比較少,並且還有一些不足的,因此還有不少開源promise的實現庫,像q.js等等,它們提供了更多的語法糖,也有了更多的適應場景。

核心代碼

var defer = function () {
    var pending = [], value;
    return {
        resolve: function (_value) {
            value = _value;
            for (var i = 0, ii = pending.length; i < ii; i++) {
                var callback = pending[i];
                callback(value);
            }
            pending = undefined;
        },
        then: function (callback) {
            if (pending) {
                pending.push(callback);
            } else {
                callback(value);
            }
        }
    }
};
複製代碼

當調用then的時候,把全部的回調函數存在一個隊列中,當調用resolve方法後,依次將隊列中的回調函數取出來執行

var ref = function (value) {
    if (value && typeof value.then === "function")
        return value;
    return {
        then: function (callback) {
            return ref(callback(value));
        }
    };
};
複製代碼

這一段代碼實現的級聯的功能,採用了遞歸。若是傳遞的是一個promise那麼就會直接返回這個promise,可是若是傳遞的是一個值,那麼會將這個值包裝成一個promise。

generator

基本用法

function * gen (x) {
    const y = yield x + 2;
    // console.log(y); // 猜猜會打印出什麼值
}

const g = gen(1);
console.log('first', g.next());  //first { value: 3, done: false }
console.log('second', g.next()); // second { value: undefined, done: true }
複製代碼

通俗的理解一下就是yield關鍵字會交出函數的執行權,next方法會交回執行權,yield會把generator中yield後面的執行結果,帶到函數外面,而next方法會把外面的數據返回給generator中yield左邊的變量。這樣就實現了數據的雙向流動。

generator實現異步編程

咱們來看generator如何是如何來實現一個異步編程(*)

const fs = require('fs');

function * gen() {
    try {
        const file = yield fs.readFile;
        console.log(file.toString());
    } catch(e) {
        console.log('捕獲到異常', e);
    }
}

// 執行器
const g = gen();

g.next().value('./config1.json', function (error, value) {
  if (error) {
    g.throw('文件不存在');
  }
  g.next(value);
});
複製代碼

那麼咱們next中的參數就會是上一個yield函數的返回結果,能夠看到在generator函數中的代碼感受是同步的,可是要想執行這個看似同步的代碼,過程卻很複雜,也就是流程管理很複雜。那麼咱們能夠借用TJ大神寫的co。

generator 配合 co

下面來看看如何使用:

const fs = require('fs');
const utils = require('util');
const readFile = utils.promisify(fs.readFile);
const co = require('co');

function * gen(path) {
    try {
        const file = yield readFile('./basic.use1.js');
        console.log(file.toString());
    } catch(e) {
        console.log('出錯啦');
    }
}

co(gen());
複製代碼

咱們看到使用co這個執行器配合generator和promise會很是方便,很是相似同步寫法,並且異步中的錯誤也能很容易被try catch到。這裏之因此要使用utils.promisify這個工具函數將普通的異步函數轉換成一個promise,是由於co may only yield a chunk, promise, generator, array, or object。使用co 配合generator最大的一個好處就是錯誤能夠try catch 到。

async/await

先來看一段async/await的異步寫法

const fs = require('fs');
const utils = require('util');
const readFile = utils.promisify(fs.readFile);
async function readJsonFile() {
    try {
        const file = await readFile('../generator/config.json');
        console.log(file.toString());
    } catch (e) {
        console.log('出錯啦');
    }

}

readJsonFile();
複製代碼

咱們能夠看到async/await的寫法十分相似於generator,實際上async/await就是generator的一個語法糖,只不過內置了一個執行器。而且當在執行過程當中出現異常,就會中止繼續執行。固然await後面必須接一個promise,並且node版本必需要>=7.6.0纔可使用,固然低版本也能夠採用babel。

補充

在開發過程當中咱們經常手頭會同時有幾個項目,那麼node的版本要求頗有多是不一樣的,那麼咱們就須要安裝不一樣版本的node,而且管理這些不一樣的版本,這裏推薦使用nvm下載好nvm,安裝,使用nvm list 查看node版本列表。使用nvm use 版本號 進行版本切換。

在Node.js中捕獲漏網之魚

process.on('uncaughtException', (error: any) => {
    logger.error('uncaughtException', error)
})
複製代碼

在瀏覽器環境中捕獲漏網之魚

window.addEventListener('onrejectionhandled', (event: any) => {
    console.error('onrejectionhandled', event)
})
複製代碼

參考文章

Promise中文迷你書

剖析Promise內部結構,一步一步實現一個完整的、能經過全部Test case的Promise類

深刻理解Promise實現細節

DJL簫氏我的博客

相關文章
相關標籤/搜索