promise、generator、async的简单应用
javascript的运行机制是单线程处理,即只有上一个任务完成后,才会执行下一个任务,这种机制也被称为“同步”。
“同步”的最大缺点,就是如果某一任务运行时间较长,其后的任务就无法执行。这样会阻塞页面的渲染,导致页面加载错误或是浏览器不响应进入假死状态。
如果这段任务采用“异步”机制,那么它就会等到其他任务运行完后再执行,不会阻塞线程。
一、es6之前实现异步的方式:
最常用的方法是采用回调函数,但普通的回调函数并不能实现异步效果:
(1)同步回调
function testFn(data, callback){
callback(data);
}
testFn('0', function(data2){
for (var i=data2; i<300000000; i++);
console.log(1);
});
console.log(2);
等一秒钟左右for循环结束,控制台输出1后,才会输出2。这是因为 testFn 的回调函数 callback 是同步执行,所以for循环会阻塞后面的任务。
(2)异步回调
想要实现回调的异步执行,必须要借助js的其它方法,例如将上面 testFn 的回调函数放到延时器中调用,延迟的时间设置为0:
function testFn(data, callback){
setTimeout(function(){
callback(data)
},0);
}
这样控制台会先输出2,接着大约一秒钟后再输出1,可见回调函数并没有阻塞后面的任务,实现了异步效果。
除了使用定时器,实现异步执行的方法还有:事件监听(例如点击事件“click”)、requestAnimationFrame、XMLHttpRequest(jq中ajax方法的核心)、WebSocket、Worker以及Node.js 的 fs.readFIle等。
二、promise、generator和async/await:
es6 引入了 generator 和 promise,es7 又增加了async/await。这三种函数有一个重要的作用,就是解决回调函数的异步执行和嵌套问题。
(因为网络上介绍这三种方法的文章很多,所以这里只介绍个人觉得相对常用的知识点)
(一)普通的回调嵌套:
定义一个函数,要求只有在获取两个数据(例如"data1"和"data2")后,才会执行“console.log”任务,通过对回调函数进行嵌套就可以达到目的:
function Test1(data, callback){
if(data){ // 本例是同步,要实现异步可以添加延时器
callback(data);
};
};
Test1("data1",function(){
Test1("data2",function(data2){
console.log(data2); // 如果不传数据就不会执行回调函数
});
});
回调函数的执行需要依赖上一个函数,这样的缺点是如果有多个回调函数,就需要嵌套很多层。这会使代码的可读性较差,并增加调试和维护的难度。
(二)promise
Promise是一个构造函数,它接收一个匿名函数作为参数:
new Promise(function(resolve, reject){
resolve(console.log(1));
reject(console.log(2));
console.log(3);
})
console.log(4);
// 输出的顺序为1、2、3、4
1、因为 promise 是构造函数(带有属性或方法的函数就叫构造函数),所以必须使用 new 实例化才能调用。而作为参数的匿名函数不需要额外调用就能执行。
2、匿名函数只能有 resolve 和 reject 两个参数。结合if判断使用的话,resolve 是条件正确时执行的方法,reject 则是错误时执行的方法。
3、将某一任务直接放到 resolve、reject 方法里,或者放到匿名函数里并不能实现异步效果。
function Test2(){
return new Promise(function(resolve, reject){
resolve(1);
});
};
Test2().then(function(num){
console.log(num);
return num;
}).then(function(num2){
console.log(num2);
})
console.log(2);
// 数字输出的顺序是2、1、1
4、为了防止匿名函数的自动执行,需要再定义一个函数,并将 promise 函数作为该函数的返回值。
5、将回调函数写在 then 或 catch 方法里才能实现异步,then 接受的是 resolve 方法传递的数据,catch 则对应的是 reject。
6、resolve 只能向第一个 then 里的函数传递数据,后面 then 里的函数只能通过前一个 then 里函数的返回值获取参数。
function pro1(){
return new Promise(function(resolve, reject){
resolve(1);
});
};
function pro2(){
return new Promise(function(resolve, reject){
});
};
Promise.all([ pro1(), pro2() ]).then().catch();
Promise.race([ pro1(), pro2() ]).then().catch();
7、all方法的作用:只有 pro1 和 pro2 都执行 resolve,Promise 才会执行 then 方法。如果其中有一个函数执行 reject,那么Promise 就执行catch方法。
8、race方法的作用:pro1 和 pro2 中只要有一个状态发生改变,Promise的状态就跟着发生改变,不论是resolve还是reject。
promise最大的作用
(一)能把嵌套改成链式调用。对上面的普通嵌套进行改造:
function Test2(data){
return new Promise(function(resolve, reject){
if(data){
resolve(data);
}
});
};
Test2(1).then(function( data1 ){
console.log( data1 );
return 2;
}).then(function( data2 ){
console.log( data2 );
})
console.log(3);
// 输出结果为3、1、2
实现效果:
(1)先输出3,表示 then 方法里的回调函数是异步执行。
(2)如果调用 Test2 时不传数字“1”,就不会执行 resolve 方法,这样即使第一个 then 方法里的函数有返回值“2”,也不会执行第二个 then 方法,实现了需求。
(二)解决 ajax 不能传值给外部变量的问题
ajax 在 success 获取的数值无法传递给外部变量,除非设置为同步模式,而 promise 的 resolve 方法可以解决这个问题。
设一个外部变量 outsideData:
var outsideData ;
function TestAjax(data){
return new Promise(function(resolve, reject){
$.ajax({
url: 'xxx.php';
type: 'GET',
datatype: 'json',
data: ' ',
success: function(res){
resolve(res.data)
}
});
});
};
TestAjax().then(function( data ){
outsideData = data;
})
(三)async/await
先说 async/await,是因为 async 函数是 Generator 函数的语法糖,容易理解,而且和 promise 函数也有很大的关联。
用 async function 定义一个函数:
async function Test(){
return 1;
}
Test().then(function(num){
console.log(num);
});
console.log( Test() );
得到的结果是
1、async function 返回的是一个 promise 对象,所以该函数可以调用 then 方法,并因此实现异步执行效果。
async function Test(){
await console.log( 1 );
console.log( 2 );
await console.log( 3 );
}
Test();
console.log( 4 );
// 结果为1、4、2、3
2、await 可以代替 then 方法,以实现异步效果。
3、但第一个 await 是同步执行。不论跟的是 promise,还是其他的函数或方法。
4、第一个 await 后面的任务,不论有没有 await 都是异步执行。但是如果要在 async 函数里再放一个函数,那前面就必须添加 await,否则会报错。
function proFn(){
return new Promise(function(resolve, reject){
resolve(2);
});
};
async function Test(){
var data1 = await proFn();
console.log(1);
console.log(data1);
};
Test();
console.log(3);
// 结果为3、1、2
5、对于第一个 await,最好的方法是使其等于一个变量,然后对这个变量进行处理。
6、如果 await 后面跟的是 promise ,那么匿名函数必须执行 resolve 方法,否则 await 后面的任务就无法执行。
async/await 最大的作用就是替代 promise 的 then 方法:
function proFn(data){
return new Promise(function(){
if(data){
resolve(data);
};
});
};
async function Test(){
var data1 = await proFn(1);
var data1 = await proFn(2);
console.log(data1);
console.log(data2);
};
Test();
console.log(3);
// 结果为3、1、2
(1)async/await 将链式调用变得更加简化。
(2)async/await 传递参数的方式也比 then 简单,不需要通过 return 来传递参数。
(四)generator
和 promise、async/await 不同,generator 本身并不具有异步执行的功能。它在异步中的主要应用,是管理异步回调的执行流程。
generator 函数的特征是使用 * 和关键词 yield:
function* Gen(){
yield 1;
}
var runGen = Gen();
console.log(runGen.next());
// 输出结果为 {value: 1, done: false};
1、函数名后面加小括号并不能调用 generator 函数,只是创建了一个指针对象,next 方法才会执行函数。
2、next 方法返回的对象带有两个属性,分别是“done”和“value”。done 的值表示 generator 函数是否运行完。value 对应的是 yield 后面的值。
function* Gen (num){
var num2 = yield console.log(num);
yield console.log(num2);
}
var runGen = Gen(1);
runGen.next(2);
runGen.next(3);
// 结果输出1和3。
3、next 方法可以传递参数,该参数是上一个 yield 表达式的值。
之所以没有输出“2”,就是因为第一个 next 方法并没有与之对应的"上一个 yield",所以传参无效。
generator 函数管理流程的应用:
使用 generator 管理异步函数,需要用到三个知识点,thunk函数、next方法得到的done属性、递归。
1、thunk函数
普通的多参数函数在调用时,需要一次性传入多个数据。
thunk 函数则是把多个参数拆开,使得在调用时,数据可以分开传入。
实现方法是定义一个函数,并且该函数的返回值也是一个函数,这样就能将参数拆开放在两个函数里:
// 普通的多参数函数:
function Test(data, callback){};
// 调用普通函数:
Test(data, callback);
// thunk函数:
function Test(data){
return function(callback){
callback();
}
}
// 调用thunk函数:
var runThunk = Test(data);
runThunk(callback);
将 callback 参数提取出来,放在作为返回值的匿名函数里,在调用该函数时 callback 和 data 所对应的数据就可以分两步传入。
2、通过 done 属性控制流程:
generator 函数代替“嵌套”去控制流程的思路,就是通过上一个 yield 的执行情况,来决定下一个 next 方法是否执行,这需要用到 done 属性:
function Test(){
setTimeout(function(){
console.log(1);
},0);
}
function* Gen(){
yield Test();
yield console.log(2);
}
var runGen = Gen();
var genObj = runGen.next();
if(!genObj.done){
runGen.next();
}
// 结果为2、1
(1)直接给 next 方法外面添加 if 判断的缺点是,假如某个 yield 后面跟的是异步函数,那么其他 yield 所对应的非异步任务就会优先执行。
如果必须保证前一个任务运行完后,才会执行下一步,就需要把 next 方法放到 value 属性里:
function Test2(){
return function(callback){
console.log(3);
callback();
}
}
function* Gen2(){
yield Test2();
yield console.log(4);
}
var runGen2 = Gen2();
var genObj2 = runGen2.next();
genObj2.value( function(){
runGen2.next();
} );
// 得到的结果是 3、4
(2)想在 value 里使用 next 方法,需要将 value 变成一个函数,这就要用到 thunk 函数。而 value 后面加一个小括号,就能调用作为返回值的函数了。
(3)如果把 next 方法直接放到 value 里,那么 next 方法得到的结果会被当成 value 的参数,先输出。所以需要给 next 方法外面再包一层函数。
下面的函数就是最终形态:
function Test3(){
return function(callback){
setTimeout(function(){
console.log(5);
callback();
},0)
}
}
function* Gen3(){
yield Test3();
yield console.log(6);
}
var runGen3 = Gen3();
var genObj3 = runGen3.next();
genObj3.value( function(){
if(genObj3.done) return;
runGen3.next();
} );
// 结果为5、6
3、使用递归自动执行 generator 函数:
首先看看手动执行 generator 的例子:
// 为了简洁,本例并没有使用异步
function Thunk(num){
return function(callback){
console.log(num);
callback();
};
};
function* Gen(){
yield Thunk(1);
yield Thunk(2);
}
var runGen = Gen();
var runNext = runGen.next();
if(runNext.done) return;
runNext.value(function(){
var runNext2 = runGen.next();
if(runNext.done) return;
runNext2.value(function(){
});
});
// 结果输出1、2
假如存在多个 yield,就需要写很多 next,这会令代码变得臃肿。通过观察可以看出使用 next 方法的部分存在很大的重复性,所以可以使用递归(也就是函数内部调用自身)对其进行改造。
var runGen = Gen();
function next(err, data) {
var runNext = runGen.next(data);
if (runNext.done) return;
runNext.value(next);
}
next();
5、修改 promise 的链式调用
function Test(num){
return new Promise( function(resolve, reject){
resolve(num);
} );
}
function* Gen(){
yield Test(1);
yield Test(2);
}
var runGen = Gen();
function next(){
var genObj = runGen.next();
if(genObj.done) return;
genObj.value.then(function(num){
console.log(num);
next();
});
}
next();
console.log(3);
// 得到的结果为 3、1、2
总结:
关于es6的研究到此就告一段落了。个人觉得“类”和“箭头函数”是一定要掌握的,因为这两点能简化代码结构。至于异步,从例子的长短也能看出,generator 没必要了解很深,还是交给 promise 和 ansyc/await 吧。
三、参考:
1、http://www.cnblogs.com/webeye/p/5383785.html (js同步的缺点)
2、http://blog.csdn.net/tywinstark/article/details/48447135 (15楼回复)
3、http://stackoverflow.com/questions/9516900/how-can-i-create-an-asynchronous-function-in-javascript (js实现异步的方法)
4、http://www.ruanyifeng.com/blog/2014/10/event-loop.html (js运行机制)
5、http://www.nowamagic.net/librarys/veda/detail/787 (浏览器假死原因)
6、https://segmentfault.com/a/1190000003096984 (异步回调的缺点)
7、https://www.oschina.net/translate/event-based-programming-what-async-has-over-sync (回调函数嵌套的缺点)
8、https://segmentfault.com/q/1010000002577322 (回调函数如何实现异步)
9、http://es6.ruanyifeng.com/#docs/promise (promise知识点)
10、http://www.cnblogs.com/lvdabao/p/es6-promise-1.html (promise需要放到另一个函数里)
11、https://segmentfault.com/a/1190000007535316(await知识点)
12、http://blog.rangle.io/javascript-asynchronous-options-2016/(generator不是异步)
13、http://es6.ruanyifeng.com/#docs/generator (generator知识点)
14、http://www.liaoxuefeng.com/wiki/ (generator函数的调用)
15、http://www.jianshu.com/p/87183851756f (promise使用例子)
16、https://segmentfault.com/q/1010000011014844 (使用return返回ajax获取的数值)