由於JavaScript是單線程的一門腳本語言(主線程是單線程)
所以異步問題是個讓人常頭疼的問題
我們來看一下常見的傳統解決方案
1.回調函數
回調函數是一種最常見 最傳統的方式 類似的這種
// node 的文件讀取
let fs = require('fs');
fs.readFile('./test1.js','utf8',function(err,data){
console.log(data)
})
這樣我們可以在回調函數裏拿到文件的內容,然而這樣有一個問題, 要是我要讀取多個文件,每一個讀取的文件都要依賴前一個讀取文件的內容
比如 我test1.js的內容是test2的路徑
那麼就要這樣寫
let fs = require('fs');
fs.readFile('./test1.js','utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
console.log(data)
})
})
要是100個 1000個文件呢 ? 由於異步調用無法用try{}catch 捕獲 萬一中間讀取失敗了一次又該怎麼做? 難道每個函數體內都if(err) 一下? 這種方式難以維護 也就是我們常說的回調地獄
2訂閱發佈模式
我現在有這樣一種需求 我需要在不同的文件裏讀取不同的內容, 等多個文件的內容都讀取完畢再一起輸出
let fs = require('fs');
let result = {};
fs.readFile('./test1.js','utf8',function(err,data){
result.test1 = data
fs.readFile('./test2','utf8',function(err,data){
result.test2 = data
console.log(result)
})
})
用回調方式會帶來什麼問題? 需求: 這些異步請求沒有依賴關係 我需要同時發起 而不是等待上一次讀取的結果
現在我們來聊聊 訂閱發佈模式
訂閱發佈模式定義了一種一對多的依賴關係,讓多個訂閱者對象同時監聽某一個主題對象。這個主題對象在自身狀態變化時,會通知所有訂閱者對象,使它們能夠自動更新自己的狀態。 通俗點就事說, 我把我要操作的事放入一個待執行的隊列裏, 等達到某一個條件,待執行隊列依次執行,那麼上代碼
let fs = require('fs');
let result = {};
class Publish {
constructor() {
this.list = []
};
on(fn){
this.list.push(fn)
};
emit(string){
alert(string)
if (Object.keys(result).length == 2) {
this.list.forEach(fn => {
fn()
})
}
}
}
let p = new Publish()
p.on(function () {
console.log(result)
})
fs.readFile('./test1.js', 'utf8', function (err, data) {
result.test1 = data
p.emit('已經讀取到test1的文件')
})
fs.readFile('./test2', 'utf8', function (err, data) {
result.test2 = data
p.emit('已經讀取到test2的文件')
})
原理其實也就是回調函數
問題:發佈訂閱跟觀察者模式有什麼區別??
3 Promise
好在我們有了Promise這個類 關於Promise的文章有很多 大家自行可以搜索一下
我們來看下Promise A+ 規範
那根據這個規範我們簡單的寫一遍promise的源碼吧
我們來定義2個文件
Promise.js和require.js
//require.js
let Promise = require('./promise.js')
let p = new Promise((resolve, reject) => {
setTimeout(()=>{
resolve(100)
},100)
})
p.then(function(data){
console.log(data)
},function(e){
console.log(e)
})
//promise.js
class Promise {
constructor(executor){
// promise的三個狀態
this.state = 'pending'
this.value = undefined
this.reason = undefined
// 有可能調用then的時候 並沒有resolve或者reject 所以這裏用來存放then之後要做的事
this.onResolvedCallbacks = []
this.onRejectedCallbacks = []
const resolve = (value) => {
// 我們需要判斷resolve出來的值是否還是一個promise
if(value instanceof Promise){
return value.then(resolve,reject)
}
// promiseA+ 規範要求這麼寫
setTimeout(()=>{
if (this.state === 'pending') {
this.state = 'resolved'
this.value = value
// 把保存起來的函數一一執行然後結果傳給下一個
this.onResolvedCallbacks.forEach( fn => {
return fn(value)
})
}
})
}
const reject = (reason) => {
setTimeout(()=>{
if (this.state === 'pending') {
this.state = 'rejected'
this.reason = reason
this.onRejectedCallbacks.forEach(fn => {
return fn(reason)
})
}
})
}
try {
executor(resolve,reject)
} catch (error) {
reject(error)
}
}
then(onFulfilled,onRejected){
// new 的時候馬上執行executor ----> 就是(resolve,reject)=>{ }() 拿到resolve跟reject 然後做狀態判斷該調用哪個
onFulfilled = typeof onFulfilled == 'function' ? onFulfilled : function (value) {
return value
};
onRejected = typeof onRejected == 'function' ? onRejected : function (value) {
throw value
};
let promise2 = new Promise((resolve,reject)=>{
if (this.state === 'resolved'){
//onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].
// 規範上要這麼做 防止直接resolve 同步調用then 這個時候promise2不存在報錯
// 執行順序參考瀏覽器事件環 哪天有空單獨寫一篇
setTimeout(()=>{
// 因爲onFulfilled都是異步調用 所以不能在new Promise的時候捕獲到
try {
let x = onFulfilled(this.value) // then成功的回調
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
})
}
if (this.state === 'rejected'){
setTimeout(() => {
try {
let x = onRejected(this.reason) // 失敗的回調
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
})
}
if (this.state === 'pending'){ // 如果executor是個異步方法 那麼會先調用then 所以這裏把成功回調跟失敗的回調都存起來
this.onResolvedCallbacks.push((value)=>{
try {
let x = onFulfilled(value)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
console.log(error)
reject(error)
}
})
this.onRejectedCallbacks.push((reason)=>{
try {
let x = onRejected(reason)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
})
}
})
// then返回一個promise
return promise2
}
catch(onRejected){
return this.then(null, onRejected);
}
static all(promises){
return new Promise(function (resolve, reject) {
let result = [];
let count = 0;
for (let i = 0; i < promises.length; i++) {
promises[i].then(function (data) {
result[i] = data;
if (++count == promises.length) {
resolve(result);
}
}, function (err) {
reject(err);
});
}
});
}
}
const resolvePromise = (promise2, x, resolve, reject)=>{
// promise2 跟 then的成功回調返回值有可能是同一個值
if(promise2 === x){
return reject(new TypeError('報錯 循環引用了'))
}
let then,called;
// 要麼對象要麼函數
if(x !== null&&((typeof x === 'object' || typeof x === 'function')) ){
try {
then = x.then // 有可能是getter定義的會報錯
// then 有可能是個函數或者普通值
if(typeof then === 'function'){
// 如果then是個函數的話 就認爲它是個promise
then.call(x,function(){
if(called) return
called = true
resolvePromise(promise2, y, resolve, reject);
},function(error){
if (called) return
called = true
reject(error)
})
}else{
resolve(x)
}
} catch (error) {
if (called) return
called = true
reject(error)
}
}else{
// x是個普通值
resolve(x)
}
}
module.exports = Promise
執行require.js的結果是
這樣我們就實現了一個promise 是不是很棒棒? 現在我們可以promise.then 鏈式調用了 然後用catch做統一錯誤處理 解決了上面錯誤捕獲的問題 還有沒有更好的方法? 當然有!
4 生成器 迭代器
在講async await 之前 我們先講一下 生成器
**當你在執行一個函數的時候,你可以在某個點暫停函數的執行,並且做一些其他工作,然後再返回這個函數繼續執行, 甚至是攜帶一些新的值,然後繼續執行。
上面描述的場景正是JavaScript生成器函數所致力於解決的問題。當我們調用一個生成器函數的時候,它並不會立即執行, 而是需要我們手動的去執行迭代操作(next方法)。也就是說,你調用生成器函數,它會返回給你一個迭代器。迭代器會遍歷每個中斷點。
next 方法返回值的 value 屬性,是 Generator 函數向外輸出數據;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 }
Generator函數的標誌就是function關鍵詞後連綴一個'*' 配合yield 暫停函數 返回的是一個迭代器 每次執行next的時候 停在yield
我們都見過類數組結構吧
let likeArray = { 0: 1, 1: 2, 2: 3, length: 3 }
let arr = [...likeArray]
//執行這段代碼會報錯 報錯信息likeArray is not iterable likeArray是不可枚舉的 那麼我們如果想實現這樣的類數組轉爲數組 怎麼辦呢
我們先看一下函數裏的argument跟類數組有什麼區別
function(){
console.log(argument)}
我們改下一下類數組結構
let likeArray = { 0: 1, 1: 2, 2: 3, length: 3, [Symbol.iterator](){
return {
next() {
return {
value: 1,
done: false
}
}
}
}
}
//在執行
let arr = [...likeArray]
控制檯報錯FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
感覺是不是有點像那麼回事了
我們再改寫
let likeArray = { 0: 1, 1: 2, 2: 3, length: 3, [Symbol.iterator](){
let index = 0
let self = this
return {
next() {
return {
done: self.length === index
value: self[index++],
}
}
}
}
}
// 輸出[1,2,3] 只有在done是false的時候表示迭代完成 就不再繼續執行了 value是每次迭代返回的值
再改寫一下
let likeArray = {
0: 1, 1: 2, 2: 3, length: 3, [Symbol.iterator]:function*() {
let index = 0;
while (index !== this.length) {
yield this[index++]
}
}
}
console.log([...likeArray]) //[1,2,3] 調用返回一個迭代器 ... 每次調用迭代器的next方法 返回{value,done}
生成器可以配合node.js中的co, 藉助於Promise,你可以使用更加優雅的方式編寫非阻塞代碼。
例子:
let fs = require('fs');
function readFile(filename) {
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 + '+' + data;
}
co(read).then(function (data) {
console.log(data);
}, function (err) {
console.log(err);
});
5 async/await
有了上面的基礎 async/await 更加容易明白了
async/await的優點有
1.內置執行器
2.更好的語義
3.更廣的適用性
let fs = require('fs');
function readFile(filename) {
return new Promise(function (resolve, reject) {
fs.readFile(filename, 'utf8', function (err, data) {
if (err)
reject(err);
else
resolve(data);
})
})
}
async function read() {
let template = await readFile('./template.txt');
let data = await readFile('./data.txt');
return template + '+' + data;
}
let result = read();
result.then(data=>console.log(data));
可以直接await 一個promise 使得異步代碼執行看起來像同步一樣 更優雅
async 函數的實現,就是將 Generator 函數和自動執行器,包裝在一個函數裏。
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 用async/await寫異步代碼 更方便維護
第一次寫文章 寫的不好多多包涵 畢竟很多東西都是站在前任人的肩膀上直接拿過來的**