摘要:以下展示它是如何工作的函數(shù)使用構造函數(shù)創(chuàng)建一個新的對象,并立即將其返回給調用者。在傳遞給構造函數(shù)的函數(shù)中,我們確保傳遞給,這是一個特殊的回調函數(shù)。
本系列文章為《Node.js Design Patterns Second Edition》的原文翻譯和讀書筆記,在GitHub連載更新,同步翻譯版鏈接。
歡迎關注我的專欄,之后的博文將在專欄同步:
Encounter的掘金專欄
知乎專欄 Encounter的編程思考
segmentfault專欄 前端小站
Asynchronous Control Flow Patterns with ES2015 and Beyond在上一章中,我們學習了如何使用回調處理異步代碼,以及如何解決如回調地獄代碼等異步問題?;卣{是JavaScript和Node.js中的異步編程的基礎,但是現(xiàn)在,其他替代方案已經出現(xiàn)。這些替代方案更復雜,以便能夠以更方便的方式處理異步代碼。
在本章中,我們將探討一些代表性的替代方案,Promise和Generator。以及async await,這是一種創(chuàng)新的語法,可在高版本的JavaScript中提供,其也作為ECMAScript 2017發(fā)行版的一部分。
我們將看到這些替代方案如何簡化處理異步控制流的方式。最后,我們將比較所有這些方法,以了解所有這些方法的所有優(yōu)點和缺點,并能夠明智地選擇最適合我們下一個Node.js項目要求的方法。
Promise我們在前面的章節(jié)中提到,CPS風格不是編寫異步代碼的唯一方法。事實上,JavaScript生態(tài)系統(tǒng)為傳統(tǒng)的回調模式提供了有趣的替代方案。最著名的選擇之一是Promise,特別是現(xiàn)在它是ECMAScript 2015的一部分,并且現(xiàn)在可以在Node.js中可用。
什么是Promise?Promise是一種抽象的對象,我們通常允許函數(shù)返回一個名為Promise的對象,它表示異步操作的最終結果。通常情況下,我們說當異步操作尚未完成時,我們說Promise對象處于pending狀態(tài),當操作成功完成時,我們說Promise對象處于resolve狀態(tài),當操作錯誤終止時,我們說Promise對象處于reject狀態(tài)。一旦Promise處于resolve或reject,我們認為當前異步操作結束。
為了接收到異步操作的正確結果或錯誤捕獲,我們可以使用Promise的then方法:
promise.then([onFulfilled], [onRejected])
在前面的代碼中,onFulfilled()是一個函數(shù),最終會收到Promise的正確結果,而onRejected()是另一個函數(shù),它將接收產生異常的原因(如果有的話)。兩個參數(shù)都是可選的。
要了解Promise如何轉換我們的代碼,讓我們考慮以下幾點:
asyncOperation(arg, (err, result) => { if (err) { // 錯誤處理 } // 正常結果處理 });
Promise允許我們將這個典型的CPS代碼轉換成更好的結構化和更優(yōu)雅的代碼,如下所示:
asyncOperation(arg) .then(result => { // 錯誤處理 }, err => { // 正常結果處理 });
then()方法的一個關鍵特征是它同步地返回另一個Promise對象。如果onFulfilled()或onRejected()函數(shù)中的任何一個函數(shù)返回x,則then()方法返回的Promise對象將如下所示:
如果x是一個值,則這個Promise對象會正確處理(resolve)x
如果x是一個Promise對象或thenable,則會正確處理(resolve)x
如果x是一個異常,則會捕獲異常(reject)x
注:thenable是一個具有then方法的類似于Promise的對象(Promise-like)。
這個特點使我們能夠鏈式構建Promise,允許輕松排列組合我們的異步操作。另外,如果我們沒有指定一個onFulfilled()或onRejected()處理程序,則正確結果或異常捕獲將自動轉發(fā)到Promise鏈的下一個Promise。例如,這允許我們在整個鏈中自動傳播錯誤,直到被onRejected()處理程序捕獲。隨著Promise鏈,任務的順序執(zhí)行突然變成簡單多了:
asyncOperation(arg) .then(result1 => { // 返回另一個Promise return asyncOperation(arg2); }) .then(result2 => { // 返回一個值 return "done"; }) .then(undefined, err => { // 捕獲Promise鏈中的異常 });
下圖展示了鏈式Promise如何工作:
Promise的另一個重要特性是onFulfilled()和onRejected()函數(shù)是異步調用的,如同上述的例子,在最后那個then函數(shù)resolve一個同步的Promise,它也是同步的。這種模式避免了Zalgo(參見Chapter2-Node.js Essential Patterns),使我們的異步代碼更加一致和穩(wěn)健。
如果在onFulfilled()或onRejected()處理程序中拋出異常(使用throw語句),則then()方法返回的Promise將被自動地reject,拋出異常作為reject的原因。這相對于CPS來說是一個巨大的優(yōu)勢,因為它意味著有了Promise,異常將在整個鏈中自動傳播,并且throw語句終于可以使用。
在以前,許多不同的庫實現(xiàn)了Promise,大多數(shù)時候它們之間不兼容,這意味著不可能在使用不同Promise庫的thenable鏈式傳播錯誤。
JavaScript社區(qū)非常努力地解決了這個限制,這些努力導致了Promises / A +規(guī)范的創(chuàng)建。該規(guī)范詳細描述了then方法的行為,提供了一個可互兼容的基礎,這使得來自不同庫的Promise對象能夠彼此兼容,開箱即用。
有關Promises / A +規(guī)范的詳細說明,可以參考Promises / A + 官方網站。
Promise / A + 的實施在JavaScript中以及Node.js中,有幾個實現(xiàn)Promises / A +規(guī)范的庫。以下是最受歡迎的:
Bluebird
Q
RSVP
Vow
When.js
ES2015 promises
真正區(qū)別他們的是在Promises / A +標準之上提供的額外功能。正如我們上述所說的那樣,該標準定義了then()方法和Promise解析過程的行為,但它沒有指定其他功能,例如,如何從基于回調的異步函數(shù)創(chuàng)建Promise。
在我們的示例中,我們將使用由ES2015的Promise,因為Promise對象自Node.js 4后即可使用,而不需要任何庫來實現(xiàn)。
作為參考,以下是ES2015的Promise提供的API:
constructor(new Promise(function(resolve, reject){})):創(chuàng)建了一個新的Promise,它基于作為傳遞兩個類型為函數(shù)的參數(shù)來決定resolve或reject。構造函數(shù)的參數(shù)解釋如下:
resolve(obj) :resolve一個Promise,并帶上一個參數(shù)obj,如果obj是一個值,這個值就是傳遞的異步操作成功的結果。如果obj是一個Promise或一個thenable,則會進行正確處理。
reject(err):reject一個Promise,并帶上一個參數(shù)err。它是Error對象的一個實例。
Promise對象的靜態(tài)方法Promise.resolve(obj): 將會創(chuàng)建一個resolve的Promise實例
Promise.reject(err): 將會創(chuàng)建一個reject的Promise實例
Promise.all(iterable):返回一個新的Promise實例,并且在iterable中所
有Promise狀態(tài)為reject時, 返回的Promise實例的狀態(tài)會被置為reject,如果iterable中至少有一個Promise狀態(tài)為reject時, 返回的Promise實例狀態(tài)也會被置為reject,并且reject的原因是第一個被reject的Promise對象的reject原因。
Promise.race(iterable):返回一個Promise實例,當iterable中任何一個Promise被resolve或被reject時, 返回的Promise實例以同樣的原因resolve或reject。
Promise實例方法Promise.then(onFulfilled, onRejected):這是Promise的基本方法。它的行為與我們之前描述的Promises / A +標準兼容。
Promise.catch(onRejected):這只是Promise.then(undefined,onRejected)的語法糖。
Promisifying一個Node.js回調風格的函數(shù)值得一提的是,一些Promise實現(xiàn)提供了另一種機制來創(chuàng)建新的Promise,稱為deferreds。我們不會在這里描述,因為它不是ES2015標準的一部分,但是如果您想了解更多信息,可以閱讀Q文檔 (https://github.com/kriskowal/... 或When.js文檔 (https://github.com/cujojs/whe... 。
在JavaScript中,并不是所有的異步函數(shù)和庫都支持開箱即用的Promise。大多數(shù)情況下,我們必須將一個典型的基于回調的函數(shù)轉換成一個返回Promise的函數(shù),這個過程也被稱為promisification。
幸運的是,Node.js中使用的回調約定允許我們創(chuàng)建一個可重用的函數(shù),我們通過使用Promise對象的構造函數(shù)來簡化任何Node.js風格的API。讓我們創(chuàng)建一個名為promisify()的新函數(shù),并將其包含到utilities.js模塊中(以便稍后在我們的Web爬蟲應用程序中使用它):
module.exports.promisify = function(callbackBasedApi) { return function promisified() { const args = [].slice.call(arguments); return new Promise((resolve, reject) => { args.push((err, result) => { if (err) { return reject(err); } if (arguments.length <= 2) { resolve(result); } else { resolve([].slice.call(arguments, 1)); } }); callbackBasedApi.apply(null, args); }); } };
前面的函數(shù)返回另一個名為promisified()的函數(shù),它表示輸入中給出的callbackBasedApi的promisified版本。以下展示它是如何工作的:
promisified()函數(shù)使用Promise構造函數(shù)創(chuàng)建一個新的Promise對象,并立即將其返回給調用者。
在傳遞給Promise構造函數(shù)的函數(shù)中,我們確保傳遞給callbackBasedApi,這是一個特殊的回調函數(shù)。由于我們知道回調總是最后調用的,我們只需將回調函數(shù)附加到提供給promisified()函數(shù)的參數(shù)列表里(args)。
在特殊的回調中,如果我們收到錯誤,我們立即reject這個Promise。
如果沒有收到錯誤,我們使用一個值或一個數(shù)組值來resolve這個Promise,具體取決于傳遞給回調的結果數(shù)量。
最后,我們只需使用我們構建的參數(shù)列表調用callbackBasedApi。
順序執(zhí)行大部分的Promise已經提供了一個開箱即用的接口來將一個Node.js風格的API轉換成一個返回Promise的API。例如,Q有Q.denodeify()和Q.nbind(),Bluebird有Promise.promisify(),而When.js有node.lift()。
在一些必要的理論之后,我們現(xiàn)在準備將我們的Web爬蟲應用程序轉換為使用Promise的形式。讓我們直接從版本2開始,直接下載一個Web網頁的鏈接。
在spider.js模塊中,第一步是加載我們的Promise實現(xiàn)(我們稍后會使用它)和Promisifying我們打算使用的基于回調的函數(shù):
const utilities = require("./utilities"); const request = utilities.promisify(require("request")); const mkdirp = utilities.promisify(require("mkdirp")); const fs = require("fs"); const readFile = utilities.promisify(fs.readFile); const writeFile = utilities.promisify(fs.writeFile);
現(xiàn)在,我們開始更改我們的download函數(shù):
function download(url, filename) { console.log(`Downloading ${url}`); let body; return request(url) .then(response => { body = response.body; return mkdirp(path.dirname(filename)); }) .then(() => writeFile(filename, body)) .then(() => { console.log(`Downloaded and saved: ${url}`); return body; }); }
這里要注意的到的最重要的是我們也為readFile()返回的Promise注冊
一個onRejected()函數(shù),用來處理一個網頁沒有被下載的情況(或文件不存在)。 還有,看我們如何使用throw來傳遞onRejected()函數(shù)中的錯誤的。
既然我們已經更改我們的spider()函數(shù),我們這么修改它的調用方式:
spider(process.argv[2], 1) .then(() => console.log("Download complete")) .catch(err => console.log(err));
注意我們是如何第一次使用Promise的語法糖catch來處理源自spider()函數(shù)的任何錯誤情況。如果我們再看看迄今為止我們所寫的所有代碼,那么我們會驚喜的發(fā)現(xiàn),我們沒有包含任何錯誤傳播邏輯,因為我們在使用回調函數(shù)時會被迫做這樣的事情。這顯然是一個巨大的優(yōu)勢,因為它極大地減少了我們代碼中的樣板文件以及丟失任何異步錯誤的機會。
現(xiàn)在,完成我們唯一缺失的Web爬蟲應用程序的第二版的spiderLinks()函數(shù),我們將在稍后實現(xiàn)它。
順序迭代到目前為止,Web爬蟲應用程序代碼庫主要是對Promise是什么以及如何使用的概述,展示了使用Promise實現(xiàn)順序執(zhí)行流程的簡單性和優(yōu)雅性。但是,我們現(xiàn)在考慮的代碼只涉及到一組已知的異步操作的執(zhí)行。所以,完成我們對順序執(zhí)行流程的探索的缺失部分是看我們如何使用Promise來實現(xiàn)迭代。同樣,網絡蜘蛛第二版的spiderLinks()函數(shù)也是一個很好的例子。
讓我們添加缺少的這一塊:
function spiderLinks(currentUrl, body, nesting) { let promise = Promise.resolve(); if (nesting === 0) { return promise; } const links = utilities.getPageLinks(currentUrl, body); links.forEach(link => { promise = promise.then(() => spider(link, nesting - 1)); }); return promise; }
為了異步迭代一個網頁的全部鏈接,我們必須動態(tài)創(chuàng)建一個Promise的迭代鏈。
首先,我們定義一個空的Promise,resolve為undefined。這個Promise只是用來作為Promise的迭代鏈的起始點。
然后,我們通過在循環(huán)中調用鏈中前一個Promise的then()方法獲得的新的Promise來更新Promise變量。這就是我們使用Promise的異步迭代模式。
這樣,循環(huán)的結束,promise變量會包含循環(huán)中最后一個then()返回的Promise對象,所以它只有當Promise的迭代鏈中全部Promise對象被resolve后才能被resolve。
注:在最后調用了這個then方法來resolve這個Promise對象
通過這個,我們已使用Promise對象重寫了我們的Web爬蟲應用程序。我們現(xiàn)在應該可以運行它了。
順序迭代模式為了總結這個順序執(zhí)行的部分,讓我們提取一個模式來依次遍歷一組Promise:
let tasks = [ /* ... */ ] let promise = Promise.resolve(); tasks.forEach(task => { promise = promise.then(() => { return task(); }); }); promise.then(() => { // 所有任務都完成 });
使用reduce()方法來替代forEach()方法,允許我們寫出更為簡潔的代碼:
let tasks = [ /* ... */ ] let promise = tasks.reduce((prev, task) => { return prev.then(() => { return task(); }); }, Promise.resolve()); promise.then(() => { //All tasks completed });
與往常一樣,通過對這種模式的簡單調整,我們可以將所有任務的結果收集到一個數(shù)組中,我們可以實現(xiàn)一個mapping算法,或者構建一個filter等等。
并行執(zhí)行上述這個模式使用循環(huán)動態(tài)地建立一個鏈式的Promise。
另一個適合用Promise的執(zhí)行流程是并行執(zhí)行流程。實際上,我們需要做的就是使用內置的Promise.all()。這個方法創(chuàng)造了另一個Promise對象,只有在輸入中的所有Promise都resolve時才能resolve。這是一個并行執(zhí)行,因為在其參數(shù)Promise對象的之間沒有執(zhí)行順序可言。
為了演示這一點,我們來看我們的Web爬蟲應用程序的第三版,它將頁面中的所有鏈接并行下載。讓我們再次使用Promise更新spiderLinks()函數(shù)來實現(xiàn)并行流程:
function spiderLinks(currentUrl, body, nesting) { if (nesting === 0) { return Promise.resolve(); } const links = utilities.getPageLinks(currentUrl, body); const promises = links.map(link => spider(link, nesting - 1)); return Promise.all(promises); }
這里的模式在elements.map()迭代中產生一個數(shù)組,存放所有異步任務,之后便于同時啟動spider()任務。這一次,在循環(huán)中,我們不等待以前的下載完成,然后開始一個新的下載任務:所有的下載任務在一個循環(huán)中一個接一個地開始。之后,我們利用Promise.all()方法,它返回一個新的Promise對象,當數(shù)組中的所有Promise對象都被resolve時,這個Promise對象將被resolve。換句話說,所有的下載任務完成,這正是我們想要的。
限制并行執(zhí)行不幸的是,ES2015的Promise API并沒有提供一種原生的方式來限制并發(fā)任務的數(shù)量,但是我們總是可以依靠我們所學到的有關用普通JavaScript來限制并發(fā)。事實上,我們在TaskQueue類中實現(xiàn)的模式可以很容易地被調整來支持返回承諾的任務。這很容易通過修改next()方法來完成:
class TaskQueue { constructor(concurrency) { this.concurrency = concurrency; this.running = 0; this.queue = []; } pushTask(task) { this.queue.push(task); this.next(); } next() { while (this.running < this.concurrency && this.queue.length) { const task = this.queue.shift(); task().then(() => { this.running--; this.next(); }); this.running++; } } }
不同于使用一個回調函數(shù)來處理任務,我們簡單地調用Promise的then()。
讓我們回到spider.js模塊,并修改它以支持我們的新版本的TaskQueue類。首先,我們確保定義一個TaskQueue的新實例:
const TaskQueue = require("./taskQueue"); const downloadQueue = new TaskQueue(2);
然后,是我們的spiderLinks()函數(shù)。這里的修改也是很簡單:
function spiderLinks(currentUrl, body, nesting) { if (nesting === 0) { return Promise.resolve(); } const links = utilities.getPageLinks(currentUrl, body); // 我們需要如下代碼,用于創(chuàng)建Promise對象 // 如果沒有下列代碼,當任務數(shù)量為0時,將永遠不會resolve if (links.length === 0) { return Promise.resolve(); } return new Promise((resolve, reject) => { let completed = 0; let errored = false; links.forEach(link => { let task = () => { return spider(link, nesting - 1) .then(() => { if (++completed === links.length) { resolve(); } }) .catch(() => { if (!errored) { errored = true; reject(); } }); }; downloadQueue.pushTask(task); }); }); }
在上述代碼中有幾點值得我們注意的:
首先,我們需要返回使用Promise構造函數(shù)創(chuàng)建的新的Promise對象。正如我們將看到的,這使我們能夠在隊列中的所有任務完成時手動resolve我們的Promise對象。
然后,我們應該看看我們如何定義任務。我們所做的是將一個onFulfilled()回調函數(shù)的調用添加到由spider()返回的Promise對象中,所以我們可以計算完成的下載任務的數(shù)量。當完成的下載量與當前頁面中鏈接的數(shù)量相同時,我們知道任務已經處理完畢,所以我們可以調用外部Promise的resolve()函數(shù)。
Promises / A +規(guī)范規(guī)定,then()方法的onFulfilled()和onRejected()回調函數(shù)只能調用一次(僅調用onFulfilled()和onRejected())。Promise接口的實現(xiàn)確保即使我們多次手動調用resolve或reject,Promise也僅可以被resolve或reject一次。
現(xiàn)在,使用Promise的Web爬蟲應用程序的第4版應該已經準備好了。我們可能再次注意到下載任務如何并行運行,并發(fā)數(shù)量限制為2。
在公有API中暴露回調函數(shù)和Promise正如我們在前面所學到的,Promise可以被用作回調函數(shù)的一個很好的替代品。它們使我們的代碼更具可讀性和易于理解。雖然Promise帶來了許多優(yōu)點,但也要求開發(fā)人員理解許多不易于理解的概念,以便正確和熟練地使用。由于這個原因和其他原因,在某些情況下,比起Promise來說,很多開發(fā)者更偏向于回調函數(shù)。
現(xiàn)在讓我們想象一下,我們想要構建一個執(zhí)行異步操作的公共庫。我們需要做什么?我們是創(chuàng)建了一個基于回調函數(shù)的API還是一個面向Promise的API?還是兩者均有?
這是許多知名的庫所面臨的問題,至少有兩種方法值得一提,使我們能夠提供一個多功能的API。
像request,redis和mysql這樣的庫所使用的第一種方法是提供一個簡單的基于回調函數(shù)的API,如果需要,開發(fā)人員可以選擇公開函數(shù)。其中一些庫提供工具函數(shù)來Promise化異步回調,但開發(fā)人員仍然需要以某種方式將暴露的API轉換為能夠使用Promise對象。
第二種方法更透明。它還提供了一個面向回調的API,但它使回調參數(shù)可選。每當回調作為參數(shù)傳遞時,函數(shù)將正常運行,在完成時或失敗時執(zhí)行回調。當回調未被傳遞時,函數(shù)將立即返回一個Promise對象。這種方法有效地結合了回調函數(shù)和Promise,使得開發(fā)者可以在調用時選擇采用什么接口,而不需要提前進行Promise化。許多庫,如mongoose和sequelize,都支持這種方法。
我們來看一個簡單的例子。假設我們要實現(xiàn)一個異步執(zhí)行除法的模塊:
module.exports = function asyncDivision(dividend, divisor, cb) { return new Promise((resolve, reject) => { // [1] process.nextTick(() => { const result = dividend / divisor; if (isNaN(result) || !Number.isFinite(result)) { const error = new Error("Invalid operands"); if (cb) { cb(error); // [2] } return reject(error); } if (cb) { cb(null, result); // [3] } resolve(result); }); }); };
該模塊的代碼非常簡單,但是有一些值得強調的細節(jié):
首先,返回使用Promise的構造函數(shù)創(chuàng)建的新承諾。我們在構造函數(shù)參數(shù)函數(shù)內定義全部邏輯。
在發(fā)生錯誤的情況下,我們reject這個Promise,但如果回調函數(shù)在被調用時作為參數(shù)傳遞,我們也執(zhí)行回調來進行錯誤傳播。
在計算結果之后,我們resolve了這個Promise,但是如果有回調函數(shù),我們也會將結果傳播給回調函數(shù)。
我們現(xiàn)在看如何用回調函數(shù)和Promise來使用這個模塊:
// 回調函數(shù)的方式 asyncDivision(10, 2, (error, result) => { if (error) { return console.error(error); } console.log(result); }); // Promise化的調用方式 asyncDivision(22, 11) .then(result => console.log(result)) .catch(error => console.error(error));
應該很清楚的是,即將開始使用類似于上述的新模塊的開發(fā)人員將很容易地選擇最適合自己需求的風格,而無需在希望利用Promise時引入外部promisification功能。
GeneratorsES2015規(guī)范引入了另外一種機制,除了其他新功能外,還可以用來簡化Node.js應用程序的異步控制流程。我們正在談論Generator,也被稱為semi-coroutines。它們是子程序的一般化,可以有不同的入口點。在一個正常的函數(shù)中,實際上我們只能有一個入口點,這個入口點對應著函數(shù)本身的調用。Generator與一般函數(shù)類似,但是可以暫停(使用yield語句),然后在稍后繼續(xù)執(zhí)行。在實現(xiàn)迭代器時,Generator特別有用,因為我們已經討論了如何使用迭代器來實現(xiàn)重要的異步控制流模式,如順序執(zhí)行和限制并行執(zhí)行。
Generators基礎在我們探索使用Generator來實現(xiàn)異步控制流程之前,學習一些基本概念是很重要的。我們從語法開始吧??梢酝ㄟ^在函數(shù)關鍵字之后附加*(星號)運算符來聲明Generator函數(shù):
function* makeGenerator() { // body }
在makeGenerator()函數(shù)內部,我們可以使用關鍵字yield暫停執(zhí)行并返回給調用者傳遞給它的值:
function* makeGenerator() { yield "Hello World"; console.log("Re-entered"); }
在前面的代碼中,Generator通過yield一個字符串Hello World暫停當前函數(shù)的執(zhí)行。當Generator恢復時,執(zhí)行將從下列語句開始:
console.log("Re-entered");
makeGenerator()函數(shù)本質上是一個工廠,它在被調用時返回一個新的Generator對象:
const gen = makeGenerator();
生成器對象的最重要的方法是next(),它用于啟動/恢復Generator的執(zhí)行,并返回如下形式的對象:
{ value:done: }
這個對象包含Generator yield的值和一個指示Generator是否已經完成執(zhí)行的符號。
一個簡單的例子為了演示Generator,我們來創(chuàng)建一個名為fruitGenerator.js的新模塊:
function* fruitGenerator() { yield "apple"; yield "orange"; return "watermelon"; } const newFruitGenerator = fruitGenerator(); console.log(newFruitGenerator.next()); // [1] console.log(newFruitGenerator.next()); // [2] console.log(newFruitGenerator.next()); // [3]
前面的代碼將打印下面的輸出:
{ value: "apple", done: false } { value: "orange", done: false } { value: "watermelon", done: true }
我們可以這么解釋上述現(xiàn)象:
第一次調用newFruitGenerator.next()時,Generator函數(shù)開始執(zhí)行,直到達到第一個yield語句為止,該命令暫停Generator函數(shù)執(zhí)行,并將值apple返回給調用者。
在第二次調用newFruitGenerator.next()時,Generator函數(shù)恢復執(zhí)行,從第二個yield語句開始,這又使得執(zhí)行暫停,同時將orange返回給調用者。
newFruitGenerator.next()的最后一次調用導致Generator函數(shù)的執(zhí)行從其最后的yield恢復,一個返回語句,它終止Generator函數(shù),返回watermelon,并將結果對象中的done屬性設置為true。
Generators作為迭代器為了更好地理解為什么Generator函數(shù)對實現(xiàn)迭代器非常有用,我們來構建一個例子。在我們將調用iteratorGenerator.js的新模塊中,我們編寫下面的代碼:
function* iteratorGenerator(arr) { for (let i = 0; i < arr.length; i++) { yield arr[i]; } } const iterator = iteratorGenerator(["apple", "orange", "watermelon"]); let currentItem = iterator.next(); while (!currentItem.done) { console.log(currentItem.value); currentItem = iterator.next(); }
此代碼應按如下所示打印數(shù)組中的元素:
apple orange watermelon
在這個例子中,每次我們調用iterator.next()時,我們都會恢復Generator函數(shù)的for循環(huán),通過yield數(shù)組中的下一個項來運行另一個循環(huán)。這演示了如何在函數(shù)調用過程中維護Generator的狀態(tài)。當繼續(xù)執(zhí)行時,循環(huán)和所有變量的值與Generator函數(shù)執(zhí)行暫停時的狀態(tài)完全相同。
傳值給Generators現(xiàn)在我們繼續(xù)研究Generator的基本功能,首先學習如何將值傳遞回Generator函數(shù)。這其實很簡單,我們需要做的只是為next()方法提供一個參數(shù),并且該值將作為Generator函數(shù)內的yield語句的返回值提供。
為了展示這一點,我們來創(chuàng)建一個新的簡單模塊:
function* twoWayGenerator() { const what = yield null; console.log("Hello " + what); } const twoWay = twoWayGenerator(); twoWay.next(); twoWay.next("world");
當執(zhí)行時,前面的代碼會輸出Hello world。我們做如下的解釋:
第一次調用next()方法時,Generator函數(shù)到達第一個yield語句,然后暫停。
當next("world")被調用時,Generator函數(shù)從上次停止的位置,也就是上次的yield語句點恢復,但是這次我們有一個值傳遞到Generator函數(shù)。這個值將被賦值到what變量。生成器然后執(zhí)行console.log()指令并終止。
用類似的方式,我們可以強制Generator函數(shù)拋出異常。這可以通過使用Generator函數(shù)的throw方法來實現(xiàn),如下例所示:
const twoWay = twoWayGenerator(); twoWay.next(); twoWay.throw(new Error());
在這個最后這段代碼,twoWayGenerator()函數(shù)將在yield函數(shù)返回的時候拋出異常。這就好像從Generator函數(shù)內部拋出了一個異常一樣,這意味著它可以像使用try ... catch塊一樣進行捕獲和處理異常。
Generator實現(xiàn)異步控制流你一定想知道Generator函數(shù)如何幫助我們處理異步操作。我們可以通過創(chuàng)建一個接受Generator函數(shù)作為參數(shù)的特殊函數(shù)來演示這一點,并允許我們在Generator函數(shù)內部使用異步代碼。這個函數(shù)在異步操作完成時要注意恢復Generator函數(shù)的執(zhí)行。我們將調用這個函數(shù)asyncFlow():
function asyncFlow(generatorFunction) { function callback(err) { if (err) { return generator.throw(err); } const results = [].slice.call(arguments, 1); generator.next(results.length > 1 ? results : results[0]); } const generator = generatorFunction(callback); generator.next(); }
前面的函數(shù)取一個Generator函數(shù)作為輸入,然后立即調用:
const generator = generatorFunction(callback); generator.next();
generatorFunction()接受一個特殊的回調函數(shù)作為參數(shù),當generator.throw()如果接收到一個錯誤,便立即返回。另外,通過將在回調函數(shù)中接收的results傳值回Generator函數(shù)繼續(xù)Generator函數(shù)的執(zhí)行:
if (err) { return generator.throw(err); } const results = [].slice.call(arguments, 1); generator.next(results.length > 1 ? results : results[0]);
為了說明這個簡單的輔助函數(shù)的強大,我們創(chuàng)建一個叫做clone.js的新模塊,這個模塊只是創(chuàng)建它本身的克隆。粘貼我們剛才創(chuàng)建的asyncFlow()函數(shù),核心代碼如下:
const fs = require("fs"); const path = require("path"); asyncFlow(function*(callback) { const fileName = path.basename(__filename); const myself = yield fs.readFile(fileName, "utf8", callback); yield fs.writeFile(`clone_of_${filename}`, myself, callback); console.log("Clone created"); });
明顯地,有了asyncFlow()函數(shù)的幫助,我們可以像我們書寫同步阻塞函數(shù)一樣用同步的方式來書寫異步代碼了。并且這個結果背后的原理顯得很清楚。一旦異步操作結束,傳遞給每個異步函數(shù)的回調函數(shù)將繼續(xù)Generator函數(shù)的執(zhí)行。沒有什么復雜的,但是結果確實很令人意外。
這個技術有其他兩個變化,一個是Promise的使用,另外一個則是thunks。
在基于Generator的控制流中使用的thunk只是一個簡單的函數(shù),它除了回調之外,部分地應用了原始函數(shù)的所有參數(shù)。返回值是另一個只接受回調作為參數(shù)的函數(shù)。例如,fs.readFile()的thunkified版本如下所示:
function readFileThunk(filename, options) { return function(callback) { fs.readFile(filename, options, callback); } }
thunk和Promise都允許我們創(chuàng)建不需要回調的Generator函數(shù)作為參數(shù)傳遞,例如,使用thunk的asyncFlow()版本如下:
function asyncFlowWithThunks(generatorFunction) { function callback(err) { if (err) { return generator.throw(err); } const results = [].slice.call(arguments, 1); const thunk = generator.next(results.length > 1 ? results : results[0]).value; thunk && thunk(callback); } const generator = generatorFunction(); const thunk = generator.next().value; thunk && thunk(callback); }
這個技巧是讀取generator.next()的返回值,返回值中包含thunk。下一步是通過注入特殊的回調函數(shù)調用thunk本身。這允許我們寫下面的代碼:
asyncFlowWithThunk(function*() { const fileName = path.basename(__filename); const myself = yield readFileThunk(__filename, "utf8"); yield writeFileThunk(`clone_of_${fileName}`, myself); console.log("Clone created") });使用co的基于Gernator的控制流
你應該已經猜到了,Node.js生態(tài)系統(tǒng)會借助Generator函數(shù)來提供一些處理異步控制流的解決方案,例如,suspend是其中一個最老的支持Promise、thunks和Node.js風格回調函數(shù)和正常風格的回調函數(shù)的 庫。還有,大部分我們之前分析的Promise庫都提供工具函數(shù)使得Generator和Promise可以一起使用。
我們選擇co作為本章節(jié)的例子。它支持很多類型的yieldables,其中一些是:
Thunks
Promises
Arrays(并行執(zhí)行)
Objects(并行執(zhí)行)
Generators(委托)
Generator函數(shù)(委托)
還有很多框架或庫是基于co生態(tài)系統(tǒng)的,包括以下一些:
Web框架,最流行的是koa
實現(xiàn)特定控制流模式的庫
包裝流行的API兼容co的庫
我們使用co重新實現(xiàn)我們的Generator版本的Web爬蟲應用程序。
為了將Node.js風格的函數(shù)轉換成thunks,我們將會使用一個叫做thunkify的庫。
順序執(zhí)行讓我們通過修改Web爬蟲應用程序的版本2開始我們對Generator函數(shù)和co的實際探索。我們要做的第一件事就是加載我們的依賴包,并生成我們要使用的函數(shù)的thunkified版本。這些將在spider.js模塊的最開始進行:
const thunkify = require("thunkify"); const co = require("co"); const request = thunkify(require("request")); const fs = require("fs"); const mkdirp = thunkify(require("mkdirp")); const readFile = thunkify(fs.readFile); const writeFile = thunkify(fs.writeFile); const nextTick = thunkify(process.nextTick);
看上述代碼,我們可以注意到與本章前面promisify化的API的代碼的一些相似之處。在這一點上,有意思的是,如果我們使用我們的promisified版本的函數(shù)來代替thunkified的版本,代碼將保持完全一樣,這要歸功于co支持thunk和Promise對象作為yieldable對象。事實上,如果我們想,甚至可以在同一個應用程序中使用thunk和Promise,即使在同一個Generator函數(shù)中。就靈活性而言,這是一個巨大的優(yōu)勢,因為它使我們能夠使用基于Generator函數(shù)的控制流來解決我們應用程序中的問題。
好的,現(xiàn)在讓我們開始將download()函數(shù)轉換為一個Generator函數(shù):
function* download(url, filename) { console.log(`Downloading ${url}`); const response = yield request(url); const body = response[1]; yield mkdirp(path.dirname(filename)); yield writeFile(filename, body); console.log(`Downloaded and saved ${url}`); return body; }
通過使用Generator和co,我們的download()函數(shù)變得簡單多了。當我們需要做異步操作的時候,我們使用異步的Generator函數(shù)作為thunk來把之前的內容轉化到Generator函數(shù),并使用yield子句。
然后我們開始實現(xiàn)我們的spider()函數(shù):
function* spider(url, nesting) { cost filename = utilities.urlToFilename(url); let body; try { body = yield readFile(filename, "utf8"); } catch (err) { if (err.code !== "ENOENT") { throw err; } body = yield download(url, filename); } yield spiderLinks(url, body, nesting); }
從上述代碼中一個有趣的細節(jié)是我們可以使用try...catch語句塊來處理異常。我們還可以使用throw來傳播異常。另外一個細節(jié)是我們yield我們的download()函數(shù),而這個函數(shù)既不是一個thunk,也不是一個promisified函數(shù),只是另外的一個Generator函數(shù)。這也毫無問題,由于co也支持其他Generators作為yieldables。
最后轉換spiderLinks(),在這個函數(shù)中,我們遞歸下載一個網頁的鏈接。在這個函數(shù)中使用Generators,顯得簡單多了:
function* spiderLinks(currentUrl, body, nesting) { if (nesting === 0) { return nextTick(); } const links = utilities.getPageLinks(currentUrl, body); for (let i = 0; i < links.length; i++) { yield spider(links[i], nesting - 1); } }
看上述代碼。雖然順序迭代沒有什么模式可以展示。Generator和co輔助我們做了很多,方便了我們可以使用同步方式開書寫異步代碼。
看最重要的部分,程序的入口:
co(function*() { try { yield spider(process.argv[2], 1); console.log(`Download complete`); } catch (err) { console.log(err); } });
這是唯一一處需要調用co(...)來封裝的一個Generator。實際上,一旦我們這么做,co會自動封裝我們傳遞給yield語句的任何Generator函數(shù),并且這個過程是遞歸的,所以程序的剩余部分與我們是否使用co是完全無關的,雖然是被co封裝在里面。
現(xiàn)在應該可以運行使用Generator函數(shù)改寫的Web爬蟲應用程序了。
并行執(zhí)行不幸的是,雖然Generator很方便地進行順序執(zhí)行,但是不能直接用來并行化執(zhí)行一組任務,至少不能僅僅使用yield和Generator。之前,在種情況下我們使用的模式只是簡單地依賴于一個基于回調或者Promise的函數(shù),但使用了Generator函數(shù)后,一切會顯得更簡單。
幸運的是,如果不限制并發(fā)數(shù)的并行執(zhí)行,co已經可以通過yield一個Promise對象、thunk、Generator函數(shù),甚至包含Generator函數(shù)的數(shù)組來實現(xiàn)。
考慮到這一點,我們的Web爬蟲應用程序第三版可以通過重寫spiderLinks()函數(shù)來做如下改動:
function* spiderLinks(currentUrl, body, nesting) { if (nesting === 0) { return nextTick(); } const links = utilities.getPageLinks(currentUrl, body); const tasks = links.map(link => spider(link, nesting - 1)); yield tasks; }
但是上述函數(shù)所做的只是拿到所有的任務,這些任務本質上都是通過Generator函數(shù)來實現(xiàn)異步的,如果在co的thunk內對一個包含Generator函數(shù)的數(shù)組使用yield,這些任務都會并行執(zhí)行。外層的Generator函數(shù)會等到yield子句的所有異步任務并行執(zhí)行后再繼續(xù)執(zhí)行。
接下來我們看怎么用一個基于回調函數(shù)的方式來解決相同的并行流。我們用這種方式重寫spiderLinks()函數(shù):
function spiderLinks(currentUrl, body, nesting) { if (nesting === 0) { return nextTick(); } // 返回一個thunk return callback => { let completed = 0, hasErrors = false; const links = utilities.getPageLinks(currentUrl, body); if (links.length === 0) { return process.nextTick(callback); } function done(err, result) { if (err && !hasErrors) { hasErrors = true; return callback(err); } if (++completed === links.length && !hasErrors) { callback(); } } for (let i = 0; i < links.length; i++) { co(spider(links[i], nesting - 1)).then(done); } } }
我們使用co并行運行spider()函數(shù),調用Generator函數(shù)返回了一個Promise對象。這樣,等待Promise完成后調用done()函數(shù)。通常,基于Generator控制流的庫都有這一功能,因此如果需要,你總是可以將一個Generator轉換成一個基于回調或基于Promise的函數(shù)。
為了并行開啟多個下載任務,我們只要重用在前面定義的基于回調的并行執(zhí)行的模式。我們應該也注意到我們將spiderLinks()轉換成一個thunk(而不再是一個Generator函數(shù))。這使得當全部并行任務完成時,我們有一個回調函數(shù)可以調用。
限制并行執(zhí)行上面講到的是將一個Generator函數(shù)轉換為一個thunk的模式,使之能夠支持其他的基于回調或基于Promise的控制流算法,并可以通過同步阻塞的代碼風格書寫異步代碼。
現(xiàn)在我們知道如何處理異步執(zhí)行流程,應該很容易規(guī)劃我們的Web爬蟲應用程序的第四版的實現(xiàn),這個版本對并發(fā)下載任務的數(shù)量施加了限制。我們有幾個方案可以用來做到這一點。其中一些方案如下:
使用先前實現(xiàn)的基于回調的TaskQueue類。我們只需要thunkify我們的Generator函數(shù)和其提供的回調函數(shù)即可。
使用基于Promise的TaskQueue類,并確保每個作為任務的Generator函數(shù)都被轉換成一個返回Promise對象的函數(shù)。
使用async,thunkify我們打算使用的工具函數(shù),此外還需要把我們用到的Generator函數(shù)轉化為基于回調的模式,以便于能夠被這個庫較好地使用。
使用基于co的生態(tài)系統(tǒng)中的庫,特別是專門為這種場景的庫,如co-limiter。
實現(xiàn)基于生產者 - 消費者模型的自定義算法,這與co-limiter的內部實現(xiàn)原理相同。
為了學習,我們選擇最后一個方案,甚至幫助我們可以更好地理解一種經常與協(xié)程(也和線程和進程)同步相關的模式。
生產者 - 消費者模式我們的目標是利用隊列來提供固定數(shù)量的workers,與我們想要設置的并發(fā)級別一樣多。為了實現(xiàn)這個算法,我們將基于本章前面定義的TaskQueue類改寫:
class TaskQueue { constructor(concurrency) { this.concurrency = concurrency; this.running = 0; this.taskQueue = []; this.consumerQueue = []; this.spawnWorkers(concurrency); } pushTask(task) { if (this.consumerQueue.length !== 0) { this.consumerQueue.shift()(null, task); } else { this.taskQueue.push(task); } } spawnWorkers(concurrency) { const self = this; for (let i = 0; i < concurrency; i++) { co(function*() { while (true) { const task = yield self.nextTask(); yield task; } }); } } nextTask() { return callback => { if (this.taskQueue.length !== 0) { return callback(null, this.taskQueue.shift()); } this.consumerQueue.push(callback); } } }
讓我們分析這個TaskQueue類的新實現(xiàn)。首先是在構造函數(shù)中。需要調用一次this.spawnWorkers(),因為這是啟動worker的方法。
我們的worker很簡單,它們只是用co()包裝的立即執(zhí)行的Generator函數(shù),所以每個Generator函數(shù)可以并行執(zhí)行。在內部,每個worker正在運行在一個死循環(huán)(while(true){})中,一直阻塞(yield)到新任務在隊列中可用時(yield self.nextTask()),一旦可以執(zhí)行新任務,yield這個異步任務直到其完成。您可能想知道我們如何能夠限制并行執(zhí)行,并讓下一個任務在隊列中處于等待狀態(tài)。答案是在nextTask()方法中。我們來詳細地看看在這個方法的原理:
nextTask() { return callback => { if (this.taskQueue.length !== 0) { return callback(null, this.taskQueue.shift()); } this.consumerQueue.push(callback); } }
我們看這個函數(shù)內部發(fā)生了什么,這才是這個模式的核心:
這個方法返回一個對于co而言是一個合法的yieldable的thunk。
只要taskQueue類生成的實例中還有下一個任務,thunk的回調函數(shù)會被立即調用?;卣{函數(shù)調用時,立馬解鎖一個worker的阻塞狀態(tài),yield這一個任務。
如果隊列中沒有任務了,回調函數(shù)本身會被放入consumerQueue中。通過這種做法,我們將一個worker置于空閑(idle)的模式。一旦我們有一個新的任務來要處理,在consumerQueue隊列中的回調函數(shù)會被調用,立馬喚醒我們這一worker進行異步處理。
現(xiàn)在,為了理解consumerQueue隊列中的空閑worker是如何恢復工作的,我們需要分析pushTask()方法。如果當前有回調函數(shù)可用的話,pushTask()方法將調用consumerQueue隊列中的第一個回調函數(shù),從而將取消對worker的鎖定。如果沒有可用的回調函數(shù),這意味著所有的worker都是工作狀態(tài),只需要添加一個新的任務到taskQueue任務隊列中。
在TaskQueue類中,worker充當消費者的角色,而調用pushTask()函數(shù)的角色可以被認為是生產者。這個模式向我們展示了一個Generator函數(shù)實際上可以跟一個線程或進程類似。實際上,生產者 - 消費者之間問題是研究進程間通信和同步時最常見的問題,但正如我們已經提到的那樣,它對于進程和線程來說,也是一個常見的例子。
限制下載任務的并發(fā)量既然我們已經使用Generator函數(shù)和生產者 - 消費者模型實現(xiàn)一個限制并行算法,并且已經在Web爬蟲應用程序第四版應用它來限制中下載任務的并發(fā)數(shù)。 首先,我們加載和初始化一個TaskQueue對象:
const TaskQueue = require("./taskQueue"); const downloadQueue = new TaskQueue(2);
然后,修改spiderLinks()函數(shù)。和之前不限制并發(fā)的版本類似,所以這里我們只展示修改的部分,主要是通過調用新版本的TaskQueue類生成的實例的pushTask()方法來限制并行執(zhí)行:
function spiderLinks(currentUrl, body, nesting) { //... return (callback) => { //... function done(err, result) { //... } links.forEach(function(link) { downloadQueue.pushTask(function*() { yield spider(link, nesting - 1); done(); }); }); } }
在每個任務中,我們在下載完成后立即調用done()函數(shù),因此我們可以計算下載了多少個鏈接,然后在完成下載時通知thunk的回調函數(shù)執(zhí)行。
配合Babel使用Async await新語法回調函數(shù)、Promise和Generator函數(shù)都是用于處理JavaScript和Node.js異步問題的方式。正如我們所看到的,Generator的真正意義在于它提供了一種方式來暫停一個函數(shù)的執(zhí)行,然后等待前面的任務完成后再繼續(xù)執(zhí)行。我們可以使用這樣的特性來書寫異步代碼,并且讓開發(fā)者用同步阻塞的代碼風格來書寫異步代碼。等到異步操作的結果返回后才恢復當前函數(shù)的執(zhí)行。
但Generator函數(shù)是更多的是用來處理迭代器,然而迭代器在異步代碼的使用顯得有點笨重。代碼可能難以理解,導致代碼易讀性和可維護性差。
但在不遠的將來會有一種更加簡潔的語法。實際上,這個提議即將引入到ESMASCript 2017的規(guī)范中,這項規(guī)范定義了async函數(shù)語法。
async函數(shù)規(guī)范引入兩個關鍵字(async和await)到原生的JavaScript語言中,改進我們書寫異步代碼的方式。
為了理解這項語法的用法和優(yōu)勢為,我們看一個簡單的例子:
const request = require("request"); function getPageHtml(url) { return new Promise(function(resolve, reject) { request(url, function(error, response, body) { resolve(body); }); }); } async function main() { const html = await getPageHtml("http://google.com"); console.log(html); } main(); console.log("Loading...");
在上述代碼中,有兩個函數(shù):getPageHtml和main。第一個函數(shù)的作用是提取給定URL的一個遠程網頁的HTML文檔代碼。值得注意的是,這個函數(shù)返回一個Promise對象。
重點在于main函數(shù),因為在這里使用了async和await關鍵字。首先要注意的是函數(shù)要以async關鍵字為前綴。意思是這個函數(shù)執(zhí)行的是異步代碼并且允許它在函數(shù)體內使用await關鍵字。await關鍵字在getPageHtml調用之前,告訴JavaScript解釋器在繼續(xù)執(zhí)行下一條指令之前,等待getPageHtml返回的Promise對象的結果。這樣,main函數(shù)內部哪部分代碼是異步的,它會等待異步代碼的完成再繼續(xù)執(zhí)行后續(xù)操作,并且不會阻塞這段程序其余部分的正常執(zhí)行。實際上,控制臺會打印字符串Loading...,隨后是Google主頁的HTML代碼。
是不是這種方法的可讀性更好并且更容易理解呢? 不幸地是,這個提議尚未定案,即使通過這個提議,我們需要等下一個版本
的ECMAScript規(guī)范出來并把它集成到Node.js后,才能使用這個新語法。 所以我們今天做了什么?只是漫無目的地等待?不是,當然不是!我們已經可以在我們的代碼中使用async await語法,只要我們使用Babel。
Babel是一個JavaScript編譯器(或翻譯器),能夠使用語法轉換器將高版本的JavaScript代碼轉換成其他JavaScript代碼。語法轉換器允許例如我們書寫并使用ES2015,ES2016,JSX和其它的新語法,來翻譯成往后兼容的代碼,在JavaScript運行環(huán)境如瀏覽器或Node.js中都可以使用Babel。
在項目中使用npm安裝Babel,命令如下:
npm install --save-dev babel-cli
我們還需要安裝插件以支持async await語法的解釋或翻譯:
npm install --save-dev babel-plugin-syntax-async-functions babel-plugin-tranform-async-to-generator
現(xiàn)在假設我們想運行我們之前的例子(稱為index.js)。我們需要通過以下命令啟動:
node_modules/.bin/babel-node --plugins "syntax-async-functions,transform-async-to-generator" index.js
這樣,我們使用支持async await的轉換器動態(tài)地轉換源代碼。Node.js運行的實際是保存在內存中的往后兼容的代碼。
Babel也能被配置為一個代碼構建工具,保存翻譯或解釋后的代碼到本地文件系統(tǒng)中,便于我們部署和運行生成的代碼。
幾種方式的比較關于如何安裝和配置Babel,可以到官方網站 https://babeljs.io 查閱相關文檔。
現(xiàn)在,我們應該對于怎么處理JavaScript的異步問題有了一個更好的認識和總結。在下面的表格中總結幾大機制的優(yōu)勢和劣勢:
總結值得一提的是,我們選擇在本章中僅介紹處理異步控制流程的最受歡迎的解決方案,或者是廣泛使用的解決方案,但是例如Fibers( https://npmjs.org/package/fibers )和Streamline( https://npmjs.org/p ackage/streamline )也是值得一看的。
在本章中,我們分析了一些處理異步控制流的方法,分析了Promise、Generator函數(shù)和即將到來的async await語法。
我們學習了如何使用這些方法編寫更簡潔,更具有可讀性的異步代碼。我們討論了這些方法的一些最重要的優(yōu)點和缺點,并認識到即使它們非常有用,也需要一些時間來掌握。這就是這幾種方式也沒有完全取代在許多情況下仍然非常有用的回調的原因。作為一名開發(fā)人員,應該按照實際情況分析決定使用哪種解決方案。如果您正在構建執(zhí)行異步操作的公共庫,則應該提供易于使用的API,即使對于只想使用回調的開發(fā)人員也是如此。
在下一章中,我們將探討另一個與異步代碼執(zhí)行相關的機制,這也是整個Node.js生態(tài)系統(tǒng)中的另一個基本構建塊:streams。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://systransis.cn/yun/89978.html
摘要:而且已開源出來,隨著容器技術發(fā)展,大文件分發(fā)一直是個重要的問題,所以是一件值得研究的技術。實用推薦檢定攻略是近期推出的一項認證,用以認證開發(fā)者的移動網頁開發(fā)技能。凈化,移除中不必要的文件技術周刊由小組出品,匯聚一周好文章,周刊原文。 業(yè)界動態(tài) 直擊阿里雙11神秘技術:PB級大規(guī)模文件分發(fā)系統(tǒng)蜻蜓 文章主要介紹了阿里的PB級大規(guī)模文件分發(fā)系統(tǒng)蜻蜓, 通過使用P2P技術同時結合智能壓縮、智...
摘要:通過本文,我們將學習如何使用來改變開發(fā)流程,從而使開發(fā)更加快速高效。中文網站詳細入門教程使用是基于的,需要要安裝為了確保依賴環(huán)境正確,我們先執(zhí)行幾個簡單的命令檢查。詳盡使用參見官方文檔,中文文檔項目地址 為了UED前端團隊更好的協(xié)作開發(fā)同時提高項目編碼質量,我們需要將Web前端使用工程化方式構建; 目前需要一些簡單的功能: 1. 壓縮HTML 2. 檢查JS 3. 編譯SA...
摘要:的新特性往往會增加代碼的,這些特性卻有助于緩解當前的性能危機,尤其像在手機設備這樣的新興市場上。聯(lián)合聲明我們短期目標是盡快實現(xiàn)少于倍的性能改善。我們會繼續(xù)針對的特性提升其性能。定期發(fā)布高質量文章。 作者:Alon Zakai 編譯:胡子大哈 翻譯原文:http://huziketang.com/blog/posts/detail?postId=58d11a9aa6d8a07e449f...
摘要:關于作者程序開發(fā)人員,不拘泥于語言與技術,目前主要從事和前端開發(fā),使用和,端使用混合式開發(fā)。合適和夠用是最完美的追求。沒錯,是一款的后端框架。的靈感來自一個名為的框架。功能亮點是圍繞實際用例構建的。讓您忘掉傳統(tǒng)查詢,擁抱優(yōu)雅的數(shù)據(jù)模型。 關于作者 程序開發(fā)人員,不拘泥于語言與技術,目前主要從事PHP和前端開發(fā),使用Laravel和VueJs,App端使用Apicloud混合式開發(fā)。合...
摘要:關于作者程序開發(fā)人員,不拘泥于語言與技術,目前主要從事和前端開發(fā),使用和,端使用混合式開發(fā)。合適和夠用是最完美的追求。沒錯,是一款的后端框架。的靈感來自一個名為的框架。功能亮點是圍繞實際用例構建的。讓您忘掉傳統(tǒng)查詢,擁抱優(yōu)雅的數(shù)據(jù)模型。 關于作者 程序開發(fā)人員,不拘泥于語言與技術,目前主要從事PHP和前端開發(fā),使用Laravel和VueJs,App端使用Apicloud混合式開發(fā)。合...
閱讀 2016·2021-11-23 09:51
閱讀 1270·2019-08-30 15:55
閱讀 1645·2019-08-30 15:44
閱讀 786·2019-08-30 14:11
閱讀 1176·2019-08-30 14:10
閱讀 946·2019-08-30 13:52
閱讀 2659·2019-08-30 12:50
閱讀 650·2019-08-29 15:04