异步编程的六种方式

前言

你可能知道,Javascript语言的执行环境是"单线程"(single thread)。

所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。

单线程的好处是实现起来比较简单,执行环境相对单纯;
坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。

常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。

为了解决这个问题,Javascript语言将任务的执行模式分成两种

  • 同步(Synchronous)
  • 异步(Asynchronous)

"同步模式"就是上一段的模式,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;

"异步模式"则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。

在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。
在服务器端,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。

以下是异步编程的六种方式:

1、回调函数的方式

这是异步编程最基本的方法。

所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,再调用这个函数

// 异步请求
ajax(url, () => {
    // 请求返回后的处理逻辑
})

// 读取文件
fs.readFile('/etc/shells', function (err, data) {
   // 文件读取完毕后的操作
   console.log(data);
});

回调函数有一个致命的弱点,就是容易写出回调地狱

ajax(url, () => {
    // 处理逻辑
    ajax(url1, () => {
        // 处理逻辑
        ajax(url2, () => {
            // 处理逻辑
        })
    })
})
  • 优点 是简单、容易理解和部署
  • 缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),流程会很混乱。
2、Promise对象

Promise就是为了解决回调地狱而产生的,将回调函数的嵌套,改成链式调用。

简单说,它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。

例子如下:

function A(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}

function step1(n) {
    console.log(`step1 with ${n}`);
    return A(n);
}

function step2(n) {
    console.log(`step2 with ${n}`);
      return A(n);
}

function step3(n) {
    console.log(`step3 with ${n}`);
     return A(n);
}

step1(100).then( time2 =>step2(time2))
.then( time3 => step3(time3))
.then( result => {
   console.log(`result is ${result}`)
});

优点:回调函数变成了链式写法,每个异步函数执行完成后,才会去执行下一个then函数;
缺点:Promise的写法只是回调函数的改进,用then()方法免去了嵌套,更为直观。但这样写也存在了很明显的问题,代码变得冗杂了,语义化并不强

3、生成器函数 Generator/ yield

Generator 函数是 ES6 提供的一种异步编程解决方案:

  • yield表达式可以暂停函数执行,next方法用于恢复函数执行,这使得Generator函数非常适合将异步任务同步化
  • yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
  • 每个yield返回的是{value:yield返回的值,done:true/false(执行状态)}

Generator的原理

generator 的方式,它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。

执行过程中,当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。

使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。

// 异步函数
function ajax(duration) {   
	return new Promise((resolve,reject)=>{
		setTimeout(()=>{
			console.log(duration+'ms延时打印输出!,当前位于'+ new Date().getSeconds() +'秒')
			resolve(duration);
		},duration)
	})
}
// 生成器函数
function* main() {
	const user = yield ajax(2000);  
	console.log('user',user);

	const posts = yield ajax(10000);
	console.log(posts);
}

// 封装执行生成器函数
function co(generator) {
	const g = generator();
	function handleResult(result) {          
		if(result.done) return; //生成器函数结束
		result.value.then(data => {         //这里与yield的返回有关
			    console.log('继续执行!',data);
				handleResult(g.next(data));
		  },error => {
				g.throw(error);
		  }
		);
	}
	handleResult(g.next());
}

co(main);

输出结果如下:
在这里插入图片描述

4、async/await 函数的实现

为了解决 Promise 的问题,async、await 在 ES7 中被提了出来,是目前为止最好的解决方案。

  • async 是“异步”的意思,而 await 是等待的意思。所以应该很好理解 async 用于申明一个 异步的function(实际上是async function 对象)
  • await 用于等待一个异步任务执行完成的的结果。并且 await 只能出现在 async 函数中。

async/await 异步编程的解决原理

async 函数是通过 generator 和 promise 实现的一个自动执行的语法糖,当函数内部执行到一个await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写。

 async function test(){
    let newTime = await new Promise((resolve,reject)=>{     //这里等待异步返回结果,再继续向下执行
        let time = 3000;     
        setTimeout(()=>{
           resolve(time);
        },time)
 
    })
    console.log(newTime+'毫秒后执行');
    let content = 'test';
    console.log(content);      //3s后,先输出  “3000毫秒后执行”,再输出 "test"
 }
 test()
  • async函数 ——返回的是一个 Promise 对象
	async function testAsync() {
	    return "hello async";
	}
	
	const result = testAsync();
	console.log(result);   // async 函数返回的是一个Promise对象
	result.then(v=>{
		console.log(v);
	})
	console.log(123)  // 先输出promise对象,其次123,最后hello async

在这里插入图片描述

async/await的参考链接

5、事件监听

这种方式下,异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

下面是两个函数f1和f2,编程的意图是f2必须等到f1执行完成,才能执行。

首先,为f1绑定一个事件(jQuery写法)

f1.on('done', f2);

上面这行代码的意思是,当f1发生done事件,就执行f2。然后,对f1进行改写:

function f1() {
  setTimeout(function () {
    // ...
    f1.trigger('done');
  }, 1000);
}

上面代码中,f1.trigger(‘done’)表示,执行完成后,立即触发done事件,从而开始执行f2

优点:是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合",有利于实现模块化
缺点:是整个程序都要变成事件驱动型,运行流程会变得很不清晰。

6、发布/订阅

我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)

下面采用的是Ben Alman的Tiny Pub/Sub,这是jQuery的一个插件

首先,f2向"信号中心"jQuery订阅"done"信号

jQuery.subscribe("done", f2);

f1进行如下改写:

function f1(){
  setTimeout(function () {
    // f1的任务代码
    jQuery.publish("done");
  }, 1000);
}

jQuery.publish(“done”)的意思是,f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引发f2的执行。

此外,f2完成执行后,也可以取消订阅(unsubscribe)

jQuery.unsubscribe("done", f2);

更多参考链接:https://blog.csdn.net/yiyueqinghui/article/details/111057634

参考链接:https://www.jianshu.com/p/d7f6077a0dd2

  • 4
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
进程间通信是指在不同进程之间传输数据或信号的机制。Linux提供了多种进程间通信的方式,包括管道、消息队列、共享内存和信号量等。以下是一些常用的进程间通信方式: 1. 管道(pipe):管道是一种半双工的通信方式,只能在具有亲缘关系的进程之间使用。数据只能单向流动,且在一个进程写入的数据只能被另一个进程读取。 2. 命名管道(named pipe):命名管道是一种特殊的文件,可以在不同进程之间共享数据。它允许不具有亲缘关系的进程之间进行通信。 3. 消息队列(message queue):消息队列是一种异步通信方式,进程通过消息队列发送消息,而不需要直接与接收进程进行交互。消息可以按照优先级进行排序,接收进程可以按照顺序接收消息。 4. 共享内存(shared memory):共享内存是一种高效的通信方式,它允许多个进程访问同一块内存空间。这种方式适合于大量数据的传输,但需要考虑同步和互斥等问题。 5. 信号量(semaphore):信号量是一种用于同步和互斥的机制,它可以用于控制进程对共享资源的访问。进程可以通过信号量实现互斥、同步和等待等操作。 在实际的应用中,通常需要结合多种进程间通信方式来实现复杂的功能。比如可以使用消息队列配合共享内存来实现高效的数据传输,或者使用信号量和共享内存来实现进程间的同步和互斥等操作。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值