ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

学了那么久JavaScript还不知道异步怎么实现?

2021-05-02 13:33:00  阅读:211  来源: 互联网

标签:function 异步 函数 JavaScript yield Promise 学了 async


JavaScript中的异步编程

ES6 诞生以前,异步编程的方法,大概有下面四种。

回调函数、事件监听、发布/订阅、Promise 对象。

注意这里的Promise对象和ES6里的Promise其实不太一样,因为之前的Promise是由commonJS社区提出的Promise规范,用于统一处理异步回调,之后ECMAscript 6 才原生提供了 Promise 对象。

在ES6中,主要涉及到的异步有Promise对象,Generator函数和async函数。

所以本文就介绍这六种异步实现方法。

  • 回调函数
  • 事件监听
  • 发布/订阅
  • Promise 对象
  • Generator函数
  • async函数

1.回调函数

A callback is a function that is passed as an argument to another function and is executed after its parent function has completed.

回调是作为参数传递给另一个函数并在其父函数完成后执行的函数。

回调函数是异步操作最基本的方法。以下代码就是一个回调函数的例子:

ajax(url, () => {
    // 处理逻辑
})

但是回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,你可能就会写出如下代码:

ajax('xxxxxxx.json', function() {
  // doing something 1
  ajax('xxxxxxx.json', function() {
    // doing something 2
    ajax('xxxxxxx.json', function() {
      // doing something 3
      ajax('xxxxxxx.json', function() {
        // doing something 4
      });
    });
  });
});

这是是单纯的嵌套代码,如若再加上业务代码,代码可读性可想而知,如果是开发起来还好,但是后期的维护和修改的难度足以让人疯掉。这就是那个**JQuery时代的“回调地狱”**问题。

回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。

2.事件监听

另一种思路是采用事件驱动模式。

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

f1f2为例。首先,为f1绑定一个事件(这里采用的 jQuery 的写法)。

f1.on('done', f2);

上面这行代码给f1注册了done事件,当f1发生done事件,就执行f2。然后,对f1进行改写:

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

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

这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以**“去耦合”(decoupling)**,有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。

3.发布/订阅

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

发布订阅其实和事件监听非常相似,事件监听的on相当于就是订阅,事件的发生就是发布。

如果不太了解的可以看我的另一篇博客,观察者模式

但是观察者模式一般是用在同步任务中的,发布订阅模式则是异步。

// 主体
let subject = new Subjet();
// 像向体订阅getData通知
subject.subscribe('getData', function(data) {
  // do something...
});

// 获取数据方法
function getDate(params) {
  // do something...
  //   获取数据后主题发布getData通知
  subject.publish('getData');
}

4.Promise对象和then

Promise详细文档请阅读:阮一峰老师的ES6入门:Promise

为了解决“回调地狱”问题,提出了Promise对象,并且后来加入了ES6标准,Promise对象简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。

Promise的状态

Promise 异步操作有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。除了异步操作的结果,任何其他操作都无法改变这个状态。

Promise 对象只有:从 pending 变为 fulfilled 和从 pending 变为 rejected 的状态改变。只要处于 fulfilled 和 rejected ,状态就不会再变了即 resolved(已定型)。

因此,Promise 的最终结果只有两种。

  • 异步操作成功,Promise 实例传回一个值(value),状态变为fulfilled
  • 异步操作失败,Promise 实例抛出一个错误(error),状态变为rejected

Promise

首先,Promise 是一个对象,也是一个构造函数。

var f1 = function(resolve, reject) {
  // ... some code
 
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
}
const promise1 = new Promise(f1);

Promise构造函数接受一个回调函数f1作为参数,f1里面是异步操作的代码。然后,返回的promise1就是一个 Promise 实例。

Promise 新建后就会立即执行。

let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve();
});

promise.then(function() {
  console.log('resolved.');
});

console.log('Hi!');

// Promise
// Hi!
// resolved

上面代码中,Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。

注意,调用resolvereject并不会终结 Promise 的参数函数的执行。

then

Promise 的设计思想是,所有异步任务都返回一个 Promise 实例。Promise 实例有一个then方法,用来指定下一步的回调函数。它的作用是为 Promise 实例添加状态改变时的回调函数。

then 方法接收两个函数作为参数,第一个参数是 Promise 执行成功时的回调,第二个参数是 Promise 执行失败时的回调,两个函数只会有一个被调用。

getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // proceed
});

上面代码中,getJSON的异步操作执行完成,就会执行then的回调函数。

通过 .then 形式添加的回调函数,不论什么时候,都会被调用。

then方法会返回一个新的Promise实例(注意,不是原来那个Promise实例)。

当然也可以自己返回一个Promise对象,达到异步操作的逐步执行的效果。

getJSON("/post/1.json").then(function(post) {
  return getJSON(post.commentURL);
}).then(function(comments) {
  // 对comments进行处理
});

如果前一个回调函数返回的是Promise对象,这时后一个回调函数就会等待该Promise对象有了运行结果,才会进一步调用。因此可以采用链式写法,即then方法后面再调用另一个then方法。

catch

catch()方法是.then(null, rejection).then(undefined, rejection)`的别名,用于指定发生错误时的回调函数。

// 写法一
const promise = new Promise(function(resolve, reject) {
  try {
    throw new Error('test');
  } catch(e) {
    reject(e);
  }
});
promise.catch(function(error) {
  console.log(error);
});

// 写法二
const promise = new Promise(function(resolve, reject) {
  reject(new Error('test'));
});
promise.catch(function(error) {
  console.log(error);
});

上面代码中,promise抛出一个错误,就被catch()方法指定的回调函数捕获。注意,上面的两种写法是等价的。reject()方法的作用,等同于抛出错误。

一般来说,不要在then()方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。

// bad
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good
promise.then(function(data) { //cb
	// success
}).catch(function(err) {
	// error
});

上面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面then方法执行中的错误,也更接近同步的写法(try/catch)。因此,建议总是使用catch()方法,而不使用then()方法的第二个参数。

跟传统的try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。

5.Generator和yield

ES6 新引入了 Generator 函数,可以通过 yield 关键字,把函数的执行流挂起,为改变执行流程提供了可能,从而为异步编程提供解决方案。 基本用法

Generator 、yield

Generator 有两个区分于普通函数的部分:

  • 一是在 function 后面,函数名之前有个 * ;
  • 函数内部有 yield 表达式。

其中 * 用来表示函数为 Generator 函数,yield 用来定义函数内部的状态。

function* func(){
 console.log("one");
 yield '1';
 console.log("two");
 yield '2'; 
 console.log("three");
 return '3';
}

调用 Generator 函数和调用普通函数一样,在函数名后面加上()即可,但是 Generator 函数不会像普通函数一样立即执行,而是返回一个指向内部状态对象的指针,所以要调用遍历器对象Iterator 的 next 方法,指针就会从函数头部或者上一次停下来的地方开始执行。

next和return

next方法的作用就是,使当前的Generator 函数返回的Iterator对象从当前指向的状态继续执行,遇到yield或者return就会停止。

next ()函数也可以传入参数,next 传入参数的时候,该参数会作为上一步yield的返回值;如果不提供参数,那么上一步yield的返回值为undefined。

yield的返回值的定义就是x = yield '1';这段代码中x的值。但是它是作为上一步的yield的返回值,你需要尝试去理解。调用next函数 遇到yield会挂起,但是不是这个yield,而是上一个yield。

第一个yield没有上一个,所以第一个next的赋值其实是没有意义的。

return 方法提供参数时,返回该参数;不提供参数时,返回 undefined 。

function* func(){
    console.log("one");
    yield '1';
    console.log("two");
    yield '2'; 
    console.log("three");
    return '3';
   }
f=func();
console.log(f.next());
// one
// {value: "1", done: false}
 
console.log(f.next());
// two
// {value: "2", done: false}

console.log(f.next());
// three
// {value: "3", done: true}

console.log(f.next());
// {value: undefined, done: true}

console.log(f.return(1000));
// { value: 1000, done: true }

//完整运行结果
// one
// { value: '1', done: false }
// two
// { value: '2', done: false }
// three
// { value: '3', done: true }
// { value: undefined, done: true }
// { value: 1000, done: true }

第一次调用 next 方法时,从 Generator 函数的头部开始执行,先是打印了 one ,执行到 yield 就停下来,并将yield 后边表达式的值 ‘1’,作为返回对象的 value 属性值,此时函数还没有执行完, 返回对象的 done 属性值是 false。

第二次调用 next 方法时,同上步 。

第三次调用 next 方法时,先是打印了 three ,然后执行了函数的返回操作,并将 return 后面的表达式的值,作为返回对象的 value 属性值,此时函数已经结束,多以 done 属性值为true 。

第四次调用 next 方法时, 此时函数已经执行完了,所以返回 value 属性值是 undefined ,done 属性值是 true 。如果执行第三步时,没有 return 语句的话,就直接返回{value: undefined, done: true}

第五步调用return方法时,此时函数已经执行完了,本来返回 value 属性值是 undefined,但是return方法提供一个参数之后,就会返回这个参数,所以最后输出的value就是传入的1000

除了使用 next ,还可以使用 for… of 循环遍历 Generator 函数生产的 Iterator 对象

yield* 表达式

yield* 表达式表示 yield 返回一个遍历器对象,用于在 Generator 函数内部,调用另一个 Generator 函数。

这里就不过多描述了。

6.async和await

async

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。和 Promise , Generator 有很大关联的。

async 函数是什么?一句话,它就是 Generator 函数的语法糖。

async function name([param[, param[, ... param]]]) { statements }
  • name: 函数名称。
  • param: 要传递给函数的参数的名称。
  • statements: 函数体语句。

async 返回值

async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。

async function helloAsync(){
    return "helloAsync";
  }
  
console.log(helloAsync())  // Promise {<resolved>: "helloAsync"}
 
helloAsync().then(v=>{
   console.log(v);         // helloAsync
})

await

await 操作符用于等待一个 Promise 对象, 它只能在异步函数 async function 内部使用。

[return_value] = await expression;

async 函数中可能会有 await 表达式,async 函数执行时,如果遇到 await 就会先暂停执行 ,等到触发的异步操作完成后,恢复 async 函数的执行并返回解析值。

await 关键字仅在 async function 中有效。如果在 async function 函数体外使用 await ,你只会得到一个语法错误

function testAwait (x) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x);
    }, 2000);
  });
}
 
async function helloAsync() {
  var x = await testAwait ("hello world");
  console.log(x); 
}
helloAsync ();
// hello world

await 返回值

await根据后面不同表达式有不同的处理方式:

  • Promise 对象:await 会暂停执行,等待 Promise 对象 resolve,然后恢复 async 函数的执行并返回解析值。

  • 非 Promise 对象:直接返回对应的值。

正常情况下,await 命令后面是一个 Promise 对象,它也可以跟其他值,如字符串,布尔值,数值以及普通函数

function testAwait(){
    console.log("testAwait");
 }
 async function helloAsync(){
    await testAwait();
    console.log("helloAsync");
 }
 helloAsync();
 // testAwait
 // helloAsync

Promise,Generator函数,async函数理解

关于JavaScript的简单异步编程价绍就这些,说实话我自己看了很久,感觉Promise,Generator函数,async函数是层层递进的。

Promise比较简单,也是最常用的,主要就是将原来的用回调函数的异步编程方法转成用relsove和reject触发事件, 用then和catch捕获成功或者失败的状态执行相应代码的异步编程的方法。

Generator函数可以看成是一个分步函数,只有主动调用next() 才能进行下一步。

async函数相当于自执行的Generator函数,会自己不断的执行,遇到await等待返回结果,然后继续,和普通的同步写法相同,提供效率。

关于三者区别我也专门整理了一下,想了解的可以看阅读。

不能区分Promise,Generator函数,async函数可以进来看看

参考

https://www.ruanyifeng.com/blog/2015/04/generator.html

http://es6.ruanyifeng.com/#docs/promise

https://blog.csdn.net/aaaaaaliang/article/details/89555422

标签:function,异步,函数,JavaScript,yield,Promise,学了,async
来源: https://blog.csdn.net/weixin_44394801/article/details/116353031

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有