前言
正文
一、同步和异步
同步
在学习异步之前我们先来看一下同步,比如在调用函数取得返回值的时候,能够直接得到预期结果(得到了预期的返回值),是按照你的代码顺序执行的,是连续的,那么就说这个函数是同步执行的。
下边看一个例子:
如果函数是同步的,即是调用函数执行的任务比较耗时,也会一直等待直到得到预期结果。 因为它是按代码执行顺序执行的。
异步
如果在调用函数返回值的时候,不直接得到预期结果(预期的返回值),而是需要通过一定的方式获得,是不连续的不按代码顺序执行的,那么就可以说这个函数是异步的。
如下所示:
上述示例中读取文件函数 readFile和网络请求的发起函数 ,send都将执行耗时操作,虽然函数会立即返回,但是不能立刻获取预期的结果,因为耗时操作交给其他线程执行,暂时获取不到预期结果。而在JavaScript中通过回调函数 function(err, data) { console.log(data); }和 onreadystatechange ,在耗时操作执行完成后把相应的结果信息传递给回调函数,通知执行JavaScript代码的线程执行回调。
简单来说:同步按你的代码顺序执行,异步不按照代码顺序执行,异步的执行效率更高。
二、首先要知道的异步机制
浏览器内核的多线程
我们都知道JavaScript是单线程的,但是浏览器的内核是多线程的;他们在内核的控制下互相配合以保持同步,一个浏览器至少实现三个常驻:Javascrpt引擎线程、GUI渲染线程、浏览器事件触发线程。
-
JS引擎:基于事件驱动单线程执行的,JS引擎一直等待这任务队列中任务的到来,然后加以处理;浏览器无论什么时候都只有一个JS线程在运行JS程序。
-
GUI渲染线程:当界面需要重绘或由于某种操作引发回流时,线程就会执行。这里需要注意的是,渲染线程和JS引擎线程是不能同时进行的。
-
事件触发线程:当一个事件被触发时,该线程会把世间添加到等待队列的队尾,等待JS引擎的处理,这些时间可来自JavaScript引擎执行当前的代码块,如:setTimeOut,也可以来自浏览器内核和其他线程如鼠标点击;AJAX异步请求等,但是由于JS 的单线程关系,所有这些事情都得排队等待JS引擎处理。
事件循环机制
如上图所示,左边的栈存储的是同步任务,就是那些能立即执行、不耗时的任务,如变量和函数的初始化、事件的绑定等等那些不需要回调函数的操作都可归为这一类。
右边的堆用来存储声明的变量、对象。下面的队列就是消息队列,一旦某个异步任务有了响应就会被推入队列中。如用户的点击事件、浏览器收到服务的响应和setTimeout中待执行的事件,每个异步任务都和回调函数相关联。
JS引擎线程用来执行栈中的同步任务,当所有同步任务执行完毕后,栈被清空,然后读取消息队列中的一个待处理任务,并把相关回调函数压入栈中,单线程开始执行新的同步任务。
JS引擎线程从消息队列中读取任务是不断循环的,每次栈被清空后,都会在消息队列中读取新的任务,如果没有新的任务,就会等待,直到有新的任务,这就叫事件循环(Eventloop)。
什么是宏任务与微任务?
我们都知道 Js 是单线程的,但是一些高耗时操作就带来了进程阻塞问题。为了解决这个问题,Js 有两种任务的执行模式:同步模式(Synchronous)和异步模式(Asynchronous)。
在异步模式下,创建异步任务主要分为宏任务与微任务两种。ES6 规范中,宏任务(Macrotask) 称为 Task, 微任务(Microtask) 称为 Jobs。宏任务是由宿主(浏览器、Node)发起的,而微任务由 JS 自身发起。
- 1)宏任务 (macrotask):优先级低,先定义的先执行。包括:ajax,setTimeout,setInterval,事件绑定,postMessage,MessageChannel(用于消息通讯)。
- 2)微任务 (microtask):优先级高,并且可以插队,不是先定义先执行。包括:promise.then,async/await [generator],requestAnimationFrame,observer,MutationObserver,setImmediate。
由上图可以看到:
从JS主线程(整体代码)开始第一次循环,发起异步任务后,由(橙色)线程执行异步操作,而JS引擎主线程继续执行堆中的其他同步任务,直到堆中的所有异步任务执行完毕。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局)。然后执行所有的micro-task,当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的macro-task,这样一直循环下去。
根据事件循环机制,我们重新梳理一下流程:
1)首先执行栈里的任务
2)先找微任务队列,如果微任务队列中有,先从微任务队列中,一般按照存放顺序获取并且去执行。
3)如果微任务队列中没有,则再去宏任务队列中查找,在宏任务队列中,一般是按照谁先到达执行的条件,就先把谁拿出来执行。
4)以此循环
明白事件循环之后我们要知道Javascript异步编程先后经历了四个阶段,分别是Callback阶段,Promise阶段,Generator阶段和Async/Await阶段。
三、回调函数(Callback)阶段
回调函数是异步操作最基本的方法。
demo1:假定有一个异步操作(asyncFn),和一个同步操作(normalFn)。
如果按照正常的JS处理机制来说,同步操作一定发生在异步之前。如果我想要将顺序改变,最简单的方式就是使用回调(callback)的方式处理。
回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况,容易出现回调地狱,可读性差),而且每个任务只能指定一个回调函数。此外不能使用 try catch 捕获错误,不能直接return。
回调函数易混淆点——传参:
一,将回调函数的参数作为与回调函数同等级的参数进行传递。
二,回调函数的参数在调用回调函数内部创建。
事件监听、发布订阅
事件监听
事件监听也是一种非常常见的异步编程模式,它是一种典型的逻辑分离方式,对代码解耦很有用处。
下边看例子:还是以函数f1和f2为例
以上,f1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行f2。
这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以“去耦合”,有利于实现模块化。
缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。
发布订阅模式
发布订阅式的应用非常 广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。
假定,一家三口,妈妈作为“发布者”(publisher)实施和发布信号,爸爸作为中介“订阅”(subscribe)和处理这个信号,最后小明"订阅者"(subscriber)知道什么时候自己可以开始执行。这就叫做“发布/订阅模式”(publish-subscribe pattern)。
下边来看代码:
这种模式下实现的异步编程,本质上还是通过回调函数实现的 ,但是依然存在回调嵌套和无法捕捉异常问题的情况,接下来进入Promise阶段,看看是否能解决这两个问题。
四、Promise阶段
Promise 并不是指某种特定的某个实现,它是一种规范(PromiseA+规范),是一套处理JavaScript异步的机制。
1.Promise的三种状态
- Promise有三种状态pending,fulfilled和rejected
- 状态转换只能是 pending到 resolved
- 或者pending到 rejected
状态一旦转换完成,不能再次转换。
可以由下图表示:
附上代码栗子:
当我们构造Promise 的时候,构造函数内部的代码是立即执行的
2.链式Promise
先看两个例子:
demo1;
当Promise创建对象调用resolve(...)或reject(...)时,这个Promise通过then(...)注册的回调函数就会在新的异步时间点上被触发。(then的链式调用); 在then中使用return,那么return的值会被Promise.resolve()包装。
demo2:以家务分配为例:
其实可以看出Promise.then()可以解决的回调地狱(callback hell),但是无法捕获异常,还需要调用回调函数来解决。
五、生成器Generators/yield
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator 最大的特点就是可以控制函数的执行。
function *
会定义一个生成器函数,并返回一个Generator(生成器)对象,其内部可以通过yield
暂停代码,通过调用next
恢复执行。
简单看一下例子:
在控制台输入hw.next():
上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(hello和world),即该函数有三个状态:hello,world 和 return 语句(结束执行)。
必须调用遍历器对象的next()
方法,使得指针移向下一个状态。每次调用next方法,内部指针就从上一次停下来的地方开始执行,直到遇到下一个yield
表达式(或return语句)为止。
generator很方便处理异步(一般要配合tj/co库来用),这里举例说一下co
,
co
是一个为Node.js和浏览器打造的基于生成器的流程控制工具,借助于Promise,你可以使用更加优雅的方式编写非阻塞代码。
安装co
库只需:npm install co
也可以自己去github找一下源码,了解一下
index.js
co 函数库可以让你不用编写 generator 函数的执行器,generator 函数只要放在 co 函数里,就会自动执行。 再来看一个例子
co 最大的好处在于通过它可以把异步的流程以同步的方式书写出来,并且可以使用 try/catch。
六、async/await
使用async/await,可以轻松地达成之前使用生成器和co函数所做到的工作; 一句话,async 函数就是 Generator 函数的语法糖。
然后用async/await实现上边(两个文件)的例子就可以这么写:
一比较就会发现,async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await。
1.async函数的特点:
1.执行 async 函数,返回的都是Promise 对象
自己可以 打印验证一下。
2.Promise.then 成功的情况对应 await
- Promise.catch 异常的情况 对应 try...catch
总结了这么多,如果还是不太理解,我推荐看一下这些 实战题(ES6Promise实战练习题)加速帮助消化。
此文章为个人学习笔记分享,技术有限,欢迎大家一起讨论学习。
参考文章:
1.JavaScript异步机制详解
2.JS 异步编程六种方案
3.Javascript异步编程的4种方法
4.JS 基础之异步(五):Generator
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!