什么是同步异步
同步:当我们发出了请求,并不会等待响应结果,而是会继续执行后面的代码,响应结果的处理在之后的事件循环中解决。
异步:那么同步的意思,就是等结果出来之后,代码才会继续往下执行。
我们可以用一个两人问答的场景来比喻异步与同步。A向B问了一个问题之后,不等待B的回答,接着问下一个问题,这是异步。A向B问了一个问题之后,然后就笑呵呵的等着B回答,B回答了之后他才会接着问下一个问题,这是同步。
(async/await使用同步的思维,来解决异步的问题)
什么是回调1
理解一个新东西,很有必须去理解下它的概念,因为这是最简洁明了,前人总结的。
回调的概念
A callback is a function that is passed as an argument to another function and is executed after its parent function has completed.
中文意思:回调是一个函数被作为一个参数传递到另一个函数里,在那个函数执行完后再执行。
有点绕,好,咱们说大白话。就是 B函数被作为参数传递到A函数里,在A函数执行完后再执行B。
下面咱们看看代码怎么实现回调。
function A(callback){
console.log("I am A");
callback(); //调用该函数
}
function B(){
console.log("I am B");
}
A(B);
这应该是最最简单的回调了,我想大家应该明白回调的释义了吧。
当然,这么简单的同步回调代码是不会用的,现实中用都是相对比较复杂带传参。
同步回调和异步回调
一开始我被回调和异步有点搞晕了。还以为回调
就一定是异步
的呢。
其实不然,相信上面的A,B函数的例子我们已经明白,回调并不一定就是异步。他们自己并没有直接关系。
下面我们可以理解下 同步回调和异步回调。
同步回调
就是上面的A B函数例子,它们就是同步的回调。
异步回调
因为js是单线程的,但是有很多情况的执行步骤(ajax请求远程数据,IO等)是非常耗时的,如果一直单线程的堵塞下去会导致程序的等待时间过长页面失去响应,影响用户体验了。
如何去解决这个问题呢,我们可以这么想。耗时的我们都扔给异步去做,做好了再来通知我们做完了,我们拿到数据继续往下走。
假设有三个函数
f1()
f2()
f3()
但是,f1
执行很耗时,而f2
需要在f1
执行完之后执行。
为了不影响 f3
的执行,我们可以把f2
写成f1
的回调函数。
//最原始的写法-同步写法
f1(); //耗时很长,严重堵塞
f2();
f3(); //导致f3执行受到影响
//改进版-异步写法
function f1(callback){
setTimeout(function () {
// f1的任务代码
callback();
}, 1000);
}
f1(f2);
f3();
上面的写法是利用setTimeOut
把f1
的逻辑包括起来,模拟javascript中的异步编程。这样的话,f1
异步了,不再堵塞f3
的执行。
顺道说下,js是单线程的,这里所谓的异步也是伪异步,并不是开了多线程的异步。它是什么原理呢,其实是任务栈,setTimeOut
方法的原理是根据后面的定时时间,过了这个定时时间后,将f1
加入任务栈,注意仅仅是加入任务栈,并不是放进去就执行,而是根据任务栈里的任务数量来确定的。
什么是回调2
回调的定义
刚开始学习javascript时,对回调函数的理解仅仅停留在知道定义阶段。什么是回调函数? 就是将一个函数作为参数传递给另一个函数,作为参数的这个函数就是回调函数。 至于为什么要用到回调函数?回调函数有什么作用? 当时对这些一无所知! 最近学习node.js涉及到了大量的异步编程,很多地方都需要用到回调函数,所以这两天深入了解了JavaScript的回调函数,下面是我对回调函数的理解。
函数也是对象
想要弄明白js回调函数,首先要清楚函数的规则,在javascript中函数是一个对象,准确的来说函数是用function()构造函数创建的一个function对象,因此我们可以将函数存储在变量中,当然也就可以将存储在变量中的函数作为一个参数传递给另一个函数,这就是回调函数。
举个例子:
var callback = function(arg3) {
console.log('callback Totle is:' + arg3)
}
function fn(arg1, arg2, cb) {
var Total = arg1 + arg2;
cb(Total);
console.log('mainFunction Totle is:' + Total)
}
fn(2, 2, callback) // 调用fn()函数,并传入2, 2, callback作为参数
上面例子中我们将一个匿名函数赋值给变量callback,同时将callback作为参数传递给了fn()函数,这时在函数fn()中callback就是回调函数
同步回调和异步回调
上面的代码执行结果为:
callback Totle is:4
mainFunction Totle is:4
不对啊! 回调函数不是应该在主函数的最后执行吗?
对,很多介绍回调函数的例子讲到这里是就完了,异步回调函数的确是应该在函数的最后执行,不过上面的例子是一个同步回调函数,函数的执行顺序依然自上而下顺序执行。 那么什么是异步回调呢? 我们又怎么实现异步回调呢? 下面我们举两个例子来说明:
示例1:
function f2() {
console.log('f2 finished')
}
function f1(cb) {
setTimeout(cb,1000) //用setTimeout()模拟耗时操作
console.log('f1 finished')
}
f1(f2); //得到的结果是 f1 finished ,f2 finished
这里我们用setTimeout()来模拟耗时操作的前提是js中的setTimeout()函数支持异步处理,所以我们得到的结果是 f1 finished ,f2 finished
示例2:
var fs = require("fs");
fs.readFile('input.txt','utf-8', function (err, data) {
if (err) return console.error(err);
console.log(data.toString());
});
console.log("程序执行结束!");
程序执行的结果是:
$ node app
程序执行结束!
我们来测试一下异步回调函数
上面例子中我们先创建了一个文件input.txt,里面的内容是:'我们来测试一下异步回调函数'
如果按照同步的思维,程序应该执行fs.readFile,直到文件读完之后才执行后面的console.log("程序执行结束!"); 然而node中的fs.readFile是支持异步处理的,因此程序执行到这儿的时候并不会阻塞,而是继续向后执行,当文件读取完毕之后再自动调用传入的匿名回调函数,因此出现了上面的结果。
什么是同步异步3
回调:
具名回调
function lean(some){
console.log(some)
}
function we(cb, something){
something += ' is coll';
cb(something)
}
we(lean,'Nodejs');
//函数名回调 Nodesj is coll
匿名回调
function we(cb, something){
something += ' is coll';
cb(something)
}
we(function(something){
console.log(something)
},'vuejs')
同步:
var c = 0;
function plus(){ c += 1}
function printIt(){ console.log(c)}
plus();
printIt();
// 1 立即就打印出了1
// 我们先调用plus()函数去累加c,然后在调用printIt()打印出c,这个是同步的过程
异步:
var c = 0;
function plus(){
setTimeout(function(){
c += 1
}, 1000)
}
function printIt(){ console.log(c)}
plus();
printIt();
//0 立即就打印出了0
// 这里我们也是先调用plus()再调用print(),为什么打印出来的还是0呢?
当执行plus()函数时因为setTimeout是异步的函数,他会在1000后才开始执行,而JavaScript解析器会把这样异步的函数放到消息队列里,继续执行同步的代码(即执行下面的printIt()函数),等同步的代码执行完,在去执行消息队列里的代码。
那如果我们想要PrintIt()函数是在plus()函数后面执行该怎么做呢?可以用回调来解决
var c = 0;
function printIt(){ console.log(c)}
function plus(callback){
setTimeout(function(){
c += 1;
callback()
}, 1000)
}
plus(printIt);
//1000毫秒后输出1
这就是异步回调。
Promise是什么,先拉出来溜溜
复杂的概念先不讲,我们先简单粗暴地把Promise用一下,有个直观感受。那么第一个问题来了,Promise是什么玩意呢?是一个类?对象?数组?函数?
先别猜了,直接打印出来看看,console.dir(Promise)
这么一看就明白了,Promise是一个构造函数,自己身上有all , reject , resolve 这几个眼熟的方法,原型上有then , catch等方法,这么说Promise new出来的对象上也是有then , catch方法的咯。
Promise的基本用法1
function runAsync(){
//因Promise是构造函数,我们先new一他的个实例p
var p = new Promise(function(resolve, reject){
console.log('我还是同步的哦1')
//做一些异步操作
setTimeout(function(){
resolve('3');
console.log('4');
}, 2000);
console.log('我还是同步的哦2')
});
return p;
//等到异步操作完成后,return出实例p,实例上有then , catch方法
}
//runAsync();
runAsync().then(function(data){
console.log(data);
//后面可以用传过来的数据做些其他操作
//......
});
//输出顺序:
我还是同步的哦1
我还是同步的哦2
4
3
//then方法会接受一个参数,即上面resolve()里传的参数。resolve()是处理异步的函数,该函数JavaScript已经定义好,我们可以如上面直接使用
setTimeout()是回调,所以4,3都是在1,2后面输出的(不理解看回调的定义),而异步是在回调里头的,所以会是在最后输出3。
这时候你应该有所领悟了,原来then里面的函数就跟我们平时的回调函数一个意思,能够在runAsync这个异步任务执行完成之后被执行。这就是Promise的作用了,简单来讲,就是能把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数。
看到这里你可能会说,把回调函数封装下,给runAsync传进去不是一样?如下
function runAsync(cb){
//因Promise是构造函数,我们先new一他的个实例p
console.log('我还是同步的哦1')
//做一些异步操作
setTimeout(function(){
cb('3');
console.log('4');
}, 2000);
console.log('我还是同步的哦2')
//等到异步操作完成后,return出实例p,实例上有then , catch方法
};
function callback(data){
console.log(data)
};
runAsync(callback);
//输出顺序:
我还是同步的哦1
我还是同步的哦2
3
4
效果也是一样的,还费劲用Promise干嘛。那么问题来了,有多层回调该怎么办?如下代码
Promise的基本用法2
Release: production-20171127052209
web-a44ad2fc4f114b24efcf.js:1 Environment: production
function fn(num) {
return new Promise(function(resolve, reject) {
if (typeof num == 'number') {
resolve();
} else {
reject();
}
})
.then(function() {
console.log('参数是一个number值');
})
.then(null, function() {
console.log('参数不是一个number值');
})
}
fn('hahha');
fn(1234);
// 参数是一个number值
// 参数不是一个number值
then方法的执行结果也会返回一个Promise对象。因此我们可以进行then的链式执行,这也是解决回调地狱的主要方式。
var fn = function(num) {
return new Promise(function(resolve, reject) {
if (typeof num == 'number') {
resolve(num);
} else {
reject('TypeError');
}
})
}
fn(2).then(function(num) {
console.log('first: ' + num);
return num + 1;
})
.then(function(num) {
console.log('second: ' + num);
return num + 1;
})
.then(function(num) {
console.log('third: ' + num);
return num + 1;
});
// first: 2
// second: 3
// third: 4
OK,了解了这些基础知识之后,我们再回过头,利用Promise的知识,对最开始的ajax的例子进行一个简单的封装。看看会是什么样子。
链式操作的用法
从表面看,Promise只是能够简化层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多。所以使用Promise的正确场景是这样的
function runAsync1(){
var p = new Promise(function(resolve, reject){
//做一些异步操作
setTimeout(function(){
console.log('异步任务1执行完成');
resolve('随便什么数据1');
}, 1000);
});
return p;
}
function runAsync2(){
var p = new Promise(function(resolve, reject){
//做一些异步操作
setTimeout(function(){
console.log('异步任务2执行完成');
resolve('随便什么数据2');
}, 2000);
});
return p;
}
function runAsync3(){
var p = new Promise(function(resolve, reject){
//做一些异步操作
setTimeout(function(){
console.log('异步任务3执行完成');
resolve('随便什么数据3');
}, 2000);
});
return p;
}
如何做到能够按顺序,每个2秒输出每个异步回调中的内容,在runAsync1中传为resolve的数据,能在接下来的then方法中拿到。如下
runAsync1()
.then(function(data){
console.log(data);
return runAsync2();
})
.then(function(data){
console.log(data);
return runAsync3();
})
.then(function(data){
console.log(data);
});
//输出结果:
异步任务1执行完成
随便什么数据1
异步任务2执行完成
随便什么数据2
异步任务3执行完成
随便什么数据3
reject的用法
事实上,我们前面的例子都是只有“执行成功”的回调,还没有“失败”的情况,reject的作用就是把Promise的状态置为rejected,这样我们在then中就能捕捉到,然后执行“失败”情况的回调。看下面的代码。
function getNumber(){
var p = new Promise(function(resolve, reject){
//做一些异步操作
setTimeout(function(){
var num = Math.ceil(Math.random()*10); //生成1-10的随机数
if(num<=5){
resolve(num);
}
else{
reject('数字太大了');
}
}, 2000);
});
return p;
}
getNumber()
.then(
function(data){
console.log('resolved');
console.log(data);
},
function(reason, data){
console.log('rejected');
console.log(reason);
}
);
getNumber函数来异步获取一个数字,2秒后执行完成,如果数字小于等于5,我们认为是“成功”了,调用resolve修改Promise的状态。否则我们认为是“失败”了,调用reject并传递一个参数,作为失败的原因。
运行getNumber并且在then中传递了两个参数,then方法可以接受两个参数,第一个对应resolve的回调,第二个对应reject的回调。所以我们能够分别拿到他们传过来的数据。多次运行这段代码,你会随机得到下面两种结果:
“resolved 1” 或者 “rejected 数字太大了”
catch的用法
我们知道Promise对象除了then方法,还有一个catch方法,他是做什么的呢?其实他很then的第二个参数一样,用来指定reject的回调,用法如下:
getNumber()
.then(function(data){
console.log('resolved');
console.log(data);
})
.catch(function(reason){
console.log('rejected');
console.log(reason);
});
效果和写在then的第二个参数里面一样。不过它还有另外一个作用:在执行resolve的回调(也就是上面then中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会报错卡死js,而是会进到这个catch方法中。请看下面的代码:
getNumber()
.then(function(data){
console.log('resolved');
console.log(data);
console.log(somedata); //此处的somedata未定义
})
.catch(function(reason){
console.log('rejected');
console.log(reason);
});
在resolve的回调中,我们console.log(somedata);而somedata这个变量是没有被定义的。如果我们不用Promise,代码运行到这里就直接在控制台报错了,不往下运行了。但是在这里,会得到这样的结果:
resolved
4
rejected
ReferenceError: somedata is not defined(...)
也就是说进到catch方法里面去了,而且把错误原因传到了reason参数中。即便是有错误的代码也不会报错了,这与我们的try/catch语句有相同的功能。
all的用法
Promise的all方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后才执行回调。我们仍旧使用上面定义好的runAsync1、runAsync2、runAsync3这三个函数,看下面的例子:
Promise
.all([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
console.log(results);
});
用Promise.all来执行,all接收一个数组参数,里面的值最终都算返回Promise对象。这样,三个异步操作的并行执行的,等到它们都执行完后才会进到then里面。那么,三个异步操作返回的数据哪里去了呢?都在then里面呢,all会把所有异步操作的结果放进一个数组中传给then,就是上面的results。所以上面代码的输出结果就是:
有了all,你就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据,是不是很酷?有一个场景是很适合用这个的,一些游戏类的素材比较多的应用,打开网页时,预先加载需要用到的各种资源如图片、flash以及各种静态文件。所有的都加载完后,我们再进行页面的初始化。
race的用法
all方法的效果实际上是「谁跑的慢,以谁为准执行回调」,那么相对的就有另一个方法「谁跑的快,以谁为准执行回调」,这就是race方法,这个词本来就是赛跑的意思。race的用法与all一样,我们把上面runAsync1的延时改为1秒来看一下:
Promise
.race([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
console.log(results);
});
这三个异步操作同样是并行执行的。结果你应该可以猜到,1秒后runAsync1已经执行完了,此时then里面的就执行了。结果是这样的:
在then里面的回调开始执行时,runAsync2()和runAsync3()并没有停止,仍旧再执行。于是再过1秒后,输出了他们结束的标志。
这个race有什么用呢?使用场景还是很多的,比如我们可以用race给某个异步请求设置超时时间,并且在超时后执行相应的操作,代码如下:
//请求某个图片资源
function requestImg(){
var p = new Promise(function(resolve, reject){
var img = new Image();
img.onload = function(){
resolve(img);
}
img.src = 'xxxxxx';
});
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);
});
requestImg函数会异步请求一张图片,我把地址写为"xxxxxx",所以肯定是无法成功请求到的。timeout函数是一个延时5秒的异步操作。我们把这两个返回Promise对象的函数放进race,于是他俩就会赛跑,如果5秒之内图片请求成功了,那么遍进入then方法,执行正常的流程。如果5秒钟图片还未成功返回,那么timeout就跑赢了,则进入catch,报出“图片请求超时”的信息。运行结果如下:
参考链接: