究竟什么是异步编程

同步编程是一种典型的请求-响应模式。当前代码耗时执行会阻塞后续代码执行。虽然它能很好的保证程序的执行但是当:请求服务器接口数据,需要根据返回的数据内容执行后续操作,读取文件和请求接口直到数据返回这一过程是需要时间的,网络越差耗费时间越长,同步的话js在这一时间不能处理其他任务,页面的交互会被阻塞。若此时ajax请求数据同步进行:浏览器锁死,不能进行其他操作,每当发出新的请求浏览器都会锁死。

多线程的任务可以并行执行。
JS单线程多个任务并发执行。
JS执行异步任务时,不需要等待响应返回。可以继续执行其他任务,在响应返回时,会得到通知,执行回调或事件处理程序。JS并发执行的基础是:JS拥有一个基于事件循环的并发模型。


事件循环:JS引擎负责解析,执行JS代码,不能单独运行,要有一个宿主环境,JS代码按块执行。


JS执行环境中存在两个结构

消息队列(任务队列):存储待处理的消息,对应的回调函数或事件处理程序。
执行栈(执行上下文栈):函数调用时,创建并插入一个执行上下文,存储函数参数和局部变量,当函数执行结束弹出执行栈帧。全局上下文第一个入栈最后一个出栈。


时间循环流程

宿主环境为JS创建线程时,会创建堆和栈,堆内存储JS对象,栈内存储执行上下文。
执行异步任务时,异步任务进入等待状态(不入栈),同时通知线程,当触发该事件时,需要向消息队列插入一个事件消息,当事件触发,线程向消息队列插入该事件消息。同步任务执行完成后,线程从队列中取出一个事件消息,对应的异步任务入栈,执行回调函数,如果未绑定回调这个任务会被丢弃,执行完任务后退栈。
当线程空闲继续拉取消息队列下一轮消息。

var eventLoop = [];
 var event;
 var i = eventLoop.length - 1; // 后进先出
 while(eventLoop[i]) {
 event = eventLoop[i--]; 
 if (event) { // 事件回调存在
 event();
 }
 // 否则事件消息被丢弃
 }
//这里注意的一点是等待下一个事件消息的过程是同步的。

总:当执行栈同步代码块依次执行完直到遇见异步任务时,异步任务进入等待状态,通知线程,异步事件触发时,往消息队列插入一条事件消息;而当执行栈后续同步代码执行完后,读取消息队列,得到一条消息,然后将该消息对应的异步任务入栈,执行回调函数;一次事件循环就完成了,也即处理了一个异步任务。


JS中异步有几种?

setTimeout ajax promise genarator


setTimeout

setTimeout(
 function() { 
 console.log("Hello!");
}, 1000);

setTimout(setInterval)并不是立即就执行的,这段代码意思是,等 1s
后,把这个 function 加入任务队列中,如果任务队列中没有其他任务了,
就执行输出 ‘Hello’

var outerScopeVar; 
helloCatAsync(); 
alert(outerScopeVar);
function helloCatAsync() { 
 setTimeout(function() { 
 outerScopeVar = 'hello'; 
 }, 2000); 
}

执行上面代码,发现 outerScopeVar 输出是 undefined,而不是 hello。之
所以这样是因为在异步代码中返回的一个值是不可能给同步流程中使用
的,因为 console.log(outerScopeVar) 是同步代码,执行完后才会执行

setTimout。
helloCatAsync(function(result) {
console.log(result);
});
function helloCatAsync(callback) {
 setTimeout(
 function() {
 callback('hello')
 }
 , 1000)
}

把上面代码改成,传递一个 callback,console输出就会是 hello。


AJAX

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
 if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 ) {
 console.log(xhr.responseText);
 } else {
 console.log( xhr.status);
 } }
xhr.open('GET', 'url', false);
xhr.send();
//上面这段代码,xhr.open 中第三个参数默认为 false 异步执行,改为 true 时为同步执行。

Promise

promise 是一个拥有 then 方法的对象或函数。
一个 promise 有三种状态 pending, rejected, resolved 状态一旦确定就不能
改变,且只能够由 pending 状态变成 rejected 或者 resolved 状态,reject
和 resolved 状态不能相互转换。
当 promise 执行成功时,调用 then 方法的第一个回调函数,失败时调用第
二个回调函数。
promise 实例会有一个 then 方法,这个 then 方法必须返回一个新的
promise。

// 异步操作放在 Promise 构造器中
const promise1 = new Promise((resolve) => {
 setTimeout(() => {
 resolve('hello');
  }, 1000);
});
// 得到异步结果之后的操作
promise1.then(value => {
 console.log(value, 'world');
}, error =>{
 console.log(error, 'unhappy')
});
异步代码,同步写法
asyncFun()
.then(cb)
.then(cb)
.then(cb)

promise 以这种链式写法,解决了回调函数处理多重异步嵌套带来的回调
地狱问题,使代码更加利于阅读,当然本质还是使用回调函数。

异常捕获
前面说过如果在异步的 callback 函数中也有一个异常,那么是捕获不到的,
原因就是回调函数是异步执行的。Promise解决

asyncFun(1).then(function (value) {
 throw new Error('出错啦');
}, function (value) {
 console.error(value);
}).then(function (value) {
}, function (result) {
 console.log('有错误', result);
});

//其实是 promise 的 then 方法中,已经自动帮我们 try catch 了这个回调函数,

Promise.prototype.then = function(cb) {
try {
cb()
} catch (e) {
 // todo
 reject(e)

then 方法中抛出的异常会被下一个级联的 then 方法的第二个参数捕获到
(前提是有),那么如果最后一个 then 中也有异常怎么办。

Promise.prototype.done = function (resolve, reject) {
 this.then(resolve, reject).catch(function (reason) {
 setTimeout(() => {
 throw reason;
 }, 0);
 });
};
asyncFun(1).then(function (value) {
 throw new Error('then resolve 回调出错啦');
}).catch(function (error) {
 console.error(error);
 throw new Error('catch 回调出错啦');
}).done((reslove, reject) => {});

我们可以加一个 done 方法,这个方法并不会返回 promise 对象,所以在
此之后并不能级联,done 方法最后会把异常抛到全局,这样就可以被全
局的异常处理函数捕获或者中断线程。这也是 promise 的一种最佳实践策
略,当然这个 done 方法并没有被 ES6 实现,所以我们在不适用第三方
Promise 开源库的情况下就只能自己来实现了。为什么需要这个 done 方
法。

const asyncFun = function (value) {
 return new Promise(function (resolve, reject) {
 setTimeout(function () {
 resolve(value);
 }, 0);
 })
};
asyncFun(1).then(function (value) {
 throw new Error('then resolve 回调出错啦');
});
(node:6312) UnhandledPromiseRejectionWarning: Unhandled promise 
rejection (rejection id: 1): Error: then resolve 回调出错啦
渡一教育
10
(node:6312) [DEP0018] DeprecationWarning: Unhandled promise rejections 
are deprecated. In the future, promise rejections that are not handled will 
terminate the Node.js process with a non-zero exit code

我们可以看到 JavaScript 线程只是报了一个警告,并没有中止线程,如果
是一个严重错误如果不及时中止线程,可能会造成损失。

局限

promise 有一个局限就是不能够中止 promise 链,例如当 promise 链中某一
个环节出现错误之后,已经没有了继续往下执行的必要性,但是 promise
并没有提供原生的取消的方式,我们可以看到即使在前面已经抛出异常,
但是 promise 链并不会停止。虽然我们可以利用返回一个处于 pending 状
态的 promise 来中止 promise 链。

const promise1 = new Promise((resolve) => {
 setTimeout(() => {
 resolve('hello');
 }, 1000);
});
promise1.then((value) => {
 throw new Error('出错啦!');
}).then(value => {
 console.log(value);
}, error=> {
 console.log(error.message);
 return result;
}).then(function () {
 console.log('DJL 箫氏');
});

当我们的一个任务依赖于多个异步任务,那么我们可以使用 Promise.all
当我们的任务依赖于多个异步任务中的任意一个,至于是谁无所谓,Promise.race

下面 promise 和 ajax 结合例子:
function ajax(url) {
 return new Promise(function(resolve, reject) {
渡一教育
12
 var xhr = new XMLHttpRequest();
 xhr.onreadystatechange = function() {
 if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 ) 
{
 resovle(xhr.responseText);
 } else {
 reject( xhr.status);
 }
 }
 xhr.open('GET', url, false);
 xhr.send();
 });
}
ajax('/test.json')
 .then(function(data){
 console.log(data);
 })
 .cacth(function(err){
 console.log(err);
 });

generator

//基本用法
function * gen (x) {
 const y = yield x + 2;
 // console.log(y); // 猜猜会打印出什么值
}
const g = gen(1);
console.log('first', g.next()); //first { value: 3, done: false }
console.log('second', g.next()); // second { value: undefined, done: true }

通俗的理解一下就是 yield 关键字会交出函数的执行权,next 方法会交回执行权,yield 会把 generator 中 yield 后面的执行结果,带到函数外面,而next 方法会把外面的数据返回给 generator 中 yield 左边的变量。这样就实现了数据的双向流动。

generator 实现异步编程

const fs = require('fs');
function * gen() {
 try {
 const file = yield fs.readFile;
 console.log(file.toString());
 } catch(e) {
 console.log('捕获到异常', e);
 } }
// 执行器
const g = gen();
g.next().value('./config1.json', function (error, value) {
 if (error) {
 g.throw('文件不存在');
 }
 g.next(value);
});

那么我们 next 中的参数就会是上一个 yield 函数的返回结果,可以看到在generator 函数中的代码感觉是同步的,但是要想执行这个看似同步的代码,过程却很复杂,也就是流程管理很复杂。


异步编程与多线程的区别

共同点:异步和多线程两者都可以达到避免调用线程阻塞的目的,从而提
高软件的可响应性。

不同点
(1)线程不是一个计算机硬件的功能,而是操作系统提供的一种逻辑功能,线程本质上是进程中一段并发运行的代码,所以线程需要操作系统投入 CPU 资源来运行和调度。多线程的优点很明显,线程中的处理程序依然是顺序执行,符合普通人的思维习惯,所以编程简单。但是多线程的缺点也同样明显,线程的使用(滥用)会给系统带来上下文切换的额外负担。并且线程间的共享变量可能造成死锁的出现
(2)异步操作无须额外的线程负担,并且使用回调的方式进行处理,在设计良好的情况下,处理函数可以不必使用共享变量(即使无法完全不用,最起码可以减少 共享变量的数量),减少了死锁的可能。当然异步操作也并非完美无暇。编写异步操作的复杂程度较高,程序主要使用回调方式进行处理,与普通人的思维方式有些
初入,而且难以调试。

这里有一个疑问。异步操作没有创建新的线程,我们一定会想,比如有一个文件操作,大量数据从硬盘上读取,若使用单线程的同步操作自然要等待会很长时间,但是若使用异步操作的话,我们让数据读取异步进行,二线程在数据读取期间去干其他的事情,我们会想,这怎么行呢,异步没有创建其他的线程,一个线程去干其他的事情去了,那数据的读取异步执行是去由谁完成的呢?实际上,本质是这样的。
熟悉电脑硬件对 DMA 这个词不陌生,硬盘、光驱的技术规格中都有明确 DMA 的模式指标,其实网卡、声卡、显卡也是有 DMA功能的。DMA 就是直 接内存访问的意思,也就是说,拥有 DMA 功能的硬件在和内存进行数据交换的时候可以不消耗 CPU 资源。只要 CPU 在发起数据传输时发送一个指令,硬件就开 始自己和内存交换数据,在传输完成之后硬件会触发一个中断来通知操作完成。这些无须消耗 CPU 时间的I/O 操作正是异步操作的硬件基础。所以即使在 DOS 这样的单进程(而且
无线程概念)系统中也同样可以发起异步的 DMA 操作。即 CPU 在数据的长时间读取过程中 ,只需要做两件事,第一发布指令,开始数据交换;第二,交换结束,得到指令,CPU 再进行后续操作。而中间读取数据漫长的等待过程,CPU 本身就不需要参与,顺序执行就是我不参与但是我要干等着,效率低下;异步执行就是,我不需要参与那我就去干其他事情去了,你做完了再通知我就可以了(回调)。
但是你想一下,如果有一些异步操作必须要 CPU 的参与才能完成呢,即我开始的那个线程是走不开的,这该怎么办呢,在.NET 中,有线程池去完成,线程池会高效率的开启一个新的线程去完成异步操作,在 python中这是系统自己去安排的,无需人工干预,这就比自己创建很多的线程更加高效。
总结:
(1)“多线程”,第一、最大的问题在于线程本身的调度和运行需要很多时间,因此不建议自己创建太大量的线程;第二、共享资源的调度比较难,涉及到死锁,上锁等相关的概念。
(2)“异步” ,异步最大的问题在于“回调”,这增加了软件设计上的难度。
在实际设计时,我们可以将两者结合起来:
(1)当需要执行 I/O 操作时,使用异步操作比使用线程+同步 I/O操作更合适。I/O 操作不仅包括了直接的文件、网络的读写,还包括数据库操作、Web Service、HttpRequest 以及.net Remoting 等跨进程的调用。
异步特别适用于大多数 IO 密集型的应用程序。
(2)而线程的适用范围则是那种需要长时间 CPU 运算的场合,例如
耗时较长的图形处理和算法执行。但是往 往由于使用线程编程的简单和
符合习惯,所以很多朋友往往会使用线程来执行耗时较长的 I/O 操作。这
样在只有少数几个并发操作的时候还无伤大雅,如果需要处 理大量的并
发操作时就不合适了。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值