资讯专栏INFORMATION COLUMN

深入前端-JavaScript异步编程

2json / 512人阅读

摘要:缺点无法取消当处于状态时,无法得知目前进展到哪一个阶段错误不能被生成器什么是函数是提供的一种异步编程解决方案,语法行为与传统函数完全不同函数有多种理解角度。

JavaScript的执行机制在上篇文章中进行了深入的探讨,那么既然是一门单线程语言,如何进行良好体验的异步编程呢
回调函数Callbacks

当程序跑起来时,一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数。但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数(callback function)。

什么是异步

"调用"在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在"调用"发出后,"被调用者"通过状态、通知来通知调用者,或通过回调函数处理这个调用。异步调用发出后,不影响后面代码的执行。
简单说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。
在异步执行的模式下,每一个异步的任务都有其自己一个或着多个回调函数,这样当前在执行的异步任务执行完之后,不会马上执行事件队列中的下一项任务,而是执行它的回调函数,而下一项任务也不会等当前这个回调函数执行完,因为它也不能确定当前的回调合适执行完毕,只要引它被触发就会执行,

地狱回调阶段

异步最早的解决方案是回调函数,如事件的回调,setInterval/setTimeout中的回调。但是回调函数有一个很常见的问题,就是回调地狱的问题
下面这几种都属于回调

事件回调

Node API

setTimeout/setInterval中的回调函数

ajax 请求

异步回调嵌套会导致代码难以维护,并且不方便统一处理错误,不能 try catch会陷入回调地狱

fs.readFile(A, "utf-8", function(err, data) {
    fs.readFile(B, "utf-8", function(err, data) {
        fs.readFile(C, "utf-8", function(err, data) {
            fs.readFile(D, "utf-8", function(err, data) {
                //....
            });
        });
    });
});

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

Promise解决地狱回调阶段

Promise 一定程度上解决了回调地狱的问题,Promise 最早由社区提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

Promise存在三个状态(state)pending、fulfilled、rejected

pending(等待态)为初始态,并可以转化为fulfilled(成功态)和rejected(失败态)

成功时,不可转为其他状态,且必须有一个不可改变的值(value)

失败时,不可转为其他状态,且必须有一个不可改变的原因(reason)

new Promise((resolve, reject)=>{resolve(value)}) resolve为成功,接收参数value,状态改变为fulfilled,不可再次改变。

new Promise((resolve, reject)=>{reject(reason)}) reject为失败,接收参数reason,状态改变为rejected,不可再次改变。

若是executor函数报错 直接执行reject();

Promise 是一个构造函数,new Promise 返回一个 promise对象
const promise = new Promise((resolve, reject) => {
       // 异步处理
       // 处理结束后、调用resolve 或 reject
});
then方法注册 当resolve(成功)/reject(失败)的回调函数
// onFulfilled 参数是用来接收promise成功的值,
// onRejected 参数是用来接收promise失败的原因
//两个回调返回的都是promise,这样就可以链式调用
promise.then(onFulfilled, onRejected); 
const promise = new Promise((resolve, reject) => {
   resolve("fulfilled"); // 状态由 pending => fulfilled
});
promise.then(result => { // onFulfilled
    console.log(result); // "fulfilled" 
}, reason => { // onRejected 不会被调用
    
})
then方法的链式调用

Promise对象的then方法返回一个新的Promise对象,因此可以通过链式调用then方法。then方法接收两个函数作为参数,第一个参数是Promise执行成功时的回调,第二个参数是Promise执行失败时的回调。两个函数只会有一个被调用,函数的返回值将被用作创建then返回的Promise对象。这两个参数的返回值可以是以下三种情况中的一种:

return 一个同步的值 ,或者 undefined(当没有返回一个有效值时,默认返回undefined),then方法将返回一个resolved状态的Promise对象,Promise对象的值就是这个返回值。

return 另一个 Promise,then方法将根据这个Promise的状态和值创建一个新的Promise对象返回。

throw 一个同步异常,then方法将返回一个rejected状态的Promise, 值是该异常。

解决层层回调问题
//对应上面第一个node读取文件的例子
function read(url) {
    return new Promise((resolve, reject) => {
        fs.readFile(url, "utf8", (err, data) => {
            if(err) reject(err);
            resolve(data);
        });
    });
}
read(A).then(data => {
    return read(B);
}).then(data => {
    return read(C);
}).then(data => {
    return read(D);
}).catch(reason => {
    console.log(reason);
});
//对应第二个ajax请求例子
ajax(url)
  .then(res => {
      console.log(res)
      return ajax(url1)
  }).then(res => {
      console.log(res)
      return ajax(url2)
  }).then(res => console.log(res))

可以看到,Promise在一定程度上其实改善了回调函数的书写方式,最明显的一点就是去除了横向扩展,无论有再多的业务依赖,通过多个then(...)来获取数据,让代码只在纵向进行扩展;另外一点就是逻辑性更明显了,将异步业务提取成单个函数,整个流程可以看到是一步步向下执行的,依赖层级也很清晰,最后需要的数据是在整个代码的最后一步获得。
所以,Promise在一定程度上解决了回调函数的书写结构问题,但回调函数依然在主流程上存在,只不过都放到了then(...)里面,和我们大脑顺序线性的思维逻辑还是有出入的。

Promise缺点

无法取消 Promise

当处于pending状态时,无法得知目前进展到哪一个阶段

错误不能被 try catch

生成器Generators/ yield 什么是Generator

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同
Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。形式上,Generator 函数是一个普通函数,但是有两个特征。

一是,function关键字与函数名之间有一个星号;

二是,函数体内部使用yield表达式,定义不同的内部状态

Generator调用方式

Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。
下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

function* foo () {  
  var index = 0;
  while (index < 2) {
    yield index++; //暂停函数执行,并执行yield后的操作
  }
}
var bar =  foo(); // 返回的其实是一个迭代器

console.log(bar.next());    // { value: 0, done: false }  
console.log(bar.next());    // { value: 1, done: false }  
console.log(bar.next());    // { value: undefined, done: true }  
了解Co

可以看到上个例子当中我们需要一步一步去调用next这样也会很麻烦,这时我们可以引入co来帮我们控制
Co是一个为Node.js和浏览器打造的基于生成器的流程控制工具,借助于Promise,你可以使用更加优雅的方式编写非阻塞代码。
Co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
说白了就是帮你自动执行你的Generator不用手动调用next

解决异步问题

我们可以通过 Generator 函数解决回调地狱的问题,可以把之前的回调地狱例子改写为如下代码:

const co = require("co");
co(
function* read() {
    yield readFile(A, "utf-8");
    yield readFile(B, "utf-8");
    yield readFile(C, "utf-8");
    //....
}
).then(data => {
    //code
}).catch(err => {
    //code
});
function *fetch() {
    yield ajax(url, () => {})
    yield ajax(url1, () => {})
    yield ajax(url2, () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()
终极解决方案Async/ await
async 函数是Generator 函数的语法糖,是对Generator做了进一步的封装。
Async特点

当调用一个 async 函数时,会返回一个 Promise 对象。

    async function async1() {
      return "1"
    }
    console.log(async1()) // -> Promise {: "1"}

当这个 async 函数返回一个值时,Promise 的 resolve 方法会负责传递这个值;

当 async 函数抛出异常时,Promise 的 reject 方法也会传递这个异常值。

async 函数中可能会有 await 表达式,这会使 async 函数暂停执行,等待 Promise 的结果出来,然后恢复async函数的执行并返回解析(resolved)。

内置执行器。 Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。

更广的适用性。co 模块约定,yield 命令后面只能是 Thunk 函数或 Promise对象。而 async 函数的 await 命令后面则可以是 Promise 或者 原始类型的值(Number,string,boolean,但这时等同于同步操作)

await特点

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

[return_value] = await expression;

await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成。若 Promise 正常处理(fulfilled),其回调的resolve函数参数作为 await 表达式的值,继续执行 async function。

若 Promise 处理异常(rejected),await 表达式会把 Promise 的异常原因抛出。

另外,如果 await 操作符后的表达式的值不是一个 Promise,则返回该值本身。

重点:遇到 await 表达式时,会让 async 函数 暂停执行,等到 await 后面的语句(Promise)状态发生改变(resolved或者rejected)之后,再恢复 async 函数的执行(再之后 await 下面的语句),并返回解析值(Promise的值)

为什么await可以暂停执行并等到Promise的状态改变再恢复执行呢

promise就是做这件事的 , 它会自动等到Promise决议以后的返回值,resolve(...)或者reject(...)都可以。
async内部会在promise.then(callback),回调函数里调用 next()... (还有用Thunk的, 也是为了做这个事的);

Async执行方式

简单说 , async/awit 就是对上面gennerator自动化流程的封装 , 让每一个异步任务都是自动化的执行 , 当第一个异步任务readFile(A)执行完如上一点说明的, async内部自己执行next(),调用第二个任务readFile(B);

这里引入ES6阮一峰老师的例子
const fs = require("fs");

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};


async function read() {
    await readFile(A);//执行到这里停止往下执行,等待readFile内部resolve(data)后,再往下执行
    await readFile(B);
    await readFile(C);
    //code
}

//这里可用于捕获错误
read().then((data) => {
    //code
}).catch(err => {
    //code
});
参考文章

http://es6.ruanyifeng.com/

https://juejin.im/post/5aa786...

https://juejin.im/post/5b83cb...

https://juejin.im/post/596e14...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/54255.html

相关文章

  • ES6-7

    摘要:的翻译文档由的维护很多人说,阮老师已经有一本关于的书了入门,觉得看看这本书就足够了。前端的异步解决方案之和异步编程模式在前端开发过程中,显得越来越重要。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。 JavaScript Promise 迷你书(中文版) 超详细介绍promise的gitbook,看完再不会promise...... 本书的目的是以目前还在制定中的ECMASc...

    mudiyouyou 评论0 收藏0
  • JavaScript 异步

    摘要:从最开始的到封装后的都在试图解决异步编程过程中的问题。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。写一个符合规范并可配合使用的写一个符合规范并可配合使用的理解的工作原理采用回调函数来处理异步编程。 JavaScript怎么使用循环代替(异步)递归 问题描述 在开发过程中,遇到一个需求:在系统初始化时通过http获取一个第三方服务器端的列表,第三方服务器提供了一个接口,可通过...

    tuniutech 评论0 收藏0
  • javascript知识点

    摘要:模块化是随着前端技术的发展,前端代码爆炸式增长后,工程化所采取的必然措施。目前模块化的思想分为和。特别指出,事件不等同于异步,回调也不等同于异步。将会讨论安全的类型检测惰性载入函数冻结对象定时器等话题。 Vue.js 前后端同构方案之准备篇——代码优化 目前 Vue.js 的火爆不亚于当初的 React,本人对写代码有洁癖,代码也是艺术。此篇是准备篇,工欲善其事,必先利其器。我们先在代...

    Karrdy 评论0 收藏0
  • 双十二大前端工程师读书清单

    摘要:本文最早为双十一而作,原标题双大前端工程师读书清单,以付费的形式发布在上。发布完本次预告后,捕捉到了一个友善的吐槽读书清单也要收费。这本书便从的异步编程讲起,帮助我们设计快速响应的网络应用,而非简单的页面。 本文最早为双十一而作,原标题双 11 大前端工程师读书清单,以付费的形式发布在 GitChat 上。发布之后在读者圈群聊中和读者进行了深入的交流,现免费分享到这里,不足之处欢迎指教...

    happen 评论0 收藏0
  • 双十二大前端工程师读书清单

    摘要:本文最早为双十一而作,原标题双大前端工程师读书清单,以付费的形式发布在上。发布完本次预告后,捕捉到了一个友善的吐槽读书清单也要收费。这本书便从的异步编程讲起,帮助我们设计快速响应的网络应用,而非简单的页面。 本文最早为双十一而作,原标题双 11 大前端工程师读书清单,以付费的形式发布在 GitChat 上。发布之后在读者圈群聊中和读者进行了深入的交流,现免费分享到这里,不足之处欢迎指教...

    余学文 评论0 收藏0

发表评论

0条评论

2json

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<