一、回调函数(callback)
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 )
假定有两个函数f1和f2,后者等待前者的执行结果。
f1(); f2();
如果f1是一个很耗时的任务,可以考虑改写f1,把f2写成f1的回调函数。
function f1(callback){ setTimeout(function () { // f1的任务代码 callback(); }, 1000); }
// 执行
f1(f2)
采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。
回调函数是异步编程最基本的方法,其优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),流程会很混乱,而且每个任务只能指定一个回调函数。
注意 区分 回调函数和异步
回调并不一定就是异步。他们自己并没有直接关系。
简单区分 同步回调 和 异步回调
同步回调 :
实例代码
function A(callback){ console.log("I am A"); callback(); //调用该函数 } function B(){ console.log("I am B"); } A(B);
异步回调:因为js是单线程的,但是有很多情况的执行步骤(ajax请求远程数据,IO等)是非常耗时的,如果一直单线程的堵塞下去会导致程序的等待时间过长页面失去响应,影响用户体验了。
如何去解决这个问题呢,我们可以这么想。耗时的我们都扔给异步去做,做好了再通知下我们做完了,我们拿到数据继续往下走。
var xhr = new XMLHttpRequest(); xhr.open('POST', url, true); //第三个参数决定是否采用异步的方式 xhr.send(data); xhr.onreadystatechange = function(){ if(xhr.readystate === 4 && xhr.status === 200){ ///do something } }
上面是一个代码,浏览器在发起一个ajax
请求,会单开一个线程去发起http请求,这样的话就能把这个耗时的过程单独去自己跑了,在这个线程的请求过程中,readystate
的值会有个变化的过程,每一次变化就触发一次 onreadystatechange
函数,进行判断是否正确拿到返回结果。
之前在学习 nodejs时也会遇到这样的问题
var fs=require('fs'); //console.log('1'); //fs.readFile('mime.json',function(err,data){ // //console.log(data); // console.log('2'); //}) //console.log('3'); function getMime(){ //1 fs.readFile('mime.json',function(err,data){ //console.log(data.toString()); return data;//3 }) //2 //return '123'; } console.log(getMime()); /*由于异步操作没有拿到数据,如何解决,通过异步操作*/
解决的 办法 是
var fs=require('fs'); function getMime(callback){ fs.readFile('mime.json',function(err,data){ callback(data); }) } getMime(function(result){ console.log(result.toString()); })
二、事件监听
采用事件驱动模式。
任务的执行不取决代码的顺序,而取决于某一个事件是否发生。
监听函数有:on,bind,listen,addEventListener,observe
还是以f1和f2为例。首先,为f1绑定一个事件(采用jquery写法)。
f1.on('done',f2);
上面代码意思是,当f1发生done事件,就执行f2。
然后对f1进行改写:
function f1(){ settimeout(function(){ //f1的任务代码 f1.trigger('done'); },1000); }
f1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行f2.
这种方法的优点:比较容易理解,可以绑定多个事件,每一个事件可以指定多个回调函数,而且可以去耦合,有利于实现模块化。
这种方法的缺点:整个程序都要变成事件驱动型,运行流程会变得不清晰。
事件监听方法:
(1)onclick方法
element.onclick=function(){ //处理函数 }
优点:写法兼容到主流浏览器
缺点:当同一个element元素绑定多个事件时,只有最后一个事件会被添加
例如:
element.onclick=handler1; element.onclick=handler2; element.onclick=handler3;
上诉只有handler3会被添加执行,所以我们使用另外一种方法添加事件
(2)attachEvent和addEvenListener方法
//IE:attachEvent elment.attachEvent("onclick",handler1); elment.attachEvent("onclick",handler2); elment.attachEvent("onclick",handler3);
上述三个方法执行顺序:3-2-1;
//标准addEventListener elment.addEvenListener("click",handler1,false); elment.addEvenListener("click",handler2,false); elment.addEvenListener("click",handler3,false);
执行顺序:1-2-3;
PS:该方法的第三个参数是泡沫获取,是一个布尔值:当为false时表示由里向外,true表示由外向里。
<div id="id1"> <div id="id2"></div> </div>
document.getElementById("id1").addEventListener("click",function(){console.log('id1');},false); document.getElementById("id2").addEventListener("click",function(){console.log('id2');},false); //点击id=id2的div,先在sonsole中输出,先输出id2,在输出id1 document.getElementById("id1").addEventListener("click",function(){console.log('id1');},false); document.getElementById("id2").addEventListener("click",function(){console.log('id2');},true); //点击id=id2的div,先在sonsole中国输出,先输出id1,在输出id2
三、发布/订阅
我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。
这个模式有多种实现,下面采用的是Ben Alman的Tiny Pub/Sub,这是jQuery的一个插件。
首先,f2向"信号中心"jQuery订阅"done"信号。
jQuery.subscribe("done", f2);
然后,f1进行如下改写:
function f1(){ setTimeout(function () { // f1的任务代码 jQuery.publish("done"); }, 1000); }
jQuery.publish("done")的意思是,f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引发f2的执行。
此外,f2完成执行后,也可以取消订阅(unsubscribe)
jQuery.unsubscribe("done", f2);
这种方法的性质与"事件监听"类似,但是明显优于后者。因为我们可以通过查看"消息中心",了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
四、promise对象(promise 模式)
(1)promise对象是commonJS工作组提出的一种规范,一种模式,目的是为了异步编程提供统一接口。
(2)promise是一种模式,promise可以帮忙管理异步方式返回的代码。他讲代码进行封装并添加一个类似于事件处理的管理层。我们可以使用promise来注册代码,这些代码会在在promise成功或者失败后运行。
(3)promise完成之后,对应的代码也会执行。我们可以注册任意数量的函数再成功或者失败后运行,也可以在任何时候注册事件处理程序。
(4)promise有两种状态:1、等待(pending);2、完成(settled)。
promise会一直处于等待状态,直到它所包装的异步调用返回/超时/结束。
(5)这时候promise状态变成完成。完成状态分成两类:1、解决(resolved);2、拒绝(rejected)。
(6)promise解决(resolved):意味着顺利结束。promise拒绝(rejected)意味着没有顺利结束。
//promise var p=new Promise(function(resolved)) //在这里进行处理。也许可以使用ajax setTimeout(function(){ var result=10*5; if(result===50){ resolve(50); }else{ reject(new Error('Bad Math')); } },1000); }); p.then(function(result){ console.log('Resolve with a values of %d',result); }); p.catch(function(){ console.error('Something went wrong'); });
(1)代码的 关键在于setTimeout()的调用。
(2)重要的是,他调用了函数resolve()和reject()。resolve()函数告诉promise用户promise已解决;reject()函数告诉promise用户promise未能顺利完成。
(3)另外还有一些使用了promise代码。注意then和catch用法,可以将他们想象成onsucess和onfailure事件的处理程序。
(4)巧妙地方是,我们将promise处理与状态分离。也就是说,我们可以调用p.then(或者p.catch)多少次都可以,不管promise是什么状态。
(5)promise是ECMAscript 6管理异步代码的标准方式,javascript库使用promise管理ajax,动画,和其他典型的异步交互。
简单的说,它的思想是:每一个异步任务返回一个promise对象,该对象有一个then方法,允许指定回调函数。比如,f1的回调函数f2,可以写成:
f1.then(f2);
f1要进行如下改写(使用jquery的实现):
function f1(){ var dfd=$.deferred(); settimeout(function(){ //f1的任务代码 dfd.resolve(); },500); return dfd.promise; }
这样写的优点:回调函数写成了链式写法,程序的流程可以看得很清楚,而且有一整套的配套方法,可以实现很多强大的功能。
比如,指定多个回调函数
f1().then(f2).then(f3);
再比如,指定发生的错误时的回调函数:
f1().then(f2).fail(f3);
而且,它有一个前面三种方法都没有的好处:如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。
所以你不用担心错过某一个事件或者信号。
这种方法的缺点:编写和理解都相对比较难。
五、优雅的async/await
相对于Promise,async/await
有什么优点?
比较场景: 级联调用,也就是几个调用依次发生的场景
-
Promise
主要用then函数的链式调用,一直点点点,是一种从左向右的横向写法。
async/await
从上到下,顺序执行,就像写同步代码一样。这更符合人编写代码的习惯 -
Promise
的then函数只能传递一个参数,虽然可以通过包装成对象,但是这会导致传递冗余信息,频繁的解析又重新组合参数,比较麻烦。
async/await
没有这个限制,就当做普通的局部变量来处理好了,用let或者const定义的块级变量,想怎么用就怎么用,想定义几个就定义几个,完全没有限制,也没有冗余的工作。 -
Promise
在使用的时候最好将同步代码和异步代码放在不同的then节点中,这样结构更加清晰。
async/await
整个书写习惯都是同步的,不需要纠结同步和异步的区别。当然,异步过程需要包装成一个Promise
对象,放在await
关键字后面,这点还是要牢记的。 -
Promise
是根据函数式编程的范式,对异步过程进行了一层封装。
async/await
是基于协程的机制,是真正的“保存上下文,控制权切换 ... ... 控制权恢复,取回上下文”这种机制,是对异步过程更精确的一种描述。
进程、线程和协程的理解
上面的文章很好地解释了这几个概念的区别。
如果不纠结细节,可以简单地认为:进程 > 线程 > 协程;
协程可以独立完成一些与界面无关的工作,不会阻塞主线程渲染界面,也就是不会卡。
协程,虽然小一点,不过能完成我们程序员交给的任务。而且我们可以自由控制运行和阻塞状态,不需要求助于高大上的系统调度,这才是重点。
-
async/await
是基于Promise
的,是进一步的一种优化。不过再写代码的时候,Promise
本身的API出现得很少,很接近同步代码的写法。
await
关键字使用时有哪些注意点?
-
只能放在
async
函数内部使用,不能放在普通函数里面,否则会报错。 -
后面放
Promise
对象,在Pending
状态时,相应的协程会交出控制权,进入等待状态。这个是本质。 -
await
是async wait
的意思,wait
的是resolve(data)
消息,并把数据data
返回。比如,下面代码中,当Promise
对象由Pending
变为Resolved
的时候,变量a
就等于data
;然后再顺序执行下面的语句console.log(a);
这真的是等待,真的是顺序执行,表现和同步代码几乎一模一样。
const a = await new Promise((resolve, reject) => {
// async process ...
return resolve(data);
});
console.log(a);
-
await
后面也可以跟同步代码,不过系统会自动转化成一个Promise
对象。
比如
const a = await 'hello world';
其实就相当于
const a = await Promise.resolve('hello world');
这跟同步代码
const a = 'hello world';
是一样的,还不如省点事,去掉这里的await
关键字。 -
await
只关心异步过程成功的消息resolve(data)
,拿到相应的数据data
。至于失败消息reject(error)
,不关心,不处理。
当然对于错误消息的处理,有以下几种方法供选择:
(1)让await
后面的Promise
对象自己catch
(2)也可以让外面的async
函数返回的Promise
对象统一catch
(3)像同步代码一样,放在一个try...catch
结构中
async
关键字使用时有哪些注意点?
-
有了这个
async
关键字,只是表明里面可能有异步过程,里面可以有await
关键字。当然,全部是同步代码也没关系。当然,这时候这个async
关键字就显得多余了。不是不能加,而是不应该加。 -
async
函数,如果里面有异步过程,会等待;
但是async
函数本身会马上返回,不会阻塞当前线程。
可以简单认为,
async
函数工作在主线程,同步执行,不会阻塞界面渲染。
async
函数内部由async
关键字修饰的异步过程,工作在相应的协程上,会阻塞等待异步任务的完成再返回。
-
async
函数的返回值是一个Promise
对象,这个是和普通函数本质不同的地方。这也是使用时重点注意的地方
(1)return newPromise();
这个符合async
函数本意;
(2)return data;
这个是同步函数的写法,这里是要特别注意的。这个时候,其实就相当于Promise.resolve(data);
还是一个Promise
对象。
在调用async
函数的地方通过简单的=
是拿不到这个data
的。
那么怎么样拿到这个data
呢?
很简单,返回值是一个Promise
对象,用.then(data => { })
函数就可以。
(3)如果没有返回,相当于返回了Promise.resolve(undefined);
-
await
是不管异步过程的reject(error)
消息的,async
函数返回的这个Promise
对象的catch
函数就负责统一抓取内部所有异步过程的错误。
async
函数内部只要有一个异步过程发生错误,整个执行过程就中断,这个返回的Promise
对象的catch
就能抓到这个错误。 -
async
函数执行和普通函数一样,函数名带个()就可以了,参数个数随意,没有限制;也需要有async
关键字。
只是返回值是一个Promise
对象,可以用then函数得到返回值,用catch抓去整个流程中发生的错误。