响应式原理
口述
Vue 的响应式是通过 Object.defineProperty 进行数据劫持 + 发布订阅模式 进行依赖收集及更新实现响应式。
通过递归的形式将 data 数据(函数/对象)将数据变为可监测的,在 new Vue 初始化 Vue 实例的时候,通过创建一个 Watcher 实例(依赖),进行解析模版中(template)用到的数据,会触发 getter ,每一个对象里面都通过闭包的形式保存一个 Dep 实例(依赖收集者), Dep 会去收集全局的 Watcher 实例,这样就可以在数据改动之后,就可以去通知 Dep 去 notify,通知依赖。
让数据变得可侦测
什么时候能够知道数据被读取或数据被改写了,就是数据的可观测。
class Observer {
constructor (value) {
this.value = value
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
function defineReactive(obj, ) {
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function () {
const value = val;
return value
},
set: function (newVal) {
const value = val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val = newVal
observe(newVal)
}
})
}
通过定义一个 Observer 类,用来将一个 object 转为可观测的 object.
并且会给 value 新增一个 ob 属性,值为 value 的 Observer 实例,避免重复的操作。
然后判断如果是对象,就调用 walk,walk 来将对象的每一个属性转换为 getter/setter 的形式。
在 defineReactive 中传入的属性还是一个对象,会继续 Observer 来递归子属性。
收集依赖
只要数据变化了,就可以去通知视图变化。 不是说只要有改变,就改变怎么页面,而是哪个依赖了数据,就去更新哪块。
因为依赖这个数据的可能有多个地方,所以给每个数据都创建一个依赖数组,哪个地方用到了数据,就把谁加到依赖数组中。
谁用到这个数据,也就会走 getter ,当改变之后,会走 setter。 所以在 getter 中收集依赖,在 setter 中更新依赖。
依赖管理器
就是每个数据都需要创建一个依赖数组,不能够使用一个数组,所以需要实例处一个个的依赖数组。
let uid = 0
class Dep {
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep.target = null
Dep 类存储了对依赖的 添加、删除、通知更新 操作.
接下来就可以在 getter 中去收集依赖了。
function defineReactive(obj, ) {
const dep = new Dep()
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function () {
const value = val;
if (Dep.target) {
dep.depend()
}
return value
},
set: function (newVal) {
const value = val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val = newVal
observe(newVal)
dep.notify()
}
})
}
依赖
Vue 中实现了一个 Watcher 类,谁使用了数据,谁就是依赖,就为谁创建一个 Watcher 实例。那么就收集它(Watcher).
然后当数据变化,通知 Watcher 实例,由 Watcher 去通知视图更新。
let uid = 0
class Watcher {
constructor (vm, expOrFn, cb, options) {
this.vm = vm
vm._watchers.push(this)
this.cb = cb
this.id = ++uid
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.value = this.get()
}
get() {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
throw e
} finally {
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
}
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
update () {
const value = this.get()
if (
value !== this.value ||
isObject(value)
} {
const oldValue = this.value
this.value = value
this.cb.call(this.vm, value, oldValue)
}
}
}
Watcher 类的逻辑:
当实例化 Watcher 的时候,先执行 constructor 构造函数。
传入 Vue 实例 vm (保存了最新的数据值),expOrFn 是, cb 是回调函数,保存了更新视图的方法。
this.getter 是一个解析函数,只要调用,就可以触发依赖,触发数据的 getter 方法。从而收集依赖。
接着在构造函数中调用 get 实例方法。
在 get 方法中通过设置全局的 window.target ,然后调用 this.getter 去收集一下依赖,在 getter 中会调用 dep.depend() ,将 window.target 上的值存放入依赖数组中。 在执行完 get 方法最后将 window.target 释放掉.
在数据修改之后,会触发 setter, 在 setter 中调用 dep.notify 通知依赖更新,然后遍历 subs 中的每一个依赖者的 update 方法。进而去更新视图。
触发 Watcher 的 update 实例方法,获取 this.get(), 会执行 this.getter, 如果 expOrFn 是一个函数,this.getter 就是这个函数,否则的话 使用 parsePath(expOrFn) 去解析这个字符串,返回一个可以获取值的新函数。
function parsePath (path) {
const segments = path.split('.')
function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
return obj[]
}
}
}
然获取到修改过后的数据值 value。 然后判断是否是和之前旧值一样的,这个时候之前通过 this.get 获取到的 value 就是旧值,然后调用 cb 去更新视图。并且把新旧值传入。
数组的侦测
数组和对象一样,也是在获取时收集依赖 ,在修改时通知依赖跟新.
class Observer {
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
上面,在 Observer 中创建依赖收集器, 用来收集数组依赖,在 Observer 中实例化 Dep, 主要是为了能够在数组拦截的方法中能够找到存储的对应依赖管理器去通知。
如果是数组的话,就会将当前数组的 proto 指向拦截方法的原型,比如:
var arr = [1, 2, 3]
arr.push() ---> 拦截对象.push()
拦截对象.push() ---> Array.prototype.push()
拦截器
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
ob.dep.notify()
return result
})
})
继续看数组中的对象如何去收集依赖的。
var data = {arr: [1, 2, 3]}
observe(data)
new Observer(value)
defineReactive(value, value[arr])
又重新 observe(arr)
new Observe(arr)
observeArray(arr)
observe(arr[i])
arr 每一项都不是一个对象, return
let childOb = !shallow && observe(val)
childOb 就是 arr 数组上的 __ob__ (Observer 实例)
if (childOb) {
childOb.dep.depend()
}
现在收集了依赖,并且在触发拦截方法,可以监听到改变,去 notify 通知依赖更新。
数组的深度检测
数组的深度检测,是指 [{}, {}, {}] 数组中的对象是可以被 getter/setter 的.
class Observer {
constructor () {
if (Array.isArray(value)) {
this.observeArray(value)
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
如果数组里面是对象, observe(items[i]) ,发现是对象,就会继续 new Observer(), 继续 defineReactive 来转为响应式的。
缺点
-
使用递归的形式在初始化的时候将所有的 data 转为 getter/setter ,将会非常的耗时。proxy 可以弥补。 -
只能够对初始化时的 data 数据进行监测,之后给对象添加的新属性将不能被监测到。需要使用 Vue 的实例方法 this.$set 和 $delete 来响应式的添加和删除。 -
Object.defineProperty 是适用于对象的,不能够适用于数组的,即使可以监听到数组, Vue 中数组是不能够使用 var arr = [1, 2, 3] arr[1] = 10 这种来修改的,尤大大说这种可以实现,但是数组的项是太多的,非常的消耗性能,也没有必要。 数组重写了 Vue 的七个可以修改数组的方法。
|