在JavaScript中,函数是一等公民,使用非常自由,无论是调用它,或者作为参数,或者作为返回值均可。
通常的语言中,函数的参数只接收基本数据类型或对象引用,返回值也是基本数据类型或对象引用。
高阶函数则是可以把函数作为参数,或是将函数作为返回值的函数。
对于程序编写,高阶函数要比普通函数灵活很多,除了通常意义的函数调用返回外,还形成了一种后续传递风格的结果接收方式,而非单一的返回值形式。
function foo(x,bar){
return bar(x);
}
上例,对于相同的foo()函数,传入的bar参数不同,则可以得到不同的结果。
const events = require("events");
const emitter = new events.EventEmitter();
emitter.on("event_foo",()=>{
//TODO
});
可以看到,事件的处理方式正是基于高阶函数的特性来完成的。
在ECMA2015中,forEach()、map()、reduce()、filter()、every()、some()都是高阶函数。
偏函数定义比较绕,所以直接看这个示例:
const toString =Object.prototype.toString;
const isString = function(obj){
return toString.call(obj) === "[object String]";
};
const isFunction = function(obj){
return toString.call(obj) === "[object Function]";
};
这个函数不复杂,但存在的问题是需要重复定义一些相似的函数,为了解决代码冗余的问题,需要引入一个新函数:
const isType = function(type){
return function(obj){
return toString.call(obj) === `[object ${type}]`;
}
};
const isString = isType("String");
const isFunction = isType("Function");
这种通过指定部分参数来产生一个新的定制函数的形式就是偏函数。
node带来的最大特性莫过于基于事件驱动的非阻塞I/O模型。
由于事件循环模型需要应对海量请求,海量请求同时作用在单线程上,这就需要防止任何一个计算耗费过多的CPU时间片。至于是计算密集型还是I/O密集型,只要计算不影响异步I/O的调度,那就不构成问题。建议对CPU的耗时不超过10ms,或者将大量的计算分解为诸多的小量计算,通过setImmediate()进行调度。只要合理利用node的异步模型与V8的高性能,就可以充分发挥CPU和I/O资源的优势。
try…catch通常用于捕获异常,但对于异步编程却不那么适用。异步I/O的实现包含2个阶段:提交请求和处理结果。这2个阶段中间有事件循环的调度,彼此不关联,所以try/catch的功效便不会发生作用。
const async = function(callback){
process.nextTick(callback);
};
try{
async(callback);
}catch (e) {
//TODO
}
调用async方法后,callback被存放起来,直到下一个事件循环Tick才会取出来执行。尝试对异步方法进行try/catch操作只能捕获当次事件循环内的异常,对callback执行时抛出的异常则无能为力。
所以,node在处理异常上形成了一种约定,将异常作为回调函数的第一个实参传回,如果为空值,则表明异步调用没有异常抛出。这就是node错误优先原则。
所以在自行编写的异步方法上,也需要遵循这样一些规则:
示例:
const async = function(callback){
process.nextTick(function(){
const result = "something";
if(err){
return callback(err);
}
return callback(null,result);
});
};
在编写异步方法时,只要将异常正确的传递给用户的回调方法即可,无须过多处理。
在node中,事物存在多个异步调用的场景很多。比如再网页渲染过程中,数据、模板、资源文件三者并不依赖,但最终渲染结果则缺一不可,可能最终的样子是这样的:
fs.readFile(template_path,"utf8",function(err,file){
db.query(sql,function(err,data){
l10n.get(function(err,resources){
//TODO
});
});
});
这在结果上没有问题,但并没有利用好异步I/O带来的并行优势。
JavaScript并没有类似PHP中sleep()这样的函数来让线程沉睡,可能会这么模拟:
const start = new Date();
while(new Date() - start < 1000){
//TODO
}
//需要阻塞的代码
但事实上却是很糟糕的,这段代码会持续占用CPU进行判断,与真正的线程沉睡相去甚远,完全破坏了事件循环的调度。有与node单线程的原因,CPU资源全部会用于为这段代码服务,导致其余任何请求都会得不到响应。
对于这类需求 ,统一规划好业务逻辑后,调用setTimeout()的效果会更好。
node借鉴了前端web workers的模式,child_process是其基础API,cluster模块是更深层次的应用。事实上前端极少会用到web workers,以后要更多面临跨线程编程,这也是一个挑战。
习惯了异步编程,偶尔出现的同步需求会因为没有同步API让人无所适从,对于异步调用,借助一些ES2015的Promise、Async等,也可以实现良好的流程控制。
异步编程的主要解决方案有以下3种:
事件监听器是回调函数的事件化,又称发布/订阅模式。
node自身提供了events模块,这个模块相比浏览器大量DOM事件要简单,不存在冒泡、preventDefault()、stopPropagation()和stopImmediatePropagation()等控制事件传递的方法。
events模块它具有addListener/on()、once()、removeListerner()、removeAllListeners()和emit()等基本的事件监听模式的方法实现。
const events = require("events");
const emitter = new events.EventEmitter();
emitter.on("event",(message)=>{
console.log(message);
});
emitter.emit("event","hello world");
可以看到,订阅事件就是一个高阶函数的应用,事件发布/订阅模式可以实现一个事件与多个回调函数的关联,这些回调函数也称为事件监听器。通过emit()发布事件后,消息会立即传递给当前事件的所有监听器执行。监听器可以灵活的增加/删除,使得事件和具体处理逻辑之间可以轻松关联与解耦。
注意:事件发布/订阅模式自身并无同步和异步调用的问题(注意下例)。但在node中,emit()多半是伴随事件循环而异步触发的,所以发布/订阅模式广泛应用于异步编程。
const events = require("events");
const emitter = new events.EventEmitter();
emitter.on("event",(message)=>{
console.log(message);
});
emitter.emit("event","hello world");
console.log("立即执行");
//执行结果
hello world
立即执行
node对事件发布/订阅的机制做了额外一些处理:
const events = require("events");
const util = require("util");
function Stream(){
events.EventEmitter.call(this);
}
util.inherits(Stream,events.EventEmitter);
const emitter = new Stream();
emitter.on("event",function(message){
console.log(message);
});
emitter.emit("event","hello world");
在事件订阅/发布模式中,通常也有一个once()方法,通过它添加的监听器只会执行一次,执行之后就会将它于事件的关联解除。利用这个特性可以处理一些重复性的事件响应。
雪崩问题就是在高访问量、大并发量的情况下缓存失效的情景,此时大量的请求涌入数据库,数据库无法同时承受如此大的查询请求,进而影响网站整体的响应速度。
以下是一条数据库查询语句的调用:
const select = function(callback){
db.select("SQL",function(results){
callback(results);
});
};
如果站点刚启动,这时缓存中是不存在数据的,如果访问量巨大,同一条SQL会被发送到数据库中反复查询,会影响服务的整体性能。
一种改进方案是添加一个状态锁:
let status = "ready";
const select = function(callback){
if(status === "ready"){
status = "pending";
db.select("SQL",function(results){
status = "ready";
callback(results);
});
}
};
这种情景下,连续多次调用select()时,只有第一次调用是生效的,后续的select()是没有数据服务的,这个时候可以引入事件队列:
const events = require("events");
const proxy = new events.EventEmitter();
let status = "ready";
const select = function(callback){
proxy.once("selected",callback);
if(status === "ready"){
status = "pending";
db.select("SQL",function(results){
proxy.emit("selected",results);
status = "ready";
});
}
};
利用once()方法将所有请求压入事件队列中,利用其执行一次的特点,保证每一个回调只会被执行一次。对于相同的SQL语句,保证每一个开始到结束的过程永远只有一次。SQL在进行查询时,新到来的相同调用只需在队列中等待数据就绪即可,一旦查询结束,得到的结果就可以被这个调用共同使用。
此处可能需要调用setMaxListeners(0)来移除警告。
一般而言,事件与监听器的关系是一对多的,但在异步编程中,也会出现事件与监听器的关系是多对一的情况,也就是说一个业务逻辑可能依赖两个通过回调或事件传递的结果。之前提及的回调嵌套过深的原因就是如此。
let count = 0;
const results = {};
const done = function(key,value){
results[key] = value;
count++;
if(count === 3){
render(results);
}
};
fs.readFile(template_path,"utf8",function(err,template){
done("template",template);
});
db.query(SQL,function(data){
done("data",data);
});
l10n.get(function(err,resources){
done("resources",resources);
});
多个异步场景中回调函数的执行并不能保证顺序,且回调函数之间彼此没有任何交集,所以需要借助一个第三方函数和第三方变量来处理异步协作的结果。通常,这个用于检测次数的变量叫做哨兵变量。结合之前的偏函数模式,改进后的代码如下:
const after = function (times,callback) {
let count = 0;
const results = {};
return function(key,value){
results[key] = value;
count++;
if(count === times){
callback(results);
}
}
};
const done = after(times,callback);
上述方案实现了多对一的目的。对于多对多的模式稍做改造即可:
const emitter = new events.EventEmitter();
const done = after(times,callback);
emitter.on("done",done);
emitter.on("done",other);
这种方案结合了前者用简单的偏函数完成多对一的收敛和事件订阅/发布模式中一对多的发散。
但上面的这种方法有个麻烦的地方就是,开发者需要去准备这个done()函数,以及在回调函数中需要从结果中把数据一个一个取出来,再进行处理。
Promise/Deferred模式在JavaScript中最早出现于Dojo中,被广泛所知是来自jQuery1.5版本,该模式在2009年被抽象为一个提议草案,发布在CommonJS规范中。CommonJS草案目前被抽象出了Promises/A、Promises/B、Promises/D这样典型的异步Promise/Deferred模型。
Promise/A提议对单个异步操作作出了以下的抽象定义:
Promises/A的提议比较简单,一个Promise对象只要具备then()方法即可。对于then()方法,有以下简单的要求:
then()方法具体定义:
then(fulfilledHandler,errorHandler,progressHandler);
一个简单实现:
const Promise = function(){
events.EventEmitter.call(this);
};
util.inherits(Promise,events.EventEmitter);
Promise.prototype.then = function (fulfilledHandler,errorHandler,progressHandler) {
if(typeof fulfilledHandler === "function"){
this.once("success",fulfilledHandler);
}
if(typeof errorHandler === "function"){
this.once("error",errorHandler);
}
if(typeof progressHandler === "function"){
this.once("progress",progressHandler);
}
return this;
};
then()方法将回调函数存起来,为了完成整个流程,还需要触发执行这些回调函数的地方,实现这些功能的对象通常被称为Deferred,即延迟对象。示例代码如下:
const Deferred = function(){
this.state = "unfulfilled";
this.promise = new Promise();
};
Deferred.prototype.resolve = function(obj){
this.state = "fulfilled";
this.promise.emit("success",obj);
};
Deferred.prototype.reject = function(err){
this.state = "failed";
this.promise.emit("error",err);
};
Deferred.prototype.progress = function(data){
this.promise.emit("progress",data);
};
如果我们要实现类似这样的效果:
res.then(()=>{
//Done
},(err)=>{
//Error
},(chunk)=>{
console.log("BODY:"+chunk);
});
只需要将Deferred与Promise结合起来,如下:
const promisify = function(res){
const deferred = new Deferred();
let result = "";
res.on("data",function(chunk){
result+=chunk;
deferred.progress(result);
});
res.on("end",function(){
deferred.resolve();
});
res.on("error",function(err){
deferred.reject(err);
});
return deferred.promise;
};
最后返回deferred.promise是为了不让外部调用resolve()和reject()方法,更改内部状态的行为交由定义者处理。以下是调用示例:
promisify(res).then(()=>{
//Done
},(err)=>{
//Error
},(chunk)=>{
//progress
console.log("body",chunk);
});
可以看Promise和Deferred的区别,Deferred主要用于内部,用于维护异步模型的状态;Promise则作用于外部,通过then()方法暴露给外部以添加自定义逻辑。
Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更加强大合理。Promise最早由社区提出和实现,现在ES6已经写入了标准,统一了用法,浏览器端和node原生提供了Promise对象。
Promise对象有以下2个特点:
有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。
Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
const fs = require("fs");
const promise = new Promise((resolve, reject)=>{
fs.readFile("file_path","utf8",(err,result)=>{
if(err){
reject(err);
}else{
resolve(result);
}
});
});
Promise的构造函数接收一个函数作为参数,该函数的2个参数分别为resolve和reject,这依然是2个函数,由JavaScript提供。
resolve()函数的作用用于将Promise从pending的状态变为fulfilled,异步操作成功时调用该方法,并将成功结果传出去。
reject()函数的作用用于将Promise从pending的状态变为rejected,异步操作失败时调用该方法,并将异常信息传出去。
Promise实例生成以后,可以用then方法分别指定fulfilled状态和rejected状态的回调函数。如:
const next = promise.then((...values)=>{
console.log(values);
},(...errors)=>{
console.log(errors);
});
当Promise变为fulfilled时则调用第一个回调,变为rejected则调用第二个回调。第二个回调函数时可选的。
调用then()方法以后返回的依然是promise,所以next可以继续调用then()方法。
Promise实例的then方法是定义在原型上的,then()方法调用以后会返回一个新的Promise实例,注意是新的。
既然可以继续调用then()方法,如:
next.then((result)=>{
console.log("fulfilled",result);
},(error)=>{
console.log("rejected",error);
});
fulfilled回调函数的参数来自于上一个fulfilled或rejected函数的返回值。来自fulfilled还好理解,为什么会有可能来自rejected函数的返回值呢?看这个例子:
const promise = new Promise((resolve, reject)=>{
reject("rejected");
});
const next = promise.then((result)=>{
return result;
},(error)=>{
return error;
});
next.then((result)=>{
console.log("fulfilled",result);
},(error)=>{
console.log("rejected",error);
});
//打印结果
fulfilled rejected
但是如果是这样的:
const promise = new Promise((resolve, reject)=>{
reject("rejected");
});
const next = promise.then((result)=>{
return result;
});
next.then((result)=>{
console.log("fulfilled",result);
},(error)=>{
console.log("rejected",error);
});
//打印结果
rejected rejected
常量promise的状态是rejected且没有定义rejected回调函数,那么next的状态就是rejected;如果promise定义了rejected回调,无论promise常量本身是fulfilled还是rejected,调用then返回的新的promise——next的状态都是fulfilled。
对于Promise实例,异常如果没有被捕获会一层层传递下去,直到被捕获。
then()的参数回调除了返回常规的值,也可以返回Promise,只有这个Promise的状态为fulfilled或rejected才会触发下一个then()方法回调,利用这一点可以很容易实现链式调用:
new Promise((resolve, reject)=>{
resolve("fulfilled");
}).then((result)=>{
return new Promise((resolve, reject)=>{
setTimeout(()=>{
reject(result);
},1000);
})
}).then(null,(err)=>{
console.log(err);//fulfilled
});
catch()方法专门用来捕获异常的,执行之后返回的依然是Promise,且catch回调函数的返回值会传入到下一个Promise的fulfilled回调函数中。
const promise = new Promise((resolve, reject)=>{
reject("rejected");
}).catch(err=>{
console.log(err);
return "异常被捕获"
}).then(result=>{
console.log(result);
});
catch除了可以捕获rejected状态以外,还可以捕获代码抛出的异常,比如打印一个未定义的变量:
const promise = new Promise((resolve, reject)=>{
console.log(abc)
}).catch(err=>{
console.log(err);
return "异常被捕获"
}).then(result=>{
console.log(result);
});
当然catch抛出的异常也可以被下一个catch捕获:
const promise = new Promise((resolve, reject)=>{
reject("rejected");
}).catch(err=>{
return abc;
}).catch(err=>{
console.log(err);
});
finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作,返回依然是Promise。
该方法是 ES2018 引入标准的,经测试目前node(v8)并不支持,所以以下均来自于Chrome浏览器的测试。
const promise = new Promise((resolve, reject)=>{
reject("rejected");
}).then(result=>{
return result;
}).finally((result)=>{
console.log("finally1",result);
return "finally执行1次";
}).finally((result)=>{
console.log("finally2",result);
return "finally执行2次";
}).then(result=>{
console.log(result);
});
可以看到,finally并不接收参数,返回值也不会传递给下一个promise。经finally返回的promise无论如何都处于fulfilled状态。
这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
该方法接收一个数组作为参数,每一个数组元素必须是Promise,如果不是就会调用Promise.resolve()方法将其转化为fulfilled状态的Promise。
只有Promise.all()的每一个Promise都为fulfilled才返回fulfilled状态,否则返回rejected。如果返回fulfilled状态,那么所有Promise的resolve()值会按顺序压入一个数组作为Promise.all()的fulfilled回调函数的参数。如果返回rejected,则第一个reject()值会作为rejected回调函数的参数。
const promiseA = new Promise((resolve, reject)=>{
resolve("promiseA");
});
const promiseB = "promiseB";
const promiseC = new Promise((resolve, reject)=>{
resolve("promiseC");
});
Promise.all([promiseA,promiseB,promiseC]).then((result)=>{
console.log(result);//[ 'promiseA', 'promiseB', 'promiseC' ]
},(error)=>{
console.log(error);
});
Promise.all()方法解决的是多个异步操作并行执行的问题
参数依然是数组,不同的是,只要子Promise实例率先改变了状态,那么整体的Promise状态就随着改变,第一个改变状态的Promise的返回值作为整体Promise对应回调的参数。
const promiseA = new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve("promiseA");
},100);
});
const promiseB = new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve("promiseA");
},200);
});
const promiseC = new Promise((resolve, reject)=>{
setTimeout(()=>{
reject("promiseC");
},50);
});
Promise.race([promiseA,promiseB,promiseC]).then((result)=>{
console.log("fulfilled",result);
}).catch(err=>{
console.log("rejected",err);
});
该方法用于将目标参数转化为Promise实例。该方法的参数分为4种情况:
const promise = new Promise((resolve, reject)=>{
resolve("promise");
});
const next = Promise.resolve(promise);
next.then((result)=>{
console.log(result)
});
console.log(next === promise);//true
Promise.resolve()将不会做任何操作,直接返回。
thenable对象指的是用于then属性方法的对象。
const next = Promise.resolve({
then:function(resolve,reject){
resolve("fulfilled");
}
});
next.then(null,(result)=>{
console.log(result)
});
Promise.then()方法会将这个thenable对象转换为Promise,并立即执行里面的then()方法。
这种情况,Promise.resolve()方法会将目标直接转化为fulfilled状态的Promise。fulfilled回调函数的参数便是Promise.resolve()的参数。
const next = Promise.resolve({
say:()=>{
console.log("hello world");
}
});
next.then((result)=>{
result.say();
});
const next = Promise.resolve();
next.then((result)=>{
console.log(result) //undefined
});
同Promise.resolve()相反,但是存在一些参数上的区别。对于Promise.reject(),其参数会原封不动的作为rejected的回调函数的参数。
const thenable = {
then(resolve, reject) {
reject('出错了');
}
};
Promise.reject(thenable).catch(e => {
console.log(e === thenable) //true
});
这一点要特别注意。
Promise无论是fulfilled还是rejected的回调,毫无疑问不会在本轮事件循环执行。
Promise.resolve("promise执行").then((result)=>{
console.log(result);
});
setImmediate(()=>{
console.log("setImmediate执行");
});
setTimeout(()=>{
console.log("setTimeout执行");
},0);
process.nextTick(()=>{
console.log("nextTick执行");
});
console.log("立即执行");
执行结果:
立即执行
nextTick执行
promise执行
setTimeout执行
setImmediate执行
立即Promise执行仅次于nextTick的执行,这一点格外重要。
不论是前端开发还是node开发,使用Promise一定要添加rejected回调或catch回调来捕获异常,这一点在node格外重要,对于单线程没有捕获的异常会导致线程退出。
Generator函数也是ES6提供的一种异步编程解决方案,其语法行为与传统函数完全不同。Generator函数是一个状态机,里面有很多种状态,执行Generator函数会返回一个遍历器对象,该遍历器可依次遍历Generator内部的所有状态。
Generator的定义需要在function关键字与函数名之间加一个*号(不需要考虑空格问题),函数内部使用yield表达式。
执行Generator函数返回的是一个指向内部状态的指针,调用遍历器对象上的next方法来让指针指向下一个状态,直到遇到yield表达式为止,并将yield表达式后边的值作为value返回;下一次调用next方法则从上一次停止的位置继续向下执行。
function*helloWorld(){
yield "hello";
yield "world";
return "end";
}
const hw = helloWorld();
console.log(hw.next());
console.log(hw.next());
console.log(hw.next());
console.log(hw.next());
//打印结果
{ value: 'hello', done: false }
{ value: 'world', done: false }
{ value: 'end', done: true }
{ value: undefined, done: true }
每次的返回值都是一个对象,对象包含value和done2个属性,value为yield表达式后边的值,done代表遍历是否结束。
由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。
需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,这是一种“惰性求值”(Lazy Evaluation)的语法功能。
function*helloWorld(){
let a = 1;
yield function(){
return a;
};
yield ++a;
yield function(){
return a;
};
return "end";
}
const hw = helloWorld();
console.log(hw.next().value());
console.log(hw.next());
console.log(hw.next().value());
//打印结果
1
{ value: 2, done: false }
2
如果Generator函数内部没有yield表达式,那这个函数就是只是普通的暂缓函数,通过调用next()方法执行。
yield表达式也只能出现在Generator函数中,负责会抛出异常。如:
function*helloWorld(){
const arr = [1,2,3,4,5,6];
arr.forEach((num)=>{
yield num;
});
}
这是错误的,forEach的回调只是普通函数。但可以用for循环:
function*helloWorld(){
const arr = [1,2,3,4,5,6];
for(let i=0;i<arr.length;i++){
yield arr[i];
}
}
yield表达式如果用在另一个表达式中,则必须加括号:
function*helloWorld(){
console.log("hello world" + (yield));
console.log("hello world" + (yield 2));
}
yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
function a(val){
console.log(val); //undefined
}
function*helloWorld(){
let yie = yield;
a(yield 2);
}
yield表达式本身不返回值,所以a()函数打印的是undefined。
普通对象是没有Iterator接口的,通过Symbol.iterator可以为其拓展功能:
const obj = {
a:1,
b:2
};
obj[Symbol.iterator] = function*(){
for(let i in this){
yield {[i]:this[i]};
}
};
console.log([...obj]);
Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。
function*gen(){}
const g = gen();
console.log(g[Symbol.iterator]() === g); //true
yield表达式本身是没有返回值的,相当于返回的是undefined。next()方法的参数,会作为上一个yield表达式的返回值。
function*gen(){
const yieldValue = yield 1;
return yield yieldValue+1;
}
const g = gen();
console.log(g.next());
console.log(g.next(2));
console.log(g.next(3));
//打印结果
{ value: 1, done: false }
{ value: 3, done: false }
{ value: 3, done: true }
因为next()方法的参数,会作为上一个yield表达式的返回值,所以第一次调用next()的传参是无效的。
Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
for…of可以遍历Generator运行生成的Iterator对象。
function*gen(){
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for(let i of gen()){
console.log(i);
}
//打印结果
//1、2、3、4、5
这里需要注意,一旦next方法的返回对象的done属性为true,for…of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for…of循环之中。可以看到使用for…of不需要调用next()方法。
除了for…of循环以外,扩展运算符(…)、解构赋值和Array.from方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 Iterator 对象,作为参数。
function*numbers(){
yield 1;
yield 2;
yield 3;
return 4
}
console.log([...numbers()]);
console.log(Array.from(numbers()));
const [x,y,z] = numbers();
console.log(x,y,z);
//打印结果
[ 1, 2, 3 ]
[ 1, 2, 3 ]
1 2 3
Generator调用之后返回的遍历器对象有一个throw方法,可以在函数体外抛出异常,然后在Generator函数内部捕获。
function*gen(){
while (true){
try{
yield;
}catch (e) {
console.log(e);
}
}
}
const g = gen();
console.log(g.next());//undefined
g.throw("hello");//hello
g.throw("world");//world
throw()方法的参数会传递给catch语句,建议是Error对象实例。
注意throw()方法和throw命令是不同的,throw命令抛出的异常只能被函数体外的catch语句捕获。
function*gen(){
try{
yield;
}catch (e) {
console.log(e);
}
}
const g = gen();
try{
g.next();
throw new Error("some error");
}catch (e) {
console.log("error",e);
}
如果throw()方法抛出的异常没有被Generator函数捕获,那么异常会向外传递,可被try…catch捕获:
function*gen(){
yield;
}
const g = gen();
try{
g.throw("some error");
}catch (e) {
console.log(e);
}
如果外部也没有捕获异常,那么程序将会报错,中断执行。
如果throw()方法抛出的异常想被Generator函数内部捕获,则至少要先调用一次next()方法,如果没有调用next(),异常将被外部的catch语句捕获。
throw()方法抛出的异常被捕获后,会顺带执行下一条yield语句:
function*gen(){
try{
yield 1;
}catch (e) {
console.log(e);
}
yield 2;
return 3;
}
const g = gen();
console.log(g.next());// {value:1,done:false}
console.log(g.throw("error"));// error {value:2,done:false}
console.log(g.next());// {value:3,done:true}
这种函数体内捕获错误的机制,大大方便了对错误的处理。多个yield表达式,可以只用一个try…catch代码块来捕获错误。
如果调用throw()方法抛出的异常没有在Generator内部捕获,那么无论外部是否捕获,继续调用遍历器对象的next()方法,返回的永远是:{value:undefined,done:true},JavaScript引擎会认为Generator遍历器函数已经遍历结束:
function*gen(){
yield 1;
yield 2;
return 3;
}
const g = gen();
console.log(g.next());// {value:1,done:false}
try{
g.throw("error");
}catch (e) {
console.log(e);// error
}
console.log(g.next());// {value:undefined,done:true}
最后,throw()方法是没有返回值的,也就是返回undefined。
Generator函数执行后返回的遍历器对象,还有一个return()方法,调用该方法将终结遍历Generator函数。
function*gen(){
yield 1;
yield 2;
return 3;
}
const g = gen();
console.log(g.next());// {value:1,done:false}
console.log(g.return("end"));// {value:'end',done:true}
console.log(g.next());// {value:undefined,done:true}
return()方法的参数会作为value的属性值原样返回。
如果Generator函数内部有try…finally语句,且try语句正在执行,那么会等待finally语句里的代码执行完毕,再终结Generator函数的遍历。
function*gen(){
while(true){
try{
yield 1;
}finally {
console.log("hello");
console.log("world");
}
}
}
const g = gen();
console.log(g.next());
try{
g.throw("error");
}catch (e) {
console.log(e);
}
console.log(g.next());
console.log(g.next());
//打印结果
{ value: 1, done: false }
hello
world
error
{ value: undefined, done: true }
{ value: undefined, done: true }
在一个Generator函数内部调用另外一个Generator函数通常是没有效果的,如下:
function*foo() {
yield "foo1";
yield "foo2";
}
function*bar(){
yield "bar1";
foo();
yield "bar2";
}
const b = bar();
for(let i of b){
console.log(i);
}
//打印结果
bar1
bar2
而yield*表达式就是用来解决这个问题的:
function*foo() {
yield "foo1";
yield "foo2";
}
function*bar(){
yield "bar1";
yield* foo();
yield "bar2";
}
const b = bar();
for(let i of b){
console.log(i);
}
//打印结果
bar1
foo1
foo2
bar2
所以可以看到yield*表达式效果等价于:
function*bar(){
yield "bar1";
for(let i of foo()){
yield i;
}
yield "bar2";
}
所以yield*后边的表达式可以是Generator函数执行返回的遍历器对象,也可以是其它具有Iterator接口的变量。
可以将Generator函数作为对象的方法:
const obj = {
gen1:function*(){
},
*gen2(){
}
};
function*gen(){}
gen.prototype.sayHello = function(){
console.log("hello world");
};
const g = gen();
console.log(g instanceof gen);//true
g.sayHello(); // hello world
Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,所以该实例可以调用Generator函数原型上的方法。
function*gen(){
this.name = "gen";
}
const g = gen();
console.log(g.name);//undefined
不可以把Generator函数当作普通构造函数,g返回的是遍历器对象,而非this。同时,既然不是构造函数,当然也不可以使用new操作符。
协程是一种程序运行方式,可以理解为”协作的线程“或”协作的函数“,协程可以由单线程实现也可以由多线程实现,前者是一种特殊的子例程,后者是一种特殊的线程。
传统的“子例程”(subroutine)采用堆栈式“后进先出”的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数。协程与其不同,多个线程(单线程情况下,即多个函数)可以并行执行,但是只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态(suspended),线程(或函数)之间可以交换执行权。也就是说,一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。
从实现上看,在内存中,子例程只使用一个栈(stack),而协程是同时存在多个栈,但只有一个栈是在运行状态,也就是说,协程是以多占用内存为代价,实现多任务的并行。
不难看出,协程适合用于多任务运行的环境。在这个意义上,它与普通的线程很相似,都有自己的执行上下文、可以分享全局变量。它们的不同之处在于,同一时间可以有多个线程处于运行状态,但是运行的协程只能有一个,其他协程都处于暂停状态。此外,普通的线程是抢先式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。
由于 JavaScript 是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束。
Generator 函数是 ES6 对协程的实现,但属于不完全实现。Generator 函数被称为“半协程”(semi-coroutine),意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。
如果将 Generator 函数当作协程,完全可以将多个需要互相协作的任务写成 Generator 函数,它们之间使用yield表达式交换控制权。
JavaScript代码运行时会产生一个全局的上下文环境,包含了当前所有的变量和函数。然后执行函数或代码块的时候,会在当前上下文环境的上层产生一个函数运行的上下文,变成当前的上下文,由此形成一个上下文环境的堆栈。
这个堆栈是”后进先出“的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,然后再执行它下层的上下文,直至所有的代码执行完成,清空堆栈。
但Generator函数不是这样的,执行产生的上下文环境一旦遇到yield命令就会暂时退出堆栈,但是并不消失,里面所有的变量和对象都处于冻结状态。等到调用next命令,这个上下文环境又重新加入调用栈,冻结的变量和对象恢复执行。
Generator本质上还是用于流程控制,yield*表达式可以实现诸如递归、数组数据展开等操作,但单纯的Generator处理异步问题还需要编写自执行函数,async函数则可以完美解决这个问题。
async是再ES2017引入的,本质上属于Generator的语法糖。
用Generator和Promise来读取2个文件:
const fs = require("fs");
function readFile(filePath){
return new Promise((resolve, reject)=>{
fs.readFile(filePath,"utf8",function(err,result){
if(!err){
resolve(result);
}else{
reject(err);
}
});
});
}
function* gen(){
const f1 = yield readFile("./file/test1.txt");
const f2 = yield readFile("./file/test2.txt");
}
Generator函数yield表达式的返回值是下一次调用next()方法的传参,而readFile又是异步函数,所以想实现串行执行,最终调用next()时还是需要回调嵌套:
const g = gen();
g.next().value.then((result)=>{
console.log(result);
return g.next().value;
}).then((result)=>{
console.log(result);
});
如果改成async函数,就简单多了:
async function gen(){
const f1 = await readFile("./file/test1.txt");
console.log(f1);
const f2 = await readFile("./file/test2.txt");
console.log(f2);
return {f1,f2}
}
gen().then((result)=>{
console.log(result);
});
不需要手动调用next()方法,async函数会自动执行。
1、await后边的表达式是一个Promise,否则会自动调用Promise.resolve()方法将其转化为Promise。
async function fn(){
return await "hello world";
}
fn().then(result=>{
console.log(result);
});
2、async函数执行后立即返回一个Promise,fulfilled回调函数的参数就是async函数内部的返回值。
async function fn(){
return {
text1:await "hello world",
text2:await "hello async function"
}
}
fn().then(result=>{
console.log(result);
});
3、async函数内部的await是串行执行的。
async function fn(){
return {
text1:await new Promise(resolve=>{
setTimeout(()=>{
resolve("hello world");
},3000)
}),
text2:await new Promise(resolve=>{
const text = "hello async function";
console.log(text);
resolve(text);
})
}
}
fn().then(result=>{
console.log(result);
});
4、当某个await表达式的Promise为rejected状态,那么async函数返回的Promise也为rejected,且不再执行后续的await。
async function gen(){
const f1 = await readFile("./file/test1.txt");
console.log(f1);
const f2 = await readFile("./file/test2.txtz");
console.log(f2);
return {f1,f2}
}
async function fn(){
return {
text1:await Promise.reject("hello world"),
text2:await new Promise(resolve=>{
const text = "hello async function";
console.log(text);
resolve(text);
})
}
}
fn().then(result=>{
console.log(result);
}).catch(err=>{
console.log(err);
});
5、await表达式整体返回Promise调用resolve()方法传入的参数。
如果该Promise是reject状态,那么就停止执行后续的代码了,所以也无从得出返回值是什么了。
6、async函数的多种声明方式。
const asyncFn = async function(){};
const asyncFn = async ()=>{};
const obj = {
async asyncFn(){}
};
const obj = {
asyncFn:async function(){}
};
const obj = {
asyncFn:async ()=>{}
};
class AsyncClass{
async asyncFn(){}
}
//node暂不支持class这种方法定义,Chrome支持
class AsyncClass{
asyncFn= async()=>{}
}
7、async函数内部抛出的错误会被外部的rejected回调函数或catch捕获。
const asyncFn = async function(){
throw new Error("error");
};
asyncFn().then(null,(err)=>{
console.log(err);
});
本章End~
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。