Generator
熟悉ES6語法的同學們肯定對Generator(生成器)函式不陌生,這是一個化非同步為同步的利器。 栗子:
function* abc() {
let count = 0;
while(true) {
let msg = yield ++count;
console.log(msg);
}
}
let iter = abc();
console.log(iter.next().value);
// 1
console.log(iter.next('abc').value);
// 'abc'
// 2
複製程式碼
首先,我們先簡單回顧一下JS的執行規則:
- JS是單執行緒的,只有一個主執行緒
- 函式內的程式碼從上到下順序執行,遇到被呼叫的函式先進入被呼叫函式執行,待完成後繼續執行
- 遇到非同步事件,瀏覽器另開一個執行緒,主執行緒繼續執行,待結果返回後,執行回撥函式
那麼,Generator函式是如何進行非同步化為同步操作的呢? 實質上很簡單,* 和 yield 是一個識別符號,在瀏覽器進行軟編譯的時候,遇到這兩個符號,自動進行了程式碼轉換:
// 非同步函式
function asy() {
$.ajax({
url: 'test.txt',
dataType: 'text',
success() {
console.log("我是非同步程式碼");
}
})
}
function* gener() {
let asy = yield asy();
yield console.log("我是同步程式碼");
}
let it = gener().next();
it.then(function() {
it.next();
})
// 我是非同步程式碼
// 我是同步程式碼
複製程式碼
// 瀏覽器編譯之後
function gener() {
// let asy = yield asy(); 替換為
$.ajax({
url: 'test.txt',
dataType: 'text',
success() {
console.log("我是非同步程式碼");
// next 之後執行以下
console.log("我是同步程式碼");
}
})
// yield console.log("我是同步程式碼");
}
複製程式碼
整個過程類似於,瀏覽器遇到識別符號 * 之後,就明白這個函式是生成器函式,一旦遇到 yield 識別符號,就會將以後的函式放入此非同步函式之內,待非同步返回結果後再進行執行。
更深一步,從記憶體上來講:
普通函式在被呼叫時,JS 引擎會建立一個棧幀,在裡面準備好區域性變數、函式引數、臨時值、程式碼執行的位置(也就是說這個函式的第一行對應到程式碼區裡的第幾行機器碼),在當前棧幀裡設定好返回位置,然後將新幀壓入棧頂。待函式執行結束後,這個棧幀將被彈出棧然後銷燬,返回值會被傳給上一個棧幀。
當執行到 yield 語句時,Generator 的棧幀同樣會被彈出棧外,但Generator在這裡耍了個花招——它在堆裡儲存了棧幀的引用(或拷貝)!這樣當 it.next 方法被呼叫時,JS引擎便不會重新建立一個棧幀,而是把堆裡的棧幀直接入棧。因為棧幀裡儲存了函式執行所需的全部上下文以及當前執行的位置,所以當這一切都被恢復如初之時,就好像程式從原本暫停的地方繼續向前執行了。
而因為每次 yield 和 it.next 都對應一次出棧和入棧,所以可以直接利用已有的棧機制,實現值的傳出和傳入。
至此,Generator 的魔力已經揭開。
Promise
Promise的用法大家應該都很熟悉:
let pr = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve("成功執行啦");
}, 2000)
})
pr.then(function(data) {
console.log(data); // 成功執行啦
})
複製程式碼
那麼 Promise 是如何實現非同步載入的呢?
Promise 並沒有大家想的那麼神祕,其本質就是一個狀態機。
想要實現一個土生土長的 Promise 其實很簡單,狀態機,我們需要幾個引數:
- __success_res 用來儲存成功時的引數
- __error_res 用來儲存失敗時的引數
- __status 用來儲存狀態
- __watchList 用來儲存執行佇列
下面就手動實現一個 Promise
class Promise1 {
constructor(fn) {
// 執行佇列
this.__watchList = [];
// 成功結果
this.__success_res = null;
// 失敗結果
this.__error_res = null;
// 狀態
this.__status = "";
fn((...args) => {
// 儲存成功資料
this.__success_res = args;
// 狀態改為成功
this.__status = "success";
// 若為非同步則回頭執行then成功方法
this.__watchList.forEach(element => {
element.fn1(...args);
});
}, (...args) => {
// 儲存失敗資料
this.__error_res = args;
// 狀態改為失敗
this.__status = "error";
// 若為非同步則回頭執行then失敗方法
this.__watchList.forEach(element => {
element.fn2(...args);
});
});
}
// then 函式
then(fn1, fn2) {
if (this.__status === "success") {
fn1(...this.__success_res);
} else if (this.__status === "error") {
fn2(...this.__error_res);
} else {
this.__watchList.push({
fn1,
fn2
})
}
}
}
複製程式碼
這樣就簡單實現了 Promise 的功能,在使用上和JS的 Promise 並無其他區別,若想實現 Promise.all 方法,則只需要進行小小的迭代:
Promise1.all = function(arr) {
// 存放結果集
let result = [];
return Promise1(function(resolve, reject) {
let i = 0;
// 進行迭代執行
function next() {
arr[i].then(function(res) {
// 存放每個方法的返回值
result.push(res);
i++;
// 若全部執行完
if (i === result.length) {
// 執行then回撥
resolve(result);
} else {
// 繼續迭代
next();
}
}, reject)
}
})
}
複製程式碼
至此,Generator 和 Promise 都已解析完成。
最後不好意思推廣一下我基於 Taro
框架寫的元件庫:MP-ColorUI。
可以順手 star 一下我就很開心啦,謝謝大家。