摘要:返回的結果是一個對象,類似于表示本次后面執行之后返回的結果。對象用于一個異步操作的最終完成或失敗及其結果值的表示簡單點說就是處理異步請求。源碼分析主要脈絡函數調用后,返回一個實例。參考鏈接解釋對象的用法的源碼及其用法
前言 知識儲備本文始發于我的個人博客,如需轉載請注明出處。
為了更好的閱讀體驗,可以直接進去我的個人博客看。
閱讀本文需要對Generator和Promise有一個基本的了解。
這里我簡單地介紹一下兩者的用法。
Generator關于Generator的用法,推薦MDN上面的解釋function *函數,里面非常詳細。
用一句話總結就是,generator函數是回調地獄的一種解決方案,它跟promise類似,但是卻可以以同步的方式來書寫代碼,而避免了promise的鏈式調用。
它的執行過程在于調用生成器函數(generator function)后,會返回一個iterator(迭代)對象,即Generator對象,但是它并不會立刻執行里面的代碼。
它有幾個方法,next(), throw()和return()。調用next()方法后,它會找到第一個yield關鍵字(直到找到程序底部或者return語句),每次程序運行到yield關鍵字時,程序便會暫停,保存當前環境里面的變量的值,然后可以跳出當前運行環境去執行yield后面的代碼,再把結果返回回來。
返回的結果是一個對象,類似于{value: "", done: false}, value表示本次yield后面執行之后返回的結果。如果是Promise實例,則是返回resolved后的值。done表示迭代器是否執行完畢,若為true,則表示當前生成器函數已經產生了最后輸出的值,即生成器函數已經返回。
下面是一個簡單的例子:
const gen = function *() { let index = 0; while(index < 3) yield index++; return "All done." }; const g = gen(); console.log(g.constructor); // output: GeneratorFunction {} console.log(g.next()); // output: { value: 0, done: false } console.log(g.next()); // output: { value: 1, done: false } console.log(g.next()); // output: { value: 2, done: false } console.log(g.next()); // output: { value: "All done.", done: true } console.log(g.next()); // output: { value: undefined, done: true }Promise
關于Promise的用法,可以查閱我之前寫過的一篇文章《關于ES6中Promise的用法》,寫得比較詳細。
Promise對象用于一個異步操作的最終完成(或失?。┘捌浣Y果值的表示(簡單點說就是處理異步請求)。Promise核心就在于里面狀態的變換,是rejected、resolved還是pending,還有就是原型鏈上的then()方法,它可以傳遞本次狀態轉換后返回的值。
進入主題由于實際需要,這幾天學習了koa2.x框架,但是它已經不推薦使用generator函數了,推薦用async/await組合。
koa2.x的最新用法:
async/await(node v7.6+):
const Koa = require("koa"); const app = new Koa(); app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); });
common 用法:
const Koa = require("koa"); const app = new Koa(); // response app.use(ctx => { ctx.body = "Hello Koa"; }); app.listen(3000);
由于本地的Node版本是v6.11.5,而使用async/await則需要Node版本v7.6以上,所以我想有沒有什么模塊能夠把koa2.x版本的語法兼容koa1.x的語法。koa1.x語法的關鍵在于generator/yield組合。通過yield可以很方便地暫停程序的執行,并改變執行環境。
這時候我找到了TJ大神寫的co模塊,它可以讓異步流程同步化,還有koa-convert模塊等等,這里著重介紹co模塊。
co在koa2.x里面的用法如下:
const Koa = require("koa"); const app = new Koa(); const co = require("co"); // response app.use(co.wrap(function *(ctx, next) { yield next(); // yield someAyncOperation; // ... ctx.body = "co"; })); app.listen(3000);
co模塊不僅可以配合koa框架充當中間件的轉換函數使用,還支持批量執行generator函數,這樣就無需手動調用多次next()來獲取結果了。
它支持的參數有函數、promise、generator、數組和對象。
// co的源碼 return onRejected(new TypeError("You may only yield a function, promise, generator, array, or object, " + "but the following object was passed: "" + String(ret.value) + """));
下面舉一個co傳遞進來一個generator函數的例子:
// 這里模擬一個generator函數調用 const co = require("co"); co(gen).then(data => { // output: then: ALL Done. console.log("then: " + data); }); function *gen() { let data1 = yield pro1(); // output: pro1 had resolved, data1 = I am promise1 console.log("pro1 had resolved, data1 = " + data1); let data2 = yield pro2(); // output: pro2 had resolved, data2 = I am promise2 console.log("pro2 had resolved, data2 = " + data2); return "ALL Done." } function pro1() { return new Promise((resolve, reject) => { setTimeout(resolve, 2000, "I am promise1"); }); } function pro2() { return new Promise((resolve, reject) => { setTimeout(resolve, 1000, "I am promise2"); }); }
我覺得co()函數很神奇,里面究竟經過了什么樣的轉換?抱著一顆好奇心,讀了一下co的源碼。
co源碼分析 主要脈絡co函數調用后,返回一個Promise實例。
co的思想就是將一個傳遞進來的參數進行合法化,再通過轉換成Promise實例返回出去。如果參數fn是generator函數的話,里面還可以自動進行遍歷,執行generator函數里面的yield關鍵字后面的內容,并返回結果,也就是不斷地調用fn().next()方法,再通過傳遞返回的Promise實例resolved后的值,從而達到同步執行generator函數的效果。
這里要注意,co里面最主要的是要理解Promise實例和Generator對象,它們是co函數里面的程序自動遍歷執行的關鍵。
下面解釋一下co模塊里面的最重要的兩部分,一個是generator函數的自動調用,另外一個是參數的Promise化。
第一,generator函數的自動調用(中文部分是我的解釋):
function co(gen) { // 保存當前的執行環境 var ctx = this; // 切割出函數調用時傳遞的參數 var args = slice.call(arguments, 1) // we wrap everything in a promise to avoid promise chaining, // which leads to memory leak errors. // see https://github.com/tj/co/issues/180 // 返回一個Promise實例 return new Promise(function(resolve, reject) { // 如果gen是一個函數,則返回一個新的gen函數的副本, // 里面綁定了this的指向,即ctx if (typeof gen === "function") gen = gen.apply(ctx, args); // 如果gen不存在或者gen.next不是一個函數 // 就說明gen已經調用完成, // 那么直接可以resolve(gen),返回Promise if (!gen || typeof gen.next !== "function") return resolve(gen); // 首次調用gen.next()函數,假如存在的話 onFulfilled(); /** * @param {Mixed} res * @return {Promise} * @api private */ function onFulfilled(res) { var ret; try { // 嘗試著獲取下一個yield后面代碼執行后返回的值 ret = gen.next(res); } catch (e) { return reject(e); } // 處理結果 next(ret); } /** * @param {Error} err * @return {Promise} * @api private */ function onRejected(err) { var ret; try { // 嘗試拋出錯誤 ret = gen.throw(err); } catch (e) { return reject(e); } // 處理結果 next(ret); } /** * Get the next value in the generator, * return a promise. * * @param {Object} ret * @return {Promise} * @api private */ // 這個next()函數是最為關鍵的一部分, // 里面幾乎包含了generator自動調用實現的核心 function next(ret) { // 如果ret.done === true, // 證明generator函數已經執行完畢 // 即已經返回了值 if (ret.done) return resolve(ret.value); // 把ret.value轉換成Promise對象繼續調用 var value = toPromise.call(ctx, ret.value); // 如果存在,則把控制權交給onFulfilled和onRejected, // 實現遞歸調用 if (value && isPromise(value)) return value.then(onFulfilled, onRejected); // 否則最后直接拋出錯誤 return onRejected(new TypeError("You may only yield a function, promise, generator, array, or object, " + "but the following object was passed: "" + String(ret.value) + """)); } }); }
對于以上代碼中的onFulfilled和onRejected,我們可以把它們看成是co模塊對于resolve和reject封裝的加強版。
第二,參數Promise化,我們來看一下co中的toPromise的實現:
function toPromise(obj) { if (!obj) return obj; if (isPromise(obj)) return obj; if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj); if ("function" == typeof obj) return thunkToPromise.call(this, obj); if (Array.isArray(obj)) return arrayToPromise.call(this, obj); if (isObject(obj)) return objectToPromise.call(this, obj); return obj; }
toPromise的本質上就是通過判定參數的類型,然后再通過轉移控制權給不同的參數處理函數,從而獲取到期望返回的值。
關于參數的類型的判斷,看一下源碼就能理解了,比較簡單。
我們著重來分析一下objectToPromise的實現:
function objectToPromise(obj){ // 獲取一個和傳入的對象一樣構造器的對象 var results = new obj.constructor(); // 獲取對象的所有可以遍歷的key var keys = Object.keys(obj); var promises = []; for (var i = 0; i < keys.length; i++) { var key = keys[i]; // 對于數組的每一個項都調用一次toPromise方法,變成Promise對象 var promise = toPromise.call(this, obj[key]); // 如果里面是Promise對象的話,則取出e里面resolved后的值 if (promise && isPromise(promise)) defer(promise, key); else results[key] = obj[key]; } // 并行,按順序返回結果,返回一個數組 return Promise.all(promises).then(function () { return results; }); // 根據key來獲取Promise實例resolved后的結果, // 從而push進結果數組results中 function defer(promise, key) { // predefine the key in the result results[key] = undefined; promises.push(promise.then(function (res) { results[key] = res; })); } }
上面理解的關鍵就在于把key遍歷,如果key對應的value也是Promise對象的話,那么調用defer()方法來獲取resolved后的值。
編寫自己的generator函數運行器通過以上的簡單介紹,我們就可以嘗試來寫一個屬于自己的generator函數運行器了,目標功能是能夠自動運行function*函數,并且里面的yield子句后面跟著的都是Promise實例。
具體代碼(my-co.js)如下:
// my-co.js module.exports = my-co; let my-co = function (gen) { // gen是一個具有Promise的生成器函數 const g = gen(); // 迭代器 // 首次調用next next(); function next(val) { let ret = g.next(val); // 調用ret if (ret.done) { return ret.value; } if (ret && "function" === typeof ret.value.then) { ret.value.then( (data) => { // 繼續循環下去 return next(data); // promise resolved }); } } };
這樣我們就可以在test.js文件中調用了:
// test.js const myCo = require("./my-co"); const fs = require("fs"); let gen = function *() { let data1 = yield pro1(); console.log("data1: " + data1); let data2 = yield pro2(); console.log("data2: " + data2); let data3 = yield pro3(); console.log("data3: " + data3); let data4 = yield pro4(data1 + " " + data2 + " " + data3); console.log("data4: " + data4); return "All done." }; // 調用myCo myCo(gen); // 延遲兩秒resolve function pro1() { return new Promise((resolve, reject) => { setTimeout(resolve, 2000, "promise1 resolved"); }); } // 延遲一秒resolve function pro2() { return new Promise((resolve, reject) => { setTimeout(resolve, 1000, "promise2 resolved"); }); } // 寫入Hello World到./1.txt文件中 function pro3() { return new Promise((resolve, reject) => { fs.appendFile("./1.txt", "Hello World ", function(err) { resolve("write-1 success"); }); }); } // 寫入content到./1.txt文件中 function pro4(content) { return new Promise((resolve, reject) => { fs.appendFile("./1.txt", content, function(err) { resolve("write-2 success"); }); }); }
控制臺輸出結果:
// output data1: promise1 resolved data2: promise2 resolved data3: write-1 success data4: write-2 success
./1.txt文件內容:
Hello World promise1 resolved promise2 resolved write-1 success
由上可知,運行的結果符合我們的期望。
雖然這個運行器很簡單,后面只支持Promise實例,并且也不支持多種參數,但是卻引導出了一個思路,促使我們思考怎么去展示我們的代碼,還有就是很有效地避免了多重then,以同步的方式來書寫異步代碼。Promise解決的是回調地獄的問題(callback hell),而Generator解決的是代碼的書寫方式。孰優孰劣,全在于個人意愿。
總結以上分析了co部分源碼的精髓,講到了co函數里面generator函數自動遍歷執行的機制,還講到了co里面最為關鍵的objectToPromise()方法。
在文章的后面我們編寫了一個屬于自己的generator函數遍歷器,其中主要的是next()方法,它可以檢測我們yield后面Promise操作是否完成。如果generator的狀態done還沒有置為true,那么繼續調用next(val)方法,并把上一次yield操作獲取到的值傳遞下去。
有時候在引用別人的模塊出現問題時,如果在網上找不到自己期望的答案,那么我們可以根據自己的能力來選擇性地分析一下作者的源碼,看源碼是一種很好的成長方式。
坦白說,這是我第一次深入分析模塊的源碼,co模塊的源碼包括注釋和空行只有230多行左右,所以這是一個很好的切入點。里面代碼雖少,但是理解卻不易。
如果以上所述有什么問題,歡迎反饋。
感謝支持。
參考鏈接MDN - Promise解釋
MDN - Generator對象的用法
TJ - co的源碼及其用法
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/92013.html
摘要:前端每周清單專注前端領域內容,以對外文資料的搜集為主,幫助開發者了解一周前端熱點分為新聞熱點開發教程工程實踐深度閱讀開源項目巔峰人生等欄目。背后的故事本文是對于年之間世界發生的大事件的詳細介紹,闡述了從提出到角力到流產的前世今生。 前端每周清單專注前端領域內容,以對外文資料的搜集為主,幫助開發者了解一周前端熱點;分為新聞熱點、開發教程、工程實踐、深度閱讀、開源項目、巔峰人生等欄目。歡迎...
摘要:前端每周清單半年盤點之與篇前端每周清單專注前端領域內容,以對外文資料的搜集為主,幫助開發者了解一周前端熱點分為新聞熱點開發教程工程實踐深度閱讀開源項目巔峰人生等欄目。與求同存異近日,宣布將的構建工具由遷移到,引發了很多開發者的討論。 前端每周清單半年盤點之 React 與 ReactNative 篇 前端每周清單專注前端領域內容,以對外文資料的搜集為主,幫助開發者了解一周前端熱點;分為...
摘要:前端每周清單專注前端領域內容,以對外文資料的搜集為主,幫助開發者了解一周前端熱點分為新聞熱點開發教程工程實踐深度閱讀開源項目巔峰人生等欄目。對該漏洞的綜合評級為高危。目前,相關利用方式已經在互聯網上公開,近期出現攻擊嘗試爆發的可能。 前端每周清單專注前端領域內容,以對外文資料的搜集為主,幫助開發者了解一周前端熱點;分為新聞熱點、開發教程、工程實踐、深度閱讀、開源項目、巔峰人生等欄目。歡...
摘要:以及模塊之前都是返回的函數之后的都是返回在語言中,函數替換的是將多參數函數,替換成單參數的版本,且只接受回調函數作為參數。 Thunk以及CO模塊 co4.0之前都是返回的thunk函數之后的都是返回promise thunk thunk:在 JavaScript 語言中,Thunk 函數替換的是將多參數函數,替換成單參數的版本,且只接受回調函數作為參數。 // 正常版本的readFi...
閱讀 1505·2021-11-22 09:34
閱讀 3320·2021-09-29 09:35
閱讀 562·2021-09-04 16:40
閱讀 2912·2019-08-30 15:53
閱讀 2582·2019-08-30 15:44
閱讀 2584·2019-08-30 14:10
閱讀 1328·2019-08-29 18:43
閱讀 2205·2019-08-29 13:26