likes
comments
collection
share

day25 行为型:通过观察者、迭代器模式看JS异步回调

作者站长头像
站长
· 阅读数 15

观察者模式

事件驱动两个主要的对象,一个是被观察对象 change observable,一个是观察者 observer。被观察对象会因为事件而发生改变,而观察者则会被这个改变驱动,做出一些反应。day25  行为型:通过观察者、迭代器模式看JS异步回调

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 事件。比较高频还有网络和后端事件。day25  行为型:通过观察者、迭代器模式看JS异步回调UI 事件比如有时我们需要程序根据监听触屏或滑动行为做出反应。网络事件我们现在大多的应用都是通过 XHR 这种模式,动态加载内容并且展示于前端的,通常会等待客户端请求通过网络到达服务器端,得到返回的状态,然后再执行任何操作。后端事件比如在 Node.js 当中,观察者模式也是非常重要的甚至可以说是最核心的模式之一,以至于被内置的 EventEmmiter 功能所支持。举个例子,Node 中的“fs”模块是一个用于处理文件和目录的 API。我们可以把一个文件当做一个对象,那么当它被打开、读取或关闭的时候,其实就是不同的状态事件,在这个过程中,如果要针对这些不同的事件做通知和处理,就会用到观察者模式。事件驱动和异步观察者模式通常和事件驱动相关,那它和异步又有什么关系呢?一些计算机程序,例如科学模拟和机器学习模型,是受计算约束的,它们连续运行,没有停顿,直到计算出结果,这种是同步编程。然而,大多数现实世界的计算机程序都是异步的,也就是说这些程序经常不得不在等待数据到达或某些事件发生时停止计算。再等待某个状态或动作才能开启程序运行。观察者模式和异步的关系在于:事件就是基于异步产生的,而我们需要通过观察对基于异步产生的事件来做出反应。day25  行为型:通过观察者、迭代器模式看JS异步回调JavaScript 提供了一系列支持异步观察者模式的功能,分别是 callback、promise/then、generator/next 和 aync/await。接下来分别看看这几种模式。Callback 模式在 JavaScript 中,回调模式(callback pattern) 就是我们在一个函数操作完时把结果作为参数传递给另外一个函数的这样一个操作。在函数式编程中,这种传递结果的方式称为连续传递样式(CPS,continous passing style)。它表示的是调用函数不直接返回结果,而是通过回调传递结果。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这里最经典的例子就是 setTimeout 了。我们通过 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

上面的例子中,其函数调用和控制流转顺序可以用下图表示:day25  行为型:通过观察者、迭代器模式看JS异步回调回调地狱在 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。Promises自从 ES6 之后,JavaScript 就引入了一系列新的内置工具来帮助处理异步事件。其中最开始的是 promise 和 then. 我们可以用 then 的连接方式,在每次 fetch 之后都调用一个 then 来进行下一层的操作。

(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 时,再返回函数内部执行下一条语句。

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在 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();
此文章为2月Day4学习笔记,内容来源于极客时间《Jvascript进阶实战课》,大家共同进步💪💪