@zwh8800
2017-08-20T07:21:48.000000Z
字数 11301
阅读 325844
blog es2015 stage3 javascript await
await 语法最早被引入到流行语言是 c# 5.0 ,微软在 c# 中添加了 await & async 关键字和一系列配套 API ,引起了开发者的一致好评。await 可以极大的简化异步编程,而使用异步编程最多的就是 javascript 了。所以开发者们也迫不及待的向 javascript 中加入 await 关键字。目前此特性已经在 babel 中比较完美的实现了。而 await 特性也被加入了 stage-3 (Candidate),不过貌似是赶不上今年的 ES2016 了,估计最晚会在 ES2017 中被正式加入 javascript 。那么本文就来深度剖析一下 await & async 的用法、好处以及实现方式。
异步编程模型对于 IO 密集型的任务具有得天独厚的优势。这里用一个例子来解释。
假如我们需要做一个爬虫,爬到的东西有两种,一种是索引页,一种是内容页。大概需要以下几步:
那么,分别使用原生支持异步编程的 NodeJS 和原生不支持异步编程 golang 的语言实现这个爬虫。分别使用两种语言 最常见 的实现方式。
因为发 http 请求和保存数据库还需要一些额外代码,会干扰视线,所以例子代码是递归的读一个文件夹,并把所有 html 文件做一些修改,然后保存到源文件。但是原理上和爬虫是相通的。
package mainimport ("io/ioutil""log""path""path/filepath""strings")const extname = ".html"var counter = 0func handleDir(dir string) {// 读取文件夹列表files, err := ioutil.ReadDir(dir)if err != nil {log.Println("error occurs when readDir", dir, err)return}// iterate 文件列表for _, file := range files {fullFilename := path.Join(dir, file.Name())// 判断文件类型if !file.IsDir() && filepath.Ext(file.Name()) == extname {counter++thisCount := counter// 打开始 loglog.Println("start processing", fullFilename,"[", thisCount, "]")// 读取文件fileData, err := ioutil.ReadFile(fullFilename)if err != nil {log.Println("error occurs when processing",fullFilename, err)continue}// 做一些处理fileString := string(fileData)fileString = strings.Replace(fileString,"http://", "https://", -1)// 保存文件if err := ioutil.WriteFile(fullFilename,[]byte(fileString), 0644); err != nil {log.Println("error occurs when processing",fullFilename, err)continue}// 打结束 loglog.Println("finish processing", fullFilename,"[", thisCount, "]")} else if file.IsDir() {// 文件夹的话递归handleDir(fullFilename)}}}func main() {handleDir("/Users/zzz/hzzz.lengzzz.com/")}
import fs from 'fs';import path from 'path';let counter = 0;const extname = '.html';function handleDir(dir) {// 读取文件夹列表fs.readdir(dir, function (err, files) {// iterate 文件列表files.map(file => {let fullFilename = path.join(dir, file);fs.stat(fullFilename, function (err, stats) {if (err) {console.error("error occurs when processing", file, err);return;}// 判断文件类型if (stats.isFile() && path.extname(file) == extname) {let thisCount = counter++;// 打开始 logconsole.log('start processing', fullFilename,'[', thisCount, ']');// 读取文件fs.readFile(fullFilename, 'utf-8',function (err, fileString) {if (err) {console.error("error occurs when processing",file, err);return;}// 做一些处理fileString = fileString.replace(/http:\/\//g, 'https://');// 写入文件fs.writeFile(fullFilename, fileString, function (err) {if (err) {console.error("error occurs when processing",file, err);return;}// 打结束 logconsole.log('finish processing', fullFilename,'[', thisCount, ']');})})} else if (stats.isDirectory()) {handleDir(fullFilename);}})})});}function main() {handleDir('/Users/zzz/hzzz.lengzzz.com/');}main();
package mainimport ("io/ioutil""log""path""path/filepath""strings")const channelBuffer = 50const workerCount = 30type payload struct {thisCount intfullFilename stringfileString string}var producerToRead chan *payloadvar readToReplace chan *payloadvar replaceToWrite chan *payloadvar writeToComplete chan *payloadfunc init() {producerToRead = make(chan *payload, channelBuffer)readToReplace = make(chan *payload, channelBuffer)replaceToWrite = make(chan *payload, channelBuffer)writeToComplete = make(chan *payload, channelBuffer)}func reader() {for data := range producerToRead {fileData, err := ioutil.ReadFile(data.fullFilename)if err != nil {log.Println("error occurs when processing",data.fullFilename, err)continue}data.fileString = string(fileData)readToReplace <- data}}func replacer() {for data := range readToReplace {data.fileString = strings.Replace(data.fileString,"http://", "https://", -1)replaceToWrite <- data}}func writeer() {for data := range replaceToWrite {if err := ioutil.WriteFile(data.fullFilename,[]byte(data.fileString), 0644); err != nil {log.Println("error occurs when processing",data.fullFilename, err)return}writeToComplete <- data}}func complete() {for data := range writeToComplete {log.Println("finish processing", data.fullFilename,"[", data.thisCount, "]")}}var counter = 0func producer(dir string) {const extname = ".html"files, err := ioutil.ReadDir(dir)if err != nil {log.Println("error occurs when readDir", dir, err)return}for _, file := range files {fullFilename := path.Join(dir, file.Name())if !file.IsDir() && filepath.Ext(file.Name()) == extname {counter++thisCount := counterlog.Println("start processing", fullFilename,"[", thisCount, "]")producerToRead <- &payload{thisCount,fullFilename,"",}} else if file.IsDir() {producer(fullFilename)}}}// 搞四个 workerfunc startWorker() {for i := 0; i < workerCount; i++ {go reader()go replacer()go writeer()go complete()}}func main() {startWorker()producer("/Users/zzz/hzzz.lengzzz.com/")}
分别看一下两个程序打出的 log ,来分析一下哪个程序会运行的更快。
start processing /1001/index.html [ 1 ]finish processing /1001/index.html [ 1 ]start processing /1001/trackback/index.html [ 2 ]finish processing /1001/trackback/index.html [ 2 ]start processing /1006/index.html [ 3 ]finish processing /1006/index.html [ 3 ]start processing /1006/trackback/index.html [ 4 ]finish processing /1006/trackback/index.html [ 4 ]start processing /101/index.html [ 5 ]finish processing /101/index.html [ 5 ]start processing /101/trackback/index.html [ 6 ]finish processing /101/trackback/index.html [ 6 ]start processing /1010/index.html [ 7 ]finish processing /1010/index.html [ 7 ]start processing /1010/trackback/index.html [ 8 ]finish processing /1010/trackback/index.html [ 8 ]start processing /1027/index.html [ 9 ]finish processing /1027/index.html [ 9 ]start processing /1027/trackback/index.html [ 10 ]finish processing /1027/trackback/index.html [ 10 ]
特点是挨个抓取,第一个没有抓完不开始抓第二个。
start processing /index.html [ 0 ]start processing /blog/index.html [ 1 ]start processing /blog/wp-login.html [ 2 ]start processing /blog/1001/index.html [ 3 ]start processing /blog/1006/index.html [ 4 ]start processing /blog/101/index.html [ 5 ]... 省略start processing /blog/page/9/index.html [ 736 ]finish processing /index.html [ 0 ]start processing /blog/date/2013/08/index.html [ 737 ]
特点是同时抓取网页,谁先抓完先处理谁,谁先处理完谁就保存。不需要等待。
start processing /blog/1001/index.html [ 1 ]start processing /blog/1001/trackback/index.html [ 2 ]start processing /blog/1006/index.html [ 3 ]start processing /blog/1006/trackback/index.html [ 4 ]start processing /blog/101/index.html [ 5 ]start processing /blog/101/trackback/index.html [ 6 ]start processing /blog/1010/index.html [ 7 ]start processing /blog/1010/trackback/index.html [ 8 ]start processing /blog/1027/index.html [ 9 ]start processing /blog/1027/trackback/index.html [ 10 ]start processing /blog/1030/index.html [ 11 ]start processing /blog/1030/trackback/index.html [ 12 ]start processing /blog/1038/index.html [ 13 ]start processing /blog/1038/trackback/index.html [ 14 ]finish processing /blog/101/trackback/index.html [ 6 ]start processing /blog/1040/index.html [ 15 ]
可以对比一下 KFC 和 麻辣烫 店的点餐方式,和上面两个程序有异曲同工之妙。
谁更快显而易见。在 KFC 里最怕前面点个全家桶,好不容易排到第一个了,结果还不如旁边队列里最后的取餐快。
所以可见,NodeJS 只需要使用一般的写法就能自动获得 性能加成 还是很有吸引力的。
golang 的代码是按照先后顺序很直观的顺下来的,可以看一下几个注释,都在同一个缩进级别。
而 NodeJS 使用了大量回调函数,本身串行的逻辑看起来却像是内嵌的感觉,从代码的缩进就能明显的看出来。大家亲切的成这种代码风格叫做 冲击波。
// 冲击波{{{{{{{// ============ >}}}}}}}
golang 使用 goroutine 重新实现之后性能得到了提升,但是可读性也是降低了一些。如果逻辑比较复杂则 channel 不很好设计。
除此之外 NodeJS 的写法还有几个坑:
let 可解决这个问题) 那么有没有一种方法,能同时具备两种写法的优点呢?答案就是前端终极武器 await & async 。
首先来看一下 await 的使用方法,我给出一个同样功能(爬虫)的示例:
import path from 'path';import { readDir, stat, readFile, writeFile } from './api_promise';let counter = 0;const extname = '.html';async function handleDir(dir) {try {// 读取文件夹列表let files = await readDir(dir);// iterate 列表files.map(async file => {let fullFilename = path.join(dir, file);try {// 检查文件类型let stats = await stat(fullFilename);if (stats.isFile() && path.extname(file) == extname) {let thisCount = counter++;// 打开始 logconsole.log('start processing', fullFilename,'[', thisCount, ']');// 读文件let fileString = await readFile(fullFilename, 'utf-8');// 改文件fileString = fileString.replace(/http:\/\//g, 'https://');// 写文件await writeFile(fullFilename, fileString);// 打结束 logconsole.log('finish processing', fullFilename,'[', thisCount, ']');} else if (stats.isDirectory()) {handleDir(fullFilename);}} catch (err) {console.error("error occurs when processing", file, err);}});} catch (err) {console.error("error occurs when readDir", dir, err);}}function main() {handleDir('/Users/zzz/hzzz.lengzzz.com/');}main();
代码清晰了不少,但是性能还和之前一样,异步的读文件,异步的写文件。另外我还加入了 try catch ,来演示 await 对 try catch 的支持。
此外,还需要做的事情是对原生的 API 进行一下包装。使之返回一个 Promise 以支持 await。
如下:
import fs from "fs";export function readDir(path) {return new Promise(function (resolve, reject) {fs.readdir(path, function (err, files) {if (err) {reject(err);return;}resolve(files);})})}
这里只举例一个了,这个文件 wrap 了四个 NodeJS API ,都是使用相同的方式包装的。
其实,await 只是一个语法糖。下面,分析一下这颗糖底层是怎么实现的。
要说 await 不得不先温习一些前置知识,比如 iterator 和协程。
在大部分语言中,要实现一个类型可以被 iterate (既让一个类型 iterable)一般需要实现一个叫 Iterable 的 interface。
class List implements Iterable {public Iterator iterator() {// ...}}
这个 Iterable 有方法能返回一个 Iterator 循环调用 Iterator 的方法 next() 可以得到下一个元素,调用 hasNext() 可以判断是否结束。
所以 Iterator 可以这样实现:
class List implements Iterable {// nested classclass ListIterator implements Iterator {int i = 0;int max = 10;public Object next() {return i++;}public boolean hasNext() {return i > max;}}public Iterator iterator() {return new ListIterator();}}
这样,就可以 iterate 一个 List 了:
List list = new List();for (Object i : list) {// ...}
在 javascript 中也不例外,这样实现一个 Iterable :
var iterable = {[Symbol.iterator]: function() {var i = 0;var iterator = {next: function () {var iteratorResult = {done: i > 10,value: i++};return iteratorResult;}};return iterator;}};for (let item of iterable) {console.log(item);}
协程是一种抽象方式,可以让一个函数中途暂停返回一些东西,然后过一段时间后再继续执行。
function routine()local i = 0i = i + 1coroutine.yield(i)i = i + 1coroutine.yield(i)i = i + 1coroutine.yield(i)end
调用三次 routine 后分别能得到 1 2 3 。原因是协程执行了一半被暂停后会保存下它自己的上下文,以便下次 resume后数据还在。
Generator 相当于 javascript 中的协程,在一个 Generator 函数中使用 yield 关键字,可以暂停函数执行,返回一个结果。
// 符号 * 代表 generatorfunction* routine() {let i = 0;yield i++;yield i++;yield i++;}
这段代码和上面的 lua 代码等价。
使用 generator 函数可以方便的实现一个 iterator:
iterable = {[Symbol.iterator]: function* () {for (let i = 0; i < 10; ++i) {yield i;}}};for (let item of iterable) {console.log(item);}
和上面的 iterable 代码等价,但是可以使用 for 循环了,是不是简洁多了?
可能大家已经想到了,async 函数就是被翻译成了 generator 函数,ya。
async function getArticle () {var test = $('.test');var comments = await getComments();test.append('<p>' + JSON.stringify(comments) + '</p>');var posts = await getPosts();test.append('<p>' + JSON.stringify(posts) + '</p>');}// 翻译成=====>function* getArticle () {var test = $('.test');var comments = yield getComments();test.append('<p>' + JSON.stringify(comments) + '</p>');var posts = yield getPosts();test.append('<p>' + JSON.stringify(posts) + '</p>');}
当调用一个 async 函数时,实际上是这样做的:
getArticle();// 翻译成=====>runner(getArticle);function runner(getIterator) {var iterator = getIterator();function next(data) {var result = iterator.next(data);if (result.done){return;}var promise = result.value;promise.then(function (data) {next(data);});}next();};
Javascript 只有一根线程,如果用 for 循环来计算 10000 个数字的和,整个 vm 都非卡死不行。
以前大家都用 setTimeout / setInterval 来把运算拆开来做:
let output = $('.power');function run2() {var i = 0, end = 10000;var cancel = setInterval(function () {let p = i * i;output.append(`<p>${p}</p>`);if (++i >= end) {clearInterval(cancel);}}, 0);}
现在可以用 await 了
function getPower(x) {return new Promise(function (resolve, reject) {setTimeout(function () {resolve(x * x);}, 0);});}async function run() {let output = $('.power');for (let i = 0; i < 10000; i++) {let p = await getPower(i);output.append(`<p>${p}</p>`);}}
这两种都不会卡 vm 但是第二种显然直观一些。