本文已参与「新人创作礼」活动,一起开启掘金创作之路。
JS同步和异步是非常重要的知识点,本文包含同步和异步区别,Js中异步编程的方式,发布订阅等知识,搭配JS线程知识更好理解。
定义
同步,可以理解为在执行完一个函数或方法之后,一直等待系统返回值或消息,这时程序是出于阻塞的,只有接收到返回的值或消息后才往下执行其他的命令。
异步,执行完函数或方法后,不必阻塞性地等待返回值或消息,只需要向系统委托一个异步过程,那么当系统接收到返回值或消息时,系统会自动触发委托的异步过程,从而完成一个完整的流程。
区别:请求发出后,是否需要等待结果,才能继续执行其他操作。
JavaScript 实现异步编程
-
回调函数
最常见的是使用回调函数的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱
,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护
。流程会很混乱,而且每个任务只能指定一个回调函数。 -
事件监听
容易理解,可以绑定多个事件,每个事件可以指定多个回调函数。但是任务的执行不取决于代码的顺序,而取决于某个事件是否发生。 -
发布/订阅(观察者模式)
性质与"事件监听"类似,但是明显优于后者。因为可以通过查看"消息中心",了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。 -
Promise 对象
优点:这样写的优点在于,回调函数变成了链式写法,程序的流程可以看得很清楚,而且有一整套的配套方法,可以实现许多强大的功能。
缺点:无法取消promise。如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。当执行多个Promise时,一堆then看起来也很不友好。 -
Generator 函数
使用 generator 的方式可以在函数的执行过程中,将函数的执行权转移出去,在函数外部我们还可以将执行权转移回来。当我们遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕的时候我们再将执行权给转移回来。因此我们在generator 内部对于异步操作的方式,可以以同步的顺序来书写。 -
async 函数
优点:内置执行器,比Generator操作更简单。async/await比*/yield语义更清晰。返回值是Promise对象,可以用then指定下一步操作。代码更整洁。可以捕获同步和异步的错误。async 函数是 generator 和 promise 实现的一个自动执行的
语法糖
,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此我们可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。
发布订阅
发布-订阅模式用来定义一种一对多的依赖关系。多个对象(订阅者:subscriber)同时监听同一对象(发布者:publisher)的数据状态变化。当发布者的数据状态发生变化的时候,就会通知所有的订阅者,同时还可能以事件对象的形式传递一些消息。
在发布-订阅模式,消息的发送方,叫做发布者(publishers),消息不会直接发送给特定的接收者,叫做订阅者。
意思就是发布者和订阅者不知道对方的存在。需要一个第三方组件,叫做信息中介,它将订阅者和发布者串联起来,它过滤和分配所有输入的消息。
那么如何过滤消息的呢?事实上这里有几个过程,最流行的方法是:基于主题以及基于内容。
其实很多JS框架中都使用到了发布-订阅者模式,比如vue的数据响应
发布订阅的优点
非常明显,一是时间上的解耦,二是对象间的解耦。
时间上的解耦
: 在异步编程中,由于无法确定异步加载的时间,有可能订阅事件的模块还没有初始化完毕而异步加载就完成了,发布者就已经发布事件了。通过发布订阅模式,可以将发布者的事件提前保存起来,等到发布者加载完毕再执行。
对象间的解耦
:发布订阅模式中,发布者和订阅者可以不必知道对方的存在,而是通过中介对象来通信。
缺点
:创建订阅者本身需要一定的时间和内存,而当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。另外,发布订阅模式将对象间完全解耦,如果过度使用的话,对象和对象之间的必要联系就会被掩盖,会导致程序难以追踪和理解。
setTimeout倒计时为什么会出现误差
setTimeout() 只是将事件插入了“任务队列”,必须等当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码消耗时间很长,也有可能要等很久,所以并没办法保证回调函数一定会在 setTimeout() 指定的时间执行。所以, setTimeout() 的第二个参数表示的是最少时间,并非是确切时间。
HTML5标准规定了 setTimeout() 的第二个参数的最小值不得小于4毫秒,如果低于这个值,则默认是4毫秒。
JS异步发展史
异步最早的解决方案是回调函数,如事件的回调,setInterval/setTimeout中的回调。但回调函数有一个很常见的问题,就是回调地狱的问题;
为了解决回调地狱的问题,提出了Promise解决方案,ES6将其写进了语言标准。Promise解决了回调地狱的问题,但是Promise也存在一些问题,如错误不能被try catch,而且使用Promise的链式调用,其实并没有从根本上解决回调地狱的问题,只是换了一种写法。
ES6中引入 Generator 函数,Generator是一种异步编程解决方案,Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权,Generator 函数可以看出是异步任务的容器,需要暂停的地方,都用yield语句注明。但是 Generator 使用起来较为复杂。
ES7又提出了新的异步解决方案:async/await,async是 Generator 函数的语法糖,async/await 使得异步代码看起来像同步代码,异步编程发展的目标就是让异步逻辑的代码看起来像同步一样。
async/await
async/await 是在 ES7 中引入的新语法,可以更加方便地进行异步操作。本质: Generator 的语法糖。
async 的返回值是 Promise 实例对象,await 可以得到异步结果。
在普通的函数前面加上 async 关键字,就成了 async 函数。
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
await 命令后面的Promise对象,运行结果可能是 rejected,此时等同于 async 函数返回的 Promise 对象被reject。因此需要加上错误处理,可以给每个 await 后的 Promise 增加 catch 方法;也可以将 await 的代码放在 try...catch 中。
await命令只能用在async函数之中,如果用在普通函数,会报错。
异步加载js的方法
defer:只支持IE如果您的脚本不会改变文档的内容,可将 defer 属性加入到<script>
标签中,以便加快处理文档的速度。因为浏览器知道它将能够安全地读取文档的剩余部分而不用执行脚本,它将推迟对脚本的解释,直到文档已经显示给用户为止。
async:HTML5属性仅适用于外部脚本,并且如果在IE中,同时存在defer和async,那么defer的优先级比较高,脚本将在页面完成时执行。
创建script标签,插入到DOM中
defer和async的区别:
由图可以看出,defer和async都是将异步加载脚本的方式,只不过defer会将加载好的脚本放入最后执行,而async会在脚本加载好之后便开始执行。
defer是“渲染完再执行”,async是“下载完就执行”
也因为上面这个特性,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。