Javascript进阶——异步编程解决方案

学习了异步编程的过程之后,我们接着整理一下常用的异步编程方式。常见的异步编程方法包含以下几种: 回调函数 事件发布/订阅 Promise Generator 函数 async 函数 回调函数 回调函数是最简单的实现异步的方式,常用的就是定时器和ajax请求。这种方式的优点就是简单易于理解,缺点是阅读性差,多层调用时耦合性高。代码形式多数如下: function f1(callback){     setTimeout(function () {       // f1的任务代码       callback();     }, 1000);   } 使用定时器的Tips: 定时任务可能不会按时执行,取决于同步任务的执行时间 定时器嵌套5次之后最小间隔时间不能低于4ms 定时器主要应用场景:防抖,节流,倒计时,动画 事件监听 事件监听的要点:任务的执行不取决代码的顺序,而取决于某一个事件是否发生。常用的就是对dom对象的事件绑定。例如: $('.element1').on('click', function(){ console.log(1); }); 发布/订阅 发布---订阅模式又叫观察者模式,它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。 简单的发布/订阅模式实现如下: class PubCenter{ constructor(){ this.events={}; } subscribe(eventName, callback){ if(this.events[eventName]){ this.events[eventName].push(callback); } else{ this.events[eventName]=[callback]; } } publish(eventName,data){ if(this.events[eventName]){ this.events[eventName].forEach(cb=>{ cb.apply(this,data); }) } } unsubscribe(eventName, callback){ if(this.events[eventName]){ this.events[eventName]=this.events[eventName].filter(cb=>{ cb!=callback; }); } } } // 使用 const pub = new Publisher(); ajax('/url', function(data){ pub.publish('ajaxSucess', data); }); pub.subscribe('ajaxSucess', function(){ console.log('ajax success'); }); 发布/订阅的优势: 松耦合 灵活 缺点: 无法确保消息被触发或被触发几次 Promise Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功) 和 rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。 Promise 对象的状态改变,只有两种可能:从 pending 变为 fulfilled 和 从 pending 变为 rejected。 ES6 原生提供了 Promise 对象。它的构造函数接受一个名为executor 的函数,此执行函数接受两个参数 resolve 和 reject,它们都是函数。 Promise 通常用于处理异步操作或阻塞代码,其示例包括文件操作,API调用,DB调用,IO调用等。这些异步操作的启动发生在执行函数中。如果异步操作成功,则通过 promise 的创建者调用resolve 函数返回预期结果,同样,如果出现意外错误,则通过调用 reject 函数传递错误具体信息。 基本用法: new Promise(function (resolve, reject) { // 一段耗时的异步操作 if (success) { resolve("成功"); // 数据处理完成 } else { reject("错误信息"); // 数据处理出错 } }).then( (res) => { console.log(res); }, // 处理成功回调 (err) => { console.log(err); } // 处理失败回调 ); // 多个Promise可以通过then方法链式调用,实现顺序执行,除了转账其他事都不在需要一层一层写嵌套代码 // 错误处理,可以通过then方法的第二个参数函数处理,也可以通过throw new Error用catch捕获处理 new Promise((resolve) => { setTimeout(() => { if (success) { resolve("第一步"); } else { reject("第一个错误"); } }, 2000); }) .then( (res) => { console.log(res); // res= '第一步' return new Promise((resolve) => { setTimeout(() => { if (success) { resolve("第二步"); } else { throw new Error("第二个错误"); } }, 2000); }); }, (err) => { console.log(err); // err='第一个错误' } ) .then((res) => { console.log(res); // res= '第二步' }) .catch((err) => { console.log(err); // err='第二个错误' }); 除此之外,ES6还提供了一些API,支持多个Promise并行执行,Promise.all和Promise.race Promise.all(iterable) : 返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise有一个失败(rejected),此实例回调失败(reject),失败原因的是第一个失败 promise 的结果。 Promise.race(iterable) : 返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。 // 同时执行p1和p2,只有p1、p2的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2的返回值组成一个数组,传递给p的回调函数 // 只要p1、p2之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数 var p= Promise.all([p1, p2]).then(function (results) { console.log(results); // 获得一个Array: ['P1', 'P2'] }); // Promise.race var p1 = new Promise(function (resolve, reject) { setTimeout(resolve, 500, 'P1'); }); var p2 = new Promise(function (resolve, reject) { setTimeout(resolve, 600, 'P2'); }); // 只要p1、p2之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的返回值 // 由于p1执行较快,Promise的then()将获得结果'P1'。p2仍在继续执行,但执行结果将被丢弃。 var p= Promise.race([p1, p2]).then(function (result) { console.log(result); // 'P1' }); Promise优势: 可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得控制异步操作更加容易 缺点: Promise 一旦创建,不能中途取消 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部 当处于 Pending 状态时,无法得知目前进展到哪一个阶段 Generator函数(生成器函数) Generator函数时ES6提供的一种异步编程解决方案,它通过function*来声明,返回一个符合可迭代协议和迭代器协议的生成器对象,它在执行时能暂停,又能从暂停处继续执行。 可迭代协议: 包含[Symbol.iterator]属性,ES6内置的可迭代对象包含String,Array,Map,Set等 迭代器协议: 具有next()方法,并且next方法返回一个包含属性done(bool),value(object)对象,done表示可迭代对象是否遍历完成,value表示可迭代对象当前的值,遍历完成后,done=true,value=undefined Generator函数要点: Generator函数通过yeild关键字来暂停和恢复,yeild关键字只能出现在generator函数里 next方法的执行:遇到yeild关键字则暂停,将yeild后面的值作为返回对象的value的值;没有yeild,则一直执行到return,将return的值作为返回对象的value;没有return,将undefined作为返回对象的value next方法可以带一个参数,该参数会被当做上一个yeild表达式的返回值 代码示例: function* generator() { let first = yield 1; let second = yield first + 2; yield second + 3; } const g = generator(); g.next(); // {value:1, done:false} g.next(3); // {value:5, done:false}, next方法有参数,将作为上一次yield返回值,此时first=3 g.next(4); // {value:7, done: false}, 同上,second=4 g.next(); // {value: undefined, done: true} // 利用yeild* 可实现生成器复用 function* gen1() { yield 1; yield 2; } function* gen2() { yield 5; yield* gen1(); yield 6; } const g2 = gen2(); g2.next(); //{value:5, done:false} g2.next(); //{value:1, done:false} g2.next(); //{value:2, done:false} g2.next(); //{value:6, done:false} g2.next(); // {value: undefined, done: true} // 利用return函数可以提前结束generator函数 function* gen3() { yield 1; yield 2; yield 3; } const g3 = gen3(); g3.next(); // {value:1, done:false} g3.return(); // {value: undefined, done: true} g3.next(); // {value: undefined, done: true} // throw应用 function* gen4() { try { let v2 = yield 1 + 2; } catch { v2 = 4; } yield v2 + 3; } const g4 = gen4(); g4.next(); // {value: 3, done: false} g4.throw(new Error("e")); //{value: 7, done: false}, throw不会中断执行,直到遇到yield g4.next(); // {value: undefined, done: true} 利用Thunk函数(参考链接5),可以实现generator自动执行: // 使用Thunk自动执行gnerator函数顺序执行异步操作(文件读取,ajax),避免代码嵌套 const Thunk = function (fn) { return function (...args) { return function (callback) { return fn.call(this, ...args, callback); }; }; }; const asyncThunk = Thunk(someAsyncfun); const gen = function* () { const res1 = yield someAsyncfun("1"); console.log(res1); const res2 = yield someAsyncfun("2"); console.log(res2); const res3 = yield someAsyncfun("3"); console.log(res3); const res4 = yield someAsyncfun("4"); console.log(res4); }; const run=function(fn){ const gen=fn(); function next(err, data){ const res=gen.next(data); if(res.done) return; res.value(next); } next(); } run(gen); async/await async函数:本质是一个语法糖,使异步编程更简单,它的返回值是一个Promise对象,return的值是Promise resolved时的value,throw的值是Promise rejected时的reason await: 只能出现在async函数内或者最外层,它的作用是等待一个Promise对象的值,如果await的Promise为rejected状态,将中断后续执行,如果要保证后续代码的执行,需要对await后面的Promise进行异常捕获或者将Promise放在try Catch中 async的使用非常简单,代码示例: async function async1() { console.log("async1 start"); await async2(); console.log("async1 end"); } async function async2() { return Promise.resolve().then(_ => { console.log("async2 promise"); }); } async1(); // 顺序打印出 // async1 start // async2 promise // async1 end // 处理异常,保证rejected之后的代码继续执行 async function f() { await Promise.reject("error").catch(err => {}); console.log(1); await 100; } async function f() { try { await Promise.reject("error"); } catch (e) { // 处理异常 } finally { } console.log(1); await 100; } 总结 最后我们展示一下不同方法实现的顺序执行异步操作,了解一下异步编程的演变过程,以node.js顺序读取文件为例: import { readFile } from "fs"; const files = ["/file1", "file2", "file3"]; // 传统回调函数 const readFileByCallback = function () { readFile(files[0], function (err, data) { console.log(data.toString()); readFile(files[1], function (err, data) { console.log(data.toString()); readFile(files[2], function (err, data) { console.log(data.toString()); }); }); }); }; // 调用 readFileByCallback(); // Promise方式 function readFilesByPromise() { const read = function (src) { return new Promise((resolve, reject) => { readFile(src, (err, data) => { if (err) reject(err); resolve(data); }); }); }; read(files[0]) .then(function (data) { console.log(data.toString()); return read(files[1]); }) .then(function (data) { console.log(data.toString()); return read(files[2]); }) .then(function (data) { console.log(data.toString()); }); } // 调用 readFilesByPromise(); // 使用Generator函数方式 function* readFilesByGenerator() { const fs = require("fs"); const files = [ "/Users/kitty/testgenerator/1.json", "/Users/kitty/testgenerator/2.json", "/Users/kitty/testgenerator/3.json" ]; let fileStr = ""; function readFile(filename) { fs.readFile(filename, function(err, data) { console.log(data.toString()); f.next(data.toString()); }); } yield readFile(files[0]); yield readFile(files[1]); yield readFile(files[2]); } const f = readFilesByGenerator(); f.next(); // async/await 方式 async function readFilesByAsync() { const read = function (src) { return new Promise((resolve, reject) => { readFile(src, (err, data) => { if (err) reject(err); resolve(data); }); }); }; const str0 = await read(files[0]); console.log(str0.toString()); const str1 = await read(files[1]); console.log(str1.toString()); const str2 = await read(files[2]); console.log(str2.toString()); } // 调用 readFilesByAsync(); 参考链接: Promises/A+ 菜鸟教程 -- JavaScript Promise 对象 可迭代协议 迭代器协议 协程概念,原理 阮一峰-Thunk 函数的含义和用法

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):