vue源码分析(九)

837 阅读6分钟

三、派发更新

当执行响应式对象的set的时候,会触发派发更新的逻辑,首先他会尝试拿到value,如果value和传入的newVal的值相同,那么他会结束执行。之后会执行到val = newVal进行赋值,然后会判断newVal是否是一个对象,如果是对象那么会执行observe把这个对象变成响应式。最后会触发 dep.notify()进行派发更新

   set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      ...
      // #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()
    }

dep.notify函数首先拿到subs,然后会遍历subs,执行subs[i].update()实际上就是执行和subs绑定的watcher的update(),wathcer的update方法会执行到queueWatcher(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()
    }
  }

queueWatcher方法会先拿到watcher的id,watcher的id 是执行this.id = ++uid得到的,这个id是一个数字的自增,所以每一个wathcher都有一个唯一的id。has在最开始的时候是一个空的对象,拿到watcher的id,他会把has中这个id的key设置为true,如果当前的has没有这个id,那么,才会执行下边的逻辑,这样做的目的是,假如你修改了同一个渲染wathcer中的数据,他会执行多次的queueWatcher,会往之后的执行队列中放入重复的watcher和更新,这样做避免了添加重复的watcher,即使修改了同一个渲染watcher的数据,那么在之后的flushSchedulerQueue当中,也只会执行一次重新渲染,相当于是多次数据操作合并为了一次更新。之后会执行queue.push(watcher)把当前的wathcer,push到queue数组当中,最后执行nextTick(flushSchedulerQueue)

// src/core/observer/scheduler.js
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)
    }
  }
}

nextTick(flushSchedulerQueue),其中的nextTick是把flushSchedulerQueue放到了下一个队列当中执行。flushSchedulerQueue首先调用queue.sort((a, b) => a.id - b.id)对queue队列根据id做一个排序,wathcer的id是先创建的id小,之后创建的id大,也就是父组件的watcher是要小于子组件的id的。为什么要做一个排序,1.组件的更新是由父到子的过程。2.组件的user watcher会在渲染watcher创建之前先创建。3.如果父组件执行了销毁,那么子组件也会被销毁,他所做的数据处理将会被跳过。排序之后会遍历queue,首先判断watcher是否有before这个方法,渲染watcher在创建的时候,会传入before这个函数, if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') }实际上是会去触发他的beforeUpdate生命周期。之后执行has[id] = null,把has中的watcher id的保留置空,然后会执行watcher.run()watcher.run()方法首先会调用const value = this.get()对新的value进行求值,在调用this.get()的过程当中,就会再次触发wathcer的getter,也就是之前传入的 updateComponent = () => { vm._update(vm._render(), hydrating) }函数,对页面进行重新渲染。执行完run之后会调用resetSchedulerState对has进行重置,执行到flushSchedulerQueue的最后会调用callUpdatedHooks(updatedQueue),实际上是对queue队列中的wathcer去调用他们的update生命周期,执行顺序是由后向前,也就是会先触发子组件的updated然后再去触发父组件的updated函数。

// src/core/observer/scheduler.js
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.
    ...
  }

  ...

  resetSchedulerState()

  // call component updated and activated hooks
  ...
  callUpdatedHooks(updatedQueue)

  ...
}
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')
    }
  }
}
// src/core/observer/watcher.js
 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
        ...
      }
    }
  }

四、nextTick

nextTick的是在执行flushSchedulerQueue的时候进行的一层处理,平时在开发中使用的this.$nextTick()和Vue.nextTick(),都是由nextTick函数实现。nextTick函数首先会定义_resolve,然后push我们传入的内容,之后会判断pending,然后把pending的值进行修改这样就保证了, 只会执行一次timerFunc

// src/core/util/next-tick.js
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
    .push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

timerFunc的目的是把当前放入的任务,放到下一个执行队列去执行,其中做了大量的降级判断和处理,首先他会判断if (typeof Promise !== 'undefined' && isNative(Promise)) 当前环境是否支持Promise,如果支持那么会通过promise.then(),来实现异步队列(promise.then是一个微任务),如果不支持promsie的话,他会判断当前环境是否支持MutationObserverMutationObserver函数提供了监视对DOM树所做更改的能力,他也是一个微任务,如果支持的话,他会首先创建一个变量let counter = 1,然后执行 const observer = new MutationObserver(flushCallbacks),把flushCallbacks作为监听的回调函数,之后创建一个文本节点,通过observe方法去监听这个文本节点,其中他设置了characterData: true这个属性是用于监听这个元素的文本是否被修改,默认是flase,在每一次触发timerFunc的时候,对文本节点的内容进行修改, counter = (counter + 1) % 2; textNode.data = String(counter),这样去模的操作,会保证这个文本节点的内容只会在1和0之间修改,不会造成最终这个文本节点的内容溢出。这样通过修改文本节点的内容,来触发MutationObserverobserve方法,再去触发对应的flushCallbacks回调函数。如果不支持MutationObserver的话他会采取setImmediate,如果不支持setImmediate函数,那么他会最终采用,setTimeout(flushCallbacks, 0)作为下一个任务队列,这两个方式都是下一个队列当中的宏任务。也就是说vue的渲染实际上是异步的,他会通过nextTick中的微任务或者宏任务的方式,把他的渲染放到之后的队列当中,这样就保证了,执行了多个数据的操作,但是会在下一个队列做一个合并的操作

let timerFunc

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

五、set

在vue当中,我们给data中的数据进行修改,如果是一个对象,我们给他直接添加属性,或者我们在修改数组的时候,我们这样赋值arr[2]=xxx,这两种操作对于vue都是监听不到的,在页面中不会进行重新重新渲染,vue官方也提供了set这个api来帮助我们进行操作。set第第一个参数可以传递一个数组或者一个对象,第二个参数传入的是key,第三个参数传的是value。如果传入的值是一个数组,首先他会让target(当前数组)的length等于当前最大的index,然后会调用当前数组的splice方法对内容进行一个插入。如果是一个对象的话,那么他会首先判断这个key是否存在于当前的对象当中, 如果存在的话那么他会直接返回,不需要进行重新的渲染。如果当前的key不存在于当前的对象当中,也就是说他是一个新的key,那么他会尝试拿到当前对象的__ob__属性,然后他会判断当前的对象是否是根部的data,如果是的话,他会报一个警告。如果!ob也就是说不是一个响应式对象,也就是说给一个普通对象进行,set操作,那么他会往这个普通对象当中去进行一个赋值。如果该对象是一个响应式对象,那么他会首先执行defineReactive(ob.value, key, val),把新添加的key,变为一个响应式对象,最后手动调用ob.dep.notify(),调用ob.dep.notify()之后会触发页面的重新渲染,他会在渲染的时候执行响应式对象的时候let childOb = !shallow && observe(val),首先定义一个childOb,把其中的对象定义下来,然后通过childOb.dep.depend()进行对象的依赖收集,这样调用ob.dep.notify()就会通知到对应的渲染watcher进行重新渲染。

// src/core/observer/index.js
/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
export function set (target: Array<any> | Object, key: any, val: any): any {
  ...
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

如果是一个数组,他会调用数组的splice方法。在创建new Observer的时候,如果传入的是一个数组,他会调用protoAugmentcopyAugment方法。protoAugment方法他会传入当前的数组和arrayMethods,arrayMethods实际上是Object.create(Array.prototype),也就是拿到了Array的原型对象。protoAugment做的事是target.__proto__ = src也就是让当前数组的原型指向了src,也就是形成了,当前数组的原型指向一个空的对象,这个空的对象的原型指向了Array.prototype

 if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    }
    /**
 * Augment a target Object or Array by intercepting
 * the prototype chain using __proto__
 */
function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}
...

在初始化的时候,还会对arrayMethods进行一个补充,methodsToPatch中定义了一些数组的方法,之后对这些方法数组进行了遍历,首先定义const original = arrayProto[method],然后通过def函数,对数据进行劫持,其中的value就是mutatormutator函数首先通过original.apply(this, args)借带了Array.prototype上对应的方法,拿到result,在最后把result,return出去。返回result的过程中,会先定义inserted插入的值,如果有插入的值的话,会通过ob.observeArray绑定响应式,最后调用ob.dep.notify()进行重新渲染。无论是set还是调用了push,splice这些方法,最终都是由vue去触发了ob.dep.notify()进行重新的渲染。所以对data中的数据如果直接使用Array.prototype[methods]修改,也是不会动态的渲染到页面中。

// src/core/observer/array.js
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})