了解发布订阅模式之后,vue响应式原理竟如此简单!

4,046 阅读4分钟

哈喽~~~

最近在复习一些常见的设计模式,联想到vue的响应式原理(2.x),决定整理一篇浅显易懂的文章阐述其核心思想。

ok,让我们开始。

基本概念

发布订阅模式基于一个主题事件通道,接收通知的对象可以通过自定义的事件订阅主题,对象会在主题事件发布时收到通知。

说到发布订阅模式,不得不提观察者模式,两者都是维护事件与依赖该事件的对象之间的关系,在有关状态发生变更时会执行相应的更新,所以也经常有人将两者混为一谈,其实两者的差异也十分明显,在发布订阅模式中事件统一由调度中心处理,且可以基于不同的主题去执行不同的事件,实现了完全的解耦,更加灵活多变

一个最简单的发布订阅模式示例

let pubSub = {
  subscribers: {},
  subscribe(key, fn) {
    if (!this.subscribers[key]) this.subscribers[key] = [];
    this.subscribers[key].push(fn);
  },
  unSubscribe(key) {
    delete this.subscribers[key];
  },
  publish(key, ...args) {
    let listeners = this.subscribers[key];
    listeners.forEach(fn => fn(...args)); // 通知所有订阅者   
  }
};
/* 为简化代码,省去了一些错误边界的处理,调试:
pubSub.subscribe('event', () => {
  console.log('first event');
});
pubSub.subscribe('event', () => {
  console.log('second event');
});
pubSub.publish('event'); // first event second event */

看到这你大概明白了,发布订阅模式就是通过在事件调度中心(subscribers)添加订阅者的事件(subscribe),等到发布事件(publish)时执行相应的事件就可以了,接下来,我们看看vue是如何一步步实现响应式系统。

vue响应式原理的实现

data

👆是vue官网下载的一张图,用来描述vue是如何通过收集的依赖,在data变更的时候触发通知变更的过程。现在看上去还是有点抽象,下面我们从源码的角度梳理主要的几个步骤对其进行一些拆分。

  1. init阶段,首先简单了解一下Object.defineProperty(obj, prop, descriptor),descriptor 有一些属性,其中get和set可以自定义属性的 Setters 和 Getters使普通对象变成可观察的对象,在此阶段vue中data的所有属性会被reactive化
// vue/src/core/observer/index.js
const dep = new Dep()  // 实例化依赖管理器 ---- 下面会介绍
function defineReactive (obj,key,val) { // 将对象转化成可观察对象
  if (arguments.length === 2) {
    val = obj[key]
  }
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      dep.depend() // 每个data的属性都会有一个dep对象,用来进行收集依赖
      return value
    },
    set: function reactiveSetter (newVal) {
      if(val === newVal){
         return;
      }
      val = newVal;
      dep.notify() // 通知依赖更新
    }
  })
}

// vue/src/core/observer/dep.js
class Dep { // 订阅者Dep 用来存放Watcher观察者对象 也被称为依赖管理器 
      static target: ?Watcher;
      subs: Array<Watcher>;
      depend () {
        if (Dep.target) {
          Dep.target.addDep(this) // 添加一个Watcher对象
        }
      }
      addSub (sub) {
        this.subs.push(sub)
      }
      notify () {
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update() // 通知所有Watcher对象更新视图
        }
      }
}
  1. mount阶段,我们把Watcher放到这里说明,因为在此阶段会创建一个Watcher类的对象,Watcher就是观察者,它负责订阅Dep,每个组件实例都对应一个Watcher实例,依赖收集在此阶段完成
// vue/src/core/observer/watcher.js (以下为简化的代码片段)
export default class Watcher {
  constructor (vm,expOrFn,cb) {
    this.vm = vm;
    this.cb = cb;
    this.getter = parsePath(expOrFn) // 这个函数会调用组件的render函数来更新虚拟DOM
    this.value = this.get()
  }
  get () {
    Dep.target = this // 将当前的Watcher赋值给了Dep.target
    const vm = this.vm
    let value = this.getter.call(vm, vm) // 调用组件的更新函数
    Dep.target = undefined
    return value
  }
  addDep (dep) {
    dep.addSub(this) // 将Watcher对象注册到该属性的Dep中 --- 依赖收集
  }
  update () { // 接收到变更时执行更新
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
}
  1. update阶段,当data中属性发生改变的时候,会调用Dep的notify函数,然后通知所有的Watcher调用update函数渲染组件,也就是派发更新阶段

拆分完成,如果还有不清楚的地方,那下面这张图形象的解释了Watcher、Dep和Observer三者之间的关系,结合之前对源码的分析,可以看到Observe扮演的角色是发布者,对对象的每一个属性进行数据监听,Watcher是连接组件和data的桥梁,Dep则扮演是一个收集依赖和管理通知更新的调度中心

(Tips: 笔者认为弄明白这几个概念对于理解响应式原理非常重要,在此基础上阅读源码效果也会好的多。)

总结

本文介绍了发布订阅模式的原理和简单实现,并根据流程梳理简述了vue响应式原理的核心思想,文章不长,希望对你有所帮助。

时间仓促,文中肯定会有一些不够严谨的地方,欢迎大家指正和讨论。

最后,感谢阅读!