vue源码之观察者模式

Posted mrzhu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue源码之观察者模式相关的知识,希望对你有一定的参考价值。

在vue进行初始化的时候,会执行到initState方法(在core/instance/state.js中),其中initState方法会执行data的初始化,在data的初始化的时候会执行observe监听

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm) // 执行initData
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
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:
 +
      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 */) // 执行监听
}

然后会到观察者模式中第一个重要文件 添加监听文件coreobserverindex.js,里面的核心代码是

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()
    }
  })
if (Dep.target) {
        dep.depend() 
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }

在这里 通过判断Dep.target是否存在,如果存在则执行 dep.depend()方法,这到了观察者模式的第二个文件 dep发布者,这在coreobserverdep.js中,dep类存在一个subs维护着多个监听者,同时存在一个静态属性target,这表示在同一时间只会存在一个target,

然后我们来看depend方法,他是执行了target的addDep方法,所以我们梳理一下,如果我们希望执行target的addDep方法可以有效果,我们需要在执行前 给Dep.target绑定一个对象,同时该对象需要实现addDep方法。

现在监听器和发布者看完了,我们继续向下看,因为在前面我们分析过,扩展模块需要实现$mount方法,然后vue初始化的时候会执行扩展模块的$mount,现在我们看到web扩展模块的$mount方法中执行了mountComponent

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

然后我们看看mountComponent中执行了什么,这在coreinstancelifecycle.js中,mountComponent方法中有这么一句:

new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, beforeUpdate)
      }
    }
  }, true /* isRenderWatcher */)

我们终于看到了观察者Watcher,这是一句初始化,我们到观察者里面看看,在观察者中我们看到三句话:

this.getter = expOrFn
this.value = this.lazy
      ? undefined
      : this.get()
value = this.getter.call(vm, vm)

 所以最后是执行了expOrFn方法,这是Watcher的第二个参数,即updateComponent方法,我们来看看updateComponent方法:

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

实际上是执行的_update,而_update我们如果看前面的文章就知道他在lifecycleMixin的时候就会初始化了,其中_update的第一个方法是vm._render(),该方法在renderMixin执行的时候也已经被初始化了,在该方法中最终会执行createElement,其中执行createElement的时候 会传入data参数,而且data参数会被调用,这里是一个关键,因为我们知道在第一步的时候我们已经针对data设置了监听器,所以在createElement方法里面调用data的时候,本质上会触发每个data的监听器,所以会触发该data的get方法:

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
    },

但是这里有一个关键,需要判断Dep.target是否存在,还记得前面我们初始化Watcher吗,我们看看Watcher初始化里面有什么:

pushTarget(this)

 这句话在初始化的时候会被调用,我们再找到这个方法:

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

  可以看到Watcher把自身给赋值给了Dep.taeget,所以这就通了,

 

在初始化的时候,首先vue会给data加上监听器,即针对每一个data都对应的创建一个Dep发布者对象,然后在该data的get方法里面,会根据Dep.target对象来判断当前观察者,如果存在观察者,则将该观察者给收集起来,这是通过观察者对象将自身赋予Dep.target实现的,在执行mount的方法时候,即开始实例化观察者Watcher,在实例化观察者Watcher的时候,观察者会做两件事情:1 将自身赋予Dep.target,即告诉发布者,我是在这一时刻的观察者,2 观察者会立刻执行传入的一个方法,而vue通过该方法去触发指定data的get方法(只需要调用该data即可实现),然后在data的get方法里面,该data对应的发布者dep会完成依赖收集。同时observe也给该data添加了set方法,我们看看set方法:

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()
    }

set方法会执行发布者的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()
    }
  }

我们看到这里会触发每一个观察者的update方法

update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

看到这里如果是同不的话直接执行run方法,否则添加到观察者队列中,但是最终观察者队列在执行的时候还是要执行run方法,我们看run方法:

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)
        }
      }
    }
  }

run方法最终会执行this.get方法,这会触发实例化该观察者实例的第二个参数,我们可以在这个参数传入当data改变的时候希望触发的方法,同时我们看到也会触发cb方法,这是vue watch的实现所需要用到的,接下来我们看vue watch的实现:

还记得最开始我们说initState方法吗,他初始化了data,同时也初始化了watch

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch) // 初始化watch
  }
}

在initWatch中我们看到,vue遍历watch的每一个属性,然后分别添加监听:

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

 我们再看createWatcher,这里传入了vm实例,watch的key 和watch key对应的方法,然后会调用$watch

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options) // 又初始化Watcher了
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

 一般我们在写watch的时候 key写的是data字段或者props字段,如果data是一个对象的时候我们一般会按照点语法来写,如:“a.b.c”, 这个时候Watcher是如何监听到他的变化的呢?我们回到Watcher文件里看看:

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
        )
      }
    }

 在watcher初始化方法中,这个时候对第二个参数做了判断,如果是函数则直接赋给getter,如果不是则使用parsePath转换一下:我们看看parsePath:

export function parsePath (path: string): any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split(‘.‘)
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

  可以看到这个方法会将我们的点语法字符串给拆解成一个对象访问方法,即将“a.b.c” 编程a.b.c,这个时候会触发c的get方法,然后即可完成添加监听

 再一下watcher队列的实现,之前我们看到触发update方法时候,会判断如果是同步则直接执行run方法,否则添加到watcher队列中,即queueWatcher方法:

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)
    }
  }
}

我们看到将watcher实例添加到watcher队列的时候会先进行判断,如果has[id]是null 表示之前没有添加过这个watcher实例,则开始添加,添加的时候开始判断 如果下一批次任务还没有开始 增直接push进去,否则则按照id大小进行排序放进去,这里涉及到watcher的id,他是一个从0开始起的一个数字,以后每次实例化watcher的时候都会加一,这表示每一个watcher的先后顺序,所以在已经开始进行微任务处理的时候,这个时候因为要开始进行watcher队列批次处理了,所以肯定要根据watcher的先后顺序来执行,这就是id的作用,因为要根据先后顺序来执行 所以肯定要先排序,如果在排序后,然后不断执行的过程中,有一个新的watcher进来了怎么办,当然是按照顺序放进去了,我们在来看nextTick,nextTick内部维护一个方法数组,负责收集所有的使用nextTick注册的方法,然后在下一次微任务执行的时候会逐个执行,所以我们看到flushSchedulerQueue方法会在每次微任务中被执行,然后flushSchedulerQueue方法会批量执行多个watcher,然后调用resetSchedulerState将wait重新设置为false,这样在下次watcher准备添加进队列的时候又可以执行nextTick注册了。同时因为每次微任务执行之间有微小的时间间隙,同时在flushSchedulerQueue执行的时候也会存在一个时间长度,在这个时间内,如果有新的watcher进来,则必须进行按顺序插入,因为队列已经开始在执行了。

 

以上是关于vue源码之观察者模式的主要内容,如果未能解决你的问题,请参考以下文章

Android源码与设计模式之notifyDataSetChanged()方法与观察者模式

设计模式之观察者模式2

vuejs源码用了啥设计模式,具体点的

观察者模式在Android开发场景中运用之通过Java源码分析

设计模式之单例模式

设计模式之观察者模式与访问者模式详解和应用