理解 Vue 数据响应原理
Vue 的数据响应是通过数据劫持结合发布者和订阅者实现的。其主要是通过 Object.defineProperty() 来实现数据劫持的。本文的例子实现数据更新驱动视图更新是直接通过操作 DOM,且是通过直接分析 DOM 来定位依赖的。其实 Vue 内部机制不完全是这样。下面我补充一些 Vue 实现数据响应的一些大体细节。这块东西真的很多,我仔细说个大概,具体细节还需要大家自己去了解。
1 数据观察
实例化 Vue 实例的时候,Vue.prototype._init 方法被第一个执行,在 initState 函数内部使用 initData 函数初始化 data 选项。这是数据响应的开始。initData 函数的主要作用是保证 options 中的 data 选项是个函数且返回的对象。同时在 Vue 实例对象上添加代理访问数据对象的同名属性。最后 通过调用了 observe 函数观测数据,将 data 变成响应式的(data 数据类型 是对象或数组时处理的 方式是不同的)。实现对象类型的 data 的观测主要是通过 defineReactive 函数
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
// 省略...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) { // watcher
// 这里闭包引用了上面的 dep 常量
dep.depend() // 收集依赖 Watcher
// 省略...
}
return value
},
set: function reactiveSetter (newVal) {
// 省略...
if (setter) { // 如果属性原本存在 set 函数则调用
setter.call(obj, newVal)
} else {
val = newVal
}
// 这里闭包引用了上面的 dep 常量
dep.notify() // 触发依赖
}
})
}
在访问器属性的 getter/setter 中,通过闭包引用了前面定义的“筐”,即 dep 常量。每一个数据字段都通过闭包引用着属于自己的 dep 常量。
那么为数组类型该如何处理???
处理数组的方式与纯对象不同,数组是一个特殊的数据结构,它有很多实例方法,并且有些方法会改变数组自身的值,这些方法有:push、pop、shift、unshift、splice、sort 以及 reverse 等。其通过拦截数组变异方法的方式得知用户调用这些变异方法,从而触发依赖。数组本身也是一个对象,所以它实例的 proto 属性指向的就是数组构造函数的原型,即 arr.proto === Array.prototype 为真。其是通过设置 proto 属性的值为一个新的对象,且该新对象的原型是数组构造函数原来的原型对象。在新对象的变异方法里收集依赖
2 收集依赖
正是因为 watcher 对表达式的求值,触发了数据属性的拦截器函数,从而收集到了依赖,当数据变化时能够触发响应。
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
updateComponent = () => {// 可以简单的认为把渲染函数生成的虚拟 DOM 渲染成真正的 DOM
// vm._render 函数的作用是调用 vm.$options.render 函数并返回生成的虚拟节点(vnode)
// vm._update 函数的作用是把 vm._render 函数生成的虚拟节点渲染成真正的 DOM
vm._update(vm._render(), hydrating)
}
在上面的代码中 Watcher 观察者实例将对 updateComponent() 函数求值, updateComponent 函数的执行会间接触发渲染函数(vm.$options.render)的执行,而渲染函数的执行则会触发数据属性的 get 拦截器函数,从而将依赖(观察者)收集,当数据变化时将重新执行 updateComponent 函数,这就完成了重新渲染。
Watcher constructor 的最后
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get() // 此时调用 Watcher 的实例方法 get
}
get () { // get 为 Watcher 的实例方法
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm) // 此时 this.getter 指的就是 updateComponent
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
3 触发依赖
修改属性值时会触发属性的 set 拦截器函数,这样就会调用 Dep 实例对象的 noitfy 方法,
set: function reactiveSetter (newVal) {
// 省略...
dep.notify()
}
export default class Dep {
// 省略...
constructor () {
this.id = uid++
this.subs = []
}
// 省略...
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
notify 方法只做了一件事,就是遍历当前 Dep 实例对象的 subs 属性所保存的所有观察者对象,并逐个调 用观察者对象的 update 方法,这就是触发响应的实现机制,那么大家应该也猜到了,重新求值的操作应该是在 update 方法中进行的,那我们就找到观察者对象的 update 方法真正的更新变化操作都是通过调用 观察者实例对象的 run 方法完成的,run 方法内判断次 Watcher 是否是激活状态,若激活则调用实例方法 getAndInvoke。
getAndInvoke (cb: Function) {
// 重新求值其实等价于重新执行渲染函数,最终结果就是重新生成了虚拟 DOM 并更新真实 DOM,这样就完成了重新渲染的过程
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 {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}
}
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论