前言

本篇主要是读 《JavaScript 设计模式核⼼原理与应⽤实践》的笔记记录。

JS设计模式

策略模式

策略模式的定义:

定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。

例,假设业务,商城中根据用户年龄打折处理,新人直接减10块,1年老用户9折,两年老用户8.5折...等等

function getPrice(tag, originPrice) {
    if(tag === "new"){
        let newPrice = originPrice - 10
        return newPrice <= 0 ? 0 : newPrice
    }
    
    if(tag === "oldUser1"){
        return originPrice * 0.9
    }
    
    if(tag === "oldUser2"){
        return originPrice * 0.85
    }
    
    // ...
}

众所周知, 这样下去,if越写越多,我们可以将它改成对象映射的方式

const priceProcessor = {
    new(originPrice) {
        let newPrice = originPrice - 10
        return newPrice <= 0 ? 0 : newPrice
    },
    oldUser1(originPrice) {
        return originPrice * 0.9
    },
    oldUser2(originPrice) {
        return originPrice * 0.85
    },
    // ...
}

function getPrice(tag, originPrice) {
    return priceProcessor[tag](originPrice)
}

这就是策略模式。品一下定义:

定义一系列的算法,把它们一个个封装起来

priceProcessor对象既是。

使它们可相互替换

“可替换”建立在封装的基础上,只是说这个“替换”的判断过程,咱们不能直接怼 if-else,而要考虑更优的映射方案。return priceProcessor[tag](originPrice)

状态模式

定义:

允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况。

把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。

例(原文):

class SuperMarry {
  constructor() {
    this._currentState = []
    
    // 行为执行代码
    this.states = {
      jump() {console.log('跳跃!')},
      move() {console.log('移动!')},
      shoot() {console.log('射击!')},
      squat() {console.log('蹲下!')}
    }
  }
  // 更改当前动作
  change(arr) {
    this._currentState = arr
    return this
  }
  
  go() {
    console.log('触发动作')
    this._currentState.forEach(T => this.states[T] && this.states[T]())
    return this
  }
}

new SuperMarry()
    .change(['jump', 'shoot'])
    .go()                    // 触发动作  跳跃!  射击!
    .go()                    // 触发动作  跳跃!  射击!
    .change(['squat'])
    .go()                    // 触发动作  蹲下!

感觉和策略模式不同的就是,它会改变自己的内部,比如上面的_currentState会经常变动。

观察者模式(发布-订阅模式)⭐️⭐️⭐

观察者模式有一个“别名”,叫发布 - 订阅模式。但它们又有着细微的差别。

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,
当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。
—— Graphic Design Patterns

代码案例:

发布者

// 定义发布者类
class Publisher {
  constructor() {
    // observers用来接收多个订阅者
    this.observers = []
    console.log('Publisher created')
  }
  // 增加订阅者
  add(observer) {
    console.log('Publisher.add invoked')
    this.observers.push(observer)
  }
  // 移除订阅者
  remove(observer) {
    console.log('Publisher.remove invoked')
    this.observers.forEach((item, i) => {
      if (item === observer) {
        this.observers.splice(i, 1)
      }
    })
  }
  // 通知所有订阅者
  notify() {
    console.log('Publisher.notify invoked')
    // 循环调用订阅者的update方法
    this.observers.forEach((observer) => {
      observer.update(this) // 订阅者必须定义update方法
    })
  }
}

订阅者

// 定义订阅者类
class Observer {
    constructor() {
        console.log('Observer created')
    }

    update() {
        console.log('Observer.update invoked')
    }
}

使用

const Pub = new Publisher()

const A = new Observer()
const B = new Observer()

Pub.add(A)
Pub.add(B)
Pub.notify();

这里例子我直接省略了具体业务,领悟了这个思想就好。

Vue数据双向绑定(响应式系统)的实现原理

官方流程图

Vue双向绑定流程图

在 Vue 中,每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新——这是一个典型的观察者模式

在Vue数据双向绑定的实现逻辑里,有这样三个关键角色:

  • observer(监听器):注意,此 observer 非彼 observer。但在Vue数据双向绑定的角色结构里,所谓的 observer 不仅是一个数据监听器,它还需要对监听到的数据进行转发——也就是说它同时还是一个发布者。
  • watcher(订阅者):observer 把数据转发给了真正的订阅者——watcher对象。watcher 接收到新的数据后,会去更新视图。
  • compile(编译器):MVVM 框架特有的角色,负责对每个节点元素指令进行扫描和解析,指令的数据初始化、订阅者的创建这些“杂活”也归它管~

image

Vue双向绑定核心代码实现

vue双向数据绑定用到了文档碎片documentFragment、Object.defineProperty、proxy及发布订阅模式。这里仅研究下发布订阅模式。

实现observer

首先我们需要实现一个方法,这个方法会对需要监听的数据对象进行遍历、给它的属性加上定制的 getter 和 setter 函数。这样但凡这个对象的某个属性发生了改变,就会触发 setter 函数,进而通知到订阅者。这个 setter 函数,就是我们的监听器:

// observe方法遍历并包装对象属性
function observe(target) {
    // 若target是一个对象,则遍历它
    if(target && typeof target === 'object') {
        Object.keys(target).forEach((key)=> {
            // defineReactive方法会给目标属性装上“监听器”
            defineReactive(target, key, target[key])
        })
    }
}

// 定义defineReactive方法
function defineReactive(target, key, val) {
    // 属性值也可能是object类型,这种情况下需要调用observe进行递归遍历
    observe(val)
    // 为当前属性安装监听器
    Object.defineProperty(target, key, {
         // 可枚举
        enumerable: true,
        // 不可配置
        configurable: false, 
        get: function () {
            return val;
        },
        // 监听器函数
        set: function (value) {
            console.log(`${target}属性的${key}属性从${val}值变成了了${value}`)
            val = value
        }
    });
}

实现订阅者 Dep:

// 定义订阅者类Dep
class Dep {
    constructor() {
        // 初始化订阅队列
        this.subs = []
    }
    
    // 增加订阅者
    addSub(sub) {
        this.subs.push(sub)
    }
    
    // 通知订阅者
    notify() {
        this.subs.forEach((sub)=>{
            sub.update()
        })
    }
}

现在我们可以改写 defineReactive 中的 setter 方法,在监听器里去通知订阅者了:

// 定义defineReactive方法
function defineReactive(target, key, val) {
    const dep = new Dep()
    
    observe(val)
    // 为当前属性安装监听器
    Object.defineProperty(target, key, {
         // 可枚举
        enumerable: true,
        // 不可配置
        configurable: false, 
        get: function () {
            return val;
        },
        // 监听器函数
        set: function (value) {
            // 通知所有订阅者
            dep.notify()
        }
    });
}

这样每一个属性都有了Dep实例,参考流程图,compile收集到需要在视图中出现到的属性,最终Watcher往属性的Dep添加订阅者订阅更新。

实现一个Event Bus/ Event Emitter

Event Bus(Vue、Flutter 等前端框架中有出镜)和 Event Emitter(Node中有出镜)出场的“剧组”不同,但是它们都对应一个共同的角色——全局事件总线
全局事件总线,严格来说不能说是观察者模式,而是发布-订阅模式。

回顾我们在Vue中使用EventBus的过程:

// main.js中
import bus from 'EventBus的文件路径'
Vue.prototype.bus = bus

// 组件中
// 监听某个事件
this.bus.$on('eventName', func)

// 组件中
// 广播某个事件
this.bus.$emit('eventName', params)

大家会发现,整个调用过程中,没有出现具体的发布者和订阅者,全程只有bus这个东西一个人在疯狂刷存在感。这就是全局事件总线的特点——所有事件的发布/订阅操作,必须经由事件中心,禁止一切“私下交易”!

自己实现一个Event Bus

class EventEmitter {
  constructor() {
    // handlers是一个map,用于存储事件与回调之间的对应关系
    this.handlers = {}
  }

  // on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数
  on(eventName, cb) {
    // 先检查一下目标事件名有没有对应的监听函数队列
    if (!this.handlers[eventName]) {
      // 如果没有,那么首先初始化一个监听函数队列
      this.handlers[eventName] = []
    }

    // 把回调函数推入目标事件的监听函数队列里去
    this.handlers[eventName].push(cb)
  }

  // emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数
  emit(eventName, ...args) {
    // 检查目标事件是否有监听函数队列
    if (this.handlers[eventName]) {
      // 这里需要对 this.handlers[eventName] 做一次浅拷贝,主要目的是为了避免通过 once 安装的监听器在移除的过程中出现顺序问题
      const handlers = this.handlers[eventName].slice()
      // 如果有,则逐个调用队列里的回调函数
      handlers.forEach((callback) => {
        callback(...args)
      })
    }
  }

  // 移除某个事件回调队列里的指定回调函数
  off(eventName, cb) {
    const callbacks = this.handlers[eventName]
    const index = callbacks.indexOf(cb)
    if (index !== -1) {
      callbacks.splice(index, 1)
    }
  }

  // 为事件注册单次监听器
  once(eventName, cb) {
    // 对回调函数进行包装,使其执行完毕自动被移除
    const wrapper = (...args) => {
      cb(...args)
      this.off(eventName, wrapper)
    }
    this.on(eventName, wrapper)
  }
}

使用

const bus = new EventEmitter()
bus.on("listDataChange", requestList)
bus.on("listDataChange", requestLogsList)

function requestList(pageNum=0){
    console.log(`开始请求业务数据,页码:${pageNum}`)
}
function requestLogsList(pageNum=0){
    console.log(`开始请求历史记录数据,页码:${pageNum}`)
}

function refresh(){
    console.log("刷新数据")
    bus.emit("listDataChange")
}
function changePage(pageNum=0){
    console.log("翻页")
    bus.emit("listDataChange", pageNum)
}

request()
refresh()
changePage(1)
changePage(2)

阅读推荐 FaceBook推出的通用EventEmiiter库的源码

观察者模式与发布-订阅模式的区别是什么?

发布-订阅模式中:

发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作。

他们之间的区别就在于是否存在第三方、发布者能否直接感知订阅者(如图所示)。

image

image

EventBus就属于发布-订阅模式。

在实际开发中,我们的模块解耦诉求并非总是需要它们完全解耦。
如果两个模块之间本身存在关联,且这种关联是稳定的、必要的,那么我们使用观察者模式就足够了。
而在模块与模块之间独立性较强、且没有必要单纯为了数据通信而强行为两者制造依赖的情况下,我们往往会倾向于使用发布-订阅模式。