web前端框架Javascript之JavaScript 异步编程史

早期的 Web 应用中,与后台进行交互时,需要进行 form 表单的提交,然后在页面刷新后给用户反馈结果。在页面刷新过程中,后台会重新返回一段 HTML 代码,这段 HTML 中的大部分内容与之前页面基本相同,这势必造成了流量的浪费,而且一来一回也延长了页面的响应时间,总是会让人觉得 Web 应用的体验感比不上客户端应用。

2004 年,AJAX 即“Asynchronous JavaScript and XML”技术横空出世,让 Web 应用的体验得到了质的提升。再到 2006 年,jQuery 问世,将 Web 应用的开发体验也提高到了新的台阶。

由于 JavaScript 语言单线程的特点,不管是事件的触发还是 AJAX 都是通过回调的方式进行异步任务的触发。如果我们想要线性的处理多个异步任务,在代码中就会出现如下的情况:

getUser(token, function (user) {

getClassID(user, function (id) {

getClassName(id, function (name) {

  console.log(name)

})

})

})

我们经常将这种代码称为:“回调地狱”。

事件与回调

众所周知,JavaScript 的运行时是跑在单线程上的,是基于事件模型来进行异步任务触发的,不需要考虑共享内存加锁的问题,绑定的事件会按照顺序齐齐整整的触发。要理解 JavaScript 的异步任务,首先就要理解 JavaScript 的事件模型。

由于是异步任务,我们需要组织一段代码放到未来运行(指定时间结束时或者事件触发时),这一段代码我们通常放到一个匿名函数中,通常称为回调函数。

setTimeout(function () {

// 在指定时间结束时,触发的回调

}, 800)

window.addEventListener(“resize”, function() {

// 当浏览器视窗发生变化时,触发的回调

})

未来运行

前面说过回调函数的运行是在未来,这就说明回调中使用的变量并不是在回调声明阶段就固定的。

for (var i = 0; i < 3; i++) {

setTimeout(function () {

console.log("i =", i)

}, 100)

}

这里连续声明了三个异步任务,100毫秒 后会输出变量 i 的结果,按照正常的逻辑应该会输出 0、1、2这三个结果。

然而,事实并非如此,这也是我们刚开始接触 JavaScript 的时候会遇到的问题,因为回调函数的实际运行时机是在未来,所以输出的 i 的值是循环结束时的值,三个异步任务的结果一致,会输出三个 i = 3。
在这里插入图片描述
经历过这个问题的同学,一般都知道,我们可以通过闭包的方式,或者重新声明局部变量的方式解决这个问题。

事件队列

事件绑定之后,会将所有的回调函数存储起来,然后在运行过程中,会有另外的线程对这些异步调用的回调进行调度的处理,一旦满足“触发”条件就会将回调函数放入到对应的事件队列(这里只是简单的理解成一个队列,实际存在两个事件队列:宏任务、微任务)中。

满足触发条件一般有以下几种情况:

鸿蒙官方战略合作共建——HarmonyOS技术社区

DOM 相关的操作进行的事件触发,比如点击、移动、失焦等行为;

IO 相关的操作,文件读取完成、网络请求结束等;

时间相关的操作,到达定时任务的约定时间;

上面的这些行为发生时,代码中之前指定的回调函数就会被放入一个任务队列中,主线程一旦空闲,就会将其中的任务按照先进先出的流程一一执行。当有新的事件被触发时,又会重新放入到回调中,如此循环���,所以 JavaScript 的这一机制通常被称为“事件循环机制”。

for (var i = 1; i <= 3; i++) {

const x = i

setTimeout(function () {

console.log(`第${x}个setTimout被执行`)

}, 100)

}

可以看到,其运行顺序满足队列先进先出的特点,先声明的先被执行。
在这里插入图片描述
线程的阻塞

由于 JavaScript 单线程的特点,定时器其实并不可靠,当代码遇到阻塞的情况,即使事件到达了触发的时间,也会一直等在主线程空闲才会运行。

const start = Date.now()

setTimeout(function () {

console.log(实际等待时间: ${Date.now() - start}ms)

}, 300)

// while循环让线程阻塞 800ms

while(Date.now() - start < 800) {}

上面代码中,定时器设置了 300ms 后触发回调函数,如果代码没有遇到阻塞,正常情况下会 300ms后,会输出等待时间。

但是我们在还没加了一个 while 循环,这个循环会在 800ms 后才结束,主线程一直被这个循环阻塞在这里,导致时间到了回调函数也没有正常运行。
在这里插入图片描述
Promise

事件回调的方式,在编码的过程中,就特别容易造成回调地狱。而 Promise 提供了一种更加线性的方式编写异步代码,有点类似于管道的机制。

// 回调地狱

getUser(token, function (user) {

getClassID(user, function (id) {

getClassName(id, function (name) {

  console.log(name)

})

})

})

// Promise

getUser(token).then(function (user) {

return getClassID(user)

}).then(function (id) {

return getClassName(id)

}).then(function (name) {

console.log(name)

}).catch(function (err) {

console.error(‘请求异常’, err)

})

Promise 在很多语言中都有类似的实现,在 JavaScript 发展过程中,比较著名的框架 jQuery、Dojo 也都进行过类似的实现。2009 年,推出的 CommonJS 规范中,基于 Dojo.Deffered 的实现方式,提出 Promise/A 规范。也是这一年 Node.js 横空出世,Node.js 很多实现都是依照 CommonJS 规范来的,比较熟悉的就是其模块化方案。

早期的 Node.js 中也实现了 Promise 对象,但是 2010 年的时候,Ry(Node.js 作者)认为 Promise 是一种比较上层的实现,而且 Node.js 的开发本来就依赖于 V8 引擎,V8 引擎原生也没有提供 Promise 的支持,所以后来 Node.js 的模块使用了 error-first callback 的风格(cb(error, result))。

const fs = require(‘fs’)

// 第一个参数为 Error 对象,如果不为空,则表示出现异常

fs.readFile(‘./README.txt’, function (err, buffer) {

if (err !== null) {

return

}

console.log(buffer.toString())

})

这一决定也导致后来 Node.js 中出现了各式各样的 Promise 类库,比较出名的就是 Q.js、Bluebird。关于 Promise 的实现,之前有写过一篇文章,感兴趣可以看看:《手把手教你实现 Promise》。

在 Node.js@8 之前,V8 原生的 Promise 实现有一些性能问题,导致原生 Promise 的性能甚至不如一些第三方的 Promise 库。
在这里插入图片描述
所以,低版本的 Node.js 项目中,经常会将 Promise 进行全局的替换:

const Bulebird = require(‘bluebird’)

global.Promise = Bulebird

Generator & co

Generator(生成器) 是 ES6 提供的一种新的函数类型,主要是用于定义一个能自我迭代的函数。通过 function * 的语法能够构造一个 Generator 函数,函数执行后会返回一个iteration(迭代器)对象,该对象具有一个 next() 方法,每次调用 next() 方法就会在 yield 关键词前面暂停,直到再次调用 next() 方法。

function * forEach(array) {

const len = array.length

for (let i = 0; i < len; i ++) {

yield i;

}

}

const it = forEach([2, 4, 6])

it.next() // { value: 2, done: false }

it.next() // { value: 4, done: false }

it.next() // { value: 6, done: false }

it.next() // { value: undefined, done: true }

next() 方法会返回一个对象,对象有两个属性 value、done:

value:表示 yield 后面的值;

done:表示函数是否执行完毕;

由于生成器函数具有中断执行的特点,将生成器函数当做一个异步操作的容器,再配合上 Promise 对象的 then 方法可以将交回异步逻辑的执行权,在每个 yeild 后面都加上一个 Promise 对象,就能让迭代器不停的往下执行。

function * gen(token) {

const user = yield getUser(token)

const cId = yield getClassID(user)

const name = yield getClassName(cId)

console.log(name)

}

const g = gen(‘xxxx-token’)

// 执行 next 方法返回的 value 为一个 Promise 对象

const { value: promise1 } = g.next()

promise1.then(user => {

// 传入第二个 next 方法的值,会被生成器中第一个 yield 关键词前面的变量接受

// 往后推也是如此,第三个 next 方法的值,会被第二个 yield 前面的变量接受

// 只有第一个 next 方法的值会被抛弃

const { value: promise2 } = gen.next(user).value

promise2.then(cId => {

const { value: promise3, done } = gen.next(cId).value

// 依次先后传递,直到 next 方法返回的 done 为 true

})

})

我们将上面的逻辑进行一下抽象,让每个 Promise 对象正常返回后,就自动调用 next,让迭代器进行自执行,直到执行完毕(也就是 done 为 true)。

function co(gen, …args) {

const g = gen(…args)

function next(data) {

const { value: promise, done } = g.next(data)

if (done) return promise

promise.then(res => {

  next(res) // 将 promise 的结果传入下一个 yield

})

}

next() // 开始自执行

}

co(gen, ‘xxxx-token’)

这也就是 koa 早期的核心库 co 的实现逻辑,只是 co 进行了一些参数校验与错误处理。通过 generator 加上 co 能够让异步流程更加的简单易读,对开发者而言肯定是阶段欢喜的一件事。

async/await

async/await 可以说是 JavaScript 异步变成的解决方案,其实本质上就是 Generator & co 的一个语法糖,只需要在异步的生成器函数前加上 async,然后将生成器函数内的 yield 替换为 await。

async function fun(token) {

const user = await getUser(token)

const cId = await getClassID(user)

const name = await getClassName(cId)

console.log(name)

}

fun()

async 函数将自执行器进行了内置,同时 await 后不限制为 Promise 对象,可以为任意值,而且 async/await 在语义上比起生成器的 yield 更加清楚,一眼就能明白这是一个异步操作。

文章来源:网络 版权归原作者所有

上文内容不用于商业目的,如涉及知识产权问题,请权利人联系小编,我们将立即处理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/54254.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

[ 华为云 ] 云计算中Region、VPC、AZ 是什么,他们又是什么关系,应该如何抉择

前几天看到一个问答帖&#xff0c;我回答完了才发现这个帖子居然是去年的也没人回复&#xff0c;其中他问了一些华为云的问题&#xff0c;对于其中的一些概念&#xff0c;这里来总结讲解一下&#xff0c;希望对学习华为云的小伙伴有所帮助。 文章目录 区域&#xff08;Region&a…

nodejs安装及多版本安装与TS环境搭建

nodejs安装及多版本安装与TS环境搭建 方法一&#xff1a; 普通安装nodejs,确定只能安装一个。网址&#xff1a;链接: 官网 不同系统下安装&#xff1a;不同系统下的nodejs 方法二&#xff1a; 借助工具nvm&#xff0c;安装多个nodejs&#xff0c;随时切换nodejs版本 什么是…

网络面试合集

传输层的数据结构是什么&#xff1f; 就是在问他的协议格式&#xff1a;UDP&TCP 2.1.1三次握手 通信前&#xff0c;要先建立连接&#xff0c;确保双方都是在线&#xff0c;具有数据收发的能力。 2.1.2四次挥手 通信结束后&#xff0c;会有一个断开连接的过程&#xff0…

Android水波纹按压效果(不按时透明)

按压后的效果&#xff08;左边"Cancle"是不按压的效果&#xff09; button_water_ripple_bg.xml <?xml version"1.0" encoding"utf-8"?> <ripple xmlns:android"http://schemas.android.com/apk/res/android"android:colo…

Java-API简析_java.io.FileReader类(基于 Latest JDK)(浅析源码)

【版权声明】未经博主同意&#xff0c;谢绝转载&#xff01;&#xff08;请尊重原创&#xff0c;博主保留追究权&#xff09; https://blog.csdn.net/m0_69908381/article/details/132038537 出自【进步*于辰的博客】 因为我发现目前&#xff0c;我对Java-API的学习意识比较薄弱…

Appium+python自动化(三十五)- 命令启动appium之 appium服务命令行参数(超详解)

简介 前边介绍的都是通过按钮点击启动按钮来启动appium服务&#xff0c;有的小伙伴或者童鞋们乍一听可能不信&#xff0c;或者会问如何通过命令行启动appium服务呢&#xff1f;且听一一道来。 一睹为快 其实相当的简单&#xff0c;不看不知道&#xff0c;一看吓一跳&#xf…

linux_进程状态

目录 一. 概念铺设 状态是什么&#xff1f; 传统操作系统的状态转换图 二. 传统操作系统状态 1. 运行 2. 阻塞 3. 挂起 三. linux 中的进程状态 1. 总体介绍 2. R 3. S 4. D kill -9 D vs S 5. T kill T vs S 6. Z 什么是僵尸状态&#xff1f; 僵尸进程的危害 …

详解Mybatis之自动映射 自定义映射问题

编译软件&#xff1a;IntelliJ IDEA 2019.2.4 x64 操作系统&#xff1a;win10 x64 位 家庭版 Maven版本&#xff1a;apache-maven-3.6.3 Mybatis版本&#xff1a;3.5.6 文章目录 一、Mybatis中的自动映射是什么&#xff1f;二、Mybatis中的自定义映射是什么&#xff1f;三、为什…

Spring的创建及使用

文章目录 什么是SpringSpring项目的创建存储Bean对象读取Bean对象getBean()方法 更简单的读取和存储对象的方式路径配置使用类注解存储Bean对象关于五大类注解使用方法注解Bean存储对象Bean重命名 Bean对象的读取 使用Resource注入对象Resource VS Autowired同一类型多个bean对…

租赁固定资产管理

智能租赁资产管理系统可以为企业单位提供RFID资产管理系统。移动APP资产管理&#xff0c;准确总结易损耗品和固定资金&#xff0c;从入库到仓库库存实时跟踪&#xff0c;控制出库和入库的全过程。同时&#xff0c;备件和耗材与所属资产设备有关&#xff0c;便于备件的申请和管理…

express学习笔记3 - 三大件

便于统一管理router&#xff0c;创建 router 文件夹&#xff0c;创建 router/index.js&#xff1a; const express require(express)// 注册路由 const router express.Router() router.get(/,function(req,res){res.send(让我们开始express之旅) }) /*** 集中处理404请求的…

【雕爷学编程】MicroPython动手做(27)——物联网之掌控板小程序3

知识点&#xff1a;什么是掌控板&#xff1f; 掌控板是一块普及STEAM创客教育、人工智能教育、机器人编程教育的开源智能硬件。它集成ESP-32高性能双核芯片&#xff0c;支持WiFi和蓝牙双模通信&#xff0c;可作为物联网节点&#xff0c;实现物联网应用。同时掌控板上集成了OLED…

python与深度学习(十二):CNN和猫狗大战二

目录 1. 说明2. 猫狗大战的CNN模型测试2.1 导入相关库2.2 加载模型2.3 设置保存图片的路径2.4 加载图片2.5 图片预处理2.6 对图片进行预测2.7 显示图片 3. 完整代码和显示结果4. 多张图片进行测试的完整代码以及结果 1. 说明 本篇文章是对上篇文章猫狗大战训练的模型进行测试。…

NIDEC COMPONENTS尼得科科宝滑动型DIP开关各系列介绍

今天AMEYA360对尼得科科宝电子滑动型DIP开关各系列参数进行详细介绍&#xff0c;方便大家选择适合自己的型号。 系列一、滑动型DIP开关 CVS 针脚数&#xff1a;1, 2, 3, 4, 8 安装类型&#xff1a;表面贴装&#xff0c;通孔 可水洗&#xff1a;无 端子类型&#xff1a;PC引脚(只…

PostgreSql 进程及内存结构

一、进程及内存架构 PostgreSQL 数据库运行时&#xff0c;使用如下命令可查询数据库进程&#xff0c;正对应上述结构图。 [postgreslocalhost ~]$ ps -ef|grep post postgres 8649 1 0 15:05 ? 00:00:00 /app/pg13/bin/postgres -D /data/pg13/data postgres …

一文掌握linux系统管理命令

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;滤波估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能等相关领域的知识和…

百度:文心千帆 网页搭建和示例测评

文章目录 官方文档代码示例token获取流式回答官网完整示例 制作一个网页端 官方文档 https://cloud.baidu.com/doc/WENXINWORKSHOP/s/flfmc9do2按照这个操作进行创建一个应用&#xff1a; 代码示例 token获取 # 填充API Key与Secret Key import requests import jsondef ma…

earth靶机详解

earth靶机复盘 靶场下载地址&#xff1a;https://download.vulnhub.com/theplanets/Earth.ova 这个靶场还是非常有意思的&#xff0c;值得去打一下。 我们对拿到的ip进行一个单独全面的扫描&#xff0c;发现有两个DNS解析。 就把这两条解析添加到hosts文件中去&#xff0c;要…

Java maven的下载解压配置(保姆级教学)

mamen基本概念 Maven项目对象模型(POM)&#xff0c;可以通过一小段描述信息来管理项目的构建&#xff0c;报告和文档的项目管理工具软件。 Maven 除了以程序构建能力为特色之外&#xff0c;还提供高级项目管理工具。由于 Maven 的缺省构建规则有较高的可重用性&#xff0c;所以…

分布式异步任务处理组件(五)

节点上线和下线的逻辑-- 节点下线分为两种--心跳失败主动或被动和主节点断开连接&#xff0c;但是节点本身没有发生重启&#xff1b;第二种就是节点宕机重启--其实这两中情况下处理逻辑都是一样的&#xff0c;只是节点本身如果还能消费到kafka的时候可以继续执行任务但是不能从…