引言

发布订阅者模式是最常见的模式之一,它是一种一对多的对应关系,当一个对象发生变化时会通知依赖他的对象,接受到通知的对象会根据情况执行自己的行为。

假设有财经报纸送报员financialDep,有报纸阅读爱好者a,b,c,那么a,b,c想订报纸就告诉financialDep,financialDep依次记录a,b,c这三个人的家庭地址,次日,送报员一大早把报纸送到a,b,c家门口的邮箱中,a,b,c收到报纸后都会认认真真的打开阅读。随着时间的推移,会有以下几种场景:

  • 有新的订阅者加入: 有一天d也想订报纸了,那么找到financialDep,financialDep把d的家庭地址记录到a,b,c的后面,次日,为a,b,c,d分别送报纸。
  • 有订阅者退出了:有一天a要去旅游了,提前给送报员financialDep打电话取消了订阅,如果不取消的话,积攒的报纸就会溢出小邮箱。
  • 有新的报社开业:有一天镇子又开了家体育类的报馆,送报员是sportDep,b和d也是球类爱好者,于是在sportDep那里做了登记,sportDep的记录中就有了b和d。
    从上面的例子中可以看出,刚开始送报员financialDep的记录中有a,b和c,先是d加进来后来是a离开,最终financialDep的记录中有b,c和d。体育类报馆开张的时候,b和d也订阅了报纸,sportDep的记录中就有了b和d。我们发现,c只订阅了财经类报刊,而b和d既订阅了财经类的报纸也定了财经类的报刊。

一、发布订阅者模式的特点

从以上例子可以发现特点:

  • 发布者可以支持订阅者的加入
  • 发布者可以支持订阅者的删除
  • 一个发布者可以有多个订阅者,一个订阅者也可以订阅多个发布者的消息那可能会有疑问,有没有可能会有发布者的删除,答案是会,但是此时,发布者已消失,订阅者再也不会收到消息,也就不会与当前发布者相关的消息诱发的行为。好比体育类报馆关停了(发布者删除)那么b和d在也不会收到体育类报纸(消息),也就不会再阅读体育类报纸(行为)。

二、vue中的发布订阅者模式

以上的例子基本就是vue中发布订阅者的大体概况,vue中的发布者是啥时候定义的?
new Vue实例化的过程中会执行this._init的初始化方法,_init方法中有方法initState

export function initState (vm: Component) {
  // ...
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  //...
}

首先看initData对于data的初始化:

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

这里首先获取data,如果data是函数又会执行getData方法。然后,获取methodsprops中的key值,如果已经定义过则在开发环境进行控制台警告。其中,proxy的目的是让访问this[key]相当于访问this._data[key]。最后,对数据进行响应式处理 observe(data, true /* asRootData */)

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

如果不是对象或者当前值是VNode的实例直接返回。如果当前当前值上有属性__ob__并且value.__ob__Observer的实例,那么说明该值已经被响应式处理过,直接将value.__ob__赋值给ob并在最后返回即可。如果满足else if中的条件,则可执行ob = new Observer(value):

/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 */
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

Observer是构造函数,通过对value是否是数组的判断,分别执行observeArraywalkobserveArray会对数组中的元素执行observe(items[i]),即通过递归的方式对value树进行深度遍历,递归的最后都会执行到walk方法。再看walk中的defineReactive(obj, keys[i])方法:

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

这里就是vue响应式原理、watcher订阅者收集、数据变化时发布者dep通知subs中订阅者watcher进行相应操作的主要流程,new Dep()实例化、Object.defineProperty方法、dep.depend()订阅者收集和dep.notify()是主要的功能。先看发布者Dep的实例化:

1、dep

import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'
let uid = 0
/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  constructor () {
    this.id = uid++
    this.subs = []
  }
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

这里的dep就相当于财经或者体育报馆,其中定义了属性idsubs,subs相当于送报员financialDep手中的笔记本,用来是用来记录订阅者的数组。发布者的消息如何发给订阅者,就需要借助Object.defineProperty:

2、Object.defineProperty

对于一个对象的属性进行访问或者设置的时候可以为其设置getset方法,在其中进行相应的操作,这也是vue响应式原理的本质,也是IE低版本浏览器不支持vue框架的原因,因为IE低版本浏览器不支持Object.defineProperty方法。

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 当访问属性的时候,进行订阅者的收集
    },
    set: function reactiveSetter (newVal) {
      // 当修改属性的时候,收到发布者消息的时候进行相应的操作
    }
  })

在vue中订阅者有computer watcher计算属性、watch watcher侦听器和render watcher渲染watcher。这里先介绍渲染watcher:

3、watcher

let uid = 0
/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
  // watcher还有很多其他自定义方法,用的时候再列举
}

Watcher实例化的最后会执行this.value = this.lazy ? undefined : this.get()方法,默认this.lazy=false,满足条件执行Watcher实例的回调this.get()方法。 pushTarget(this)定义在dep.js文件中,为全局targetStack中推入当前订阅者,是一种栈的组织方式。Dep.target = target表示当前订阅者是正在计算中的订阅者,全局同一时间点有且只有一个。 然后执行value = this.getter.call(vm, vm),这里的this.getter就是

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

进行当前vue实例的渲染,在渲染过程中会创建vNode,进而访问数据data中的属性,进入到get方法中,触发dep.depend()

4、dep.depend

dep.depend()是在访问obj[key]的时候进行执行的,在渲染过程中Dep.target就是渲染watcher,条件满足,执行Dep.target.addDep(this),即执行watcher中的

    /**
   * Add a dependency to this directive.
   */
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

newDepIdsdepIds分别表示当前订阅者依赖的当前发布者和旧发布者idSet集合,newDeps表示当前发布者实例的数组列表。首次渲染时this.newDepIds中不包含idthis.newDepIds添加了发布者的idthis.newDeps中添加了dep实例。同时,this.depIds中不包含id,继而执行到dep.addSub(this)

addSub (sub: Watcher) {
    this.subs.push(sub)
}

这个动作就表示订阅者watcher订阅了发布者dep发布的消息,当前发布者的subs数组中订阅者数量+1,等下次数据变化时发布者就通过dep.notify()的方式进行消息通知。

5、dep.notify

notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
}

const subs = this.subs.slice()对订阅者进行浅拷贝,subs.sort((a, b) => a.id - b.id)按照订阅者的id进行排序,最后循环订阅者,订阅者触发update方法:

/**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

this.dirty表示计算属性,这里是falsethis.sync表示同步,这里是false,最后会走到queueWatcher(this):

/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

这里未刷新状态flushing === false时会在队列queue中推入订阅者watcher,如果没有在等待状态waiting===false时执行nextTickflushSchedulerQueue的执行推入异步队列中,等待所有的同步操作执行完毕再去按照次序执行异步的flushSchedulerQueue。需要了解nextTick原理请移步:https://www.atool.online/article/261842.htm

/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id
  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  queue.sort((a, b) => a.id - b.id)
  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }
  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()
  resetSchedulerState()
  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)
  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}
function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}

这里主要做了四件事:

  • 对队列queue进行排序
  • 遍历执行watcherrun方法
  • resetSchedulerState进行重置,清空queue,并且waiting = flushing = false进行状态重置
  • callUpdatedHooks执行callHook(vm, 'updated')生命周期钩子函数 这里的run是在Watcher的时候定义的:
/**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

active默认为true,执行到const value = this.get()就开始了数据变化后的渲染的操作,好比订阅者收到报纸后认真读报一样。get方法中,value = this.getter.call(vm, vm)渲染执行完以后,会通过popTargettargetStack栈顶的元素移除,并且通过Dep.target = targetStack[targetStack.length - 1]修改当前执行的元素。最后执行this.cleanupDeps:

6、订阅者取消订阅

 /**
   * Clean up for dependency collection.
   */
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

首先通过while的方式循环旧的this.deps发布者的数组,如果当前订阅者所依赖的发布者this.newDepIds中没有包含旧的发布者,那么,就让发布者在this.subs中移除订阅者,这样就不会让发布者dep进行额外的通知,这种额外的通知可能会引起未订阅者的行为(可能消耗内存资源或引起不必要的计算)。后面的逻辑就是让新旧发布者iddep进行交换,方便下次发布者发布消息后的清除操作。

小结

vue中的发布订阅者是在借助Object.defineProperty将数据变成响应式的过程中定义了dep,在get过程中dep对于订阅者的加入进行处理,在set修改数据的过程中dep通知订阅者进行相应的操作。

以上就是vue2从数据变化到视图变化发布订阅模式详解的详细内容,更多关于vue2数据视图变化发布订阅模式的资料请关注阿兔在线工具其它相关文章!

点赞(0)

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部