高级前端软件工程师知识整理之异步篇

1. 介绍 Promise函数,它的内部实现是什么?

Promise 是异步编程的一种解决方案,比传统的解决方案回调函数CallBack更合理,。Promise对象有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。一旦状态确定就无法改变,Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected

Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

Promise的内部实现代码

const promise = new Promise(function(resolve, reject) {
  // 注意,Promise 新建后就会立即执行
  // ... some code
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

resolve表示异步操作成功,reject表示异步操作失败,通过then来监听结果,then中的第二个监听失败的参数时可选的,可以使用catch来代替,如:

promise.then(function(value) {
  // success
}).catch(function(error){
  // failure
});

关于catch的机制,还有一点值得注意,它可以捕获多层then抛出的错误,如:

promise.then(function(value) {
  // return Promise(...)
}).then(function(){
  // success
}).catch(function(error){
  // failure
});

无论是在第一个then执行过程中发生错误,还是第二个then发生错误,都会在最后的catch中统一捕获。

关于Promise嵌套,如:

const p1 = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error('fail')), 3000)
})

const p2 = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(p1), 1000)
})

p2.then(result => console.log(result))
  .catch(error => console.log(error))
// Error: fail

p1p2都是 Promise 的实例,但是p2resolve方法将p1作为参数,即一个异步操作的结果是返回另一个异步操作。这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。即由于p2返回的是另一个 Promise,导致p2自己的状态无效了。所以看p2的then或catch函数响应的是哪个异步状态,那就是p1。

另外还有一点,resolve和reject不等同于return,不会终止代码继续往下执行,但一般来说,调用resolvereject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolvereject的后面。所以,最好在它们前面加上return语句,这样就不会有意外。

new Promise((resolve, reject) => {
  return resolve(1);
  // 后面的语句不会执行
  console.log(2);
})

关于Promise.prototype.finally(),不管promise最后的状态,在执行完thencatch指定的回调函数以后,都会执行finally方法指定的回调函数。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

关于Promise.all(),用于将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.all([p1, p2, p3]);

Promise.all方法接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会先调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。p的状态由p1p2p3决定,分成两种情况:

  • 只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3resolve返回值组成一个数组,传递给p的回调函数,作为p.then(resolver, reject)中resolver的回调参数。
  • 只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数,并被catch捕获或作为p.then(resolver, reject)中reject的回调参数。
const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
.then(result => result);

const p2 = new Promise((resolve, reject) => {
  throw new Error('报错了');
})
.then(result => result);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 报错了

其中p2报错,错误被Promise.all().catch捕获。但有一种情况,就是p1或p2也加了catch,则错误会在p1或p2的catch中捕获,不会冒泡到Promise.all().catch,此时Promise.all()认为虽然你发生错误,但实际上你自己catch之后返回的是一个新的 Promise 实例,最终执行resolve。如:

const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
.then(result => result)
.catch(e => e);

const p2 = new Promise((resolve, reject) => {
  throw new Error('报错了');
})
.then(result => result)
.catch(e => e);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 报错了]

关于Promise.race(),同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.race([p1, p2, p3]);

只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

关于Promise.resolve(),实际上是返回一个新的Promise对象

Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

Promise.reject() 同理。

2. 介绍 Generator 函数及其异步应用?

Generator 函数是 ES6 提供的一种异步编程解决方案,语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

Generator 函数有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态。

关于Generator基本用法:

function* generatorFun() {
	yield 'hello';
	yield 'world';
	return 'ending';
	yield 'after return';
}

var fun = generatorFun();
console.log(fun); // Generator对象
console.log(fun.next()); // {value: "hello", done: false} value表示表达式的计算结果,done表示遍历是否已经结束
console.log(fun.next()); // {value: "world", done: false}
console.log(fun.next()); // {value: "ending", done: true}
console.log(fun.next()); // {value: undefined, done: true}

即该函数有三个状态:hello,world 和 return 语句,由于after return已经在return后面,所以代码不会被执行。调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的Generator指针对象。要执行函数内的代码,必须使用next方法,next方法的返回值是一个{value: xxx, done:xxx}对象,同时使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行

关于next 方法的参数yield表达式本身没有返回值,或者说总是返回undefinednext方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

a的执行过程:

  • 第一个next,value值等于执行yield (x + 1) ,为6,由于yield表达式本身没有返回值,所以y等于undefined。
  • 第二个next,value值等于执行yield (y / 3),由于y等于undefined,所以value为NaN,由于yield表达式本身没有返回值,z等于undefined。
  • 第三个next,value值等于执行(x + y + z),实际上是 (5 + undefined + undefined),所以value为NaN。

b的执行过程:

  • 第一个next,value值等于执行yield (x + 1) ,为6,由于yield表达式本身没有返回值,所以y等于undefined。
  • 第二个next(12),入参12等于上一次next表达式的赋值,即y = 2 * 12即24,value值等于执行yield (y / 3),所以value为8
  • 第二个next(13),入参13等于上一次next表达式的赋值,即z = 13,value值等于执行(x + y + z) = 5 + 24 + 13即42。

关于for...of 循环,不需要next方法就可以执行yield表达式,只要遇到done为true则马上停止执行。

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5

关于异常捕获,是如果内部有catch,则内部优先捕获,且只捕获一次,其余情况均由外部捕获。另外,一旦发生异常,则Generator函数停止继续执行。

var errorHandle = (msg) => {
	throw new Error();
}

function* generatorFun() {
	try {
		yield 1;
		yield errorHandle('Error Msg A');
		yield 2; // 没机会继续执行
		yield errorHandle('Error Msg B'); // 没机会继续执行
	} catch(e) {
		console.log("内部捕获:", e);
	}
}

var hw = generatorFun();
try {
	console.log(hw.next());
	console.log(hw.next());
	console.log(hw.next());
} catch(e) {
	console.log("外部捕获:", e);
}

// {value: 1, done: false}
// 内部捕获: Error

有些情况需要从外部抛出异常,可以使用Generator.prototype.throw(),规则:内部优先捕获,且只捕获一次

var errorHandle = (msg) => {
	throw new Error();
}

function* generatorFun() {
	try {
		yield 1;
		yield errorHandle('Error Msg A');
		yield 2;
		yield errorHandle('Error Msg B');
	} catch(e) {
		console.log("内部捕获:", e);
	}
}

var hw = generatorFun();
try {
	console.log(hw.next());
	console.log(hw.throw('Error throw A'));
	console.log(hw.next());
	console.log(hw.throw('Error throw B'));
} catch(e) {
	console.log("外部捕获:", e);
}

// {value: 1, done: false}
// 内部捕获: Error throw A
// 外部捕获: Error throw B

关于终结状态遍历,可以使用Generator.prototype.return(),入参为最终输出的value值,且done属性总是返回true。

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

var g = gen();

g.next()        // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next()        // { value: undefined, done: true }

关于yield* 表达式,是指Generator函数中嵌套Generator函数。如果不加*号将无效。

function* foo() {
	yield 'a';
	yield 'b';
}

function* bar() {
	yield 'x';
	yield* foo();
	yield 'y';
}

for(let v of bar()) {
	console.log(v);
}
// "x"
// "a"
// "b"
// "y"

关于异步加载的应用,如实现顺序和并发请求加载多张图片:

function loadImg(name) {
	return new Promise(function(resolve, reject) {
		setTimeout(function() {
			resolve(name);
		}, 2000)
	});
}

function* funGenerator() {
	yield loadImg('/img1.png');
	yield loadImg('/img2.png');
	yield loadImg('/img3.png');
}

// 异步顺序加载
function sequentialLoad(generator, callback) {
	var n = generator.next();
	if(!n.done) {
		n.value.then((response) => {
			console.log(response);
			sequentialLoad(generator, callback);
		}).catch(function(err) {
			callback({
				state: 'failed',
				msg: '加载失败:' + err
			});
		})
	} else {
		callback({
			state: 'success',
			msg: '加载完成'
		});
	}
}

// 异步并发加载
function concurrentLoad(generator, callback) {
	var count = 0;
	for(var p of generator) {
		count++;
		p.then((response) => {
			count--;
			if(count == 0) {
				callback({
					state: 'success',
					msg: '加载完成'
				});
			}
		}).catch(function(err) {
			callback({
				state: 'failed',
				msg: '加载失败:' + err
			});
		})
	}
}

var generator = funGenerator();
/**顺序加载*/
sequentialLoad(generator, function(response) {
	console.log(response);
});
/**并发加载*/
//concurrentLoad(generator, function(response) {
//	console.log(response);
//});

3. 对async、await的理解,内部原理?

async、await其实是Generator函数的语法糖,把*号换成了async,把yield换成了await,另外,async、await极大的补足了Generator函数的缺点,包括:

  • async、await定义的函数不需要next即就可以执行,即自带执行器。
  • async、await定义的函数返回的是一个Promise对象,该对象通常使用then和catch获取结果或监听异常。
  • async、await函数return出来的值,将会作为then回调函数的第一个参数。
function loadImg(name) {
	return new Promise(function(resolve, reject) {
		setTimeout(function() {
			resolve(name);
		}, 2000)
	});
}

const asyncReadFile = async function() {
	var promiseArray = [];
	const f1 = await loadImg('/img1.png'); // 等待2秒
	console.log(f1) // 打印'/img1.png'
	const f2 = await loadImg('/img2.png'); // 等待2秒
	console.log(f2) // 打印'/img1.png'
	promiseArray.push(f1);
	promiseArray.push(f2);
	return promiseArray; 
};
var asyncFun = asyncReadFile();
// return出来的返回值,将会作为then回调函数的第一个参数
asyncFun.then(function(response) {
	console.log(response); // 4秒钟后,打印["/img1.png", "/img2.png"]
}).catch(function(err) {
	console.log('error:',err);
});

在上面代码中,当async函数内有多个await,将会按顺序执行(也叫继发执行),一个await完成后才开始执行下一个await。但只要其中有一个await的报出Promise.reject或函数内抛出错误异常throw new Error(),则函数停止往下执行且会被catch捕获。

那么,有时我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。

async function f() {
  try {
    await Promise.reject('出错了');
  } catch(e) {}
  return await Promise.resolve('hello world');
}

f().then(v => console.log(v))

async、await 异步并发执行算法:

function loadImg(name) {
	return new Promise(function(resolve, reject) {
		setTimeout(function() {
			resolve(name);
		}, 2000)
	});
}

async function logInOrder(urls) {
	// 并发读取远程URL
	const textPromises = urls.map(async url => {
		const response = await loadImg(url);
		return response;
	});

	// 按次序输出
	for(const textPromise of textPromises) {
		console.log(await textPromise);
	}
}

var urls = ['/img1.png', '/img2.png'];
logInOrder(urls);

async、await 异步顺序执行算法:

function loadImg(name) {
	return new Promise(function(resolve, reject) {
		setTimeout(function() {
			resolve(name);
		}, 2000)
	});
}

async function logInOrder(urls) {
  for (const url of urls) {
    const response = await loadImg(url);
    console.log(response);
  }
}

var urls = ['/img1.png', '/img2.png'];
logInOrder(urls);

4. 通过什么可以做到并发请求?

  • 通过Promise.all函数实现并发。
  • 通过async、await 配合Array.map实现并发。
  • 通过Generator配合for...of实现并发,实现过程请看文章对Generator介绍的最后一个例子。

Promise.all实现并发:

const p1 = new Promise((resolve, reject) => {
		console.log('p1');
		setTimeout(function() {
			resolve('hello');
		}, 2000)
	})
	.then(result => result)
	.catch(e => e);

const p2 = new Promise((resolve, reject) => {
		console.log('p2');
		setTimeout(function() {
			reject('报错了');
		}, 2000)
	})
	.then(result => result)
	.catch(e => e);

Promise.all([p1, p2])
	.then(result => console.log(result))
	.catch(e => console.log(e));
// ["hello", Error: 报错了]

async、await 配合Array.map实现并发:

function loadImg(name) {
	return new Promise(function(resolve, reject) {
		setTimeout(function() {
			resolve(name);
		}, 2000)
	});
}

async function logInOrder(urls) {
	// 并发读取远程URL
	const textPromises = urls.map(async url => {
		const response = await loadImg(url);
		return response;
	});

	// 按次序输出
	for(const textPromise of textPromises) {
		console.log(await textPromise);
	}
}

var urls = ['/img1.png', '/img2.png'];
logInOrder(urls);

 

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值