JS 非同步發展流程(回撥函式=>Async/await)

WilliamCao發表於2019-02-15

非同步程式設計的語法目標,就是怎樣讓它更像同步程式設計

什麼是非同步?

非同步任務指的是,不進入主執行緒、而進入"任務佇列"(task queue)的任務,只有"任務佇列"通知主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒執行。 非同步執行的執行機制如下:

  1. 所有同步任務都在主執行緒上執行,形成一個執行棧(execution context stack)。
  2. 主執行緒之外,還存在一個"任務佇列"(task queue)。只要非同步任務有了執行結果,就在"任務佇列"之中放置一個事件。
  3. 一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。
  4. 主執行緒不斷重複上面的第三步。


1、 回撥函式

場景: 讀取一個檔案

let fs = require('fs')
fs.readFile('./1.txt', 'utf8', function(err, data){
    // 回撥的特點是第一個引數一般為錯誤物件
    if (err) { 
        // 如果err有值說明程式出錯了
        console.log(err)
    } else { 
        // 否則表示成功獲取到資料data
        console.log(data)
    }
})
複製程式碼

當然回撥函式也有它的缺點:

  • 無法捕獲錯誤(使用try catch)
funnction readFile (fileName) {
    fs.readFile(fileName, 'utf8', function (data) {
        if (err) {
            console.log(err)
        } else {
            console.log(data)
        }
    })
}
try {
    readFile('./1.txt')
} catch (e) { 
    // 如果上邊讀取檔案出錯,獲取不到錯誤資訊
    console.log('err', e)
}
複製程式碼
  • 不能return readFile 方法中無法返回讀取到檔案的內容(data)
  • 回撥地獄
fs.readFile('./data1.txt', 'utf8', function (err, data1) {
  fs.readFile('./data2.txt', 'utf8', function (err, data2) {
    fs.readFile('./data3.txt', 'utf8', function (err, data3) {
      fs.readFile('./data4.txt', 'utf8', function (err, data4) {
        fs.readFile('./data5.txt', 'utf8', function (err, data5) {
            console.log(data1, data2, data3, data4, data5)
        })
      })
    })
  })
})
// 這樣的程式碼稱為惡魔金字塔;且有以下問題
// 1、程式碼非常難看
// 2、難以維護
// 3、效率比較低,因為它們是序列的;一次只能請求一個檔案
複製程式碼

2、事件釋出訂閱

為了解決回撥巢狀的問題

let EventEmitter = require('events')
// nodejs核心模組之一,包含兩個核心方法: 
// on >> 表示註冊監聽,emit >> 表示發射事件

let eve = new EventEmitter()
let html = {} // 存放頁面模板和資料
eve.on('reading', function (key, value) {
    html[key] = value
    if (Object.keys(html).length == 2) {
        console.log(html)
    }
})
fs.readFile('./template.txt', 'utf8', function (err, template) {
    eve.emit('reading', template) 
    // 觸發reading事件,執行事件的回撥函式向html中填入模板
})
fs.readFile('./data.txt', 'utf8', function (err, template) {
    eve.emit('reading', template) 
    // 觸發reading事件,執行事件的回撥函式向html中填入資料
})
複製程式碼

3、哨兵變數

事件釋出訂閱已經可以解決回撥巢狀的問題,但是還需要引入events模組; 利用哨兵變數一樣可以解決回撥巢狀的問題,且不需要引入其他模組

// 定義一個哨兵函式來處理
function done (key, value) {
    html[key] = value
    if (Object.keys(html).length == 2) {
        console.log(html)
    }
}

fs.readFile('./template.txt', 'utf8', function (err, template) {
    done('template', template)
})
fs.readFile('./data.txt', 'utf8', function (err, template) {
    done('data', data)
})

// 可以封裝一個高階函式去生成哨兵函式
function render (length, cb) {
    let htm = {}
    return function (key, value) {
        html[key] = value
        if (Object.keys(html).length == length) {
            cb(html)
        }
    }
}

let done = render(2, function (html) {
    console.log(html)
})
複製程式碼

4、Promise

上述方法都是用回撥函式來處理非同步;我們的目標是把非同步往同步的方向靠攏

//promise的用法不再闡述,可自行查閱文件
let promise1 = new Promise(function (resolve, reject) {
    fs.readFile('./1.txt', 'utf8', function (err, data) {
        resolve(data)
    })
})

promise1.then(function (data) {
    console.log(data)
})
複製程式碼

5、Generator(生成器)

當我們在呼叫一個函式的時候,它並不會馬上執行,而是需要我們手動的去執行迭代操作(next方法);簡單來說,呼叫生成器函式會返回一個迭代器,可以用迭代器來執行遍歷每個中斷點(yield) 呼叫next方法會有返回值value,是生成器函式對外輸出的資料;next方法還可以接受引數,是向生成器函式內部輸入的資料

  • 生成器簡單使用
// 方法名前邊加*就是生成器函式
function *foo () {
  var index = 0;
  while (index < 2) {
    yield index++; //暫停函式執行,並執行yield後的操作
  }
}
var bar =  foo(); // 返回的其實是一個迭代器

console.log(bar.next());    // { value: 0, done: false }
console.log(bar.next());    // { value: 1, done: false }
console.log(bar.next());    // { value: undefined, done: true }
複製程式碼
  • 生成器 + Promise解決非同步的實現
function readFile (filaName) {
    return new Promise(function (resolve, reject) {
        fs.readFile(filename, function (err, data) {
          if (err) {
            reject(err)
          } else {
            resolve(data)
          }
    })
}
function *read() {
    let template = yield readFile('./template.txt')
    let data = yield readFile('./data.txt')
    return {
        template: template,
        data: data
    }
}
// 生成迭代器r1
let r1 = read()
let templatePromise = r1.next().value
templatePromise.then(function(template) {
    // 將獲取到的template的內容傳遞給生成器函式
    let dataPromsie = r1.next(template).value
    dataPromise.then(function(data) {
        //最後一次執行next傳入data的值;最後返回{template, data}
        let result = r1.next(data).value
        console.log(result)
    })
})
複製程式碼
  • 生成器 + promise的實現已經有了一些同步的樣子;藉助一些工具(co),可以優雅的編寫上述的程式碼
//實現 co 方法
//引數是一個生成器函式
function co (genFn) {
    let r1 = genFn()
    return new Promise(function(resolev, reject) {
        !function next(lastVal) {
            let p1 = r1.next(lastVal)
            if (p1.done) {
                resolve(p1.value)
            } else {
                p1.value.then(next, reject)
            }
        }()
    })
}

//現在獲取上邊的result可以這樣來取
co(read).then(function(result) {
    console.log(result)
})
複製程式碼

6、Async/await

Async其實是一個語法糖,它的實現就是將Generator函式和自動執行器(co),包裝在一個函式中

//實現 co 方法
//引數是一個生成器函式
async function read() {
  let template = await readFile('./template.txt');
  let data = await readFile('./data.txt');
  return template + '+' + data;
}

// 等同於
function read(){
  return co(function*() {
    let template = yield readFile('./template.txt');
    let data = yield readFile('./data.txt');
    return template + '+' + data;
  });
}
複製程式碼

非同步程式設計發展的目標就是讓非同步邏輯的程式碼看起來像同步一樣,發展到Async/await,是處理非同步程式設計的一個里程碑。


參考文章:

相關文章