Javascript非同步程式設計的前世今生

Macchiato發表於2018-01-22

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),流程會很混亂,而且每個任務只能指定一個回撥函式

  1. 無法捕獲錯誤 try catch

  2. 不能return

  3. 回撥地獄 (

    1. 非常難看
    2. 非常難以維護
    3. 效率比較低,因為它們是序列的)
// 回撥金字塔
 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

相關文章