learn-tech/专栏/JavaScript进阶实战课/24行为型:通过观察者、迭代器模式看JS异步回调.md
2024-10-16 06:37:41 +08:00

13 KiB
Raw Blame History

                        因收到Google相关通知网站将会择期关闭。相关通知内容
                        
                        
                        24 行为型通过观察者、迭代器模式看JS异步回调
                        你好,我是石川。

说完了创建和结构型的设计模式这一讲我们来学习行为型的设计模式。我们前面说前端编程是事件驱动event driven之所以这么说是因为前端编程几乎离不开用户和应用的交互通常我们的程序会根据用户的屏幕点击或者页面滑动操作而做出相应的反应。这一点从我们前面讲到的React和Vue的例子中也可以发现响应式编程reactive programming的思想对前端应用设计有着很强的影响。

今天我们会讲到行为型设计模式中的观察者模式它是事件驱动在设计层面上的体现。通过这一讲的内容你也可以更了解JS开发中事件驱动和异步的特性。

事件驱动

说到事件驱动就离不开两个主要的对象一个是被观察对象change observable一个是观察者observer。被观察对象会因为事件而发生改变而观察者则会被这个改变驱动做出一些反应。

我们可以通过上图一个简单的例子来了解下这种模式。假设我们有两个观察者1和2它们的初始值分别是11和21。observable是被观察对象这个时候如果被观察对象做出了增加1的行为观察者1和2的值就会更新为12和22。下面我们可以看看它的实现通常一个被观察者的实现是模版式的而观察者则是根据不同的反应需求来设计不同的逻辑。

class Observable { constructor() { this.observers = []; } subscribe(func) { this.observers.push(func); } unsubscribe(func) { this.observers = this.observers.filter(observer => observer !== func); } notify(change) { this.observers.forEach(observer => {observer.update(change);}); } }

class Observer { constructor(state) { this.state = state; this.initState = state; } update(change) { let state = this.state; switch (change) { case 'increase': this.state = ++state; break; case 'decrease': this.state = --state; break; default: this.state = this.initState; } } }

// 使用 var observable = new Observable();

var observer1 = new Observer(11); var observer2 = new Observer(21);

observable.subscribe(observer1); observable.subscribe(observer2);

observable.notify('increase');

console.log(observer1.state); // 12 console.log(observer2.state); // 22

在这个事件驱动的案例里用到的就是观察者observer模式。观察者模式是行为型模式当中的一种并且算是出镜率最高的、被谈及最多的一种模式了它是事件驱动在设计层面的体现。事件驱动最常见的就是UI事件了比如有时我们需要程序根据监听触屏或滑动行为做出反应。除了UI事件驱动还有两个事件驱动的场景使用频率非常高就是网络和后端事件。

我们先说网络事件这是观察者模式使用频率非常高的一个场景原因是我们现在大多的应用都是通过XHR这种模式动态加载内容并且展示于前端的通常会等待客户端请求通过网络到达服务器端得到返回的状态然后再执行任何操作。这就需要我们通过观察者模式来订阅不同的状态并且根据状态做出不同的行为反应。

另外一个场景是后端事件比如在Node.js当中观察者模式也是非常重要的甚至可以说是最核心的模式之一以至于被内置的EventEmmiter功能所支持。举个例子Node 中的“fs”模块是一个用于处理文件和目录的API。我们可以把一个文件当做一个对象那么当它被打开、读取或关闭的时候其实就是不同的状态事件在这个过程中如果要针对这些不同的事件做通知和处理就会用到观察者模式。

事件驱动和异步

我们前面说了观察者模式通常和事件驱动相关,那它和异步又有什么关系呢?

这个关系不难理解一些计算机程序例如科学模拟和机器学习模型是受计算约束的它们连续运行没有停顿直到计算出结果这种是同步编程。然而大多数现实世界的计算机程序都是异步的也就是说这些程序经常不得不在等待数据到达或某些事件发生时停止计算。Web 浏览器中的JavaScript程序通常是事件驱动的这意味着它们在实际执行任何操作之前会等待用户点击。或者在网络事件或后端事件中也是要等待某个状态或动作才能开启程序运行。

所以回到上面的问题,观察者模式和异步的关系在于:事件就是基于异步产生的,而我们需要通过观察对基于异步产生的事件来做出反应。

JavaScript提供了一系列支持异步观察者模式的功能分别是callback、promise/then、generator/next 和 aync/await。下面让我们分别来看看这几种模式吧。

Callback模式

在 JavaScript 中回调模式callback pattern 就是我们在一个函数操作完时把结果作为参数传递给另外一个函数的这样一个操作。在函数式编程中这种传递结果的方式称为连续传递样式CPScontinous passing style。它表示的是调用函数不直接返回结果而是通过回调传递结果。作为一个通用的概念CPS不代表一定是异步操作它也可以是同步操作。下面我们可以针对同步和异步分别来看一下。

同步CPS

我们先来看看同步的CPS。下面的这个加法函数你应该很容易理解我们把a和b的值相加然后返回结果。这种方式叫做直接样式direct style

function add (a, b) { return a + b; }

那如果用callback模式来做同步CPS会是怎样呢。在这个例子里syncCPS不直接返回结果而是通过callback来返回 a 加 b 的结果。

function syncCPS (a, b, callback) { callback(a + b); }

console.log('同步之前'); syncCPS(1, 2, result => console.log(结果: ${result})); console.log('同步之后');

// 同步之前 // 结果: 3 // 同步之后

异步CPS

下面我们再看看异步的CPS。这里最经典的例子就是setTimeout了通过示例代码你可以看到同样的异步CPS不直接返回结果而是通过callback来返回a加b的结果。但是在这里我们通过setTimeout让这个结果是在0.1秒后再返回这里我们可以看到在执行到setTimeout时它没有在等待结果而是返回给asyncCPS执行下一个console.log(‘异步之后’)的任务。

function asyncCPS (a, b, callback) { setTimeout(() => callback(a + b), 100); }

console.log('异步之前'); asyncCPS(1, 2, result => console.log(结果: ${result})) console.log('异步之后');

// 异步之前 // 异步之后 // 结果: 3

在上面的例子中,其函数调用和控制流转顺序可以用下图表示:

你可能会有疑问就是在同步CPS的例子中这种方式有没有意义呢答案是没有。因为我们上面只是举个例子来看同步CPS是可以实现的但其实如果函数是同步的根本没有用CPS的必要。使用直接样式而不是同步CPS来实现同步接口始终是更加合理的实践。

回调地狱

在ES6之前我们几乎只能通过callback来做异步回调。举个例子在下面的例子中我们想获取宝可梦的machineInfo机器数据可以通过网上一个公开的库基于XMLHttpRequest来获取。

需要基于这样一个链条 pockmon=>moveInfo=>machineInfo。

(function () { var API_BASE_URL = 'https://pokeapi.co/api/v2'; var pokemonInfo = null; var moveInfo = null; var machineInfo = null;

var pokemonXHR = new XMLHttpRequest(); pokemonXHR.open('GET', ${API_BASE_URL}/pokemon/1); pokemonXHR.send();

pokemonXHR.onload = function () { pokemonInfo = this.response var moveXHR = new XMLHttpRequest(); moveXHR.open('GET', pokemonInfo.moves[0].move.url); moveXHR.send();

moveXHR.onload = function () {
  moveInfo = this.response;
  var machineXHR = new XMLHttpRequest();
  machineXHR.open('GET', moveInfo.machines[0].machine.url);
  machineXHR.send();
  
  machineXHR.onload = function () { }
}

} })();

你可以看到在这个例子里我们每要获取下一级的接口数据都要重新建立一个新的HTTP请求而且这些回调函数都是一层套一层的。如果是一个大型项目的话这么多层的嵌套是很不好的代码结构这种多级的异步嵌套调用的问题也被叫做“回调地狱callback hell是使用callback来做异步回调时要面临的难题。 这个问题怎么解呢下面我们就来看看promise和async的出现是如何解决这个问题的。

ES6+的异步模式

自从ES6开始JavaScript中就逐步引入了很多硬核的工具来帮助处理异步事件。从一开始的Promise到生成器Generator和迭代器Iterator再到后来的async/await。回调地狱的问题被一步步解决了让异步处理重见阳光。下面我们从Promise开始看看这个问题是怎么一步步被解决的。

Promises

自从ES6之后JavaScript就引入了一系列新的内置工具来帮助处理异步事件。其中最开始的是promise和then. 我们可以用then的连接方式在每次fetch之后都调用一个then来进行下一层的操作。我们来看这段代码这里减少了很多XMLHttpRequest的代码但是仍然没有脱离一层层的调用。所以这种代码也不优雅。

(function () { var API_BASE_URL = 'https://pokeapi.co/api/v2'; var pokemonInfo = null; var moveInfo = null; var machineInfo = null;

var showResults = () => { console.log('Pokemon', pokemonInfo); console.log('Move', moveInfo); console.log('Machine', machineInfo); };

fetch(${API_BASE_URL}/pokemon/1) .then((response) => { pokemonInfo = response; fetch(pokemonInfo.moves[0].move.url) }) .then((response) => { moveInfo = response; fetch(moveInfo.machines[0].machine.url) }) .then((response) => { machineInfo = response; showResults(); }) })();

生成器和迭代器

那么怎么才能像同步的方式一样来执行异步的调用呢在ES6版本中在引入Promise和then的同时也引入了生成器Generator和迭代器Interator的概念。生成器是可以让函数中一行代码执行完后通过yield先暂停然后执行外部代码等外部代码执行中出现next时再返回函数内部执行下一条语句。是不是很神奇这个例子中的next其实就是行为型模式中的迭代器模式的一种体现。

function* getResult() { var pokemonInfo = yield fetch(${API_BASE_URL}/pokemon/1); var moveInfo = yield fetch(pokemonInfo.moves[0].move.url); var machineInfo = yield fetch(moveInfo.machines[0].machine.url); }

var result = showResults();

result.next().value.then((response) => { return result.next(response).value }).then((response) => { return result.next(response).value }).then((response) => { return result.next(response).value

async/await

但是使用next也有问题就是这样的回调链条也会非常的长。意识到了promise/then的问题后在ES8的版本中JavaScript又引入了async/await的概念。这样每一次获取信息的异步操作如pokemonInfo、moveInfo等都可以独立通过await来进行写法上又可以保持和同步类似的简洁性。下面我们来看看

async function showResults() { try { var pokemonInfo = await fetch(${API_BASE_URL}/pokemon/1) console.log(pokemonInfo) var moveInfo = await fetch(pokemonInfo.moves[0].move.url) console.log(moveInfo) var machineInfo = await fetch(moveInfo.machines[0].machine.url) console.log(machineInfo) } catch (err) { console.error(err) } } showResults();

总结

今天我带你通过观察者和迭代器等模式了解了异步编程的设计模式。可以说事件驱动、响应式编程、异步编程包含了JavaScript中很大一部分设计模式的核心概念。所以这篇文章虽然篇幅不大但是确实是了解和应用JavaScript的核心非常重要的内容。

当然,异步编程是一个很大的话题,我们今天一次也讲不完,后面我们还会用一讲内容继续来看异步中的并行和串行开发,并且在讲到多线程的时候,我们会更深入理解异步的实现逻辑。

思考题

最后给你留一道思考题前面我们说CPS就是回调那反之我们可以说回调就是CPS吗

欢迎在留言区分享你的答案、交流学习心得或者提出问题,如果觉得有收获,也欢迎你把今天的内容分享给更多的朋友。我们下期再见!