源码分析:深入Vue响应式原理 派发更新

207 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Vue 响应式原理

Vue的响应式的核心是利用了Object.defineProperty API(不清楚响应式对象逻辑可以点击这里),这样我们在获取定义的响应式数据的时候,就会触发数据的get方法(了解get实现原理可以点击这里),改变响应式数据的时候,就会触发数据的set方法。那这两个方法内部又实现了哪些逻辑呢? 本节我们去分析Vue源码,当改变响应式数据的值的时候,了解Vue的内部做了哪些事情:

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 创建Dep实例
  const dep = new Dep()

  // 获取obj描述符
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 不可配置则直接返回,不做响应式处理
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  // 存储预先定义的get方法
  const setter = property && property.set
  ......

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    
    ......
    
    set: function reactiveSetter (newVal) {
      // 计算value的值
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      // 新赋的值与之前的值作对比,如果值相等,直接返回
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      ......
      // setter有定义,调用setter
      if (setter) {
        setter.call(obj, newVal)
      } else {
      // 未定义setter,直接将新值赋给旧值
        val = newVal
      }
      // 这儿的逻辑之后的文章去分析
      childOb = !shallow && observe(newVal)
      // 通知dep去更新
      dep.notify()
    }
  })
}

可以看到,当改变响应式数据的值的时候,set方法会将新值与旧值坐下对比,相等的情况下直接返回,不做任何处理。不相等的情况下再对旧值赋值,同时调用dep的notify方法。在依赖收集的章节我们也分析了Dep类中的一些方法,不清楚可以点击这里,接下来我们去分析dep.notify函数,定义在dep类中:

/**
 * 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 = []
  }

  ...

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    // 遍历订阅这个dep的所有Watcher,依次执行Watcher.update函数
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

继续去看Watcher上的update函数:

Watcher.update()

// 定义在Watcher类中
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
  /* istanbul ignore else */
  // 判断是否是计算属性Watcher
  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.
    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.
      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) {
    // 自定义Watcher中定义了sync(同步)会进入这个逻辑
    this.run()
  } else {
    // render Watcher进入这个逻辑,我们本次还是分析render Watcher
    queueWatcher(this)
  }
}

继续看queueWatcher函数:

queueWatcher

const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let flushing = false
let waiting = 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.
 * 将一个观察者推入观察者队列。具有重复id的作业将被跳过,除非它在队列刷新时被推送。
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // 如果id没有缓存过,进入逻辑
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      // flush为false的时候,向queue队列push Watcher
      queue.push(watcher)
    } else {
      // 这段逻辑是在flushing过程中如果queue中出现watcher变更的情况会进入
      // if already flushing, splice the watcher based on its id
      // 如果已经刷新,则根据观察者的id拼接观察者
      // if already past its id, it will be run next immediately.
      // 如果已经超过了它的id,它将立即运行。
      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) {
      // wating置为true,保证nextTick执行一次
      waiting = true
      // 执行nextTick
      nextTick(flushSchedulerQueue)
    }
  }
}

从上面的代码可以看出,queueWatcher是将Watcher推入queue数组中进行存储,并执行nextTick,关于nextTick,可以查看我的另一篇分析文章,这个函数就是将放在里面的函数放在下一个tick中去执行,等同步任务全部执行完再触发。

从这里我们也清楚了一个问题,当改变数据触发页面重新渲染,这是个异步的过程。

/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  // flushing赋值为true
  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.
  // 1. 组件的更新是先父后子
  // 2. 组件的自定义watcher应该在render watcher前面运行(因为自定义watcher创建在先)
  // 3. 如果组件在父组件watcher运行期间被销毁,那么他应该被跳过
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  // queue的长度不要缓存,因为我们在运行现有的watchers的时候,可能会推入新的watchers
  for (index = 0; index < queue.length; index++) {
    // 依次拿到watcher
    watcher = queue[index]
    // 如果watcher有before属性,则执行
    if (watcher.before) {
      // render Watcher的时候,这儿是调用生命周期函数beforeUpdated
      watcher.before()
    }
    id = watcher.id
    // 将has[id]重置
    has[id] = null
    // 执行Watcher.run,下面我们看这个逻辑
    watcher.run()
    // in dev build, check and stop circular updates.
    // 这个地方防止无限循环bug,有一种情况会进入这里
    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()

  // 重置队列状态,flushing,waiting等置为初始状态
  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  // 调用updated生命周期函数
  callUpdatedHooks(updatedQueue)

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

watcher.run()与getAndInvoke():

/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
  // this.active为true
  if (this.active) {
    // 执行getAndInvoke
    this.getAndInvoke(this.cb)
  }
}

getAndInvoke (cb: Function) {
  // 调用Watcher.get函数计算value值
  const value = this.get()
  // 判断新值与旧值是否相同,执行传入的cb函数,render Watcher 这儿传入的是个空函数
  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
    this.dirty = false
    if (this.user) {
      // 自定义watcher进入这个逻辑
      try {
        cb.call(this.vm, value, oldValue)
      } catch (e) {
        handleError(e, this.vm, `callback for watcher "${this.expression}"`)
      }
    } else {
      // computed Watcher 进入这个逻辑
      cb.call(this.vm, value, oldValue)
    }
  }
}

到这里我们清楚了,当我们改变数据值的时候,会遍历数据中实例化的dep收集的依赖,通知Watcher做更新,将所有的Watcher推入一个队列,放在nextTick的时候去执行,所以页面的更新渲染是个异步的过程。

执行的过程中,会对队列中的watchers进行排序,排序完成后遍历queue队列,执行钩子函数beforeUpdated,再依次执行定义的Watcher.get函数,完成queue的遍历后,再执行钩子函数updated。