✨从响应式讲起,Observable:穿个马甲你就不认识啦?(附实战)

简介: ✨从响应式讲起,Observable:穿个马甲你就不认识啦?(附实战)

前言



在 JS 中谈到 “响应式” ,你会想起什么?


1. 最初的 Object.observe ,已经被弃用了。。。

image.png


3. 还有 Object.defineProperty,它是 Vue2 响应式的核心。

image.png


2. 后来,ES6 有 Proxy 劫持了,很棒,Vue3 就是基于它的。

image.png


4. 再有,React 一词的中文就是“反应”、“响应”的意思,hooks 是 react 的最新“响应式”的解决方案;

image.png


还有吗? —— 其实在原生 JS 中还有~


5. 比如 addEventListener,也是一种响应式吧,当目标元素被点击后,就会通知一个回调函数,进行特定的操作。


var handler = (e) => {
  console.log(e);
  document.body.removeEventListener('click', handler);
}
document.body.addEventListener('click', handler);


6. 还有,比如考察 Event loop ,常要背的微任务:MutationObserver 一定也别忘记。

// 得到要观察的元素
var elementToObserve = document.querySelector("#targetElementId");
// 创建一个叫 `observer` 的新 `MutationObserver` 实例,
// 并将回调函数传给它
var observer = new MutationObserver(function() {
    console.log('callback that runs when observer is triggered');
});
// 在 MutationObserver 实例上调用 `observe` 方法,
// 并将要观察的元素与选项传给此方法
observer.observe(elementToObserve, {subtree: true, childList: true});


7. 还有,设计模式中常问的“观察者模式”,这个面试常考。

class Producer {
  constructor() {
    this.listeners = [];
  }
  addListener(listener) {
    if(typeof listener === 'function') {
      this.listeners.push(listener)
    } else {
      throw new Error('listener 必须是 function')
    }
  }
  removeListener(listener) {
    this.listeners.splice(this.listeners.indexOf(listener), 1)
  }
  notify(message) {
    this.listeners.forEach(listener => {
      listener(message);
    })
  }
}
var egghead = new Producer(); 
function listener1(message) {
  console.log(message + 'from listener1');
}
function listener2(message) {
  console.log(message + 'from listener2');
}
egghead.addListener(listener1); // 注册监听
egghead.addListener(listener2);
egghead.notify('A new course!!') // 执行
// a new course!! from listener1
// a new course!! from listener2


代码可复制在控制台中调试。

通过回顾以上 7 点,“抛开其它不谈,这个响应式就没什么问题吗?”

不得不承认:响应式思想根植在前端 Script 和 DOM 的交互中

我们进一步想想:为什么是响应式?

噢,其实,不为别的,就是为了偷懒!


偷懒的点在于,我们不想手动去触发函数的回调,设置响应式正是为了摆脱在时间上有异步操作而带来的困扰。


“我不管你什么时候操作,只要你操作了,就去触发XXX...”

响应式可以玩出各种各样的花来,这些其实就像是同一个事物在不同角度的展现。就像小学的那篇课文:《画杨桃》一样。关键在于你怎么看,是在其中的一面看,还是以全局视角来看。

image.png


按照这个思路继续往前,介绍今天的主角,基于 响应式 的新的花样:Observable,—— 它是 RxJS 的最最基础、最最核心的东西。


Observable 序列



整个 RxJS 最最基础的概念之一就是 Observable

什么是 Observable ?

网上看过很多解释,都不如人意,本瓜最后得出结论,不如就将其直接理解为一个 序列


什么是序列?

数组可能是我们用的最多的序列了。

你知道在 JS 中,数组还能这样迭代吗?

var arr = [1, 2, 3];
var iterator = arr[Symbol.iterator]();
iterator.next();
// { value: 1, done: false }
iterator.next();
// { value: 2, done: false }
iterator.next();
// { value: 3, done: false }
iterator.next();
// { value: undefined, done: true }

即使,不用 Symbol.iterator,我们也可以自己写一个迭代数组的方法。


自制 Iterator

class IteratorFromArray {
  constructor(arr) {
    this._array = arr;
    this._cursor = 0;
  }
  next() {
    return this._cursor < this._array.length ?
    { value: this._array[this._cursor++], done: false } :
    { done: true };
  }
}
var iterator = new IteratorFromArray([1,2,3]);
iterator.next();


有一个 next 方法,返回 {value:val,done:false} 或者 {done:true}

这样看来,Iterator Pattern 似乎不难,但对比数组遍历它同时带来了两个优势:


  1. 它渐进式取值的特性可以拿来做延迟运算(Lazy evaluation),让我们能用它来处理特殊结构(前面文章提过);
  2. 因为 iterator 本身是序列,所以可以作所有阵列的运算方法像 map, filter... 等;


这个就厉害啦,这意味着 IteratorFromArray 函数还能再进一步处理:比如用 map 的思路:


class IteratorFromArray {
  constructor(arr) {
    this._array = arr;
    this._cursor = 0;
  }
  next() {
    return this._cursor < this._array.length ?
    { value: this._array[this._cursor++], done: false } :
    { done: true };
  }
  map(callback) {
    const iterator = new IteratorFromArray(this._array);
    return { 
      next: () => {
        const { done, value } = iterator.next();
        return {
          done: done,
          value: done ? undefined : callback(value)
        }
      }
    }
  }
}
var iterator = new IteratorFromArray([1,2,3]);
var newIterator = iterator.map(value => value + 3);
newIterator.next();
// { value: 4, done: false }
newIterator.next();
// { value: 5, done: false }
newIterator.next();
// { value: 6, done: false }


“不是讲 Observable 吗,怎么讲 Iterator 去了。。。”

—— Observable 和 Iterator 很像、很像


它们有一样的共性,即:它们都是渐进式取值,以及适用阵列的运算。

要说其唯一的区别可能是,Observable 序列更侧重于在“时间”这个维度上描述,即 Observable 的值会随着时间进行推送。


Observable 执行



以下所有介绍的 Observable 代码示例都可以在  jsfiddle 下运行

cdn 依赖是:cdnjs.cloudflare.com/ajax/libs/r…

同步和异步

我们先测一个不带时间状态的同步的 Observable

image.png


在控制台依次输出:

image.png


测试地址

再测一个带时间状态的 Observable

image.png

image.png


同步结束后,执行异步的回调。


测试地址


细心的你一定发现了 subscribe 关键字的调用。subscribe 就是用来执行 Observable 的,就像是调用一个 function。


subscribe


通常 subscribe 参数中的对象有三个值,分别是:next、error、complete,对应 observer 的三个状态:next、error、complete;


var observable = Rx.Observable
  .create(function (observer) {
      observer.next('Jerry');
      observer.next('Anna');
                        observer.complete();
  })
observable.subscribe({
  next: function(value) {
    console.log(value);
  },
  error: function(error) {
    console.log(error)
  },
  complete: function() {
    console.log('complete')
  }
})

测试地址

觉得理解起来麻烦,就通俗认为 subscribe 就是来处理 observer.next 的值的~


操作符



上述就是最简单的 Observable 推送值、取值的过程。

接下来,简单认识下如何新建 Observable 以及 转换 Observable 。(都知道 RxJS 操作符很强大,它们其实大部分都是来操作 Observable 的。)


新建 Observable


Observable 有许多创建实例的方法,介绍最常见的几个~

  • create


create 前面都用的是这个,直接创建;

  • of


当我们想要同步的传递多个值时,可以用 of 这个 operator 来作简洁的表达

var source = Rx.Observable.of('Jerry', 'Anna');
source.subscribe(console.log);


测试地址

  • from


还可以用 from 来接收数组,创建 Observable

var arr = ['Jerry', 'Anna', 123, 456, 'juejin'] 
var source = Rx.Observable.from(arr);
source.subscribe(console.log);


测试地址

  • fromEvent

fromEvent 可以新建一个事件的 Observable

var source = Rx.Observable.fromEvent(document.body, 'click');


还有比如 fromEventPattern 可以新建类事件 Observable ,比如同时具有添加监听、移除监听的方法。


  • interval

每隔一定时间间隔产生值的 Observable

var source = Rx.Observable.interval(1000);


转换 Observable


常见的转换 Observable 比如像是 map, filter, contactAll......等等,所有这些函数都会拿到原本的observable 并回传一个新的observable。


  • map
// 生成一个间隔为1秒的时间序列,每秒输出的值为秒数*2
var source = Rx.Observable.interval(1000);
var newest = source.map(x => x*2); 
newest.subscribe(console.log);
// 0
// 2
// 4
// 6
...


测试地址

  • filter
// 生成一个间隔为1秒的时间序列,过滤掉奇数秒
var source = Rx.Observable.interval(1000);
var newest = source.filter(x => x % 2 === 0); 
newest.subscribe(console.log);
// 0
// 2
// 4
// 6
..


测试地址

  • concatAll

有时我们的 Observable 送出的元素又是一个 observable,就像是二维阵列,阵列里面的元素是阵列。

这时我们就可以用 concatAll 把它摊平成一维阵列,concatAll 把所有元素 concat 起来。

// 生成一个间隔为1秒的时间序列,取前 5 个值,
// 再生成一个间隔为 0.5 秒的时间序列,取前 2 个值
// 再生成一个间隔为 2 秒的时间序列,取前 1 个值
// 把这些值返回给一个 Observable,相当于是二维的 Observable,再用 concatAll 拉平;
var obs1 = Rx.Observable.interval(1000).take(5);
var obs2 = Rx.Observable.interval(500).take(2);
var obs3 = Rx.Observable.interval(2000).take(1);
var source = Rx.Observable.of(obs1, obs2, obs3);
var example = source.concatAll();
example.subscribe(console.log);
// 0
// 1
// 2
// 3
// 4
// 0
// 1
// 0


时间线的弹珠图示意:(ps: 不懂弹珠图的可看下一小节释义)


source : (o1                 o2      o3)|
           \                  \       \
            --0--1--2--3--4|   -0-1|   ----0|
                    concatAll()
example: --0--1--2--3--4-0-1----0|


测试地址

observable 操作的 API 有很多,一下子就记全、记清也是不现实的,我们应该 在学中用,在用中记,多看几遍就熟了,常用、关键的方法其实也不多。 rx.js.org-操作符分类


弹珠图


我们在传达事物时,文字其实是最糟的手段,虽然文字是我们平时沟通的基础,但常常千言万语也比不过一张清楚的图。


我们把描绘 observable 的图示称为弹珠图。

- 来表达一小段时间,这些 - 串起就代表一个observable。|则代表observable 结束


比如:

var source = Rx.Observable.interval(1000);


弹珠图:

-----0-----1-----2-----3--...
var source = Rx.Observable.interval(1000);
var newest = source.map(x => x + 1); 


弹珠图:

source: -----0-----1-----2-----3--...
            map(x => x + 1)
newest: -----1-----2-----3-----4--...


最常用操作


当操作比较复杂的时候,需要用到弹珠图来理解,rxviz.com/ 这个网站可以专门来绘制弹珠图。


  • merge

merge 用来合并 observable

var source = Rx.Observable.interval(500).take(3);
var source2 = Rx.Observable.interval(300).take(6);
var example = source.merge(source2);
example.subscribe(console.log);
source : ----0----1----2|
source2: --0--1--2--3--4--5|
            merge()
example: --0-01--21-3--(24)--5|


测试地址

可以看到 merge 和 concatAll 有区别:concatAll 是一个 Observable 彻底走完,再走下一个,merge 是同时跑,不管谁先推送值,都将其先取。

  • combineLatest


它会取得各个 observable 最后送出的值,再输出成一个值;

var source = Rx.Observable.interval(500).take(3);
var newest = Rx.Observable.interval(300).take(6);
var example = source.combineLatest(newest, (x, y) => x + y);
example.subscribe(console.log);
source : ----0----1----2|
newest : --0--1--2--3--4--5|
    combineLatest(newest, (x, y) => x + y);
example: ----01--23-4--(56)--7|


测试地址

  • withLatestFrom

withLatestFrom 运作方式跟 combineLatest 有点像,只是他有主从的关系,只有在主要的 observable 送出新的值时,才会执行 callback;


var main = Rx.Observable.from('hello').zip(Rx.Observable.interval(500), (x, y) => x);
var some = Rx.Observable.from([0,1,0,0,0,1]).zip(Rx.Observable.interval(300), (x, y) => x);
var example = main.withLatestFrom(some, (x, y) => {
    return y === 1 ? x.toUpperCase() : x;
});
example.subscribe(console.log);
main   : ----h----e----l----l----o|
some   : --0--1--0--0--0--1|
withLatestFrom(some, (x, y) =>  y === 1 ? x.toUpperCase() : x);
example: ----h----e----l----L----O|

测试地址


实战



OK,理论讲太多,也会乏味。就上面的 api 其实就已经够了,我们可以通过他们用短短几行代码实现复杂的功能。


基础拖拉


短短 15 行代码就可以实现一个基础的拖拽功能。在线测试地址

image.png

const dragDOM = document.getElementById('drag');
const body = document.body;
const mouseDown = Rx.Observable.fromEvent(dragDOM, 'mousedown');
const mouseUp = Rx.Observable.fromEvent(body, 'mouseup');
const mouseMove = Rx.Observable.fromEvent(body, 'mousemove');
mouseDown
  .map(event => mouseMove.takeUntil(mouseUp))
  .concatAll()
  .map(event => ({ x: event.clientX, y: event.clientY }))
  .subscribe(pos => {
    dragDOM.style.left = pos.x + 'px';
    dragDOM.style.top = pos.y + 'px';
  })


思路:

  1. 获取 dragDOM
  2. fromEvent 创建 mousedown、mouseup、mousemove 事件。
  3. 当第一次 mouseDown 时,监听 mouseMove,直到 mouseUp;
  4. 这个过程中,修改 dragDOM 的left、top 值;

只要能看懂 Observable operators,代码可读性非常高。既简洁,又易维护。


视频拖拉


接着拖拽的需求,再进一步。

我们在网页中看视频的时候,经常遇到这样的场景:下拉滚动条,视频缩放到右小角,并且可以拖拽。

image.png


用 RxJS Observable,35 行代码即能实现:

const video = document.getElementById('video');
const anchor = document.getElementById('anchor');
const scroll = Rx.Observable.fromEvent(document, 'scroll');
const mouseDown = Rx.Observable.fromEvent(video, 'mousedown')
const mouseUp = Rx.Observable.fromEvent(document, 'mouseup')
const mouseMove = Rx.Observable.fromEvent(document, 'mousemove')
const validValue = (value, max, min) => {
    return Math.min(Math.max(value, min), max)
}
scroll
.map(e => anchor.getBoundingClientRect().bottom < 0)
.subscribe(bool => {
    if(bool) {
        video.classList.add('video-fixed');
    } else {
        video.classList.remove('video-fixed');
    }
})
mouseDown
    .filter(e => video.classList.contains('video-fixed'))
    .map(e => mouseMove.takeUntil(mouseUp))
    .concatAll()
    .withLatestFrom(mouseDown, (move, down) => {
        return {
            x: validValue(move.clientX - down.offsetX, window.innerWidth - 320, 0),
            y: validValue(move.clientY - down.offsetY, window.innerHeight - 180, 0)
        }
    })
    .subscribe(pos => {
        video.style.top = pos.y + 'px';
        video.style.left = pos.x + 'px';
    })


思路分为 3 部分:

  1. 获取 DOM 以及鼠标事件;
  2. 监听滚动,当包含视频的 dom 相对于浏览器视窗的位置小于 0 ,则说明已触底。给视频添加一个标识;
  3. 拖拽;


备注:validValue 是为了不让视频超出浏览器视窗之外。

在线测试地址

代码真的太凝练了~


结语



本篇, 我们讲到了响应式的思想其实根植在前端开发的 Script 和 Dom 的交互中。根绝这种思想,衍生了很多写法,但是万变不离其宗,都是“响应式”。


响应式的另一种展示:RxJS Observable 又换了一个新的马甲,监听动作、沿着时间线去推送值、渐进式取值、值可以作阵列变化(map、filter 等等),这是本篇核心。

我们可以借助 操作符,用极少的代码量实现较为复杂的功能,代码看起来非常简洁、清晰。


感受感受事件流,只是善用这些操作符还需要时间来学习、使用、沉淀。。。

image.png



相关文章
|
6月前
|
JavaScript 前端开发 编译器
解密Vue 3:透过原理看框架,揭开它的神秘面纱
解密Vue 3:透过原理看框架,揭开它的神秘面纱
|
存储 前端开发 JavaScript
AntV X6源码探究简析
AntV是蚂蚁金服全新一代数据可视化解决方案,其中X6主要用于解决图编辑领域相关的解决方案,其是一款图编辑引擎,内置了一下编辑器所需的功能及组件等,本文旨在通过简要分析x6源码来对图编辑领域的一些底层引擎进行一个大致了解,同时也为团队中需要进行基于X6编辑引擎进行构建的图编辑器提供一些侧面了解,在碰到问题时可以较快的找到问题点。
389 0
|
2月前
|
JavaScript 前端开发 算法
react只停留在表层?五大知识点带你梳理进阶知识
该文章深入探讨了React进阶主题,包括PropTypes与默认属性的使用、虚拟DOM的工作机制、Refs的高级用法、生命周期方法的详解以及CSS动画在React中的集成技巧。
|
6月前
|
JavaScript API 开发工具
Vue3甜点探秘:史上最甜的语法糖
Vue3甜点探秘:史上最甜的语法糖
47 4
|
存储 JavaScript 开发者
几句话带你理解Vuex基础概念
几句话带你理解Vuex基础概念
65 0
|
6月前
|
缓存 JavaScript 前端开发
vue核心面试题汇总【查缺补漏】(二)
vue核心面试题汇总【查缺补漏】(二)
|
6月前
|
缓存 移动开发 JavaScript
vue核心面试题汇总【查缺补漏】(一)
vue核心面试题汇总【查缺补漏】(一)
132 0
|
6月前
|
前端开发
前端知识笔记(四)———深浅拷贝的区别,如何实现?
前端知识笔记(四)———深浅拷贝的区别,如何实现?
58 0
|
存储 Java 编译器
【C++杂货铺】继承由浅入深详细总结(下)
【C++杂货铺】继承由浅入深详细总结
42 0
|
安全 编译器 程序员
【C++杂货铺】继承由浅入深详细总结(上)
【C++杂货铺】继承由浅入深详细总结
34 0