Javascript語言的執行環境是"單執行緒"(single thread)
所謂"單執行緒",就是指一次只能完成一件任務。如果有多個任務,就必須排隊,前面一個任務完成,再執行後面一個任務,以此類推
到這裡肯能有人會疑惑,js既然是單執行緒的,就是隻能在一個任務結束之後才能執行下一個任務。和非同步的“後一個任務不等前一個任務結束就執行”是矛盾的嗎?這個問題在下一篇文章 《JavaScript中的EventLoop》中會有詳細的解讀。
一、什麼是非同步程式設計?
1)同步程式設計:等待,序列;即一個任務執行完成之後第二個任務才開始執行,第二個任務要等第一個任務。
缺點:阻塞。
2)非同步程式設計:不等,並行;指每一個任務有一個或多個回撥函式(callback),前一個任務結束後,不是執行後一個任務,而是執行回撥函式,後一個任務則是不等前一個任務結束就執行,所以程式的執行順序與任務的排列順序是不一致的。
優點:非阻塞。
二、非同步程式設計的發展過程
所謂的非同步程式設計,當然指的是函式的非同步程式設計,在這裡我們來了解一下js中的函式。
在js中函式是一等公民,即可以作為引數,也可以做返回值。
在這裡我們用提一下高階函式,通過高階函式的作用來說明上述的兩個觀點
1)高階函式可以用來批量的生成函式
// 判斷一個引數是不是字串
function isString(str) {
return Object.prototype.toString.call(str) == '[object String]'
}
console.log(isString('hello world'));
// 判斷一個引數是不是一個陣列
function isArray(arr) {
return Object.prototype.toString.call(arr) == '[object Array]'
}
console.log(isArray(['hello', 'world']));
// 使用高階函式批量生成函式
function isType(type) {
return function (params) {
return Object.prototype.toString.call(params) == `[object ${type}]`
}
}
let isString = isType('String')
console.log(isString('hello world'));
let isArray = isType('Array')
console.log(isArray(['hello', 'world']));
// 高階函式的這個作用證實了上面的第二個觀點,函式可以作為返回值
複製程式碼
2)高階函式可以用於生成需要呼叫多次才會執行的函式
function eat() {
console.log('吃完了')
}
let count = 0
function nextTask(times, fn) {
!function () {
if (++count == times) {
fn()
}
}()
}
nextTask(3, eat)
nextTask(3, eat)
nextTask(3, eat)
// 高階函式的這個作用證實了上面的第一個觀點,/函式可以作為引數傳到另外一個函式裡面
複製程式碼
1) 非同步程式設計的第一個實現--回撥函式,這是最基本的方式
1) 什麼是會回撥函式?
回撥函式就是一個通過函式指標呼叫的函式。如果你把函式的指標(地址)作為引數傳遞給另一個函式,當這個指標被用來呼叫其所指向的函式時,我們就說這是回撥函式。回撥函式不是由該函式的實現方直接呼叫,而是在特定的事件或條件發生時由另外的一方呼叫的,用於對該事件或條件進行響應。
我對回撥函式的解讀就是現在不調,回頭再調。
2) 回撥函式的特點
(1) 特點: error first,在呼叫回撥函式的時候,第一個引數永遠是錯誤物件
(2) 優點: 回撥函式的優點是簡單、容易理解和部署
(3) 缺點: 缺點是不利於程式碼的閱讀和維護,各個部分之間高度耦合(Coupling),流程會很混亂,而且每個任務只能指定一個回撥函式
-
無法捕獲錯誤 try catch
-
不能return
-
回撥地獄 (
- 非常難看
- 非常難以維護
- 效率比較低,因為它們是序列的)
// 回撥金字塔
fs.readFile('./1.txt', 'utf8', function (err, data1) {
fs.readFile('./2.txt', 'utf8', function (err, data2) {
fs.readFile('./3.txt', 'utf8', function (err, data3) {
fs.readFile('./4.txt', 'utf8', function (err, data4) {
console.log(data1 + data2 + data3 + data4);
})
})
})
})
複製程式碼
2) 非同步程式設計的第二個實現--事件釋出訂閱
該模式主要解決了上面回撥金字塔巢狀的問題
在講事件釋出訂閱之前我們先來看一下node自帶的事件監聽模組EventEmitter,是node的核心模組
裡面有兩個核心方法,一個叫on emit,on表示註冊監聽,emit表示發射事件
第一步:引入這個模組
let EventEmitter = require('events')
複製程式碼
第二步:建立一個該模組的例項
let event = new EventEmitter()
// 讀到的內容物件
let readedContent = {}
複製程式碼
第三步:監聽一個事件
event.on('ready', function(key, value) {
readedContent[key] = value
// 什麼時候結束呢?到達指定讀取的長度時停止 指定讀兩次
if (Object.keys(readedContent.length) == 2) {
console.log(readedContent)
}
})
複製程式碼
第四步:觸發事件
fs.readFile('./1.txt', 'utf8', function (err, template) {
//1事件名 2引數往後是傳遞給回撥函式的引數
eve.emit('ready','template',template);
})
fs.readFile('./2.txt', 'utf8', function (err, data) {
eve.emit('ready','data',data);
})
複製程式碼
EventEmitter 的實現原理
function EventEmitter(){
this.events = {};
this._maxListeners = 10;
}
EventEmitter.prototype.setMaxListeners = function(maxListeners){
this._maxListeners = maxListeners;
}
EventEmitter.prototype.listeners = function(event){
return this.events[event];
}
EventEmitter.prototype.on = EventEmitter.prototype.addListener = function(type,listener){
if(this.events[type]){
this.events[type].push(listener);
if(this._maxListeners!=0&&this.events[type].length>this._maxListeners){
console.error(`MaxListenersExceededWarning: Possible EventEmitter memory leak detected. ${this.events[type].length} ${type} listeners added. Use emitter.setMaxListeners() to increase limit`);
}
}else{
this.events[type] = [listener];
}
}
EventEmitter.prototype.once = function(type,listener){
let wrapper = (...rest)=>{
listener.apply(this);
this.removeListener(type,wrapper);
}
this.on(type,wrapper);
}
EventEmitter.prototype.removeListener = function(type,listener){
if(this.events[type]){
this.events[type] = this.events[type].filter(l=>l!=listener)
}
}
EventEmitter.prototype.removeAllListeners = function(type){
delete this.events[type];
}
EventEmitter.prototype.emit = function(type,...rest){
this.events[type]&&this.events[type].forEach(listener=>listener.apply(this,rest));
}
module.exports = EventEmitter;
複製程式碼
3) 非同步程式設計的第三個實現--哨兵變數
function render(length,cb){
let html={};
return function(key,value){
html[key] = value;
if(Object.keys(html).length == length){
cb(html);
}
}
}
let done = render(3,function(html){
console.log(html);
});
fs.readFile('./template.txt', 'utf8', function (err, template) {
done('template',template);
})
fs.readFile('./data.txt', 'utf8', function (err, data) {
done('data',data);
})
複製程式碼
4) 非同步程式設計的第四個實現--promise
Promises物件是CommonJS工作組提出的一種規範,目的是為非同步程式設計提供統一介面。 簡單說,它的思想是,每一個非同步任務返回一個Promise物件,該物件有一個then方法,允許指定回撥函式
let Promise = require('./Promise');
let p1 = new Promise(function(resolve,reject){
resolve(100);
});
let p2 = p1.then(function(value){
console.log('成功1=',value);
},function(reason){
console.log('失敗1=',reason);
})
p2.then(function(value){
console.log('成功2=',value);
},function(reason){
console.log('失敗2=',reason);
})
複製程式碼
優點:優點在於,回撥函式變成了鏈式寫法,程式的流程可以看得很清楚,而且有一整套的配套方法,可以實現許多強大的功能
5) 非同步程式設計的第五個實現-- async/await
async/await號稱非同步的終級解決方案,是最簡單的
let Promise = require('bluebird');
let readFile = Promise.promisify(require('fs').readFile);
async function read() {
//await後面必須跟一個promise,
let a = await readFile('./1.txt','utf8');
console.log(a);
let b = await readFile('./2.txt','utf8');
console.log(b);
let c = await readFile('./3.txt','utf8');
console.log(c);
return 'ok';
}
read().then(data => {
console.log(data);
});
複製程式碼
優點:
1.簡潔
2.有很好的語義
3.可以很好的處理非同步 throw error return try catch
現在koa2裡已經可以支援async/await