草庐IT

一文明白:JavaScript异步编程

莫听穿林打叶声 何妨吟啸且徐行 2023-03-28 原文

同步和异步

JS是单线程

JavaScript语言的一大特点是单线程,同一时间只能做一件事

(单线程的JS 就是一个傻子,脑子一根筋,做着当前的这件事情,没有完成之前,绝对不会做下一件事情)

当然,这是由其诞生的初衷所决定的——处理页面中用户的交互,以及操作DOM

用户不可能同时进行两个操作,边添加边删除

当然会出现一个问题:所有的任务需要排队,前一个结束,才会执行下一个(要是前面有人很磨蹭,后面的人需要等很久),造成页面渲染的不连贯

console.log(1)

setTimeout(function(){
    console.log(3)
},100000000);

console.log(2)//快点吧,等的我花都谢了

同步和异步

问题总有解决方案,利用多核CPU的计算能力,HTML5提出了Web Worker标准,允许JS脚本创建多个线程,于是JS出现了——同步和异步

  • 同步:前一个任务结束执行下一个任务,任务的执行顺序和任务的排列顺序是一致的
  • 异步:在执行某一任务(要花费很长时间)的同时,可以执行其他任务

所以上面那个代码结果是什么呢?

JS没有那么傻,不会一直等待,所以打印了:1 2 3

//那这个呢?
console.log(1)

setTimeout(function(){
    console.log(3)
},0);

console.log(2)

知道同步和异步是什么之后,我们要学习——同步任务和异步任务

  • 同步任务(synchronous)(非耗时任务):同步任务都在主线程上执行,形成一个执行栈
  • 异步任务(asynchronous)(耗时任务):JS的异步任务都是通过回调函数实现的,如:
  1. 普通事件:click、resize等
  2. 资源加载:load、error等
  3. 定时器:setInterval、setTimeout等

JS执行机制

从内存角度(上图)理解不难发现,同步任务和异步任务根本身处两个区域,当执行任务时:

  1. 先执行执行栈中的同步任务
  2. 异步任务(回调函数)放入任务队列中
  3. 执行完所有的同步任务,就会一次读取任务队列中的异步任务,结束等待,进入执行栈开始执行
//通过底下的;例子练习一下吧(结果不唯一哦)
console.log(1);
document.onclick=function(){
    console.log('click');
}
console.log(2);
setTimeout(function(){
    console.log(3);
},3000)

你会发现,当点击鼠标时,click不断被打印,说明主线程不断获取着任务队列中的异步任务,这种主线程不断

的重复获得任务、执行任务,再获得任务、执行任务的这种机制就是——事件循环(Event Loop)!!!!

宏任务和微任务

JavaScript把异步任务又做了进一步划分——宏任务和微任务

宏任务(macrotask):

  • 异步Ajax请求
  • setTimeout、setInterval
  • 文件操作
  • 其他宏任务

微任务(microtask):

  • Promise.then、.catch和.finally
  • process.nextTick
  • 其他微任务

既然细分,势必涉及到二者的执行顺序:

每一个宏任务执行完成之后,都会检查是否存在待执行的微任务,如果有,则执行完所有的微任务,再继续执行下一个宏任务

哈哈,到这里你是不是以为已经学了很多,课件到此就结束了

其实才刚刚开始,使用回调函数只是JavaScript 的异步编程发展的第一个阶段,也只是异步解决方案的其中一种

可以说JavaScript 的异步编程发展经过了四个阶段:

  1. 回调函数、发布订阅
  2. Promise
  3. co 自执行的 Generator 函数
  4. async / await

接下来,我们主要讲一下Promiseasync / await

Promise

Promise本意是承诺,在程序中的意思就是承诺我过一段时间后会给你一个结果。 什么时候会用到过一段时间?

答案是异步操作,异步是指可能比较长时间才有结果的才做,例如网络请求、读取本地文件等

回调地狱

之所以诞生Promise,是因为回调地狱的问题,废话不多说,先看下面代码:

function demo(num) {
  setTimeout(function () {
    console.log("1");
    if (num > 5) {
      setTimeout(function () {
        console.log(num);
      }, 500);
    } else {
      setTimeout(function () {
        console.log("3");
        setTimeout(function () {
          console.log("4");
          setTimeout(function () {
            console.log("5");
          }, 3000);
        }, 500);
      }, 500);
    }
  }, 500);
}
demo(6);

是不是看到想把电脑砸了??

回调函数固然好,可以处理js浏览器中很多需要等待的任务,增加浏览器执行代码的效率,提高用户的使用试验。但是当有一个函数中,嵌套了一个回调函数然后在里面又嵌套了一个,无穷的嵌套也就造成了回调地狱问题。

这时候原本让人赏析悦目的代码有序执行的过程俨然变得更加难理解。

回调的嵌套会使得代码的可读性下降,对开发者项目后期改bug调试和维护造成很大的困难!

所以一些新特性可以解决以上回调地狱问题。

Promise构造函数

Promise是一个构造函数,我们可以创建Promise的实例对象

const p=new Promise()

new出来的Promise实例对象,代表一个异步操作

Promise的三种状态

  • Pending----Promise对象实例创建时候的初始状态

  • Fulfilled----可以理解为成功的状态

  • Rejected----可以理解为失败的状态

.then方法

Promise.prototype上包含一个.then()方法,每一次new Promise()构造函数得到的实例对象,都可以通过原型链的方式访问到.then()方

法,.then()方法用来预先指定成功和失败的回调函数

p.then(成功的回调函数,失败的回调函数)
p.then(result=>{},error=>{})
//调用.then()方法时,成功的回调函数必选,失败的回调函数可选

Promise使用

当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的

new Promise((resolve, reject) => {
  console.log('new Promise')
  resolve('success')
})
console.log('end')
// new Promise => end

执行一个new Promise构造函数

我们可以利用Promise构造函数的特性进行实例化

let p = new Promise((resolve, reject) => {
    //做一些异步操作
    setTimeout(() => {
        console.log('执行完成');
        resolve('我是成功!!');
    }, 2000);
    p.then(()=>{})
});

也可以使用返回实例函数的方式接收(推荐)

let step = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("在红岩学技术太有趣了!");
    }, 1000);
  });
};
let p = step();
p.then((res)=>{
	console.log(res);
})

Promise的构造函数接收一个参数:函数,并且这个函数需要传入两个参数:

  • resolve :异步操作执行成功后的回调函数

  • reject:异步操作执行失败后的回调函数

而resolve和reject则和下面的then链式回调的状态息息相关

then链式调用

所以,从表面上看,Promise只是能够简化层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得

回调函数能够及时调用,它比传递回调函数要简单、灵活的多。而且then函数本身为promise构造函数的实例,故接受链式调用,所以使

用Promise的正确场景是这样的:

let p = new Promise((resolve, reject) => {
  //做一些异步操作
  setTimeout(() => {
    console.log("执行完成");
    resolve("好耶");
  }, 2000);
})
  .then(
    (data) => {
      console.log(data);
      return data;
      //此时输出data为resolve传入的参数
    },
    (error) => {
      console.log(error);
      //此时输出error为reject传入的参数
    }
  )
  .then((data) => {
    console.log(data);
    return data;
    //好耶
  })
  .then((data) => {
    console.log(data);
    //好耶
  })
  .then((data) => {
    console.log(data);
    //undefined
  })
  .catch((error) => {
    console.log(data);
  });

当resolve执行后,promise状态指定为resolved,执行成功的回调。每一次then的执行中参数的data都为上一次异步函数执行的返回值。若上一次无返回值,则输出undefined.错误同理,这就是then链式调用

在结尾加上catch进行错误捕获,用来中断链条,并且捕获错误原因。

我们可以看出,用then执行的函数每一步的执行都会去等待上一步的结果,在视觉上通过then来维系,可读性好,同时还能解决令人眼花缭乱的回调地狱问题,可以说是很优美的代码流程了!

catch的用法

我们知道Promise对象除了then方法,还有一个catch方法,它是做什么用的呢?其实它和then的第二个参数一样,用来指定reject的回调。用法是这样:

p.then((data) => {
    console.log('resolved',data);
}).catch((err) => {
    console.log('rejected',err)
});

效果和写在then的第二个参数里面一样。不过它还有另外一个作用:在执行resolve的回调(也就是上面then中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会报错卡死js,而是会进到这个catch方法中。请看下面的代码:

p.then((data) => {
    console.log('resolved',data);
    console.log(somedata); //此处的somedata未定义
})
.catch((err) => {
    console.log('rejected',err);
});

在resolve的回调中,我们console.log(somedata);而somedata这个变量是没有被定义的。如果我们不用Promise,代码运行到这里就直接在控制台报错了,不往下运行了。但是在这里,会得到这样的结果:

也就是说进到catch方法里面去了,而且把错误原因传到了reason参数中。即便是有错误的代码也不会报错了,这与我们的try/catch语句有相同的功能

all的用法:谁跑的慢,以谁为准执行回调。

all接收一个数组参数,里面的值最终都算返回Promise对象

Promise的all方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后才执行回调。看下面的例子:

let Promise1 = new Promise(function(resolve, reject){})
let Promise2 = new Promise(function(resolve, reject){})
let Promise3 = new Promise(function(resolve, reject){})

let p = Promise.all([Promise1, Promise2, Promise3])

p.then(funciton(){
  // 三个都成功则成功  
}, function(){
  // 只要有失败,则失败 
})

有了all,你就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据,是不是很酷?有一个场景是很适合用这个的,一些游戏类的素材比较多的应用,打开网页时,预先加载需要用到的各种资源如图片、flash以及各种静态文件。所有的都加载完后,我们再进行页面的初始化。

race的用法:谁跑的快,以谁为准执行回调

race的使用场景:比如我们可以用race给某个异步请求设置超时时间,并且在超时后执行相应的操作,代码如下:

 //请求某个图片资源
    function requestImg(){
        var p = new Promise((resolve, reject) => {
            var img = new Image();
            img.onload = function(){
                resolve(img);
            }
            img.src = '图片的路径';
        });
        return p;
    }
    //延时函数,用于给请求计时
    function timeout(){
        var p = new Promise((resolve, reject) => {
            setTimeout(() => {
                reject('图片请求超时');
            }, 5000);
        });
        return p;
    }
    Promise.race([requestImg(), timeout()]).then((data) =>{
        console.log(data);
    }).catch((err) => {
        console.log(err);
    });

requestImg函数会异步请求一张图片,我把地址写为"图片的路径",所以肯定是无法成功请求到的。timeout函数是一个延时5秒的异步操作。我们把这两个返回Promise对象的函数放进race,于是他俩就会赛跑,如果5秒之内图片请求成功了,那么遍进入then方法,执行正常的流程。如果5秒钟图片还未成功返回,那么timeout就跑赢了,则进入catch,报出“图片请求超时”的信息。运行结果如下:

async/await

接下来要讲的非常重要,真要实现完美的异步还是得看async/await,这也是目前开发环境中最普遍、最好用的方法。

async/await是ES8引入的新语法,用来简化Promise异步操作,在它出现之前,开发者只能通过链式调用处理Promise异步操作。

async和await是基于promise实现的,可以完美的解决回调地狱问题,优秀的封装也让它实现上述功能所用代码更少,并且可读性更强!

let step = (time) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("前端太有趣了!");
    }, time);
  });
};
async function demo() {
  let word = await step(500);
  console.log(word);//前端太有趣了!
}
demo();

注意:

  • 记住await后面一定要接promise风格的函数且有resolve返回值,否则返回undefined
  • function中使用了await,则function必须被async修饰
  • 在async方法中,第一个await之前的代码会同步执行,await之后的代码会异步执行

当我们不断增加这种异步操作时,整个代码得结构反而如同同步一样清晰,使得回调地狱问题得到了完美的解决

async function demo(){
	let word1 = await step(500);
	let word2 = await step(1000);
	let word3 = await step(1500);
}

我们也可以对step进行Promise封装从而达到异步函数也像同步的执行顺序一样执行,非常人性化!

注意:在await之前的代码属于同步调用,在await之后的代码则会进入异步队列,会在前面的await得到返回值以后再执行后续的代码

let step = (time) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("前端太有趣了!");
    }, time);
  });
};
async function demo() {
  let word1 = await step(500);
  console.log(word1); //前端太有趣了!
  let word2 = await step(1000);
  console.log(word2);//前端太有趣了!
  let word3 = await step(1500);
  return "函数执行完成";
}
demo().then((res) => {
  console.log(res);//函数执行完成
});

async也是基于promise封装的函数,可以调用以后返回一个实例可以通过then的方式接受函数的返回值并进行处理!

建议阅读

https://juejin.cn/post/6844903487805849613

https://juejin.cn/post/6844904094079926286

https://mp.weixin.qq.com/s/wugntKhMZpgr6RtB1AwAmQ

练习

分析以下代码输出顺序,将原因结果注释到代码代码旁

setTimeout(() => {
  console.log('setTimeout start');
  new Promise((resolve) => {
    console.log('promise1 start');
    resolve();
  }).then(() => {
    console.log('promise1 end');
  })
  console.log('setTimeout end');
}, 0);
function promise2() {
  return new Promise((resolve) => {
    console.log('promise2');
    resolve();
  })
}
async function async1() {
  console.log('async1 start');
  await promise2();
  console.log('async1 end');
}
async1();
console.log('script end');

(练习答案见评论区)

有关一文明白:JavaScript异步编程的更多相关文章

  1. ruby-on-rails - 如何在 ruby​​ 中使用两个参数异步运行 exe? - 2

    exe应该在我打开页面时运行。异步进程需要运行。有什么方法可以在ruby​​中使用两个参数异步运行exe吗?我已经尝试过ruby​​命令-system()、exec()但它正在等待过程完成。我需要用参数启动exe,无需等待进程完成是否有任何ruby​​gems会支持我的问题? 最佳答案 您可以使用Process.spawn和Process.wait2:pid=Process.spawn'your.exe','--option'#Later...pid,status=Process.wait2pid您的程序将作为解释器的子进程执行。除

  2. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

  3. 网络编程套接字 - 2

    网络编程套接字网络编程基础知识理解源`IP`地址和目的`IP`地址理解源MAC地址和目的MAC地址认识端口号理解端口号和进程ID理解源端口号和目的端口号认识`TCP`协议认识`UDP`协议网络字节序socket编程接口`sockaddr``UDP`网络程序服务器端代码逻辑:需要用到的接口服务器端代码`udp`客户端代码逻辑`udp`客户端代码`TCP`网络程序服务器代码逻辑多个版本服务器单进程版本多进程版本多线程版本线程池版本服务器端代码客户端代码逻辑客户端代码TCP协议通讯流程TCP协议的客户端/服务器程序流程三次握手(建立连接)数据传输四次挥手(断开连接)TCP和UDP对比网络编程基础知识

  4. ruby-on-rails - 在 Ruby on Rails 中发送响应之前如何等待多个异步操作完成? - 2

    在我做的一些网络开发中,我有多个操作开始,比如对外部API的GET请求,我希望它们同时开始,因为一个不依赖另一个的结果。我希望事情能够在后台运行。我找到了concurrent-rubylibrary这似乎运作良好。通过将其混合到您创建的类中,该类的方法具有在后台线程上运行的异步版本。这导致我编写如下代码,其中FirstAsyncWorker和SecondAsyncWorker是我编写的类,我在其中混合了Concurrent::Async模块,并编写了一个名为“work”的方法来发送HTTP请求:defindexop1_result=FirstAsyncWorker.new.async.

  5. ruby - 我正在学习编程并选择了 Ruby。我应该升级到 Ruby 1.9 吗? - 2

    我完全不是程序员,正在学习使用Ruby和Rails框架进行编程。我目前正在使用Ruby1.8.7和Rails3.0.3,但我想知道我是否应该升级到Ruby1.9,因为我真的没有任何升级的“遗留”成本。缺点是什么?我是否会遇到与普通gem的兼容性问题,或者甚至其他我不太了解甚至无法预料的问题? 最佳答案 你应该升级。不要坚持从1.8.7开始。如果您发现不支持1.9.2的gem,请避免使用它们(因为它们很可能不被维护)。如果您对gem是否兼容1.9.2有任何疑问,您可以在以下位置查看:http://www.railsplugins.or

  6. ruby-on-rails - 使用 javascript 更改数据方法不会更改 ajax 调用用户的什么方法? - 2

    我遇到了一个非常奇怪的问题,我很难解决。在我看来,我有一个与data-remote="true"和data-method="delete"的链接。当我单击该链接时,我可以看到对我的Rails服务器的DELETE请求。返回的JS代码会更改此链接的属性,其中包括href和data-method。再次单击此链接后,我的服务器收到了对新href的请求,但使用的是旧的data-method,即使我已将其从DELETE到POST(它仍然发送一个DELETE请求)。但是,如果我刷新页面,HTML与"new"HTML相同(随返回的JS发生变化),但它实际上发送了正确的请求类型。这就是这个问题令我困惑的

  7. ruby - 如何以编程方式删除实例上的 "singleton information"以使其编码(marshal)? - 2

    我创建了一个由于“在运行时执行的单例元类定义”而无法编码的对象(这段代码的描述是否正确?)。这是通过以下代码执行的:#defineclassXthatmyusesingletonclassmetaprogrammingfeatures#throughcallofmethod:break_marshalling!classXdefbreak_marshalling!meta_class=class我该怎么做才能使对象编码正确?是否可以从对象instance_of_x的classX中“移除”单例组件?我真的需要一个建议,因为我们的一些对象需要通过Marshal.dump序列化机制进行缓存。

  8. Ruby 元编程问题 - 2

    我正在查看Ruby日志记录库Logging.logger方法并从sourceatgithub提出问题与这段代码有关:logger=::Logging::Logger.new(name)logger.add_appendersappenderlogger.additive=falseclass我知道类 最佳答案 这实际上删除了方法(当它实际被执行时)。这是确保close不会被调用两次的保障措施。看起来好像有嵌套的“class 关于Ruby元编程问题,我们在StackOverflow上找到一

  9. ruby - Paperclip:以编程方式分配图像并设置其名称 - 2

    使用Paperclip,我想从这样的URL抓取图像:require'open-uri'user.photo=open(url)问题是我最后得到一个像“open-uri20110915-4852-1o7k5uw”这样的文件名。有什么方法可以更改user.photo上的文件名?作为一个额外的变化,Paperclip将我的文件存储在S3上,所以如果我可以在初始分配中设置我想要的文件名就更好了,这样图像就会上传到正确的S3key。像这样:user.photo=open(url),:filename=>URI.parse(url).path 最佳答案

  10. ruby - 如何以编程方式检查证书是否已被吊销? - 2

    我正在开发一个xcode自动构建系统。在执行一些预构建验证时,我想检查指定的证书文件是否已被撤销。我了解securityverify-cert验证其他证书属性但不验证吊销。我如何检查撤销?我正在用Ruby编写构建系统,但我对任何语言的想法都持开放态度。我阅读了这个答案(Openssl-Howtocheckifacertificateisrevokedornot),但指向底部的链接(DoesOpenSSLautomaticallyhandleCRLs(CertificateRevocationLists)now?)进入的Material对我的目的来说有点过于复杂(用户上传已撤销的证书是一

随机推荐