【Vue源码】第十七节深入响应式原理之订阅模式(派发更新)

setter

当我们修改数据之后,会触发数据的setter方法,然后更新视图,从setter下手:

Object.defineProperty(obj, key, {
    
    
    enumerable: true,
    configurable: true,
    // ...
    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()
      }
      if (setter) {
    
    
        setter.call(obj, newVal)
      } else {
    
    
        val = newVal
      }
      // 如果shallow是false,将newVal设置为响应式
      childOb = !shallow && observe(newVal)
      // 通知订阅者,分析notify
      dep.notify()
    }
  })

可见派发更新离不开Dep上的notify函数。在这里,它会遍历所有的 subs,也就是 Watcher 的实例数组,把在依赖过程中订阅的的所有观察者,也就是 watcher,都触发它们的 update过程

notify

class Dep {
    
    
  // ...
  notify () {
    
    
  // 遍历所有的 subs,也就是 Watcher 的实例数组,然后调用每一个 watcher 的 update 方法
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
    
    
      // 分析update
      subs[i].update()
    }
  }
}

watcher有很多中类型,有computed watchersync watcher、用户自定义watcher(即watch)、deep watcher 和_render时的render watcher。在update时,需要考虑computed watchersync watcher。我们这次是以render watcher为例子,所以直接分析queueWatcher:

class Watcher {
    
    
  // ...
  update () {
    
    
    // 先不讨论computed和sync的情况
    if (this.computed) {
    
    
      // ...
    } else if (this.sync) {
    
    
      // ...
    } else {
    
    
      // 分析queueWatcher
      queueWatcher(this)
    }
  }
} 
const queue: Array<Watcher> = []
let has: {
    
     [key: number]: ?true } = {
    
    }
let waiting = false
let flushing = false
/**
 * 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
  // 首先用 has 对象保证同一个 Watcher 只添加一次
  if (has[id] == null) {
    
    
    has[id] = true
    // if else中都是新增watcher,判断条件是flushing
    // render watcher一般下会走queue.push(watcher)
    // 而在后期更新执行flushSchedulerQueue中会执行到watcher.run函数,
    // 会重新新增watcher,而触发queueWatcher函数,此时flushing是ture.
    // 这时候就会执行else语句,
    // 找到第一个待插入 watcher 的 id 比当前队列中 watcher 的 id 大的位置。把 watcher 按照 id的插入到队列中
    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)
    }
    // 最后通过 waiting 保证对 nextTick(flushSchedulerQueue) 的调用逻辑只有一次
    if (!waiting) {
    
    
      waiting = true
      // 分析flushSchedulerQueue
      nextTick(flushSchedulerQueue)
    }
  }
}

flushSchedulerQueue

flushSchedulerQueue中主要执行了三个步骤:

  • 队列排序;
  • 队列遍历;
  • 状态恢复。
let flushing = false
let index = 0
/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
    
    
  flushing = true
  let watcher, id

  // 对队列做了从小到大的排序
  // 组件的更新由父到子;因为父组件的创建过程是先于子的,所以 watcher 的创建也是先父后子,执行顺序也应该保持先父后子
  // 用户的自定义 watcher 要优先于渲染 watcher (_render时执行的new Watcher)执行;因为用户自定义 watcher 是在渲染 watcher 之前创建的。
  // 如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行
  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
    // 在这里的时候由于执行了run函数,可能会再次添加新的watcher
    // 所以会再次调用queueWatcher函数,虽然has[id] === null;
    // 但是此时flushing是true,所以会执行else语句
    // 从后往前找,找到第一个待插入 watcher 的 id 比当前队列中 watcher 的 id 大的位置。
    // 把 watcher 按照 id的插入到队列中,因此 queue 的长度发生了变化。
    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()
 
  // 就是把这些控制流程状态的一些变量恢复到初始值,如waiting = flushing
  // 把 watcher 队列清空
  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    
    
    devtools.emit('flush')
  }
}

我们啃一下run函数:

class Watcher {
    
    
  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    
    
    if (this.active) {
    
    
      this.getAndInvoke(this.cb)
    }
  }

  getAndInvoke (cb: Function) {
    
    
    // 先通过 this.get() 得到它当前的值
    const value = this.get()
    if (
      // 新旧值不等
      value !== this.value ||
      // 新值是对象类型
      isObject(value) ||
      // 或者deep是true
      this.deep
    ) {
    
    
      // 获得旧的值
      const oldValue = this.value
      // 新的值
      this.value = value
      this.dirty = false
      // 如果时自定义添加的watch
      if (this.user) {
    
    
        try {
    
    
          cb.call(this.vm, value, oldValue)
        } catch (e) {
    
    
          handleError(e, this.vm, `callback for watcher "${
      
      this.expression}"`)
        }
      } else {
    
    
        cb.call(this.vm, value, oldValue)
      }
    }
  }
}

这个过程又利用了队列做了进一步优化,在 nextTick 后执行所有 watcherrun,最后执行它们的回调函数

nextTick

在学习nextTick之前要学习一下JS的运行机制。

JS 执行是单线程的,它是基于事件循环的。事件循环大致分为以下几个步骤:

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要清空所有的 micro task。遍历每个macro task时,执行macro task并遍历执行macro task中的 micro task。

在浏览器环境中,常见的 macro task 有

  • setTimeout、
  • MessageChannel、
  • postMessage、
  • setImmediate;

常见的 micro task 有

  • MutationObsever
  • Promise.then。
import {
    
     noop } from 'shared/util'
import {
    
     handleError } from './error'
import {
    
     isIOS, isNative } from './env'

const callbacks = []
let pending = false

function flushCallbacks () {
    
    
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    
    
    copies[i]()
  }
}

let microTimerFunc    // 微观函数
let macroTimerFunc    // 宏观函数
let useMacroTask = false  // 是否是宏观函数

// 优先检测是否支持原生 setImmediate
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    
    
  macroTimerFunc = () => {
    
    
    setImmediate(flushCallbacks)
  }
// 检测是否支持原生的 MessageChannel
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
    
    
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    
    
    port.postMessage(1)
  }
} else {
    
    
  // 如果也不支持的话就会降级为 setTimeout 0
  macroTimerFunc = () => {
    
    
    setTimeout(flushCallbacks, 0)
  }
}

// 检测浏览器是否原生支持 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
    
    
  const p = Promise.resolve()
  microTimerFunc = () => {
    
    
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
} else {
    
    
  // 不支持的话直接指向 macro task 的实现
  microTimerFunc = macroTimerFunc
}

/**
 * Wrap a function so that if any code inside triggers state change,
 * the changes are queued using a (macro) task instead of a microtask.
 * next-tick.js 还对外暴露了 withMacroTask 函数,
 * 它是对函数做一层包装,确保函数执行过程中对数据任意的修改,
 * 触发变化执行 nextTick 的时候强制走 macroTimerFunc。
 * 比如对于一些 DOM 交互事件,如 v-on 绑定的事件回调函数的处理,会强制走 macro task。
 */
export function withMacroTask (fn: Function): Function {
    
    
  return fn._withTask || (fn._withTask = function () {
    
    
    useMacroTask = true
    const res = fn.apply(null, arguments)
    useMacroTask = false
    return res
  })
}

export function nextTick (cb?: Function, ctx?: Object) {
    
    
  let _resolve
  // cb 压入 callbacks 数组
  callbacks.push(() => {
    
    
    if (cb) {
    
    
      try {
    
    
        cb.call(ctx)
      } catch (e) {
    
    
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
    
    
      _resolve(ctx)
    }
  })
  if (!pending) {
    
    
    pending = true
    // 最后一次性地根据 useMacroTask 条件执行 macroTimerFunc 或者是 microTimerFunc,即执行flushCallbacks
    // flushCallbacks 的逻辑非常简单,对 callbacks 遍历,然后执行相应的回调函数
    if (useMacroTask) {
    
    
      macroTimerFunc()
    } else {
    
    
      microTimerFunc()
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    
    
    return new Promise(resolve => {
    
    
      _resolve = resolve
    })
  }
}

next-tick.js 申明了 microTimerFuncmacroTimerFunc 2 个变量,它们分别对应的是 micro task 的函数和 macro task 的函数。对于 macro task 的实现,优先检测是否支持原生 setImmediate,这是一个高版本 IE 和 Edge 才支持的特性,不支持的话再去检测是否支持原生的 MessageChannel,如果也不支持的话就会降级为 setTimeout 0;而对于 micro task 的实现,则检测浏览器是否原生支持 Promise,不支持的话直接指向 macro task 的实现。

由此可见,我们在setter的最后是用了nextTick方法,说明视图的更新是一个异步的过程。

检测变化的注意事项

不是每次更改数据,都能触发setter方法,引起视图变化的。

修改或者新增对象的属性

var vm = new Vue({
    
    
  data:{
    
    
    obj: {
    
    
        a:1,
    },
    test1:1
  }
})
// vm.test2是非响应的
vm.test2 = 1
// vm.obj.b 是非响应的
vm.obj.b = 2
// vm.obj.b 是响应式的
Vue.$set(this.obj,'b', 2);

上面的test2根级别的响应式属性,obj.b嵌套对象

Vue无法检测到对象属性的添加和删除。对于已经创建的实例,Vue 不能动态添加根级别的响应式属性,可以使用 Vue.set(object, key, value) 方法向嵌套对象添加响应式属性。

分析Vue中的set方法:

**
 * 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 (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    
    
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${
      
      (target: any)}`)
  }
  // 如果 target 是数组且 key 是一个合法的下标,则之前通过 splice 去添加进数组然后返回
  // 这里的 splice 其实已经不仅仅是原生数组的 splice 了
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    
    
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  // 接着又判断 key 已经存在于 target 中,则直接赋值返回,
  // 则直接赋值返回,因为这样的变化是可以观测到了
  if (key in target && !(key in Object.prototype)) {
    
    
    target[key] = val
    return val
  }
  // 接着再获取到 target.__ob__ 并赋值给 ob
  // 它是在 Observer 的构造函数执行的时候初始化的,表示 Observer 的一个实例
  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
  }
  // 如果它不存在,则说明 target 不是一个响应式的对象,则直接赋值并返回
  if (!ob) {
    
    
    target[key] = val
    return val
  }
  // 把新添加的属性变成响应式对象
  defineReactive(ob.value, key, val)
  // 手动的触发依赖通知,更新视图
  ob.dep.notify()
  return val
}

对数组的操作

// 非响应的
vm.items[indexOfItem] = newValue;
// 响应的
Vue.set(vm.items, indexOfItem, newValue);

// 非响应的
vm.items.length = newLength
// 响应的
vm.items.splice(newLength)

这里的spilce不是原生数组的splice方法,在我们将数据变为响应式数据(Observe)时,会改变数据为数组类型的数组API:

export class Observer {
    
    
  constructor (value: any) {
    
    
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
    
    
      // protoAugment的作用是将当前数组的_proto__指向Vue封装好的array api;
      // 这些封装好的array api 都会继承Array,其中push、unshift和splice方法获取到插入的值后
      // 会把新增的值变为响应式,并调用obj.dep.notify通知视图更新
      // copyAugment是用户自己对array的api的封装
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
    
    
      // ...
    }
  }
}

在vue中封装的数组API有:

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

总结

在vue中不是所有改变数据的操作都可以改变视图,主要要注意对对象和数组的修改:

  • 对于对象而言,对于已经创建的实例,Vue 不能动态添加根级别的响应式属性,可以使用 Vue.set(object, key, value) 方法向嵌套对象添加响应式属性。
  • 对于数组而言,Vue封装了push、pop、shift、unshift、splice、sort、reverse方法,其中push、unshift、splice会对新增的数据转化为响应式数据,还有就是这些封装好的api会触发notify更新视图。

介绍多种watcher

  • render watcher
  • computer watcher
  • user watcher
  • deep watcher
  • sync watcher

render watcher

渲染watcher在之前已经介绍过了,就略过。

computer watcher

_initState时对computed的处理:

const computedWatcherOptions = {
    
     computed: true }
function initComputed (vm: Component, computed: Object) {
    
    
  // 函数首先创建 vm._computedWatchers 为一个空对象
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()
  // 接着对 computed 对象做遍历,拿到计算属性的每一个 userDef,然后尝试获取这个 userDef 对应的 getter 函数
  for (const key in computed) {
    
    
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
    
    
      warn(
        `Getter is missing for computed property "${
      
      key}".`,
        vm
      )
    }

    if (!isSSR) {
    
    
      // 为每一个 getter 创建一个 watcher,这个 watcher 和渲染 watcher 有一点很大的不同,
      // 它是一个 computed watcher,所以new Watcher的update是,iscomputed是true
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // 判断如果 key 不是 vm 的属性,则调用 defineComputed(vm, key, userDef)
    if (!(key in vm)) {
    
    
      // defineComputed中利用 Object.defineProperty 给计算属性对应的 key 值添加 getter 和 setter,
      // setter 通常是计算属性是一个对象,并且拥有 set 方法的时候才有,否则是一个空函数
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
    
    
      if (key in vm.$data) {
    
    
        warn(`The computed property "${
      
      key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
    
    
        warn(`The computed property "${
      
      key}" is already defined as a prop.`, vm)
      }
    }
  }
}

这时候就有computed watcher,当我们改变数据时,依旧换执行setter方法,通知Dep执行notify方法,让Wactherupdate,在_update中有这么一段代码:

if (this.computed) {
    
    
    this.value = undefined
    this.dep = new Dep()
  } 

可以发现computed watcher并不会立刻求值。我们来走一遍流程,假设你使用了computed

var vm = new Vue({
    
    
  data: {
    
    
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    
    
    fullName: function () {
    
    
      return this.firstName + ' ' + this.lastName
    }
  }
})

首先在initState时,会调用initComputed,创建computed watcher,和绑定settergetter事件;当_render时,会访问到fullName,就触发到了getter,会调用createComputedGetter会调用watcher.evaluate()watcher.depend()函数。在watcher.evaluate()中执行computed中的方法(就是执行了 return this.firstName + ' ' + this.lastName);而watcher.depend()j就会调用Depdependcomputed watcher添加在render watcher中,即render watcher订阅了computed watcher

还需要注意的是,由于 this.firstNamethis.lastName 都是响应式对象,这里会触发它们的 getter,它们会把自身持有的 dep 添加到当前正在计算的 watcher 中,这个时候 Dep.target 就是这个 computed watcher

computed中依赖的值(firstNamelastName)发生改变时,会触发 watcher.update() 方法:

/* istanbul ignore else */
if (this.computed) {
    
    
  // A computed property watcher has two modes: lazy and activated.
  // It initializes as lazy by default, and only becomes activated when
  // it is depended on by at least one subscriber, which is typically
  // another computed property or a component's render function.
  // computed watcher有两种模式: lazy和activated
  // 默认是lazy模式
  // 只有在以下情况下才会变为activated
  // 只有当至少有一个订阅者依赖于它时才会被激活,这通常是另一个计算属性或组件的render函数。
  if (this.dep.subs.length === 0) {
    
    
    // In lazy mode, we don't want to perform computations until necessary,
    // so we simply mark the watcher as dirty. The actual computation is
    // performed just-in-time in this.evaluate() when the computed property
    // is accessed.
    // 在lazy模式下,直到必要时我们才执行计算,因此我们将观察者标记为dirty。
    // 当访问computed属性时,实际计算在this.evaluate()中即时执行。
    this.dirty = true
  } else {
    
    
    // In activated mode, we want to proactively perform the computation
    // but only notify our subscribers when the value has indeed changed.
    // 在激活模式下,我们希望主动执行计算,但仅在值确实发生更改时通知订阅方。
    this.getAndInvoke(() => {
    
    
      this.dep.notify()
    })
  }
} else if (this.sync) {
    
    
  // ...
} else {
    
    
  // ...
}

由于我们的computed watcherrender watcher订阅了,所以我们走this.getAndInvoke, 在这个函数中,函数会重新计算,然后对比新旧值,如果变化了就执行回调函数,即执行this.dep.notify,触发了render watcher重新渲染,由此可见当computed依赖的值发生改变时,会先判断计算后的值是否发生改变才会触发render watcher重新渲染,而不是依赖的值改变就触发。

user watcher

initStateuser watcher的初始化发生在computed watcher的初始化之后。在initWatch中,会对handler调用 createWatcher 方法;在 createWatcher 方法中又会调用vm.$watch;在vm.$watch中去实例化Watcher,由于是vm.$watch调用的,所以是user watcher。通过实例化 watcher 的方式,一旦我们 watch 的数据发送变化,它最终会执行 watcherrun 方法。在vm.$watch中还会判断immediate的值,看看是否要立即调用一次handler

deep watcher

user watcher中将deep设置为true,就是deep watcher

deep watcherget时会调用traverse对一个对象做深层递归遍历;因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher,这个函数实现还有一个小的优化,遍历过程中会把子响应式对象通过它们的 dep id 记录到 seenObjects,避免以后重复访问。那么在执行了 traverse 后,我们再对 watch 的对象内部任何一个值做修改,也会调用 watcher 的回调函数了。

sync watcher

user watcher中将sync设置为true,就是deep watcher

<c-input :value.sync="value" />

原先的watchersetter时,即时响应式数据发生了变化,在 nextTick 后才会真正执行 watcher 的回调函数,所以更新视图是一个异步的过程。但sync watcher不同的地方在于setter过程执行update时,直接调用run,将异步更新转化为同步更新。

update () {
    
    
  if (this.computed) {
    
    
    // ...
  } else if (this.sync) {
    
    
    this.run()
  } else {
    
    
    queueWatcher(this)
  }
}

总结

在这里插入图片描述

  • data和props会调用getter中的Dep.dependDep.target指向的Wacher依赖收集到Dep中;发生改变时会调用setter方法,调用deep.notify通知Dep.target指向的Wacher进行update,同理,其他watcher也是;
  • computed watcher的更新取决于依赖的值改变时计算后得到的结果是否改变,所以这也是为什么computedwatche更适合监听计算属性的原因;
  • user watcher中有deepimmediate;
  • 由于响应式是异步的,主要依靠了nextTick,所以可以使用sync变为同步更新。

猜你喜欢

转载自blog.csdn.net/qq_34086980/article/details/106034279