当前位置 : 主页 > 网络编程 > JavaScript >

vue2从数据变化到视图变化发布订阅模式详解

来源:互联网 收集:自由互联 发布时间:2023-02-08
目录 引言 一、发布订阅者模式的特点 二、vue中的发布订阅者模式 1、dep 2、Object.defineProperty 3、watcher 4、dep.depend 5、dep.notify 6、订阅者取消订阅 小结 引言 发布订阅者模式是最常见的模
目录
  • 引言
  • 一、发布订阅者模式的特点
  • 二、vue中的发布订阅者模式
    • 1、dep
    • 2、Object.defineProperty
    • 3、watcher
    • 4、dep.depend
    • 5、dep.notify
    • 6、订阅者取消订阅
  • 小结

    引言

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

    假设有财经报纸送报员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.jb51.net/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数据视图变化发布订阅模式的资料请关注易盾网络其它相关文章!

    上一篇:Vue 入口与 initGlobalAPI实例剖析
    下一篇:没有了
    网友评论