异步编程
异步是什么,有哪些操作是异步的,有什么用,怎么用
-
同步是什么,异步又是什么
同步是指同一时间只能做一件事,也就是说一件事情做完了才能做另外一件事
异步是指多件事可以在同一时间执行
-
异步的操作
-
ajax请求
-
定时器setTimeout、setInterval
-
JS的异步加载(例如外部文件的异步加载,只兼容IE的defer(也可以异步加载script标签内部的js代码)、async(不能将js代码写在有async属性的script标签内),支持chrome,safari,firefox,opera浏览器、动态创建script标签(封装代码如下) )
function loadAsyncScript(url, callback) { var script = document.createElement('script'); script.type = 'text/javascript'; if (script.readyState) { // 兼容IE浏览器 // 脚本加载完成事件 script.onreadystatechange = function () { if (script.readyState === 'complete' || script.readyState === 'loaded') { callback(); } } } else { // Chrome, Safari, FireFox, Opera可执行 // 脚本加载完成事件 script.onload = function () { callback(); } } script.src = url; //将src属性放在后面,保证监听函数能够起作用 document.head.appendChild(script); } //封装的方法loadAsyncScript(url, callback)传入两个参数,第一个js文件的地址,第二个是回掉函数,让的用户可以再里面执行异步加载文件里面的方法或者对象。
- DOM事件、IO
-
-
异步有什么用
javascript是单线程的,优点是操作简单,执行环境单纯,但是缺点很明显,如果某一个任务耗时很长,后面的任务就很会一直等待,可能伴随出现假死现象。所以可以在需要等待但又不能阻塞(就是 一个时间只能做一个事情,上一件事情没做完,不能做下一件事情)程序的时候使用异步,解决后面任务一直排队等待的问题,提升效率
主线程的任务以同步的方式执行完毕,才会去依次执行任务列队中的异步任务
-
异步编程的四种方法
- 回调函数
//有两个函数f1和f2,后者等待前者的执行结果。 f1(); f2(); //如果f1是一个很耗时的任务,可以考虑改写f1,把f2写成f1的回调函数。 function f1(callback){ setTimeout(function () { // f1的任务代码 callback(); }, 1000); } //执行代码就变成下面这样: f1(f2); //采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。 //多层嵌套容易产生回调地狱。
//回调地狱 //比如,我想打印4句话,但是每句话都在前一句话的基础上延迟2秒输出。代码如下: setTimeout(function(){ console.log("first"); setTimeout(function(){ console.log("second"); setTimeout(function(){ console.log("third"); setTimeout(function(){ console.log("fourth"); },2000); },2000); },2000); },2000); //这就产生了回调地狱的问题,内容越多,嵌套的层次越多
- 事件监听
//实现事件监听 /* DOM 可以被监听事件的对象 .on("eventName",function) 第一个参数为监听的事件名,第二个参数为 绑定 的方法 .removeOn("eventName",function) 第一个参数为监听的事件名,第二个参数为 解绑 的方法 .trigger("eventName") 参数接收事件名,触发该事件(并触发该事件绑定的所有方法) */ //以下是用事件监听解决异步的方案 //生成两个DOM实例 let dom1 = new DOM(); let dom2 = new DOM(); //异步模拟 let f2 = function () { setTimeout(function () { console.log("开始触发事件:"); console.log("dom1 'done' 事件 触发:"); dom1.trigger('done', 20);//触发f3 console.log("dom2 'done' 事件 触发:"); dom2.trigger("done", "123");//触发f3 }, 100); }; let f3 = function (data) { console.log("f3 run",data) }; //为dom1,dom2的'done' 事件绑定 f3 方法 console.log("dom1 On f3"); dom1.on("done", f3); console.log("dom2 On f3"); dom2.on("done", f3); f2(); setTimeout(function () { console.log("dom1 removeOn f3"); dom1.removeOn("done", f3); f2(); }, 200); //事件监听模式不用关心被绑定的函数什么时候执行,我们只需要在特定的时候触发对应的事件就可以了。同时这个模式也相当的依赖事件,变成事件驱动,运行流程会变得很不清晰。
- Promise
//ES6中引入了Promise,Promise是异步的一种解决方案 //Promise简单来说就是一个容器,里面保存着未来才会结束的的事件(一般是异步的)的结果 //promise有以下两个特点: 1、对象的状态只有异步操作的结果决定,其他外界任何操作无法影响promise对象的状态(pending、resolve、reject) 2、一旦状态改变就不会再变,任何时候得到的都是这个结果 //首先需要创建一个Promise对象,该对象的构造函数中接收一个回调函数,回调函数中可以接收两个参数,resolve和reject。注意,这个回调函数是在Promise创建后就会调用。它实际上就是异步操作的第一步。那第二步操作再在哪里做呢?Promise把两个步骤分开了,第二步通过Promise对象的then方法实现。 let pm = new Promise(function(resolve,reject){ //dosomething }); console.log("go on"); pm.then(function(){ console.log("异步完成"); }); //不过要注意的是,then方法的回调函数不是说只要then方法一调用它就会调用,而是在Promise的回调函数中通过调用resolve触发的。 let pm = new Promise(function(resolve,reject){ resolve(); }); console.log("go on"); pm.then(function(){ console.log("异步完成"); }); //实际上Promise实现异步的原理和之前纯用回调函数的原理是一样的。只是Promise的做法是显示的将两个步骤分开来写。then方法的回调函数同样会先放入队列中,等待所有的同步方法执行完后,同时Promise中的resolve也被调用后,该回调函数才会执行。 //调用resolve时还可以把数据传递给then的回调函数。 let pm = new Promise(function(resolve,reject){ resolve("data data data"); }); console.log("go on"); pm.then(function(data){ console.log("异步完成",data); }); //reject是出现错误时调用的方法。它触发的不是then中的回调函数,而是catch中的回调函数。比如: let err = false; let pm = new Promise(function(resolve,reject){ if(!err){ resolve("this is data"); }else{ reject("fail"); } }); console.log("go on"); pm.then(function(data){ console.log("异步完成",data); }); pm.catch(function(err){ console.log("出现错误",err); }); //下面,我把刚才时间函数的异步操作用Promise实现一次。当然,其中setTimeout还是需要使用,只是在它外面包裹一个Promise对象。 let pm = new Promise(function(resolve,reject){ setTimeout(function(){ resolve(); },2000); }); console.log("go on"); pm.then(function(){ console.log("异步完成"); }); //接下来做做同步效果。 let timeout = function(time){ return new Promise(function(resolve,reject){ setTimeout(function(){ resolve(); },time); }); } console.log("go on"); timeout(2000).then(function(){ console.log("first"); return timeout(2000); }).then(function(){ console.log("second"); return timeout(2000); }).then(function(){ console.log("third"); return timeout(2000); }).then(function(){ console.log("fourth"); return timeout(2000); }); //由于需要多次创建Promise对象,所以用了timeout函数将它封装起来,每次调用它都会返回一个新的Promise对象。当then方法调用后,其内部的回调函数默认会将当前的Promise对象返回。当然也可以手动返回一个新的Promise对象。我们这里就手动返回了一个新的计时对象,因为需要重新开始计时。后面继续用then方法来触发异步完成的回调函数。这样就可以做到同步的效果,从而避免了过多的回调嵌套带来的“回调地狱”问题。 //如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。所以,不用担心是否错过了某个事件或信号
- async和await
//在ES7中,加入了async函数来处理异步。它实际上只是生成器的一种语法糖而已,简化了外部执行器的代码,同时利用await替代yield,async替代生成器的(*)号。下面还是来看个例子: async function delay(){ await new Promise((resolve) => {setTimeout(()=>{resolve()},2000)}); console.log("go on); } delay(); //把生成器的(*)号被换成了async。async关键字必须写在function的前面。如果是箭头函数,则写在参数的前面: const delay = async () => {} //在函数中,第一句用了await。它替代了yield。后面同样需要跟上一个Promise对象。接下来的打印语句会在上面的异步操作完成后执行。外部调用时就和正常的函数调用一样,但它的实现原理和生成器是类似的。因为有了async关键字,所以它的外部一定会有相应的执行器来执行它,并在异步操作完成后执行回调函数。只不过这一切都被隐藏起来了,由JS引擎帮助我们完成。我们需要做的就是加上关键字,在函数中使用await来执行异步操作。这样,可以大大的简化异步操作。同时,能够像同步方法一样去处理它们。 //接下来我们再来看看更细节的一些问题。await后面必须是一个Promise对象,这个很好理解。因为该Promise对象会返回给外部的执行器,并在异步动作完成后执行resolve,这样外部就可以通过回调函数处理它,并将结果传递给生成器。 //如果await后面跟的不是Promise对象 const delay = async () => { let data = await "hello"; console.log(data); } //这样的代码是允许的,不过await会自动将hello字符串包装一个Promise对象。就像这样: let data = await new Promise((resolve,reject) => resolve("hello")); //创建了Promise对象后,立即执行resolve,并将字符串hello传递给外部的执行器。外部执行器的回调函数再将这个hello传递回来,并赋值给data变量。所以,执行该代码后,马上就会输出字符串hello。虽然代码能够这样写,但是await在这里的意义并不大,所以await还是应该用来处理异步方法,同时该异步方法应该使用Promise对象。 //async函数里面有返回值 const delay = async () => { await new Promise((resolve) => {setTimeout(()=>{resolve()},2000)}); return "finish"; } let result = delay(); console.log(result); //在delay函数中先执行等待2秒的异步操作,然后返回字符串finish。外部调用时我用一个变量接收它的返回值。最后输出的结果是: // 没有任何等待立即输出 Promise { <pending> } // 2秒后程序结束 //我们可以看到,没有任何等待立即输出了一个Promise对象。而整个程序是在2秒钟后才结束的。由此看出,获取async函数的返回结果实际上是return出来的一个Promise对象。假如return后面跟着的本来就是一个Promise对象,那么它会直接返回。但如果不是,则会像await一样包裹一个Promise对象返回。所以,想要得到返回的具体内容应该这样: const delay = async () => { await new Promise((resolve) => {setTimeout(()=>{resolve()},2000)}); return "finish"; } let result = delay(); console.log(result); result.then(function(data){ console.log("data:",data); }); //执行的结果: // 没有任何等待立即输出 Promise { <pending> } //等待2秒后输出 data: finish //那如果函数没有任何返回值,得到的又是什么呢?我将上面代码中取掉return,再次运行: // 没有任何等待立即输出 Promise { <pending> } //等待2秒后输出 data: undefined //可以看到,仍然可以得到Promise对象,但由于函数没有返回值,所以就不会有任何数据传递出来,那么打印的结果就是undefined。 //用JQuery的AJAX方法实现。 async function getUsers(){ let response = await fetch("/users"); let data = await response.json(); console.log("data",data); } getUsers(); //这是fetch方法的实现。 //从这两个例子可以看出,async和生成器两种方式都很类似,但async可以不借助任何的第三方模块,也更易于理解,async表示该函数要做异步处理。await表示后面的代码是一个异步操作,等待该异步操作完成后再执行后面的动作。如果异步操作有返回的数据,则在左边用一个变量来接收它。 //await可以让异步操作变为同步的效果,那需要让多个异步操作同时进行怎么办呢?方法就是执行异步方法时不加await,这样它们就可以同时进行,然后在获取结果时用await。比如: function time(ms){ return new Promise((resolve,reject) => { setTimeout(()=>{resolve()},ms); }); } const delay = async () => { let t1 = time(2000); let t2 = time(2000); await t1; console.log("t1 finish"); await t2; console.log("t2 finish"); } delay(); //我先把时间函数的异步操作封装成了函数,并返回Promise对象。在delay函数中调用了两次time方法,但没有用await。也就是说这两个时间函数的执行是“同时”(其实还是有先后顺序)进行的。然后将它们的Promise对象分别用t1和t2表示。先用await t1。表示等待t1的异步处理完成,然后输出t1 finish。接着再用await t2,等待t2的异步处理完成,最后输出t2 finish。由于这两个时间函数是同时执行,而且它们的等待时间也是一样的。所以,当2秒过后,它们都会执行相应的回调函数。运行的结果就是:等待2秒后,先输出t1 finish,紧接着立即输出 t2 finish。 const delay = async () => { await time(2000); console.log("t1 finish"); await time(2000);; console.log("t2 finish"); } //如果是这样写,那么执行的结果会是等待2秒后输出t1 finish。再等待2秒后输出t2 finish。 //async确实是一个既好用、又简单的异步处理方法。但是它的问题就是不兼容老的浏览器,只有支持了ES7的浏览器才能使用它。 //最后,还需要注意一个问题:await关键字必须写在async定义的函数中。