为了账号安全,请及时绑定邮箱和手机立即绑定

Vue 源码解析:深入响应式原理(下)

Watcher

我们先来看一下 Watcher 类的实现,它的源码定义如下:

<!-源码目录:src/watcher.js-->
export default function Watcher (vm, expOrFn, cb, options) {
  // mix in options
  if (options) {
    extend(this, options)
  }
  var isFn = typeof expOrFn === 'function'
  this.vm = vm
  vm._watchers.push(this)
  this.expression = expOrFn
  this.cb = cb
  this.id = ++uid // uid for batching
  this.active = true
  this.dirty = this.lazy // for lazy watchers
  this.deps = []
  this.newDeps = []
  this.depIds = new Set()
  this.newDepIds = new Set()
  this.prevError = null // for async error stacks
  // parse expression for getter/setter
  if (isFn) {
    this.getter = expOrFn
    this.setter = undefined
  } else {
    var res = parseExpression(expOrFn, this.twoWay)
    this.getter = res.get
    this.setter = res.set
  }
  this.value = this.lazy
    ? undefined
    : this.get()
  // state for avoiding false triggers for deep and Array
  // watchers during vm._digest()
  this.queued = this.shallow = false
}

Directive 实例在初始化 Watche r时,会传入指令的 expression。Watcher 构造函数会通过 parseExpression(expOrFn, this.twoWay) 方法对 expression 做进一步的解析。在前面的例子中, expression 是times,passExpression 方法的功能是把 expression 转换成一个对象,如下图所示:

image

可以看到 res 有两个属性,其中 exp 为表达式字符串;get 是通过 new Function 生成的匿名方法,可以把它打印出来,如下图所示:

image

可以看到 res.get 方法很简单,它接受传入一个 scope 变量,返回 scope.times。对于传入的 scope 值,稍后我们会进行介绍。在 Watcher 构造函数的最后调用了 this.get 方法,它的源码定义如下:

<!-源码目录:src/watcher.js-->
Watcher.prototype.get = function () {
  this.beforeGet()
  var scope = this.scope || this.vm
  var value
  try {
    value = this.getter.call(scope, scope)
  } catch (e) {
    if (
      process.env.NODE_ENV !== 'production' &&
      config.warnExpressionErrors
    ) {
      warn(
        'Error when evaluating expression ' +
        '"' + this.expression + '": ' + e.toString(),
        this.vm
      )
    }
  }
  // "touch" every property so they are all tracked as
  // dependencies for deep watching
  if (this.deep) {
    traverse(value)
  }
  if (this.preProcess) {
    value = this.preProcess(value)
  }
  if (this.filters) {
    value = scope._applyFilters(value, null, this.filters, false)
  }
  if (this.postProcess) {
    value = this.postProcess(value)
  }
  this.afterGet()
  return value
}

Watcher.prototype.get 方法的功能就是对当前 Watcher 进行求值,收集依赖关系。它首先执行 this.beforeGet 方法,源码定义如下:

<!-源码目录:src/watcher.js-->
Watcher.prototype.beforeGet = function () {
  Dep.target = this
}

Watcher.prototype.beforeGet 很简单,设置 Dep.target 为当前 Watcher 实例,为接下来的依赖收集做准备。我们回到 get 方法,接下来执行 this.getter.call(scope, scope) 方法,这里的 scope 是 this.vm,也就是当前 Vue 实例。这个方法实际上相当于获取 vm.times,这样就触发了对象的 getter。在第一小节我们给 data 添加 Observer 时,通过 Object.defineProperty 给 data 对象的每一个属性添加 getter 和 setter。回顾一下代码:

<!-源码目录:src/observer/index.js-->
Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
        if (isArray(value)) {
          for (var e, i = 0, l = value.length; i < l; i++) {
            e = value[i]
            e && e.__ob__ && e.__ob__.dep.depend()
          }
        }
      }
      return value
    },
       …
  })

当获取 vm.times 时,会执行到 get 方法体内。由于我们在之前已经设置了 Dep.target 为当前 Watcher 实例,所以接下来就调用 dep.depend() 方法完成依赖收集。它实际上是执行了 Dep.target.addDep(this),相当于执行了 Watcher 实例的 addDep 方法,把 Dep 实例添加到 Watcher 实例的依赖中。addDep 方法的源码定义如下:

<!-源码目录:src/watcher.js-->
Watcher.prototype.addDep = function (dep) {
  var id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}

Watcher.prototype.addDep 方法就是把 dep 添加到 Watcher 实例的依赖中,同时又通过 dep.addSub(this) 把 Watcher 实例添加到 dep 的订阅者中。addSub 方法的源码定义如下:

<!-源码目录:src/observer/dep.js-->
Dep.prototype.addSub = function (sub) {
  this.subs.push(sub)
}

至此,指令完成了依赖收集,并且通过 Watcher 完成了对数据变化的订阅。

接下来我们看一下,当 data 发生变化时,视图是如何自动更新的。在前面的例子中,我们通过 setInterval 每隔 1s 执行一次 vm.times++,数据改变会触发对象的 setter,执行 set 方法体的代码。回顾一下代码:

<!-源码目录:src/observer/index.js-->
Object.defineProperty(obj, key, {
     …
   set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val
      if (newVal === value) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = observe(newVal)
      dep.notify()
    }
  })

这里会调用 dep.notify() 方法,它会遍历所有的订阅者,也就是 Watcher 实例。然后调用 Watcher 实例的 update 方法,源码定义如下:

<!-源码目录:src/watcher.js-->
Watcher.prototype.update = function (shallow) {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync || !config.async) {
    this.run()
  } else {
    // if queued, only overwrite shallow with non-shallow,
    // but not the other way around.
    this.shallow = this.queued
      ? shallow
        ? this.shallow
        : false
      : !!shallow
    this.queued = true
    // record before-push error stack in debug mode
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.debug) {
      this.prevError = new Error('[vue] async stack trace')
    }
    pushWatcher(this)
  }
}

Watcher.prototype.update 方法在满足某些条件下会直接调用 this.run 方法。在多数情况下会调用 pushWatcher(this) 方法把 Watcher 实例推入队列中,延迟 this.run 调用的时机。pushWatcher 方法的源码定义如下:

<!-源码目录:src/batcher.js-->
export function pushWatcher (watcher) {
  const id = watcher.id
  if (has[id] == null) {
    // push watcher into appropriate queue
    const q = watcher.user
      ? userQueue
      : queue
    has[id] = q.length
    q.push(watcher)
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushBatcherQueue)
    }
  }
}

pushWatcher 方法把 Watcher 推入队列中,通过 nextTick 方法在下一个事件循环周期处理 Watcher 队列,这是 Vue.j s的一种性能优化手段。因为如果同时观察的数据多次变化,比如同步执行 3 次 vm.time++,同步调用 watcher.run 就会触发 3 次 DOM 操作。而推入队列中等待下一个事件循环周期再操作队列里的 Watcher,因为是同一个 Watcher,它只会调用一次 watcher.run,从而只触发一次 DOM 操作。接下来我们看一下 flushBatcherQueue 方法,它的源码定义如下:

<!-源码目录:src/batcher.js-->
function flushBatcherQueue () {
  runBatcherQueue(queue)
  runBatcherQueue(userQueue)
  // user watchers triggered more watchers,
  // keep flushing until it depletes
  if (queue.length) {
    return flushBatcherQueue()
  }
  // dev tool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
  resetBatcherState()
}

flushBatcherQueue 方法通过调用 runBatcherQueue 来 run Watcher。这里我们看到 Watcher 队列分为内部 queue 和 userQueue,其中 userQueue 是通过 $watch() 方法注册的 Watcher。我们优先 run 内部queue 来保证指令和 DOM 节点优先更新,这样当用户自定义的 Watcher 的回调函数触发时 DOM 已更新完毕。接下来我们看一下 runBatcherQueue 方法,它的源码定义如下:

<!-源码目录:src/batcher.js-->
function runBatcherQueue (queue) {
  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (let i = 0; i < queue.length; i++) {
    var watcher = queue[i]
    var 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] > config._maxUpdateCount) {
        warn(
          'You may have an infinite update loop for watcher ' +
          'with expression "' + watcher.expression + '"',
          watcher.vm
        )
        break
      }
    }
  }
  queue.length = 0
}

runBatcherQueued 的功能就是遍历 queue 中 Watcher 的 run 方法。接下来我们看一下 Watcher 的 run 方法,它的源码定义如下:

<!-源码目录:src/watcher.js-->
Watcher.prototype.run = function () {
  if (this.active) {
    var 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; but only do so if this is a
      // non-shallow update (caused by a vm digest).
      ((isObject(value) || this.deep) && !this.shallow)
    ) {
      // set new value
      var oldValue = this.value
      this.value = value
      // in debug + async mode, when a watcher callbacks
      // throws, we also throw the saved before-push error
      // so the full cross-tick stack trace is available.
      var prevError = this.prevError
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' &&
          config.debug && prevError) {
        this.prevError = null
        try {
          this.cb.call(this.vm, value, oldValue)
        } catch (e) {
          nextTick(function () {
            throw prevError
          }, 0)
          throw e
        }
      } else {
        this.cb.call(this.vm, value, oldValue)
      }
    }
    this.queued = this.shallow = false
  }
}

Watcher.prototype.run 方法再次对 Watcher 求值,重新收集依赖。接下来判断求值结果和之前 value 的关系。如果不变则什么也不做,如果变了则调用 this.cb.call(this.vm, value, oldValue) 方法。这个方法是 Directive 实例创建 Watcher 时传入的,它对应相关指令的 update 方法来真实更新 DOM。这样就完成了数据更新到对应视图的变化过程。 Watcher 巧妙地把 Observer 和 Directive 关联起来,实现了数据一旦更新,视图就会自动变化的效果。尽管 Vue.js 利用 Object.defineProperty 这个核心技术实现了数据和视图的绑定,但仍然会存在一些数据变化检测不到的问题,接下来我们看一下这部分内容。

今天我们就讲到这里,更多精彩内容关注我们的书籍《Vue.js 权威指南》

点击查看更多内容
26人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
Web前端工程师
手记
粉丝
1.8万
获赞与收藏
3712

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消