您好,欢迎来到九壹网。
搜索
您的当前位置:首页Vue 源码解析:深入响应式原理(下)

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

来源:九壹网


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

Watcher

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

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 转换成一个对象,如下图所示:

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

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

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 方法,源码定义如下:

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。回顾一下代码:

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 方法的源码定义如下:

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 方法的源码定义如下:

Dep.prototype.addSub = function (sub) {

this.subs.push(sub)

}

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

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

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 方法,源码定义如下:

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 方法的源码定义如下:

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 方法,它的源码定义如下:

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 方法,它的源码定义如下:

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 方法,它的源码定义如下:

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 这个核心技术实现了数据和视图的绑定,但仍然会存在一些数据变化检测不到的问题,后续我会更新这部分内容。

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- 91gzw.com 版权所有 湘ICP备2023023988号-2

违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务