1.1 什么叫异步
异步 (async) 是相对于同步 (sync) 而言的, 很好理解.
同步就是一件事一件事的执行. 只有前一个任务执行完毕, 才能执行后一个任务. 而异步比如:
- setTimeout(function cbFn(){
- console.log('learnInPro');
- }, 1000);
- console.log('sync things');
setTimeout 就是一个异步任务, 当 JS 引擎顺序执行到 setTimeout 的时候发现他是个异步任务, 则会把这个任务挂起, 继续执行后面的代码. 直到 1000ms 后, 回调函数 cbFn 才会执行, 这就是异步, 在执行到 setTimeout 的时候, JS 并不会傻呵呵的等着 1000ms 执行 cbFn 回调函数, 而是继续执行了后面的代码.
1.2 为啥要在 JS 中使用异步
由于 javascript 是单线程的, 只能在 JS 引擎的主线程上运行的, 所以 js 代码只能一行一行的执行, 不能在同一时间执行多个 js 代码任务, 这就导致如果有一段耗时较长的计算, 或者是一个 ajax 请求等 IO 操作, 如果没有异步的存在, 就会出现用户长时间等待, 并且由于当前任务还未完成, 所以这时候所有的其他操作都会无响应.
1.3 那为啥 JS 不设计成多线程的
这主要跟 javascript 的历史有关, js 最开始只是为了处理一些表单验证和 DOM 操作而被创造出来的, 所以主要为了语言的轻量和简单采用了单线程的模式. 多线程模型相比单线程要复杂很多, 比如多线程需要处理线程间资源的共享问题, 还要解决状态同步等问题.
如果 JS 是多线程的话, 当你要执行往 div 中插入一个 DOM 的操作的同时, 另一个线程执行了删除这个 div 的操作, 这个时候就会出现很多问题, 我们还需要为此增加锁机制等.
好, 那么现在我们知道了单线程的 JS 为了不出现长时间等待的状况, 会使用异步来处理. 比如当执行一个 ajax 操作的时候, 当 js 发出请求后, 不会傻了吧唧的在那里等着服务器数据返回, 而是去继续执行后面的任务, 等到服务器数据返回以后再通知 js 引擎去处理.
那么常见的异步模式有哪些呢?
回调函数
事件监听
发布 / 订阅模式(又称观察者模式)
promise
后来 ES6 中, 引入了 Generator 函数; ES7 中, async/await 更是将异步编程带入了一个全新的阶段.
这些异步模式我们会在后面详细来说, 这里我们有个概念就好.
1.4 JS 如何实现异步
具体 JS 是如何实现异步操作的呢?
答案就是
JS 的事件循环机制(Event Loop)
.
具体来说:
当 JS 解析执行时, 会被引擎分为两类任务,
同步任务(synchronous)
和
异步任务(asynchronous)
.
对于同步任务来说, 会被推到执行栈按顺序去执行这些任务.
对于异步任务来说, 当其可以被执行时, 会被放到一个
任务队列(task queue)
里等待 JS 引擎去执行.
当执行栈中的所有同步任务完成后, JS 引擎才会去任务队列里查看是否有任务存在, 并将任务放到执行栈中去执行, 执行完了又会去任务队列里查看是否有已经可以执行的任务. 这种循环检查的机制, 就叫做
事件循环(Event Loop)
.
对于任务队列, 其实是有更细的分类. 其被分为
微任务 (microtask) 队列
&
宏任务 (macrotask) 队列
宏任务: setTimeout,setInterval 等, 会被放在宏任务 (macrotask) 队列.
微任务: Promise 的 then,Mutation Observer 等, 会被放在微任务 (microtask) 队列.
Event Loop 的执行顺序是:
首先执行执行栈里的任务.
执行栈清空后, 检查微任务 (microtask) 队列, 将可执行的微任务全部执行.
取宏任务 (macrotask) 队列中的第一项执行.
回到第二步.
注意: 微任务队列每次全执行, 宏任务队列每次只取一项执行.
我们举个例子:
- setTimeout(() => {
- console.log('我是第一个宏任务');
- Promise.resolve().then(() => {
- console.log('我是第一个宏任务里的第一个微任务');
- });
- Promise.resolve().then(() => {
- console.log('我是第一个宏任务里的第二个微任务');
- });
- }, 0);
- setTimeout(() => {
- console.log('我是第二个宏任务');
- }, 0);
- Promise.resolve().then(() => {
- console.log('我是第一个微任务');
- });
- console.log('执行同步任务');
最后的执行结果是:
- // 执行同步任务
- // 我是第一个微任务
- // 我是第一个宏任务
- // 我是第一个宏任务里的第一个微任务
- // 我是第一个宏任务里的第二个微任务
- // 我是第二个宏任务
1.5 JS 异步编程模式
这里我们已经知道了 JS 中异步的运行机制, 我们翻回头来详细的了解一下常见的各种异步的编程模式.
回调函数
事件监听
发布 / 订阅模式
- Promise
- Generator
- async/await
1.5.1 回调函数
回调函数是异步操作最基本的方法.
比如: 我有一个异步操作(asyncFn), 和一个同步操作(normalFn).
- function asyncFn() {
- setTimeout(() => {
- console.log('asyncFn');
- }, 0)
- }
- function normalFn() {
- console.log('normalFn');
- }
- asyncFn();
- normalFn();
- // normalFn
- // asyncFn
如果按照正常的 JS 处理机制来说, 同步操作一定发生在异步之前. 如果我想要将顺序改变, 最简单的方式就是使用回调的方式处理.
- function asyncFn(callback) {
- setTimeout(() => {
- console.log('asyncFn');
- callback();
- }, 0)
- }
- function normalFn() {
- console.log('normalFn');
- }
- asyncFn(normalFn);
- // asyncFn
- // normalFn
1.5.2 事件监听
另一种思路是采用事件驱动模式. 这种思路是说异步任务的执行不取决于代码的顺序, 而取决于某个事件是否发生.
比如一个我们注册一个按钮的点击事件或者注册一个自定义事件, 然后通过点击或者 trigger 的方式触发这个事件.
1.5.3 发布 / 订阅模式(又称观察者模式)
这个重点讲下, 发布 / 订阅模式像是事件监听模式的升级版.
在发布 / 订阅模式中, 你可以想象存在一个消息中心的地方, 你可以在那里 "注册一条消息", 那么被注册的这条消息可以被感兴趣的若干人 "订阅", 一旦未来这条 "消息被发布", 则所有订阅了这条消息的人都会得到提醒.
这个就是发布 / 订阅模式的设计思路. 接下来我们一点一点实现一个简单的发布 / 订阅模式.
首先我们先实现一个消息中心.
- // 先实现一个消息中心的构造函数, 用来创建一个消息中心
- function MessageCenter(){
- var _messages = {}; // 所有注册的消息都存在这里
- this.regist = function(){}; // 用来注册消息的方法
- this.subscribe = function(){}; // 用来订阅消息的方法
- this.fire = function(){}; // 用来发布消息的方法
- }
这里一个消息中心的雏形就创建好了, 接下来我们只要完善下 regist,subscribe 和 fire 这三个方法就好了.
- function MessageCenter(){
- var _messages = {};
- // 对于 regist 方法, 它只负责注册消息, 就只接收一个注册消息的类型 (标识) 参数就好了.
- this.regist = function(msgType){
- // 判断是否重复注册
- if(typeof _messages[msgType] === 'undefined'){
- _messages[msgType] = []; // 数组中会存放订阅者
- }else{
- console.log('这个消息已经注册过了');
- }
- }
- // 对于 subscribe 方法, 需要订阅者和已经注册了的消息进行绑定
- // 由于订阅者得到消息后需要处理消息, 所以他是一个个的函数
- this.subscribe = function(msgType, subFn){
- // 判断是否有这个消息
- if(typeof _messages[msgType] !== 'undefined'){
- _messages[msgType].push(subFn);
- }else{
- console.log('这个消息还没注册过, 无法订阅')
- }
- }
- // 最后我们实现下 fire 这个方法, 就是去发布某条消息, 并通知订阅这条消息的所有订阅者函数
- this.fire = function(msgType, args){
- // msgType 是消息类型或者说是消息标识, 而 args 可以设置这条消息的附加信息
- // 还是发布消息时, 判断下有没有这条消息
- if(typeof _messages[msgType] === 'undefined') {
- console.log('没有这条消息, 无法发布');
- return false;
- }
- var events = {
- type: msgType,
- args: args || {}
- };
- _messages[msgType].forEach(function(sub){
- sub(events);
- })
- }
- }
这样, 一个简单的发布 / 订阅模式就完成了, 当然这只是这种模式的其中一种简单实现, 还有很多其他的实现方式.
就此我们就可以用他来处理一些异步操作了.
- var msgCenter = new MessageCenter();
- msgCenter.regist('A');
- msgCenter.subscribe('A', subscribeFn);
- function subscribeFn(events) {
- console.log(events.type, events.args);
- }
- // -----
- setTimeout(function(){
- msgCenter.fire('A', 'fire msg');
- }, 1000);
- // A, fire msg
我们在这篇文章里深入讲解了什么是异步, 为什么要有异步以及在 JS 中引擎是如何处理异步的, 后面我们讲解了几种异步编程模式并重点讲了下发布 / 订阅模式,
在下一章里面我们重点把另外几种异步编程模式 Promise,Generator,async/await 讲完.
来源: https://www.cnblogs.com/learninpro/p/9166356.html