banner
布语

布语

集中一点, 登峰造极⚡️ 布语、布羽、不语、卜语......
github
twitter

Generator的藝術

該主題分享參閱文章自阮一峰老師的 ECMAScript6 入門 => https://es6.ruanyifeng.com/#docs/generator#%E7%AE%80%E4%BB%8B

參閱列表:

  1. 阮一峰老師的《Generator 函數的含義與用法》: 點擊參閱
  2. 稀土掘金平台作者前端羅納爾多的《異步編程解決方案 generator》: 點擊參閱

直觀使用#

function* printStrGenerator() {
    let str1 = yield "Hello";
    console.log(str1);
    let str2 = yield str1 + "CodeGorgeous";
    console.log(str2);
    return "Done";
}

const print = printStrGenerator();
console.log(print); // printStrGenerator {<suspended>}
console.log(print.next()); // {done: false, value: "Hello"}
console.log(print.next("My is")); // {done: false, value: "CodeGorgeous"}
console.log(print.next("Test")); // {done: true, value: "Done"}
console.log(print.next()); // {done: true, value: undefined}
console.log(print); // printStrGenerator {<closed>}

Generator 產生的背景#

在 Js 中因為 JavaScript 為單線程操作,所以當我們想要發出一個請求時如果一味的等待請求響應再接著執行代碼那麼注定要浪費非常不穩定的時間,隨之而來的就是為了解決這個問題推出一個方式叫做異步代碼編程,大致可以理解為當代碼執行到一個異步操作後會繼續執行後續代碼,等待異步操作完成後觸發相應的操作然後接著執行同步代碼,相應的提出這種思想後就推出了幾種解決方案:

  1. 回調函數
  2. 事件監聽
  3. 發布 / 訂閱
  4. Promise

我們通過幾個例子來重新看一下這幾種解決方案及其優缺點

  • 回調函數

    • 示例:
    // 複製文件操作
    fs.readFile("./test/index.html", 'utf-8', (err, data) => {
        if (err) return;
        // 創建文件並複製寫入內容
        fs.writeFile("./test/index-copy.html", data, 'utf-8', (err) => {
            if (err) return;
            console.log("複製成功");
        })
    });
    
    • 優點:
      1. 不會阻塞代碼執行,易於書寫
    • 缺點:
      1. 一個異步操作依賴前一個異步操作,易形成回調地獄 (callback hell)(高耦合)
      2. 代碼不易維護
  • 事件監聽

    • 示例:
      const fn = () => {
          fs.readFile("./test/index.html", 'utf-8', (err, data) => {
              if (err) return;
              fn.trigger("readFileOver", data);
          });
      };
      
      fn.eventList = [];
      
      fn.on = (type, callback) => {
          fn.eventList.push({
              type,
              fn: callback
          });
      };
      
      fn.trigger = (type, ...args) => {
          fn.eventList.forEach(item => {
              (item.type === type) && (item.fn(...args));
          });
      };
      
      fn.on("readFileOver", (data) => {
          fs.writeFile("./test/index-copy.html", data, 'utf-8', (err) => {
              if (err) return;
              console.log("複製成功");
          });
      });
      
      fn();
      
    • 優點:
      1. 相較於回調函數方式來講基本不會形成回調地獄 (降低耦合)
      2. 可以自行綁定多個事件並可單獨配置調整回調函數
  • 發布 / 訂閱 (觀察者模式)

    • 示例:
      class EventBus {
          constructor() {
              this.event = [];
          };
          /**
          * 訂閱
          * @param {*} eventType 事件類型
          * @param {*} callback 回調函數
          */
          add(eventType, callback) {
              this.event.push({
                  type: eventType,
                  fn: callback
              });
          };
          /**
          * 發布
          * @param {*} eventType 事件類型
          * @param  {...any} args 參數
          */
          trigger(eventType, ...args) {
              // 此處就不對已觸發事件進行清除操作
              this.event.forEach(item => {
                  if (item.type === eventType) {
                      item.fn(...args);
                  }
              });
          }
      };
      // 創建實例
      const eventBus = new EventBus();
      
      // 訂閱readFileOver事件
      eventBus.add("readFileOver", (data) => {
          fs.writeFile("./test/index-copy1.html", data, 'utf-8', (err) => {
              if (err) return;
              console.log("複製成功1");
          });
      });
      eventBus.add("readFileOver", (data) => {
          fs.writeFile("./test/index-copy2.html", data, 'utf-8', (err) => {
              if (err) return;
              console.log("複製成功2");
          });
      });
      
      fs.readFile("./test/index.html", 'utf-8', (err, data) => {
          if (err) return;
          // 發布readFileOver事件
          eventBus.trigger("readFileOver", data);
      });
      
    • 優點:
      1. 相較於事件監聽來講發布 / 訂閱模式更容易管理觸發機制更加清晰了解到當前存在的事件情況
  • Promise

    • 示例:
      new Promise((resolve, reject) => {
          fs.readFile("./test/index.html", 'utf-8', (err, data) => {
              if (err) reject(err);
              resolve(data);
          });
      }).then(resp => {
          // 創建文件並複製寫入內容
          fs.writeFile("./test/index-copy.html", resp, 'utf-8', (err) => {
              if (err) return;
              console.log("複製成功");
          });
      }).catch(err => {
          console.log("Error:", err);
      });
      
    • 優點:
      1. 重傳統的回調函數內嵌模式改變為鏈式調用模式
      2. 更加清晰的代碼流程
      3. 可讀性提升

在傳統的編程語言中早已有異步編程解決方案,其中還有一種方案叫做協程, 其大致意思為當個線程互相協作完成某個異步任務.
在 Js 中是以 Generator 函數的方式出現,以複製文件舉例:

function* copyFile() {
    const data = yield fs.readFile("./test/index.html", 'utf-8', (err, data) => {
        if (err) return;
        else fn.next(data);
    });
    yield fs.writeFile("./test/index-copy.html", data, 'utf-8', (err) => {
        if (err) return;
        console.log("複製成功");
        fn.next();
    })
};
const fn = copyFile();
fn.next()

為什麼 Generator 能夠解決異步問題
Generator 最重要的一個特點就是可以交出函數的執行權,執行權代表著函數內代碼是否可以執行以及暫停函數內代碼執行的權力,
通過手動操控執行權可以在適當的時機 (例如:接收到請求響應後) 繼續執行函數內的代碼.

Generator 的使用#

如何創建一個 Generator 函數
Generator 與普通函數最大的區別就是聲明一個 Generator 函數時需要帶上一個標誌 * 號,例如:

function* generator() {}
function * generator() {}
function*generator() {}
function *generator() {}

這四種均為 Generator 函數的書寫方式 (無優劣之分), 一般來講會推薦第零位的這種寫法,大多數格式化代碼的情況下會把 Generator 格式化為第零位該種代碼格式。還需要注意的一點就是 Generator 函數是無法當作構造函數使用的.

Generator 函數的執行特點
當你 Generator 函數執行後你會發現你在該函數內寫的任何東西都沒有開始執行,這就是你可以管理函數執行權的一種體現。你接收到該函數返回值默認會是一個處於暫停狀態的 generator 對象,你可以通過該對象身上的 next () 進行執行一次函數內代碼執行權,該次執行的代碼範圍是:如果是第一次執行則是函數開始到下一次 yield 表達式的位置 /return 位置,其他則是從上一次 yield 表達式位置執行到下一次 yield 表達式位置 /return 位置。每次執行 next () 方法你都會得到一個對象: {value: undefined, done: false}, 該對象中 value 代表著 yield 表達式 /return 表達式的返回值,done 代表是該 Generator 是否為最後一次函數內代碼權的執行,true 代表著後續你在使用 next () 函數會是一個沒有意義的行為,false 則否之.

Generator 執行示意圖
Generator 執行示意圖

yield 表達式#

從上對 Generator 介紹及特點,我們可以了解到在 Generator 中 yield 表達式的重要性,yield 在 Generator 中代表著每次代碼執行範圍 (即從上個 yield 到該次 yield) 中間的代碼,同樣既然 yield 是否可以在其他函數內部使用呢?顯然是不可以的,因為在其他函數中使用沒有存在的意義,因為其他函數是不能控制函數內部執行權的.

其中最需要注意的一點就是 yield 返回值是無法賦值給變量的,yield 值是會傳遞到 next () 函數執行的返回值出現,你在調用的 next 函數時傳遞的參數會作為上一次 yield 執行的結果出現在賦值操作上的,這樣用語言描述是比較混亂的,下面我們用一張圖來進行解釋:
yield 與 next 的關係

Generator 與 Interator#

了解 Interator
Interator 是 Es6 中引入的新的遍歷機制,該機制具有以下幾種特點:

  1. 具有 Symbol.iterator 接口屬性
  2. 可使用 forof 遍歷 (forof 僅可遍歷具有 Interator 接口的數據)
  3. Symbol.iterator 屬性返回的函數具有 Generator 特性即控制執行權的生成器
    因為 Generator 就是一個遍歷器生成器,所以我們可以給不具有 Symbol.iterator 屬性的對象手動的添加一個遍歷器接口使得該對象可以使用 forof 遍歷以及一些方法,有興趣可以看一看下面這段代碼:
const obj = {};
obj[Symbol.iterator] = function* () {
    yield "CodeGorgeous";
    yield "MaoMao";
    yield "BestFriend";
};
for (const item of arr) {
    console.log(item);// CodeGorgeous // MaoMao // BestFriend
};
const arrObj = [...obj];
console.log(arrObj);// [ 'CodeGorgeous', 'MaoMao', 'BestFriend' ]

Generator 與 throw#

在正常的一段 Js 代碼中我們可以通過 trycatch 表達式對一段代碼進行包裹以防止當該段代碼出現錯誤時影響到其他代碼的執行。如果你編寫過 / 看過一些第三庫的代碼,你有時候會看到例如在一個函數內會首先判定傳遞進入的參數是否符合預期類型,如果不符合則會對其進行手動使用throw拋出一個錯誤用於提示開發者,在 Generator 中則另外提供了一個 throw 方法用於在生成器中拋出錯誤.

function* printStrGenerator() {
    let index = 0;
    while (true) {
        try {
            yield `This is ${index ++}`
        } catch (error) {
            console.log("Generator內部捕獲錯誤", error);
        }
    }
};

const g = printStrGenerator();
console.log(g.next()); // {value: 'This is 0', done: false}
try {
    g.throw(new Error("An expected error occurred in the code")); // Generator內部捕獲錯誤 Error: An expected error occurred in the code
    g.throw(new Error("An expected error occurred in the code")); // Generator內部捕獲錯誤 Error: An expected error occurred in the code
} catch (error) {
    console.log("Window外部捕獲錯誤");
}
console.log(g.next()); // {value: 'This is 3', done: false}

我們從該段例子中可以看出,Generator.prototype.throw 並不只具有拋出錯誤的功能,同時具有執行一次 next () 的特性,在該段示例中因為我使用的是一個 while 無限循環 + trycatch 使用,所以每次拋出錯誤都會被 Generator 內部捕獲錯誤 (但是你一定要明確每次這部分都是在不同塊級作用域的並不是同一個作用域一直用), 如果你在書寫每個 yield 表達式並未被函數內部 trycatch 捕獲 / 當前 Generator 已經關閉但是你仍 throw 拋出這兩種情況會被外部相應的 trycatch 所捕獲 (內部捕獲和外部捕獲具有完全不同的意義). 還需要注意一點的是 Generator 函數執行過程中內部如果拋出錯誤但內部未捕獲 (ps: 肯定要外部捕獲,因為外部要還是不捕獲這個程序就掛掉了) 則會終止掉 Generator 的執行如果後續繼續執行了 next 則只會返回一個{value: undefined, done: true}, 感覺說著說著又模糊了起來,不妨我們建立一個對照組即可清晰觀察到差異:

  • Generator 內部未捕獲,由外部捕獲
function* printStrGenerator() {
    let index = 0;
    while (true) {
        yield `This is ${index ++}`;
    }
};

const g = printStrGenerator();
try {
    console.log(g.next()); // {value: 'This is 0', done: false}
    console.log(g.throw(new Error("This is Error"))); // 這一步執行會Generator會報錯
} catch (error) {
    console.log(g.next()); // {value: undefined, done: true}
    console.log(g); // printStrGenerator {<closed>}
}
  • Generator 內部捕獲,外部不捕獲
function* printStrGenerator() {
    let index = 0;
    while (true) {
        try {
            yield `This is ${index ++}`;
        } catch (error) {
            console.log(error);
        }
    }
};

const g = printStrGenerator();
console.log(g.next()); // {value: 'This is 0', done: false}
console.log(g.throw(new Error("This is Error"))); // Error: This is Error // {value: 'This is 1', done: false}
console.log(g.next()); // {value: 'This is 2', done: false}
console.log(g); // printStrGenerator {<suspended>}

Generator 與 return#

在正常情況下 return 作為終止函數執行並返回默認返回 undefined 的情況下出現,在此處我們討論的是 Generator.prototype.return 該方法,為什麼我會提及這兩個不同層次的東西呢 (ps: 一個語法層次,一個就是個函數), 不過在 Generator 中原型鏈 (原型鏈不熟悉的可以看一下額外順帶的知識部分) 的 return 同樣能夠達到這樣的效果,同樣我們觀察一個例子就能夠看出效果:

function* printStrGenerator() {
    let index = 0;
    while (true) {
        yield `This is ${index ++}`;
    }
};

const g = printStrGenerator();

console.log(g.next()); // {value: 'This is 0', done: false}
console.log(g.return("Done")); // {value: 'Done', done: true}
console.log(g.next()); // {value: undefined, done: true}

嵌套式 Generator 的處理方案#

可以想到這麼一種場景,我把不同的異步分離抽成了不同的 generator, 我使用的時候放到一個總的 Generator 組合使用管理即可,這樣的想法確實很 Nice, 一方面能夠想到抽離增強復用性,另一方面組合式使用更便於對邏輯的分離 (不需要在使用該函數時思考其函數內部), 再另一方面就是維護性高當某個 Generator 出問題時正常只需要修復該函數即可不需要再去調整其他地方的代碼,好了接下來我們動手先試一試感受一下吧

function* getUserInfoGenerator(generator) {
    return yield (setTimeout(() => {
        const useInfo = {
            nickname: "CodeGorgeous",
            openid: "aszxzxcISadaxzxxxxxx"
        };
        console.log("獲取用戶信息成功");
        generator && generator(useInfo);
    }, Math.random() * 5000));
}

function* getProjectConfigGenerator(generator) {
    return yield (setTimeout(() => {
        const projectConfig = {
            theme: "default",
            lang: "zh-CN",
            title: "集中一點, 登峰造極⚡"
        };
        console.log("獲取項目配置成功");
        generator && generator(projectConfig);
    }, Math.random() * 5000))
}

function* dispatchGenerator() {
    yield "1";
    console.log("開始獲取用戶信息");
    const useInfo = yield getUserInfoGenerator;
    yield "2";
    console.log(`開始獲取的項目配置`, useInfo);
    // console.log(`開始獲取${useInfo && useInfo.nickname}用戶的項目配置`, useInfo);
    const projectConfig = yield getProjectConfigGenerator;
    console.log(projectConfig);
}

// 執行調度
const g = dispatchGenerator();
// 這裡是純純的突發奇想做一個Generator自動執行器(寫個這個好折磨人呀, 頭髮又開始掉光了)

function run(generator) {
    function next(data) {
        var result = generator.next(data);
        if (result.done) return;
        if (typeof result.value === 'function' && result.value[Symbol.toStringTag] === "GeneratorFunction") {
            const temp = result.value(next);
            run(temp);
        } else {
            next();
        }
    }
    next();
};
run(g);

但是在 Generator 中 yield 表達式提供了另一種方式將嵌套式 Generator 扁平化該種方式就是當你碰到一個內嵌 Generator 是可以使用yield*, 我們將上面寫的代碼稍微加以改造一下

function* getUserInfoGenerator(generator) {
    return yield (setTimeout(() => {
        const useInfo = {
            nickname: "CodeGorgeous",
            openid: "aszxzxcISadaxzxxxxxx"
        };
        console.log("獲取用戶信息成功");
        generator && generator.next(useInfo);
    }, Math.random() * 5000));
}

function* getProjectConfigGenerator(generator) {
    return yield (setTimeout(() => {
        const projectConfig = {
            theme: "default",
            lang: "zh-CN",
            title: "集中一點, 登峰造極⚡"
        };
        console.log("獲取項目配置成功");
        generator && generator.next(projectConfig);
    }, Math.random() * 5000))
}

function* dispatchGenerator() {
    console.log("開始獲取用戶信息");
    const useInfo = yield* getUserInfoGenerator(g);
    console.log(`開始獲取${useInfo.nickname}用戶的項目配置`, useInfo);
    const projectConfig = yield* getProjectConfigGenerator(g);
    console.log(projectConfig);
}

// 執行調度
const g = dispatchGenerator();
g.next();

需要注意的一點就是我上面演示的兩個例子中對於異步操作的處理是不合理的,雖然能夠實現功能,但是真實要應用於異步操作請看Generator 異步場景應用, 我會在此章節重新書寫一遍規範的合理的代碼.

漫談 Generator 與協程#

這部分打算再開一個小文章進行記載,因為感覺漫談著容易談偏.

Generator 異步場景應用#

Generator 其本身是無法處理異步場景的,其主要功能就是作為異步操作的容器而出現,在平時開發中一般也不會使用 Generator 的,自己平時遇見的一個庫 (redux-saga) 其基本就是根據 Generator 進行開發的,有興趣可以了解一下 React 及其生態圈,那我們平時開發可以通過什麼方式解決呢,這時就配合我們最開始的提到的異步解決方案使用,下面我就使用了兩種方式進行示例:

  • 解決方案:
    1. 回調函數
      • 回調函數的時機及時交回函數執行權
          function getUserInfoGenerator(callback) {
              return (setTimeout(() => {
                  const useInfo = {
                      nickname: "CodeGorgeous",
                      openid: "aszxzxcISadaxzxxxxxx"
                  };
                  console.log("獲取用戶信息成功");
                  callback(useInfo);
              }, Math.random() * 5000))
          }
      
          function getProjectConfigGenerator(callback) {
              return (setTimeout(() => {
                  const projectConfig = {
                      theme: "default",
                      lang: "zh-CN",
                      title: "集中一點, 登峰造極⚡"
                  };
                  console.log("獲取項目配置成功");
                  callback(projectConfig);
              }, Math.random() * 5000))
          }
      
          function* dispatchGenerator() {
              console.log("開始獲取用戶信息");
              const useInfo = yield getUserInfoGenerator;
              console.log(`開始獲取${useInfo && useInfo.nickname}用戶的項目配置`, useInfo);
              const projectConfig = yield getProjectConfigGenerator;
              console.log(projectConfig);
          }
      
          // 執行調度
          const g = dispatchGenerator();
      
          function run(generator) {
              function next(data) {
                  var result = generator.next(data);
                  if (result.done) return;
                  if (typeof result.value === 'function') {
                      result.value((data) => {
                          next(data);
                      });
                  } else {
                      next(result.value);
                  }
              }
              next();
          };
          run(g);
      
    2. Promise
      • 通過 then 的時機及時交回函數執行權
              function getUserInfoGenerator() {
                  return new Promise(resolve => {
                      setTimeout(() => {
                          const useInfo = {
                              nickname: "CodeGorgeous",
                              openid: "aszxzxcISadaxzxxxxxx"
                          };
                          console.log("獲取用戶信息成功");
                          resolve(useInfo);
                      }, Math.random() * 5000)
                  });
              }
      
              function getProjectConfigGenerator() {
                  return new Promise(resolve => {
                      setTimeout(() => {
                          const projectConfig = {
                              theme: "default",
                              lang: "zh-CN",
                              title: "集中一點, 登峰造極⚡"
                          };
                          console.log("獲取項目配置成功");
                          resolve(projectConfig);
                      }, Math.random() * 5000)
                  })
              }
      
              function* dispatchGenerator() {
                  console.log("開始獲取用戶信息");
                  const useInfo = yield getUserInfoGenerator();
                  console.log(`開始獲取${useInfo && useInfo.nickname}用戶的項目配置`, useInfo);
                  const projectConfig = yield getProjectConfigGenerator();
                  console.log(projectConfig);
              }
      
              // 執行調度
              const g = dispatchGenerator();
      
              function run(generator) {
                  function next(data) {
                      var result = generator.next(data);
                      if (result.done) return;
                      if (typeof result.value === 'object' && result.value.constructor.name === "Promise") {
                          result.value.then(data => {
                              next(data);
                          });
                      } else {
                          next(result.value);
                      }
                  }
                  next();
              };
              run(g);
      

Generator 與 async#

async 為 ES2017 版本推出的一種函數,async 的出現會讓管理異步操作更加方便簡潔,為什麼這裡會從 Generator 討論到 async 呢?下面我們觀察一下下面兩種方式:

  • Generator 方式
function getUserInfoGenerator(callback) {
    return (setTimeout(() => {
        const useInfo = {
            nickname: "CodeGorgeous",
            openid: "aszxzxcISadaxzxxxxxx"
        };
        console.log("獲取用戶信息成功");
        callback(useInfo);
    }, Math.random() * 5000))
}

function getProjectConfigGenerator(callback) {
    return (setTimeout(() => {
        const projectConfig = {
            theme: "default",
            lang: "zh-CN",
            title: "集中一點, 登峰造極⚡"
        };
        console.log("獲取項目配置成功");
        callback(projectConfig);
    }, Math.random() * 5000))
}

function* dispatchGenerator() {
    console.log("開始獲取用戶信息");
    const useInfo = yield getUserInfoGenerator;
    console.log(`開始獲取${useInfo && useInfo.nickname}用戶的項目配置`, useInfo);
    const projectConfig = yield getProjectConfigGenerator;
    console.log(projectConfig);
}

// 執行調度
const g = dispatchGenerator();

function run(generator) {
    function next(data) {
        var result = generator.next(data);
        if (result.done) return;
        if (typeof result.value === 'object' && result.value.constructor.name === "Promise") {
            result.value.then(data => {
                next(data);
            });
        } else {
            next(result.value);
        }
    }
    next();
};
run(g);
  • async 方式
function getUserInfoGenerator() {
    return new Promise(resolve => {
        setTimeout(() => {
            const useInfo = {
                nickname: "CodeGorgeous",
                openid: "aszxzxcISadaxzxxxxxx"
            };
            console.log("獲取用戶信息成功");
            resolve(useInfo);
        }, Math.random() * 5000)
    });
}

function getProjectConfigGenerator() {
    return new Promise(resolve => {
        setTimeout(() => {
            const projectConfig = {
                theme: "default",
                lang: "zh-CN",
                title: "集中一點, 登峰造極⚡"
            };
            console.log("獲取項目配置成功");
            resolve(projectConfig);
        }, Math.random() * 5000)
    })
}

async function dispatchGenerator() {
    console.log("開始獲取用戶信息");
    const useInfo = await getUserInfoGenerator();
    console.log(`開始獲取${useInfo && useInfo.nickname}用戶的項目配置`, useInfo);
    const projectConfig = await getProjectConfigGenerator();
    console.log(projectConfig);
}
dispatchGenerator();

我們可以看出兩者書寫方式差距體現在:

  1. Generator 函數的符號 * 替換為 async
  2. yield 替換為 await
  3. Generator 函數自動執行完畢需要依靠執行器,async 函數則是直接調用該函數即可
    所以說理論來講 async 就是 Generator 函數的語法糖,是一種更加精簡的寫法省去的 Generator 的執行器,所以理論上來講 Generator 或多或少都是間接性的出現在代碼中只是直觀沒有感受到罷了.
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。