Skip to content

Instantly share code, notes, and snippets.

@riskers
Last active November 28, 2019 17:44
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save riskers/450c8029fa2443ce62a6cec0b1329062 to your computer and use it in GitHub Desktop.
Save riskers/450c8029fa2443ce62a6cec0b1329062 to your computer and use it in GitHub Desktop.
JavaScript 异步编程

Promise

基本结构

// 创造 Promise 实例

//Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。
var promise = new Promise(function(resolve,reject){	
	//... some code
    if( /*异步操作成功*/ ){
    	resolve('success')	; //可以是任意值,字符串、对象,甚至是另一个Promise对象
    }else{
    	reject('error')
    }
})

//使用 Promise 实例
//then方法分别指定Resolved状态和Reject状态的回调函数。
promise.then(function(value){
	//...
},function(error){
	//...
})

简单例子

function timeout(ms){
	return new Promise(resolve,reject){
    	setTimeout(resolve,ms,'done');
    }
}

timeout(2000).then((v)=>{
	console.log(v);			//2000ms 后打出 done
})

异步加载图片例子

function loadImage(url){
	return new Promise( function(resolve,reject){
		var image = new Image();
		image.src = url;

		image.onload = function(){
			resolve(image)
		}

		image.onerror = function(){
			reject('error')
		}
	})
}

loadImage('http://p3.qhimg.com/d/inn/d0d222c6/b.png')
	.then((v)=>{
		console.log(v)
	})

resolve函数和reject函数如果带有参数,那么在参数会传递给回调函数。

  • reject函数的参数通常是 Error 对象的实例
  • resolve函数的参数除了正常值外,还可以是另一个Promise实例(表示这个异步操作的结果可能是一个值,也可能是另一个异步操作)

then

Promise 实例具有 then 方法,作用是为 Promise 实例添加状态改变时的回调函数。

有两个参数:resolve回调函数,reject回调函数

可以链式调用,前一个回调函数可能返回另一个Promise对象,这时后一个回调函数,会等待该Promise对象状态发生变化,才会被调用。

getJSON("/post/1.json").then(function(post) {
  return getJSON(post.commentURL);
}).then(function funcA(comments) {
  console.log("Resolved: ", comments);
}, function funcB(err){
  console.log("Rejected: ", err);
});

上面代码中,第一个then方法指定的回调函数,返回的是另一个Promise对象。这时,第二个then方法指定的回调函数,就会等待这个新的Promise对象状态发生变化。如果变为Resolved,就调用funcA,如果状态变为Rejected,就调用funcB。

catch

下面一共三个Promise对象,一个由getJSON产生,两个由then产生,它们之中任何一个抛出错误,都会被最后一个 catch 捕获。

p.then(function(){
	return getJSON('xx')
}).then(function(){
	...
}).catch(function(){
	...
})

Promise.all

Promise.all 将多个 Promise 实例包装成一个新的 Promise 实例

接受一个数组作为参数

  • 所有的 Promise 实例状态都变为 fulfilled 时, all 状态变为 fulfilled
  • 只要有一个 Promise 实例被 rejected,all 状态变为 rejected
function readFile(file){
	return new Promise(function(resolve,reject){
		fs.readFile(file,function(err,data){
			if(err){
				reject(err)
			}else{
				resolve(data)
			}
		})
	})
}


var p1 = readFile('test3.js')

var p2 = readFile('test2.js')

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

p.then(function(v){
	console.log(v);		//将2个文件的内容包在数组中输出 [<Buffer>,<Buffer>]
})

Promise.race

Promise.all ,但只要 Promise 实例数组中有一个状态被改变,race 状态就改变

Promise.resolve

将现有对象转为 Promise 对象

Promise.resolve('foo')

//等价于
new Promise(function(resolve){
	resolve('foo')
});

Promise.reject

Promise.resolve

Generator函数

Generator函数是一个状态机,封装了多个内部状态。执行Generator函数会返回一个遍历对象,可以一次遍历Generator函数内部的每一个状态。

每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield语句后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

function* helloGenerator(){
	yield 'hello';
    yield 'world';
    yield 'end';
}
var hg = helloGenerator()

hg.next();	//{ value: 'hello', done: false }
hg.next();	//{ value: 'world', done: false }
hg.next();	//{ value: 'end', done: true }
hg.next();	//{ value: undefined, done: true }

和普通函数不同的是,调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象(Iterator Object)。

必须调用遍历器对象的next方法,使得指针移向下一个状态。调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield语句(或return语句)为止。换言之,Generator函数是分段执行的,yield语句是暂停执行的标记,而next方法可以恢复执行。

yield

yield是暂停标志,后面的表达式只有当调用 next 方法时才会执行。

next

yield没有返回值(总返回undefined),next方法可以带一个参数,该参数被当做上一个yield语句的返回值。

function* f() {
  for(var i=0; true; i++) {
    var reset = yield i;
    if(reset) { i = -1; }
  }
}

var g = f();

g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }

如果next方法没有参数,每次运行到yield语句,变量reset的值总是undefined。当next方法带一个参数true时,当前的变量reset就被重置为这个参数(即true),因此i会等于-1,下一轮循环就会从-1开始递增。

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}	y = 2 * undefined ,因为 yield 返回值是 undefined (除非外部给 next() 传值)
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }	yield 返回12 , y = 2 * 12 , z = 24 / 3
b.next(13) // { value:42, done:true }	yield 返回13 , z = 13 ,x + y + z = 5 + 24 + 13 = 42

这个功能有很重要的语法意义。Generator函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next方法的参数,就有办法在Generator函数开始运行之后,继续向函数体内部注入值。也就是说,可以在Generator函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。

function* dataConsumer() {
  console.log('Started');
  console.log(`1. ${yield}`);
  console.log(`2. ${yield}`);
  return 'result';
}

let genObj = dataConsumer();
genObj.next(); // Started
genObj.next('a') // 1. a
genObj.next('b') // 2. b

return

返回给定的值,并且终结遍历Generator函数。

Generator 函数的异步

function readFile(name){
	return new Promise(function(resolve,reject){
		fs.readFile(name,function(err,data){
			if(err){
				reject(error)
			}
			resolve(data)
		})
	})
}

function* gen(){
	var f1 = yield readFile('index.js')
	var f2 = yield readFile('test2.js')
	console.log(f1)
	console.log(f2)
}

var g = gen();
g.next().value.then(function(data){
  g.next(data).value.then(function(data){
    g.next(data);
  });
});

g.next().value 是一个 Promise 对象

co

co模块用于 Generator 函数自动执行

function readFile(name){
	return new Promise(function(resolve,reject){
		fs.readFile(name,function(err,data){
			if(err){
				reject(error)
			}
			resolve(data)
		})
	})
}

co(function* (){
	var f1 = yield readFile('index.js')
	var f2 = yield readFile('test2.js')
	console.log(f1)
	console.log(f2)
})

async/await

async function asyncReadFile(){
	var f1 = await readFile('index.js')
	var f2 = await readFile('test2.js')

	console.log(f1)
	console.log(f2)
}

asyncReadFile()

对比 Generator:

  • 内置执行器,Generator没有,所以才有 co
  • async/await 对比 * 和 yield 更好理解
  • yield 和 await 后面都是 Promise 对象
  • 返回值是 Promise, Generator函数返回值是 Iterator 对象

语法:

  1. async 函数返回一个Promise对象
async function f(){
	return 'a';
}
f().then((v)=>{
	console.log(v);	// a
})
async function a(){
	throw new Error('error')
}

a().then((v)=>{
	console.log(v)	
},(e)=>{
	console.log(e)	// Error: error
})
  1. async 函数返回的Promise对象,必须等到内部所有 await 命令的Promise对象执行完,才会发生状态改变。即,只有 async 函数内部的异步操作执行完,才会执行 then 方法指定的回调函数。

  2. await 命令后面应该是一个 Promise 对象,如果不是,会被转成一个立即 resolve 的 Promise 对象。

  3. 只要一个 await 后面的 Promise 变为 reject ,那么整个 async 函数都会中断执行

  4. await 后面的异步操作错误,那么等同于 async 函数返回的 Promise 对象被 reject

用法

async 函数执行的时候,一旦遇到 await 就会等待,等到 await 后的异步操作完成,再接着执行后面的语句。

function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value)
}

asyncPrint('hello world', 50);

50ms 后 才会 console.log

注意点

1. await 后的Promise对象,运行结果可能是 rejected,最好把 await 命令放在 try...catch 中

async function myFunction() {
  try {
    await somethingThatReturnsAPromise();
  } catch (err) {
    console.log(err);
  }
}

// 另一种写法

async function myFunction() {
  await somethingThatReturnsAPromise()
  .catch(function (err) {
    console.log(err);
  };
}

2. 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发

let foo = await getFoo();
let bar = await getBar();

上面代码中,getFoo和getBar是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发。

// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;

Promise 、Generator 、async 比较

Promise 语义不明显 Generator 需要执行器 async 最好

const f = (txt) => {
	return new Promise((resolve,reject)=>{
		setTimeout(()=>{
			resolve(txt)
		},2000)
	})
}

一个定时器做Promise对象

//Promise
f('axs')
	.then(function(v){
		console.log(v)
		return f('zxc')
	})
	.then(function(v){
		console.log(v)
	})
// Generator
function* gen(){
	var t1 = yield f('asd')
	console.log(t1)

	var t2 = yield f('xzc')
	console.log(t2)
}

var g = gen()
g.next().value.then(function(data){
	g.next(data).value.then(function(data){
		g.next(data)
	})
})
//co
co(function* (){
	var t1 = yield f('asd')
	console.log(t1)

	var t2 = yield f('xzc')
	console.log(t2)
})
//async/await
const test = async () => {
	const t1 = await f('asd');
	console.log(t1)

	const t2 = await f('xzc')
	console.log(t2)
}

test();

这是最基本的 jQuery 中 Promise 的用法:

function t(delay){
   var defer = $.Deferred()
   setTimeout(function(){
        defer.resolve('delay is' + delay)
   },delay)
   return defer.promise()
}

t(1000).then(function(e){
	console.log(e)
})
t(500).then(function(e){
	console.log(e)
})

API

  • jQuery.Deferred() 创建一个新的Deferred对象的构造函数,可以带一个可选的函数参数,它会在构造完成后被调用。

  • jQuery.when() 通过该方式来执行基于一个或多个表示异步任务的对象上的回调函数

  • jQuery.ajax() 执行异步Ajax请求,返回实现了promise接口的jqXHR对象

  • deferred.then( doneFilter [, failFilter ] [, progressFilter ] ) 当Deferred(延迟)对象解决,拒绝或仍在进行中时,调用添加处理程序。

  • deferred.done() 当延迟成功时调用一个函数或者数组函数.

  • deferred.fail() 当延迟失败时调用一个函数或者数组函数.。

  • deferred.always() 当Deferred(延迟)对象解决或拒绝时,调用添加处理程序。

  • deferred.resolve(ARG1,ARG2,…) 调用Deferred对象注册的‘done’回调函数并传递参数

  • deferred.resolveWith(context,args) 调用Deferred对象注册的‘done’回调函数并传递参数和设置回调上下文

  • deferred.isResolved 确定一个Deferred对象是否已经解决。

  • deferred.reject(arg1,arg2,…) 调用Deferred对象注册的‘fail’回调函数并传递参数

  • deferred.rejectWith(context,args) 调用Deferred对象注册的‘fail’回调函数并传递参数和设置回调上下文

  • deferred.promise() 返回promise对象,这是一个伪造的deferred对象:它基于deferred并且不能改变状态所以可以被安全的传递

串行

只要在 thenreturn 另一个 Promise 对象即可

t(1000).then(function(e){
    console.log(e)
    return t(2000)
}).then(function(e){
    console.log(e)
})

并行

$.when(t(500),t(1000)).then(function(d1,d2){
    console.log(d1)
    console.log(d2)
})

参考

@riskers
Copy link
Author

riskers commented May 31, 2017

Nodejs v8 中的 util.promisify:

一般情况下 await 的参数应是一个返回 Promise 对象的函数,而 fs.readFile 并不是一个 Promise
util.promisify 就可以把 几乎所有标准库 API 都转换为返回 Promise 对象的函数

const util = require('util')
const fs = require('fs')
const readFile = util.promisify(fs.readFile)

async function a () {
  let result = await readFile('a.txt')
  let result2 = await readFile('b.txt')
  console.log(result)
  console.log(result2)
};

a()

@riskers
Copy link
Author

riskers commented Aug 2, 2017

今天又发现一个 async/await 、Promise 与 generator 的一个大区别:只有 generator 能够在外层得到值,即 async/await 和 Promise 只能在 then 中得到值

function* getFive(){ 
  yield 5;
}
getFive().next().value; // 5
async function getFive() {
  await 5;
}
getFive().then(e=>{
  console.log(e); // 5
})

@riskers
Copy link
Author

riskers commented Feb 13, 2018

EventLoop 和 taskQueue

  • macro-task: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering
  • micro-task: process.nextTick, Promises(这里指浏览器实现的原生 Promise), Object.observe, MutationObserver

任务队列分为 macrotasks 和 microtasks。在每一次事件循环中,macrotask 只会提取一个执行,而 microtask 会一直提取,直到 microtasks 队列清空。

注:macrotask queues 我们会直接称为 task queues,micro-task在ES2015规范中称为Job。

事件循环每次只会入栈一个 macrotask ,主线程执行完该任务后又会先检查 microtasks 队列并完成里面的所有任务后再执行 macrotask

demo1

setTimeout(function(){
    console.log(4)
},0);

new Promise(function(resolve){
  console.log(1)
  for( var i=0 ; i<10000 ; i++ ){
    i==9999 && resolve()
  }
  console.log(2)
}).then(function(){
  console.log(5)
});

console.log(3);

输出 1 2 3 5 4

demo2

setImmediate(function() {
  console.log(6);
});

setTimeout(function () {
  console.log(5);
}, 0);

new Promise(function (resolve) {
  console.log(1);
  resolve();
  console.log(2);
}).then(function () {
  console.log(4);
});

process.nextTick(function() {
  console.log(3);
});

输出 1 2 3 4 5 6 或者 1 2 3 4 6 5

http://www.ruanyifeng.com/blog/2014/10/event-loop.html

可以知道 setImmediate与setTimeout(fn,0) 是不确定哪个先执行的。

而 process.nextTick 方法指定的回调函数,总是在当前"执行栈"的尾部触发。

demo3

new Promise(resolve => {
  resolve()
}).then(() => {
  console.log(5)
})

new Promise(resolve => {
  resolve(1)
  new Promise(resolve => {
    resolve()
  }).then(() => {
    console.log(2)
  })
  console.log(4)
}).then(t => console.log(t))
console.log(3)

输出 4 3 5 2 1

macrotasks 中先 4 3,microtasks 中进入 5 2 1(队列先进先出)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment