[TOC]
1. JS异步编程的方式
JS 异步编程的方式有:
- 回调函数
- 事件监听
- Promise
- Generator
- async/await
1.1 什么是同步?
所谓的同步就是在执行某段代码时,在该代码没有得到返回结果之前,其他代码暂时是无法执行的,但是一旦执行完成拿到返回值之后,就可以执行其他代码了。换句话说,在此段代码执行完未返回结果之前,会阻塞之后的代码执行,这样的情况称为同步。
1.2 什么是异步?
所谓异步就是当某一代码执行异步过程调用发出后,这段代码不会立刻得到返回结果。而是在异步调用发出之后,一般通过回调函数处理这个调用之后拿到结果。异步调用发出后,不会影响阻塞后面的代码执行,这样的情形称为异步。
1.3 JS 编程中为什么需要异步?
JavaScript 是单线程的,如果 JS 都是同步代码执行意味着什么呢?这样可能会造成阻塞,如果当前我们有一段代码需要执行时,如果使用同步的方式,那么就会阻塞后面的代码执行;而如果使用异步则不会阻塞,我们不需要等待异步代码执行的返回结果,可以继续执行该异步任务之后的代码逻辑。因此在 JS 编程中,会大量使用异步来进行编程
1.4. JS 异步编程方式发展历程
1.4.1 回调函数
从历史发展的脉络来看,早些年为了实现 JS 的异步编程,一般都采用回调函数的方式,比如比较典型的事件的回调,或者用 setTimeout/ setInterval 来实现一些异步编程的操作,但是使用回调函数来实现存在一个很常见的问题,那就是回调地狱。
fs.readFile(A, 'utf-8', function(err, data) {
fs.readFile(B, 'utf-8', function(err, data) {
fs.readFile(C, 'utf-8', function(err, data) {
fs.readFile(D, 'utf-8', function(err, data) {
//....
});
});
});
});
从上面的代码可以看出,其逻辑为先读取 A 文本内容,再根据 A 文本内容读取 B,然后再根据 B 的内容读取 C。为了实现这个业务逻辑,上面实现的代码就很容易形成回调地狱。回调实现异步编程的场景也有很多,比如:
- ajax 请求的回调
- 定时器中的回调
- 事件回调
- Nodejs 中的一些方法回调。
异步回调如果层级很少,可读性和代码的维护性暂时还是可以接受,一旦层级变多就会陷入回调地狱,上面这些异步编程的场景都会涉及回调地狱的问题。下面我们来看一下针对上面这个业务场景,改成 Promise 来实现异步编程,会是什么样子的呢?
1.4.2 Promise
为了解决回调地狱的问题,之后社区提出了 Promise 的解决方案,ES6 又将其写进了语言标准,采用 Promise 的实现方式在一定程度上解决了回调地狱的问题。
还是针对上面的这个场景来看下先读取 A 文本内容,再根据 A 文本内容读取 B 文件,接着再根据 B 文件的内容读取 C 文件
function read(url) {
return new Promise((resolve, reject) => {
fs.readFile(url, 'utf8', (err, data) => {
if(err) reject(err);
resolve(data);
});
});
}
read(A).then(data => {
return read(B);
}).then(data => {
return read(C);
}).then(data => {
return read(D);
}).catch(reason => {
console.log(reason);
});
从上面的代码可以看出,针对回调地狱进行这样的改进,可读性的确有一定的提升,优点是可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数,但是 Promise 也存在一些问题,即便是使用 Promise 的链式调用,如果操作过多,其实并没有从根本上解决回调地狱的问题,只是换了一种写法,可读性虽然有所提升,但是依旧很难维护。不过 Promise 又提供了一个 all 方法,对于这个业务场景的代码,用 all 来实现可能效果会更好。
function read(url) {
return new Promise((resolve, reject) => {
fs.readFile(url, 'utf8', (err, data) => {
if(err) reject(err);
resolve(data);
});
});
}
// 通过 Promise.all 可以实现多个异步并行执行,同一时刻获取最终结果的问题
Promise.all([read(A), read(B), read(C)]).then(data => {
console.log(data);
}).catch(err =>
console.log(err)
);
1.4.3 Generator
Generator 也是一种异步编程解决方案,它最大的特点就是可以交出函数的执行权,Generator 函数可以看出是异步任务的容器,需要暂停的地方,都用 yield 语法来标注。Generator 函数一般配合 yield 使用,Generator 函数最后返回的是迭代器。如果你对迭代器不太了解,可以自行补习一下这部分内容。
function* gen() {
let a = yield 111;
console.log(a);
let b = yield 222;
console.log(b);
let c = yield 333;
console.log(c);
let d = yield 444;
console.log(d);
}
let t = gen();
t.next(1); //第一次调用next函数时,传递的参数无效,故无打印结果
t.next(2); // a输出2;
t.next(3); // b输出3;
t.next(4); // c输出4;
t.next(5); // d输出5;
从上面的代码中可以看到输出结果,第一次的 next 虽然执行了但是并未输出结果,后面的每次执行 next 会把参数传入然后打印出来,等到最后一次 next 对应的 yield 执行完之后,控制台会打印 “{value: undefined, done: true}” 的输出结果,标识该 Generator 函数已经执行完毕,即 done:true
1.4.4 async/await
ES6 之后 ES7 中又提出了新的异步解决方案:async/await,async 是 Generator 函数的语法糖,async/await 的优点是代码清晰(不像使用 Promise 的时候需要写很多 then 的方法链),可以处理回调地狱的问题。async/await 写起来使得 JS 的异步代码看起来像同步代码,其实异步编程发展的目标就是让异步逻辑的代码看起来像同步一样容易理解
function testWait() {
return new Promise((resolve,reject)=>{
setTimeout(function(){
console.log("testWait");
resolve();
}, 1000);
})
}
async function testAwaitUse(){
await testWait()
console.log("hello");
return 123;
// 输出顺序:testWait,hello
// 第十行如果不使用await输出顺序:hello , testWait
}
console.log(testAwaitUse());
执行上面的代码,从结果中可以看出,在正常的执行顺序下,testWait 这个函数由于使用的是 setTimeout 的定时器,回调会在一秒之后执行,但是由于执行到这里采用了 await 关键词,testAwaitUse 函数在执行的过程中需要等待 testWait 函数执行完成之后,再执行打印 hello 的操作。但是如果去掉 await ,打印结果的顺序就会变化。
因此,async/await 不仅仅是 JS 的异步编程的一种方式,其可读性也接近于同步代码,让人更容易理解
总结
2.异步编程:Promise
如果一定要解释 Promise 到底是什么,简单来说它就是一个容器,里面保存着某个未来才会结束的事件(通常是异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。
Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。我们来简单看一下 Promise 实现的链式调用代码,如下所示。
function read(url) {
return new Promise((resolve, reject) => {
fs.readFile(url, 'utf8', (err, data) => {
if(err) reject(err);
resolve(data);
});
});
}
read(A).then(data => {
return read(B);
}).then(data => {
return read(C);
}).then(data => {
return read(D);
}).catch(reason => {
console.log(reason);
});
结合上面的代码,我们一起来分析一下 Promise 内部的状态流转情况,Promise 对象在被创建出来时是待定的状态,它让你能够把异步操作返回最终的成功值或者失败原因,和相应的处理程序关联起来。
一般 Promise 在执行过程中,必然会处于以下几种状态之一。
- 待定(pending):初始状态,既没有被完成,也没有被拒绝。
- 已完成(fulfilled):操作成功完成。
- 已拒绝(rejected):操作失败。
待定状态的 Promise 对象执行的话,最后要么会通过一个值完成,要么会通过一个原因被拒绝。当其中一种情况发生时,我们用 Promise 的 then 方法排列起来的相关处理程序就会被调用。因为最后 Promise.prototype.then 和 Promise.prototype.catch 方法返回的是一个 Promise, 所以它们可以继续被链式调用。
关于 Promise 的状态流转情况,有一点值得注意的是,内部状态改变之后不可逆。文字描述比较晦涩,直接通过一张图就能很清晰地看出 Promise 内部状态流转的情况。
从上图可以看出,我们最开始创建一个新的 Promise 返回给 p1 ,然后开始执行,状态是 pending,当执行 resolve 之后状态就切换为 fulfilled,执行 reject 之后就变为 rejected 的状态。
2.1 Promise 如何解决回调地狱
回调地狱有两个主要的问题:
- 多层嵌套的问题;
- 每种任务的处理结果存在两种可能性(成功或失败),那么需要在每种任务执行结束后分别处理这两种可能性。
这两种问题在“回调函数时代”尤为突出,Promise 的诞生就是为了解决这两个问题。Promise 利用了三大技术手段来解决回调地狱:回调函数延迟绑定、返回值穿透、错误冒泡。
let readFilePromise = filename => {
return new Promise((resolve, reject) => {
fs.readFile(filename, (err, data) => {
if (err) {
reject(err)
} else {
resolve(data)
}
})
})
}
readFilePromise('1.json').then(data => {
return readFilePromise('2.json')
});
从上面的代码中可以看到,回调函数不是直接声明的,而是通过后面的 then 方法传入的,即延迟传入,这就是回调函数延迟绑定
let x = readFilePromise('1.json').then(data => {
return readFilePromise('2.json') //这是返回的Promise
});
x.then(/* 内部逻辑省略 */)
我们根据 then 中回调函数的传入值创建不同类型的 Promise,然后把返回的 Promise 穿透到外层,以供后续的调用。这里的 x 指的就是内部返回的 Promise,然后在 x 后面可以依次完成链式调用。这便是返回值穿透的效果,这两种技术一起作用便可以将深层的嵌套回调写成下面的形式。
readFilePromise('1.json').then(data => {
return readFilePromise('2.json');
}).then(data => {
return readFilePromise('3.json');
}).then(data => {
return readFilePromise('4.json');
});
这样前面产生的错误会一直向后传递,被 catch 接收到,就不用频繁地检查错误了。从上面的这些代码中可以看到,Promise 解决效果也比较明显:实现链式调用,解决多层嵌套问题;实现错误冒泡后一站式处理,解决每次任务中判断错误、增加代码混乱度的问题。
2.2 Promise 的静态方法
从语法、参数以及方法的代码几个方面来分别介绍 all、allSettled、any、race 这四种方法。
2.2.1 all 方法
语法: Promise.all(iterable)
参数: 一个可迭代对象,如 Array。
描述: 此方法对于汇总多个 promise 的结果很有用,在 ES6 中可以将多个 Promise.all 异步请求并行操作,返回结果一般有下面两种情况
- 当所有结果成功返回时按照请求顺序返回成功。
- 当其中有一个失败方法时,则进入失败方法。
我们来看下业务的场景,对于下面这个业务场景页面的加载,将多个请求合并到一起,用 all 来实现可能效果会更好,请看代码片段。
//1.获取轮播数据列表
function getBannerList(){
return new Promise((resolve,reject)=>{
setTimeout(function(){
resolve('轮播数据')
},300)
})
}
//2.获取店铺列表
function getStoreList(){
return new Promise((resolve,reject)=>{
setTimeout(function(){
resolve('店铺数据')
},500)
})
}
//3.获取分类列表
function getCategoryList(){
return new Promise((resolve,reject)=>{
setTimeout(function(){
resolve('分类数据')
},700)
})
}
function initLoad(){
Promise.all([getBannerList(),getStoreList(),getCategoryList()])
.then(res=>{
console.log(res)
}).catch(err=>{
console.log(err)
})
}
initLoad()
从上面代码中可以看出,在一个页面中需要加载获取轮播列表、获取店铺列表、获取分类列表这三个操作,页面需要同时发出请求进行页面渲染,这样用 Promise.all 来实现,看起来更清晰、一目了然。
2.2.2 allSettled 方法
Promise.allSettled 的语法及参数跟 Promise.all 类似,其参数接受一个 Promise 的数组,返回一个新的 Promise。唯一的不同在于,执行完之后不会失败,也就是说当 Promise.allSettled 全部处理完成后,我们可以拿到每个 Promise 的状态,而不管其是否处理成功。
const resolved = Promise.resolve(2);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function (results) {
console.log(results);
});
// 返回结果:
// [
// { status: 'fulfilled', value: 2 },
// { status: 'rejected', reason: -1 }
// ]
从上面代码中可以看到,Promise.allSettled 最后返回的是一个数组,记录传进来的参数中每个 Promise 的返回值,这就是和 all 方法不太一样的地方。也可以根据 all 方法提供的业务场景的代码进行改造,其实也能知道多个请求发出去之后,Promise 最后返回的是每个参数的最终状态
2.2.3 any 方法
语法: Promise.any(iterable)
参数: iterable 可迭代的对象,例如 Array。
描述: any 方法返回一个 Promise,只要参数 Promise 实例有一个变成 fulfilled 状态,最后 any 返回的实例就会变成 fulfilled 状态;如果所有参数 Promise 实例都变成 rejected 状态,包装实例就会变成 rejected 状态
const resolved = Promise.resolve(2);
const rejected = Promise.reject(-1);
const anyPromise = Promise.any([resolved, rejected]);
anyPromise.then(function (results) {
console.log(results);
});
// 返回结果:
// 2
从改造后的代码中可以看出,只要其中一个 Promise 变成 fulfilled 状态,那么 any 最后就返回这个 Promise。由于上面 resolved 这个 Promise 已经是 resolve 的了,故最后返回结果为 2。
2.2.4 race 方法
语法: Promise.race(iterable)
参数: iterable 可迭代的对象,例如 Array。
描述: race 方法返回一个 Promise,只要参数的 Promise 之中有一个实例率先改变状态,则 race 方法的返回状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 race 方法的回调函数。
我们来看一下这个业务场景,对于图片的加载,特别适合用 race 方法来解决,将图片请求和超时判断放到一起,用 race 来实现图片的超时判断。请看代码片段。
//请求某个图片资源
function requestImg(){
var p = new Promise(function(resolve, reject){
var img = new Image();
img.onload = function(){ resolve(img); }
img.src = 'http://www.baidu.com/img/flexible/logo/pc/result.png';
});
return p;
}
//延时函数,用于给请求计时
function timeout(){
var p = new Promise(function(resolve, reject){
setTimeout(function(){ reject('图片请求超时'); }, 5000);
});
return p;
}
Promise.race([requestImg(), timeout()])
.then(function(results){
console.log(results);
})
.catch(function(reason){
console.log(reason);
});
从上面的代码中可以看出,采用 Promise 的方式来判断图片是否加载成功,也是针对 Promise.race 方法的一个比较好的业务场景。
综上,这四种方法的参数传递形式基本是一致的,但是最后每个方法实现的功能还是略微有些差异
总结
3.Generator、Async、await等异步编程的语法糖
3.1. Generator 基本介绍
Generator(生成器)是 ES6 的新关键词,学习起来比较晦涩难懂,那么什么是 Generator 的函数呢?通俗来讲 Generator 是一个带星号的“函数”(它并不是真正的函数),可以配合 yield 关键字来暂停或者执行函数。我们来看一段使用 Generator 的代码,如下所示。
function* gen() {
console.log("enter");
let a = yield 1;
let b = yield (function () {return 2})();
return 3;
}
var g = gen() // 阻塞住,不会执行任何语句
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())
// output:
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: true }
// { value: undefined, done: true }
结合上面的代码,我们分析一下 Generator 函数的执行情况。Generator 中配合使用 yield 关键词可以控制函数执行的顺序,每当执行一次 next 方法,Generator 函数会执行到下一个存在 yield 关键词的位置。
总结下来,Generator 的执行有这几个关键点。
- 调用 gen() 后,程序会阻塞住,不会执行任何语句。
- 调用 g.next() 后,程序继续执行,直到遇到 yield 关键词时执行暂停。
- 一直执行 next 方法,最后返回一个对象,其存在两个属性:value 和 done
3.2. yield基本介绍
yield 同样也是 ES6 的新关键词,配合 Generator 执行以及暂停。yield 关键词最后返回一个迭代器对象,该对象有 value 和 done 两个属性,其中 done 属性代表返回值以及是否完成。yield 配合着 Generator,再同时使用 next 方法,可以主动控制 Generator 执行进度。
function* gen1() {
yield 1;
yield* gen2();
yield 4;
}
function* gen2() {
yield 2;
yield 3;
}
var g = gen1();
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())
// output:
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// { value: 4, done: false }
// {value: undefined, done: true}
从上面的代码中可以看出,使用 yield 关键词的话还可以配合着 Generator 函数嵌套使用,从而控制函数执行进度。这样对于 Generator 的使用,以及最终函数的执行进度都可以很好地控制,从而形成符合你设想的执行顺序。即便 Generator 函数相互嵌套,也能通过调用 next 方法来按照进度一步步执行。
3.3. thunk 函数介绍
let isString = (obj) => {
return Object.prototype.toString.call(obj) === '[object String]';
};
let isFunction = (obj) => {
return Object.prototype.toString.call(obj) === '[object Function]';
};
let isArray = (obj) => {
return Object.prototype.toString.call(obj) === '[object Array]';
};
....
可以看到,其中出现了非常多重复的数据类型判断逻辑,平常业务开发中类似的重复逻辑的场景也同样会有很多。我们将它们做一下封装,如下所示。
let isType = (type) => {
return (obj) => {
return Object.prototype.toString.call(obj) === `[object ${type}]`;
}
}
那么封装了之后我们可以这么来使用,从而来减少重复的逻辑代码,如下所示。
let isString = isType('String');
let isArray = isType('Array');
isString("123"); // true
isArray([1,2,3]); // true
相应的 isString 和 isArray 是由 isType 方法生产出来的函数,通过上面的方式来改造代码,明显简洁了不少。像 isType 这样的函数我们称为 thunk 函数,它的基本思路都是接收一定的参数,会生产出定制化的函数,最后使用定制化的函数去完成想要实现的功能。
3.4. Generator 和 thunk 结合
const readFileThunk = (filename) => {
return (callback) => {
fs.readFile(filename, callback);
}
}
const gen = function* () {
const data1 = yield readFileThunk('1.txt')
console.log(data1.toString())
const data2 = yield readFileThunk('2.txt')
console.log(data2.toString)
}
let g = gen();
g.next().value((err, data1) => {
g.next(data1).value((err, data2) => {
g.next(data2);
})
})
readFileThunk 就是一个 thunk 函数,上面的这种编程方式就让 Generator 和异步操作关联起来了。上面第三段代码执行起来嵌套的情况还算简单,如果任务多起来,就会产生很多层的嵌套,可读性不强,因此我们有必要把执行的代码封装优化一下,如下所示
function run(gen){
const next = (err, data) => {
let res = gen.next(data);
if(res.done) return;
res.value(next);
}
next();
}
run(g);
改造完之后,我们可以看到 run 函数和上面的执行效果其实是一样的。代码虽然只有几行,但其包含了递归的过程,解决了多层嵌套的问题,并且完成了异步操作的一次性的执行效果
3.5. Generator 和 Promise 结合
还是利用上面的输出文件的例子,对代码进行改造,如下所示
// 最后包装成 Promise 对象进行返回
const readFilePromise = (filename) => {
return new Promise((resolve, reject) => {
fs.readFile(filename, (err, data) => {
if(err) {
reject(err);
}else {
resolve(data);
}
})
}).then(res => res);
}
let g = gen();
// 这块和上面 thunk 的方式一样
const gen = function* () {
const data1 = yield readFilePromise('1.txt')
console.log(data1.toString())
const data2 = yield readFilePromise('2.txt')
console.log(data2.toString)
}
// 这块和上面 thunk 的方式一样
function run(gen){
const next = (err, data) => {
let res = gen.next(data);
if(res.done) return;
res.value.then(next);
}
next();
}
run(g);
从上面的代码可以看出,thunk 函数的方式和通过 Promise 方式执行效果本质上是一样的,只不过通过 Promise 的方式也可以配合 Generator 函数实现同样的异步操作
3.6. co 函数库
co 函数库是著名程序员 TJ 发布的一个小工具,用于处理 Generator 函数的自动执行。核心原理其实就是上面讲的通过和 thunk 函数以及 Promise 对象进行配合,包装成一个库。它使用起来非常简单,比如还是用上面那段代码,第三段代码就可以省略了,直接引用 co 函数,包装起来就可以使用了,代码如下
const co = require('co');
let g = gen();
co(g).then(res =>{
console.log(res);
})
这段代码比较简单,几行就完成了之前写的递归的那些操作。那么为什么 co 函数库可以自动执行 Generator 函数,它的处理原理是什么呢?
- 因为 Generator 函数就是一个异步操作的容器,它需要一种自动执行机制,co 函数接受 Generator 函数作为参数,并最后返回一个 Promise 对象。
- 在返回的 Promise 对象里面,co 先检查参数 gen 是否为 Generator 函数。如果是,就执行该函数;如果不是就返回,并将 Promise 对象的状态改为 resolved。
- co 将 Generator 函数的内部指针对象的 next 方法,包装成 onFulfilled 函数。这主要是为了能够捕捉抛出的错误。
- 关键的是 next 函数,它会反复调用自身。
关于 co 的内部原理,你可以去 co 的源码库学习
3.7. sync/await 介绍
JS 的异步编程从最开始的回调函数的方式,演化到使用 Promise 对象,再到 Generator+co 函数的方式,每次都有一些改变,但又让人觉得不彻底,都需要理解底层运行机制。
而 async/await 被称为 JS 中异步终极解决方案,它既能够像 co+Generator 一样用同步的方式来书写异步代码,又得到底层的语法支持,无须借助任何第三方库。
// readFilePromise 依旧返回 Promise 对象
const readFilePromise = (filename) => {
return new Promise((resolve, reject) => {
fs.readFile(filename, (err, data) => {
if(err) {
reject(err);
}else {
resolve(data);
}
})
}).then(res => res);
}
// 这里把 Generator的 * 换成 async,把 yield 换成 await
const gen = async function() {
const data1 = await readFilePromise('1.txt')
console.log(data1.toString())
const data2 = await readFilePromise('2.txt')
console.log(data2.toString)
}
从上面的代码中可以看到,虽然简单地将 Generator 的 * 号换成了 async,把 yield 换成了 await,但其实 async 的内部做了不少工作。
总结下来,async 函数对 Generator 函数的改进,主要体现在以下三点:
- **内置执行器:**Generator 函数的执行必须靠执行器,因为不能一次性执行完成,所以之后才有了开源的 co 函数库。但是,async 函数和正常的函数一样执行,也不用 co 函数库,也不用使用 next 方法,而 async 函数自带执行器,会自动执行。
- **适用性更好:**co 函数库有条件约束,yield 命令后面只能是 Thunk 函数或 Promise 对象,但是 async 函数的 await 关键词后面,可以不受约束。
- **可读性更好:**async 和 await,比起使用 * 号和 yield,语义更清晰明了。
async function func() {
return 100;
}
console.log(func());
// Promise {<fulfilled>: 100}
从执行的结果可以看出,async 函数 func 最后返回的结果直接是 Promise 对象,比较方便让开发者继续往后处理。而之前 Generator 并不会自动执行,需要通过 next 方法控制,最后返回的也并不是 Promise 对象,而是需要通过 co 函数库来实现最后返回 Promise 对象。
总结
4. 实现一个 EventEmitter
4.1 Events 基本介绍
Node.js的events 模块对外提供了一个 EventEmitter 对象,用于对 Node.js 中的事件进行统一管理。因为 Node.js 采用了事件驱动机制,而 EventEmitter 就是 Node.js 实现事件驱动的基础。在 EventEmitter 的基础上,Node.js 中几乎所有的模块都继承了这个类,以实现异步事件驱动架构。
EventEmitter的简单使用情况,代码如下。
var events = require('events');
var eventEmitter = new events.EventEmitter();
eventEmitter.on('say',function(name){
console.log('Hello',name);
})
eventEmitter.emit('say','Jonh');
以上代码中,新定义的eventEmitter 是接收 events.EventEmitter 模块 new 之后返回的一个实例,eventEmitter 的 emit 方法,发出 say 事件,通过 eventEmitter 的 on 方法监听,从而执行相应的函数。
4.2 常用的 EventEmitter 模块的 API
除了上面的那段代码中已经使用的 on 和emit 这两个 API,EventEmitter还提供了其他的 API 方法通过一个表格简单整理了一下对应的方法和功能总结。
除此之外,还有两个特殊的事件,不需要额外手动添加,下表所示的就是 Node.js 的 EventEmitter 模块自带的特殊事件。
4.2.1 addListener 和 removeListener、on 和 off 方法对比
addListener 方法的作用是为指定事件添加一个监听器,其实和 on 方法实现的功能是一样的,on 其实就是 addListener 方法的一个别名。二者实现的作用是一样的,同时 removeListener 方法的作用是为移除某个事件的监听器,同样 off 也是 removeListener 的别名。
var events = require('events');
var emitter = new events.EventEmitter();
function hello1(name){
console.log("hello 1",name);
}
function hello2(name){
console.log("hello 2",name);
}
emitter.addListener('say',hello1);
emitter.addListener('say',hello2);
emitter.emit('say','John');
//输出hello 1 John
//输出hello 2 John
emitter.removeListener('say',hello1);
emitter.emit('say','John');
//相应的,监听say事件的hello1事件被移除
//只输出hello 2 John
4.2.2 removeListener 和 removeAllListeners
removeListener 方法是指移除一个指定事件的某一个监听器,而 removeAllListeners 指的是移除某一个指定事件的全部监听器
var events = require('events');
var emitter = new events.EventEmitter();
function hello1(name){
console.log("hello 1",name);
}
function hello2(name){
console.log("hello 2",name);
}
emitter.addListener('say',hello1);
emitter.addListener('say',hello2);
emitter.removeAllListeners('say');
emitter.emit('say','John');
//removeAllListeners 移除了所有关于 say 事件的监听
//因此没有任何输出
4.2.3 on 和 once 方法区别
on 和 once 的区别是:on 的方法对于某一指定事件添加的监听器可以持续不断地监听相应的事件;而 once 方法添加的监听器,监听一次后,就会被消除
var events = require('events');
var emitter = new events.EventEmitter();
function hello(name){
console.log("hello",name);
}
emitter.on('say',hello);
emitter.emit('say','John');
emitter.emit('say','Lily');
emitter.emit('say','Lucy');
//会输出 hello John、hello Lily、hello Lucy,之后还要加也可以继续触发
emitter.once('see',hello);
emitter.emit('see','Tom');
//只会输出一次 hello Tom
也就是说,on 方法监听的事件,可以持续不断地被触发,而 once 方法只会触发一次
4.3 实现一个浏览器端的EventEmitter
EventEmitter 是在Node.js 中 events 模块里封装的,那么在浏览器端实现一个这样的 EventEmitter 是否可以呢?其实自己封装一个能在浏览器中跑的EventEmitter,并应用在你的业务代码中还是能带来不少方便的,它可以帮你实现自定义事件的订阅和发布,从而提升业务开发的便利性。
那么结合上面介绍的内容,实现一个基础版本的EventEmitter,包含基础的on、 of、emit、once、allof 这几个方法
function EventEmitter() {
this.__events = {}
}
EventEmitter.VERSION = '1.0.0';
从上面的代码中可以看到,我们先初始化了一个内部的__events 的对象,用来存放自定义事件,以及自定义事件的回调函数。
4.3.1 实现 EventEmitter的 on 的方法
EventEmitter.prototype.on = function(eventName, listener){
if (!eventName || !listener) return;
// 判断回调的 listener 是否为函数
if (!isValidListener(listener)) {
throw new TypeError('listener must be a function');
}
var events = this.__events;
var listeners = events[eventName] = events[eventName] || [];
var listenerIsWrapped = typeof listener === 'object';
// 不重复添加事件,判断是否有一样的
if (indexOf(listeners, listener) === -1) {
listeners.push(listenerIsWrapped ? listener : {
listener: listener,
once: false
});
}
return this;
};
// 判断是否是合法的 listener
function isValidListener(listener) {
if (typeof listener === 'function') {
return true;
} else if (listener && typeof listener === 'object') {
return isValidListener(listener.listener);
} else {
return false;
}
}
// 顾名思义,判断新增自定义事件是否存在
function indexOf(array, item) {
var result = -1
item = typeof item === 'object' ? item.listener : item;
for (var i = 0, len = array.length; i < len; i++) {
if (array[i].listener === item) {
result = i;
break;
}
}
return result;
}
从上面的代码中可以看出,on 方法的核心思路就是,当调用订阅一个自定义事件的时候,只要该事件通过校验合法之后,就把该自定义事件 push 到 this.__events 这个对象中存储,等需要出发的时候,则直接从通过获取 __events 中对应事件的 listener 回调函数,而后直接执行该回调方法就能实现想要的效果。
4.3.2 emit 方法实现触发效果
EventEmitter.prototype.emit = function(eventName, args) {
// 直接通过内部对象获取对应自定义事件的回调函数
var listeners = this.__events[eventName];
if (!listeners) return;
// 需要考虑多个 listener 的情况
for (var i = 0; i < listeners.length; i++) {
var listener = listeners[i];
if (listener) {
listener.listener.apply(this, args || []);
// 给 listener 中 once 为 true 的进行特殊处理
if (listener.once) {
this.off(eventName, listener.listener)
}
}
}
return this;
};
EventEmitter.prototype.off = function(eventName, listener) {
var listeners = this.__events[eventName];
if (!listeners) return;
var index;
for (var i = 0, len = listeners.length; i < len; i++) {
if (listeners[i] && listeners[i].listener === listener) {
index = i;
break;
}
}
// off 的关键
if (typeof index !== 'undefined') {
listeners.splice(index, 1, null)
}
return this;
};
从上面的代码中可以看出 emit 的处理方式,其实就是拿到对应自定义事件进行 apply 执行,在执行过程中对于一开始 once 方法绑定的自定义事件进行特殊的处理,当once 为 true的时候,再触发 off 方法对该自定义事件进行解绑,从而实现自定义事件一次执行的效果。
4.3.3 once 方法和 alloff的实现
EventEmitter.prototype.once = function(eventName, listener){
// 直接调用 on 方法,once 参数传入 true,待执行之后进行 once 处理
return this.on(eventName, {
listener: listener,
once: true
})
};
EventEmitter.prototype.allOff = function(eventName) {
// 如果该 eventName 存在,则将其对应的 listeners 的数组直接清空
if (eventName && this.__events[eventName]) {
this.__events[eventName] = []
} else {
this.__events = {}
}
};
从上面的代码中可以看到,once 方法的本质还是调用 on 方法,只不过传入的参数区分和非一次执行的情况。当再次触发 emit 方法的时候,once 绑定的执行一次之后再进行解绑。
这样,alloff 方法也很好理解了,其实就是对内部的__events 对象进行清空,清空之后如果再次触发自定义事件,也就无法触发回调函数了。
总结
EventEmitter 采用的正是发布-订阅模式
另外,观察者模式和发布-订阅模式有些类似的地方。发布-订阅模式其实是观察者模式的一种变形,区别在于:发布-订阅模式在观察者模式的基础上,在目标和观察者之间增加了一个调度中心。
通过这一学习,你应该基本能实现一个 EventEmitter 了。单就浏览器端使用场景来说,其实也有运用同样的思路解决问题的工具,在 Vue 框架中不同组件之间的通讯里,有一种解决方案叫 EventBus。和 EventEmitter的思路类似,它的基本用途是将 EventBus 作为组件传递数据的桥梁,所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件,所有组件都可以收到通知,使用起来非常便利,其核心其实就是发布-订阅模式的落地实现。
5. 实现一个符合 Promise/A+ 规范的 Promise
5.1 Promise/A+ 规范
官方的地址为:promisesaplus.com/
5.1.1 术语
Promise/A+ 规范的基本术语,如下所示
“promise” is an object or function with a then method whose behavior conforms to this specification. “thenable” is an object or function that defines a then method. “value” is any legal JavaScript value (including undefined, a thenable, or a promise). “exception” is a value that is thrown using the throw statement. “reason” is a value that indicates why a promise was rejected.
翻译过来,它所描述的就是以下五点
- “promise”:是一个具有 then 方法的对象或者函数,它的行为符合该规范。
- “thenable”:是一个定义了 then 方法的对象或者函数。
- “value”:可以是任何一个合法的 JavaScript 的值(包括 undefined、thenable 或 promise)。
- “exception”:是一个异常,是在 Promise 里面可以用 throw 语句抛出来的值。
- “reason”:是一个 Promise 里 reject 之后返回的拒绝原因。
5.1.2 状态描述
看完了术语部分,我们再看下Promise/A+ 规范中,对 Promise 的内部状态的描述,如下所示。
A promise must be in one of three states: pending, fulfilled, or rejected. When pending, a promise: may transition to either the fulfilled or rejected state. When fulfilled, a promise: must not transition to any other state. must have a value, which must not change. When rejected, a promise: must not transition to any other state. must have a reason, which must not change. Here, “must not change” means immutable identity (i.e. ===), but does not imply deep immutability.
将上述描述总结起来,大致有以下几点。
- 一个 Promise 有三种状态:pending、fulfilled 和 rejected。
- 当状态为 pending 状态时,即可以转换为 fulfilled 或者 rejected 其中之一。
- 当状态为 fulfilled 状态时,就不能转换为其他状态了,必须返回一个不能再改变的值。
- 当状态为 rejected 状态时,同样也不能转换为其他状态,必须有一个原因的值也不能改变。
5.1.3 then 方法
关于 then 方法的英文解读和翻译,总结:一个 Promise 必须拥有一个 then 方法来访问它的值或者拒绝原因。
then 方法有两个参数:
promise.then(onFulfilled, onRejected)
onFulfilled 和 onRejected 都是可选参数
onFulfilled 和 onRejected 特性
如果 onFulfilled 是函数,则当 Promise 执行结束之后必须被调用,最终返回值为 value,其调用次数不可超过一次。而 onRejected 除了最后返回的是 reason 外,其他方面和 onFulfilled 在规范上的表述基本一样。
多次调用
then 方法其实可以被一个 Promise 调用多次,且必须返回一个 Promise 对象。then 的写法如下所示,其中 Promise1 执行了 then 的方法之后,返回的依旧是个 Promise2,然后我们拿着 Promise2 又可以执行 then 方法,而 Promise2 是一个新的 Promise 对象,又可以继续进行 then 方法调用。
promise2 = promise1.then(onFulfilled, onRejected);
规范里面还有很大一部分讲解的是 Promise 的解决过程。其实只看规范的话,整体感觉很空洞,方才已经将规范的主要部分列举了,这些内容基本可以指导我们自己实现一个 Promise 了。
5.2 一步步实现 Promise
按照 Promise/A+ 的规范,第一步就是构造函数。
5.2.1 构造函数
这一步的思路是:Promise 构造函数接受一个 executor 函数,executor 函数执行完同步或者异步操作后,调用它的两个参数 resolve 和 reject。请看下面的代码,大致的构造函数框架就是这样的。
function Promise(executor) {
var self = this
self.status = 'pending' // Promise当前的状态
self.data = undefined // Promise的值
self.onResolvedCallback = [] // Promise resolve时的回调函数集
self.onRejectedCallback = [] // Promise reject时的回调函数集
executor(resolve, reject) // 执行executor并传入相应的参数
}
从上面的代码中可以看出,我们先定义了一个 Promise 的初始状态 pending,以及参数执行函数 executor,并且按照规范设计了一个 resolve 回调函数集合数组 onResolvedCallback 以及 一个 reject 回调函数集合数组,那么构造函数的初始化就基本完成了。
接下来还需要添加什么东西呢?那就是需要在构造函数中完善 resolve 和 reject 两个函数,完善之后的代码如下。
function Promise(executor) {
var self = this
self.status = 'pending' // Promise当前的状态
self.data = undefined // Promise的值
self.onResolvedCallback = [] // Promise resolve时的回调函数集
self.onRejectedCallback = [] // Promise reject时的回调函数集
function resolve(value) {
// TODO
}
function reject(reason) {
// TODO
}
try { // 考虑到执行过程中有可能出错,所以我们用try/catch块给包起
executor(resolve, reject) // 执行executor
} catch(e) {
reject(e)
}
}
resolve 和 reject 内部应该怎么实现呢?我们根据规范知道这两个方法主要做的事情就是返回对应状态的值 value 或者 reason,并把 Promise 内部的 status 从 pending 变成对应的状态,并且这个状态在改变了之后是不可以逆转的。
那么这两个函数应该怎么写呢?可以看下面的这段代码
function Promise(executor) {
// ...上面的省略
function resolve(value) {
if (self.status === 'pending') {
self.status = 'resolved'
self.data = value
for(var i = 0; i < self.onResolvedCallback.length; i++) {
self.onResolvedCallback[i](value)
}
}
}
function reject(reason) {
if (self.status === 'pending') {
self.status = 'rejected'
self.data = reason
for(var i = 0; i < self.onRejectedCallback.length; i++) {
self.onRejectedCallback[i](reason)
}
}
}
// 下面的省略
}
述代码所展示的,基本就是在判断状态为 pending 之后,把状态改为相应的值,并把对应的 value 和 reason 存在内部的 data 属性上面,之后执行相应的回调函数。逻辑比较简单,无非是由于 onResolveCallback 和 onRejectedCallback 这两个是数组,需要通过循环来执行。
好了,构造函数基本就实现了,那么再看看如何实现 then 方法,从而保证可以实现链式调用。
5.2.2 实现 then 方法
根据标准,要考虑几个问题。
then 方法是 Promise 执行完之后可以拿到 value 或者 reason 的方法,并且还要保持 then 执行之后,返回的依旧是一个 Promise 方法,还要支持多次调用(上面标准中提到过)。
因此 then 方法实现的思路也有了,请看下面的一段代码
// then方法接收两个参数onResolved和onRejected,分别为Promise成功或失败后的回调
Promise.prototype.then = function(onResolved, onRejected) {
var self = this
var promise2
// 根据标准,如果then的参数不是function,则需要忽略它
onResolved = typeof onResolved === 'function' ? onResolved : function(v) {}
onRejected = typeof onRejected === 'function' ? onRejected : function(r) {}
if (self.status === 'resolved') {
return promise2 = new Promise(function(resolve, reject) {
})
}
if (self.status === 'rejected') {
return promise2 = new Promise(function(resolve, reject) {
})
}
if (self.status === 'pending') {
return promise2 = new Promise(function(resolve, reject) {
})
}
}
从上面的代码中可以看到,在 then 方法内部先初始化了 Promise2 的对象,用来存放执行之后返回的 Promise,并且还需要判断 then 方法传参进来的两个参数必须为函数,这样才可以继续执行。
上面只是搭建了then方法框架的整体思路,但是不同状态的返回细节处理也需要完善,通过仔细阅读标准,完善之后的 then 的代码如下。
Promise.prototype.then = function(onResolved, onRejected) {
var self = this
var promise2
// 根据标准,如果then的参数不是function,则需要忽略它
onResolved = typeof onResolved === 'function' ? onResolved : function(value) {}
onRejected = typeof onRejected === 'function' ? onRejected : function(reason) {}
if (self.status === 'resolved') {
// 如果promise1的状态已经确定并且是resolved,我们调用onResolved,考虑到有可能throw,所以还需要将其包在try/catch块里
return promise2 = new Promise(function(resolve, reject) {
try {
var x = onResolved(self.data)
if (x instanceof Promise) {
// 如果onResolved的返回值是一个Promise对象,直接取它的结果作为promise2的结果
x.then(resolve, reject)
}
resolve(x) // 否则,以它的返回值作为promise2的结果
} catch (e) {
reject(e) // 如果出错,以捕获到的错误作为promise2的结果
}
})
}
// 此处与前一个if块的逻辑几乎相同,区别在于所调用的是onRejected函数
if (self.status === 'rejected') {
return promise2 = new Promise(function(resolve, reject) {
try {
var x = onRejected(self.data)
if (x instanceof Promise) {
x.then(resolve, reject)
}
} catch (e) {
reject(e)
}
})
}
if (self.status === 'pending') {
// 如果当前的Promise还处于pending状态,我们并不能确定调用onResolved还是onRejected,只能等到Promise的状态确定后,才能确定如何处理
return promise2 = new Promise(function(resolve, reject) {
self.onResolvedCallback.push(function(value) {
try {
var x = onResolved(self.data)
if (x instanceof Promise) {
x.then(resolve, reject)
}
} catch (e) {
reject(e)
}
})
self.onRejectedCallback.push(function(reason) {
try {
var x = onRejected(self.data)
if (x instanceof Promise) {
x.then(resolve, reject)
}
} catch (e) {
reject(e)
}
})
})
}
}
根据上面的代码可以看出,基本实现了一个符合标准的 then 方法。但是标准里提到了,还要支持不同的 Promise 进行交互,关于不同的 Promise 交互其实Promise 标准说明中有提到。其中详细指定了如何通过 then 的实参返回的值来决定 Promise2 的状态。
关于为何需要不同的 Promise 实现交互,原因应该是 Promise 并不是 JS 一开始存在的标准,如果你使用的某一个库中封装了一个 Promise 的实现,想象一下如果它不能跟你自己使用的 Promise 实现交互的情况,其实还是会有问题的,因此我们还需要调整一下 then 方法中执行 Promise 的方法。
另外还有一个需要注意的是,在 Promise/A+ 规范中,onResolved 和 onRejected 这两项函数需要异步调用,关于这一点,标准里面是这么说的:
In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack.
在实践中,这一要求确保onFulfilled和onRejected在调用事件循环之后异步执行,并使用新的堆栈。
所以需要对代码做一点变动,即在处理 Promise 进行 resolve 或者 reject 的时候,加上 setTimeout(fn, 0)。
下面就结合上面两点调整,写出完整版的代码
try {
module.exports = Promise
} catch (e) {}
function Promise(executor) {
var self = this
self.status = 'pending'
self.onResolvedCallback = []
self.onRejectedCallback = []
function resolve(value) {
if (value instanceof Promise) {
return value.then(resolve, reject)
}
setTimeout(function() { // 异步执行所有的回调函数
if (self.status === 'pending') {
self.status = 'resolved'
self.data = value
for (var i = 0; i < self.onResolvedCallback.length; i++) {
self.onResolvedCallback[i](value)
}
}
})
}
function reject(reason) {
setTimeout(function() { // 异步执行所有的回调函数
if (self.status === 'pending') {
self.status = 'rejected'
self.data = reason
for (var i = 0; i < self.onRejectedCallback.length; i++) {
self.onRejectedCallback[i](reason)
}
}
})
}
try {
executor(resolve, reject)
} catch (reason) {
reject(reason)
}
}
function resolvePromise(promise2, x, resolve, reject) {
var then
var thenCalledOrThrow = false
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise!'))
}
if (x instanceof Promise) {
if (x.status === 'pending') {
x.then(function(v) {
resolvePromise(promise2, v, resolve, reject)
}, reject)
} else {
x.then(resolve, reject)
}
return
}
if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) {
try {
then = x.then
if (typeof then === 'function') {
then.call(x, function rs(y) {
if (thenCalledOrThrow) return
thenCalledOrThrow = true
return resolvePromise(promise2, y, resolve, reject)
}, function rj(r) {
if (thenCalledOrThrow) return
thenCalledOrThrow = true
return reject(r)
})
} else {
resolve(x)
}
} catch (e) {
if (thenCalledOrThrow) return
thenCalledOrThrow = true
return reject(e)
}
} else {
resolve(x)
}
}
Promise.prototype.then = function(onResolved, onRejected) {
var self = this
var promise2
onResolved = typeof onResolved === 'function' ? onResolved : function(v) {
return v
}
onRejected = typeof onRejected === 'function' ? onRejected : function(r) {
throw r
}
if (self.status === 'resolved') {
return promise2 = new Promise(function(resolve, reject) {
setTimeout(function() { // 异步执行onResolved
try {
var x = onResolved(self.data)
resolvePromise(promise2, x, resolve, reject)
} catch (reason) {
reject(reason)
}
})
})
}
if (self.status === 'rejected') {
return promise2 = new Promise(function(resolve, reject) {
setTimeout(function() { // 异步执行onRejected
try {
var x = onRejected(self.data)
resolvePromise(promise2, x, resolve, reject)
} catch (reason) {
reject(reason)
}
})
})
}
if (self.status === 'pending') {
// 这里之所以没有异步执行,是因为这些函数必然会被resolve或reject调用,而resolve或reject函数里的内容已是异步执行,构造函数里的定义
return promise2 = new Promise(function(resolve, reject) {
self.onResolvedCallback.push(function(value) {
try {
var x = onResolved(value)
resolvePromise(promise2, x, resolve, reject)
} catch (r) {
reject(r)
}
})
self.onRejectedCallback.push(function(reason) {
try {
var x = onRejected(reason)
resolvePromise(promise2, x, resolve, reject)
} catch (r) {
reject(r)
}
})
})
}
}
Promise.prototype.catch = function(onRejected) {
return this.then(null, onRejected)
}
// 最后这个是测试用的,后面会说
Promise.deferred = Promise.defer = function() {
var dfd = {}
dfd.promise = new Promise(function(resolve, reject) {
dfd.resolve = resolve
dfd.reject = reject
})
return dfd
}
最终版的 Promise 的实现还是需要经过规范的测试(Promise /A+ 规范测试的工具地址为:github.com/promises-ap… deferred 方法(即 exports.deferred 方法),上面提供的代码中我已经将其加了进去。
最后,执行如下代码 npm 安装之后,即可执行测试
npm i -g promises-aplus-tests
promises-aplus-tests Promise.js