Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

派发更新原理 #29

Open
Jouryjc opened this issue Nov 12, 2018 · 0 comments
Open

派发更新原理 #29

Jouryjc opened this issue Nov 12, 2018 · 0 comments

Comments

@Jouryjc
Copy link
Owner

Jouryjc commented Nov 12, 2018

上一篇我们分析依赖收集的过程。那么将这些依赖收集起来有什么用呢?答案是在数据变化时能派发更新。本文就一起通过例子一起去了解派发更新的过程。本文 demo:

<template>
  <div>
    <p>{{text}}</p>
    <button @click="_handleChange">改变text</button>
  </div>
</template>

<script>

export default {
  name: 'app',
  
  data () {
    return {
      text: 'hello vue'
    };
  },

  methods: {
    _handleChange () {
      this.text = 'hello change';
    }
  }
}
</script>

还记得之前 defineReactive 中给数据都设置了 getter 和 setter,上一篇依赖收集原理分析了 getter 的过程,在数据发生改变时就会触发 setter 过程。分析源码前,希望你能够使用 vue-cli 运行上面的 demo,并且在 setter 函数中打好断点,一起感受 setter 的过程。

// ...
set: function reactiveSetter (newVal) {
    // 获取 getter 函数,这里就是 src/core/instance/lifecycle.js 的 updateComponent
    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 的情况,会对新设置的值变成一个响应式对象
    childOb = !shallow && observe(newVal)
    // 通知订阅者
    dep.notify()
}

当我们点击按钮改变 text 的值时,便会触发 proxy 的代理,变成设置 this._datas.text 的值,便会触发 setter。其中的关键逻辑:

// 通知订阅者
dep.notify()

看到 Dep 类:

class Dep {
  // ...
  notify () {
  // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

这段代码的逻辑比较简单,遍历所有 subs 即 Watcher 实例数组,调用其 update 方法:

class Watcher {
  // ...
  update () {
    if (this.computed) {
      if (this.dep.subs.length === 0) {
        this.dirty = true
      } else {
        this.getAndInvoke(() => {
          this.dep.notify()
        })
      }
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}  

把英文注释删掉之后, update 的逻辑非常清晰。这里不是计算属性,所以直接就调 queueWatcher,这个函数定义在 src/core/observer/scheduler.js 中:

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

  // 确保只会push一次
  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

    // 保证只执行一次nextTick
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

这里引入了一个队列的概念,这也是 Vue 在做派发更新的时候的一个优化点,它并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列里,然后在 nextTick 后执行 flushSchedulerQueue。flushSchedulerQueue 定义在 src/core/observer/scheduler.js 中:

function flushSchedulerQueue () {
  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.组件的更新由父到子;因为父组件的创建过程是先于子的,所以 watcher 的创建也是先父后子,执行顺序也应该保持先父后子。
   * 2.用户的自定义 watcher 要优先于渲染 watcher 执行;因为用户自定义 watcher 是在渲染 watcher 之前创建的。
   * 3.如果一个组件在父组件的 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
  // 这里不缓存队列长度的原因是在 watcher.run() 的时候,很可能用户会再次添加新的 watcher
  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')
  }
}

flushSchedulerQueue 在做完排序(排序原因在注释中说明)之后,遍历队列做 run 调用。watcher.run() 有个细节要注意一下,每次都会去重新计算队列长度,因为在执行 watcher.run() 的时候可能会有新的 watcher,这时候要重新计算,走 queueWatcher 的过程。queueWatcher 会使 queue 发生改变。

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

  getAndInvoke (cb: Function) {
    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
      this.dirty = false
      if (this.user) {
        try {

          // 这就是为什么我们写watch时能拿到新值和旧值
          cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        cb.call(this.vm, value, oldValue)
      }
    }
  }

执行 watcher.run() ,就是执行 getAndInvoke:

  • 拿到值 value , 如果满足新旧值不等、新值是对象类型、deep 模式任何一个条件,则执行 watcher 的回调
  • 对于渲染 watcher 而言,它在执行 this.get() 方法求值的时候,会执行 getter 方法。执行 updateComponent 也就会重新渲染页面。

最后,遍历执行完所有 watcher.run() 后,会执行 resetSchedulerState()

function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  waiting = flushing = false
}

把流程中的一些标志位恢复,并将队列清空。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant