TOP1

异步编程的方案演进

在上个章节中我们介绍到了 JavaScript 是一门单线程非阻塞的编程语言,异步操作是基于 事件循环 的并发模型来处理的。 但是站在浏览器的角度JavaScript异步操作的本质还是没有发生变化。

本质是啥?异步调用需要通过多线程的方式让调用方与被调方线程通讯,同时我们通过某些机制(回调/事件/消息)让被调方有了结果时通知调用方

我们知道JavaScript是单线程 , 但是浏览器是多线程的。浏览器开设了其他线程去辅助JavaScript线程的运行。
浏览器有很多线程,例如:

  1. GUI 渲染线程
  2. JavaScript引擎线程
  3. 定时器触发线程 (setTimeout)
  4. 浏览器事件线程 (onclick)
  5. http 异步线程
  6. EventLoop轮询处理线程

异步编程-发布订阅 - 图1
JavaScript 引擎线程,我们把它称为 主线程 ,它的作用是用于解析执行 JavaScript 同步代码。那如果遇到异步事件则会把任务挂起继续执行后面的代码。

如图:
image.png
当异步事件挂起之后,同时就会来分析这是个定时器任务,还是I/O任务还是一个网络请任务。 很明显JavaScript 线程处理不了这些任务,那么就会分派到对应的线程中, 定时器任务交给定时器触发线程,网络任务交给网络线程,I/O 任务交给I/O线程。 当这些线程处理完毕就会像队列中添加结束事件,通过callback(回调)的形式通知调用方。

image.png
所以我们经常会听到JavaScript异步编程最早的解决方案是回调函数,如事件的回调,setInterval/setTimeout中的回调。就这么来的大家明白了吗?

但是回调函数有两个很致命的问题:

  • 违法直觉
  • 地狱回调

下面代码演示了假如我们有三个网络请求,第二个必须等第一个结束才能发出,第三个必须等第二个结束才能发起,如果我们使用回调就会变成这样:

  1. const request = require("request");
  2. request('https://www.baidu.com', function (error, response) {
  3. if (!error && response.statusCode == 200) {
  4. console.log('get times 1');
  5. request('https://www.baidu.com', function(error, response) {
  6. if (!error && response.statusCode == 200) {
  7. console.log('get times 2');
  8. request('https://www.baidu.com', function(error, response) {
  9. if (!error && response.statusCode == 200) {
  10. console.log('get times 3');
  11. }
  12. })
  13. }
  14. })
  15. }
  16. });

由于浏览器端 ajax 会有跨域问题,上述例子我是用 Node 运行的。这个例子里面有三层回调,我们已经有点晕了,如果再多几层,那真的就是地狱了,维护难/阅读理解难。

为了解决这个问题社区经过长时间的摸索得出来很多的方案:

  • 发布/订阅 (设计模式)
  • deferred对象 (早期的类似 promise 对象)
  • Promise
  • Generator
  • async/await

前两个属于社区版本的实现,后三者已经成为 JavaScript 语言规范的一部分了。 大家要注意了我们这里所说的解决方案指得并不是要改变 JavaScript 异步模型。 目的是 以同步的方式书写异步代码。 以此来解决地狱回调,嵌套的异步逻辑,代码可阅读性可维护性的问题。

TOP2

发布/订阅

发布订阅模式是一种设计模式,并不仅仅用于JS中,这种模式可以帮助我们解开 “回调地狱”。他的流程如下图所示:
异步编程-发布订阅 - 图4
这个设计由三部分组成。

  1. 发布者: 通过消息中心发布消息
  2. 消息中心:负责存储消息与订阅者的对应关系,有消息触发时,负责通知订阅者
  3. 订阅者:去消息中心订阅自己感兴趣的消息

有了这种模式,前面处理几个相互依赖的异步操作就不用陷入”回调地狱”了,只需要让后面的订阅前面的成功消息,前面的成功后发布消息就行了。

有个地方要特别说明下大家仔细看这张图:
在发布订阅模式里,发布者,并不会直接通知订阅者,换句话说,发布者和订阅者,彼此互不相识。

互不相识那他们之间如何交流?
答案是,通过第三者,也就是在消息中心。

发布者只需告诉消息中心,我要发的消息,topic1(话题)是AAA;
订阅者只需告诉消息中心,我要订阅topic1(话题)的AAA的消息;

于是,当消息中心收到发布者发过来消息,并且topic1是AAA时,就会把消息推送给订阅了topic是AAA的订阅者。当然也有可能是订阅者自己过来拉取,看具体实现。

我们实现最简单的发布订阅代码:

  1. let eventMap = {};
  2. function pub(msg, ...rest) {
  3. eventMap[msg] && eventMap[msg].forEach((cb) => {
  4. cb(...rest);
  5. });
  6. }
  7. function sub(msg, cb) {
  8. eventMap[msg] = eventMap[msg] || [];
  9. eventMap[msg].push(cb);
  10. }

有了我们自己的 Pub Sub,我们就可以用它来解决前面的回调地狱问题了:

  1. const request = require("request");
  2. const pubSub = new PubSub();
  3. request('https://www.baidu.com', function (error, response) {
  4. if (!error && response.statusCode == 200) {
  5. console.log('get times 1');
  6. // 发布请求1成功消息
  7. pubSub.publish('request1Success');
  8. }
  9. });
  10. // 订阅请求1成功的消息,然后发起请求2
  11. pubSub.subscribe('request1Success', () => {
  12. request('https://www.baidu.com', function (error, response) {
  13. if (!error && response.statusCode == 200) {
  14. console.log('get times 2');
  15. // 发布请求2成功消息
  16. pubSub.publish('request2Success');
  17. }
  18. });
  19. })
  20. // 订阅请求2成功的消息,然后发起请求3
  21. pubSub.subscribe('request2Success', () => {
  22. request('https://www.baidu.com', function (error, response) {
  23. if (!error && response.statusCode == 200) {
  24. console.log('get times 3');
  25. // 发布请求3成功消息
  26. pubSub.publish('request3Success');
  27. }
  28. });
  29. });

TOP3

扩展:观察者与发布订阅者的区别?

面试中经常会问到观察者模式,和发布订阅模式,有什么区别? 很多人在没有深入系统的了解过这两者的差异会误以为他们是一样的。

但发布订阅模式和观察者模式还是有些本质上的差异。刚刚我们讲到的发布订阅模式是完全解耦的。事件的发布者无需关注订阅者的侦听器如何实现业务逻辑,甚至不用关注有多少个侦听器存在。数据通过消息的方式可以灵活的传递。

观察者模式

在观察者模式里面就是被观察者(Subject),它只需维护一套观察者(Observer)的集合,将有关状态的任何变更自动通知给它们 watcher(观察者), 这个设计是松耦合的。

而在观察者模式中,观察者是知道被观察者(Subject)的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只能通过消息代理进行通信。

TOP4

实例: Vue 数据侦测观察者

image.png

观察者(Observer)的集合 data。
watcher 重新生成render function 更新 Virtual DOM

依赖收集 : 编译器- 如何知道当前组件触摸(依赖)了哪些数据? (vm.$options.render)
触发更新 :数据跟新把 watcher 放入nextTick 更新队列中去。
容器: dep