响应式原理
口述
Vue 的响应式是通过 Object.defineProperty 进行数据劫持 + 发布订阅模式 进行依赖收集及更新实现响应式。
通过递归的形式将 data 数据(函数/对象)将数据变为可监测的,在 new Vue 初始化 Vue 实例的时候,通过创建一个 Watcher 实例(依赖),进行解析模版中(template)用到的数据,会触发 getter ,每一个对象里面都通过闭包的形式保存一个 Dep 实例(依赖收集者), Dep 会去收集全局的 Watcher 实例,这样就可以在数据改动之后,就可以去通知 Dep 去 notify,通知依赖。
让数据变得可侦测
什么时候能够知道数据被读取或数据被改写了,就是数据的可观测。
// src/core/observer/index.js
class Observer {
constructor (value) {
// 传进来的 obj
this.value = value
// 记录组件使用的次数
this.vmCount = 0
// 给value新增一个__ob__属性,值为该value的Observer实例
// 相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作
def(value, '__ob__', this)
// 进入对象是数组还是对象
if (Array.isArray(value)) {
} else {
// obj是一个对象
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, ) {
// 如果只传了obj和key,那么val = obj[key]
if ((!getter || setter) && arguments.length === 2) {
// 如果没有 val, 直接使用第三个参数。
val = obj[key]
}
// 如果传入的属性值还是一个 object ,就去递归的来侦测变化
let childOb = observe(val);
// 转为 setter / getter 函数
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 中更新依赖。
依赖管理器
就是每个数据都需要创建一个依赖数组,不能够使用一个数组,所以需要实例处一个个的依赖数组。
// src/core/observer/dep.js
let uid = 0
class Dep {
constructor () {
// Dep 的 id
this.id = uid++
// 初始化一个 subs 数组 (用来存放依赖)
this.subs = []
}
// 添加依赖
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 删除依赖
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 添加依赖
depend () {
// 如果存在全局 target
if (Dep.target) {
// 就去调用 addDep 收集整个 this(watcher)
Dep.target.addDep(this)
}
}
// 通知所有的依赖更新
notify () {
const subs = this.subs.slice()
// 遍历所有的依赖 (watcher实例)
for (let i = 0, l = subs.length; i < l; i++) {
// 执行依赖的 update 方法 (存储了更新视图的方法)
subs[i].update()
}
}
}
// 全局对象 target (靶子)
Dep.target = null
Dep 类存储了对依赖的 添加、删除、通知更新 操作.
接下来就可以在 getter 中去收集依赖了。
// src/core/observer/index.js
function defineReactive(obj, ) {
// 实例化一个依赖管理器,生成一个依赖管理器数组 dep
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)
// 在 setter 中通知依赖更新
dep.notify()
}
})
}
依赖
Vue 中实现了一个 Watcher 类,谁使用了数据,谁就是依赖,就为谁创建一个 Watcher 实例。那么就收集它(Watcher).
然后当数据变化,通知 Watcher 实例,由 Watcher 去通知视图更新。
// src/core/observer/watcher.js
let uid = 0
class Watcher {
constructor (vm, expOrFn, cb, options) {
// vm 实例
this.vm = vm
// 将当前 Watcher 实例添加到 Vue 实例的 _watchers 中
vm._watchers.push(this)
// cb (保存了更新视图的回调)
this.cb = cb
// id
this.id = ++uid
// 如果第二个参数是一个函数
if (typeof expOrFn === 'function') {
// getter 就是这个函数
this.getter = expOrFn
} else {
// 返回一个解析的 getter 函数. 会调用 dep.depend()收集依赖。
this.getter = parsePath(expOrFn)
}
this.value = this.get()
}
get() {
// 通过 window.target = this 把实例自身赋值给全局唯一对象 window.target 上。
pushTarget(this)
let value
const vm = this.vm
try {
// 通过调用 getter ,就会触发数据上的 getter,然后收集依赖
value = this.getter.call(vm, vm)
} catch (e) {
throw e
} finally {
if (this.deep) {
traverse(value)
}
// 清空 window.target
popTarget()
this.cleanupDeps()
}
}
addDep (dep: Dep) {
// 获取到dep.id
const id = dep.id
// 如果newDepIds这个集合没有这个 id 了。
if (!this.newDepIds.has(id)) {
// 就去添加 id。
this.newDepIds.add(id)
// 给添加到数组中
this.newDeps.push(dep)
// 如果没有 depIds
if (!this.depIds.has(id)) {
// 通知 dep 来收集订阅者 收集自己
dep.addSub(this)
}
}
}
// 通过 update 来让 Watcher 去更新视图
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) 去解析这个字符串,返回一个可以获取值的新函数。
// src/core/util/lang.js
function parsePath (path) {
// 对于 template 中 例如: a.b.c ---> ['a', 'b', 'c']
const segments = path.split('.')
function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
return obj[]
}
}
}
然获取到修改过后的数据值 value。 然后判断是否是和之前旧值一样的,这个时候之前通过 this.get 获取到的 value 就是旧值,然后调用 cb 去更新视图。并且把新旧值传入。
数组的侦测
数组和对象一样,也是在获取时收集依赖 ,在修改时通知依赖跟新.
// src/core/observer/index.js
class Observer {
constructor (value: any) {
this.value = value
// 实例化一个依赖管理器,用来收集数组依赖
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 判断是否支持 __proto__
if (hasProto) {
// 把 value.__proto__ = arrayMethods; 用于方法拦截
protoAugment(value, arrayMethods)
} else {
// 不支持的话,把拦截器中重写的7个方法循环加入到 value 上。
copyAugment(value, arrayMethods, arrayKeys)
}
// 将数组中的所有对象形式都转换为可被侦测的响应式
this.observeArray(value)
} else {
// obj是一个对象
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()
拦截器
// 获取 Array 的原型
const arrayProto = Array.prototype
// 创建一个对象作为拦截器, 这个对象的原型指向 arrayProto
export const arrayMethods = Object.create(arrayProto)
// 改变数组自身内容的 7 个方法
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)
// this就是数据value, 主要是能访问到 value 上的 __ob__ (有依赖管理器)
const ob = this.__ob__
let inserted
switch (method) {
// 如果是 push 或 unshift,传入参数就是新增的元素
case 'push':
case 'unshift':
inserted = args
break
// 如果是 splice ,传入的参数列表中下标为 2 的就是新增的元素
case 'splice':
inserted = args.slice(2)
break
}
// 如果有元素,就调用 observeArray 函数将新增的元素转换为响应式
if (inserted) ob.observeArray(inserted)
// 如果数组调用修改数组的方法,就调用 dep.notify 就可以通知依赖更新
ob.dep.notify()
return result
})
})
继续看数组中的对象如何去收集依赖的。
// // src/core/observer/index.js
// 假设现在的数据是 data: {arr: [1, 2, 3]}
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
// 在 defineReactive 中
let childOb = !shallow && observe(val)
childOb 就是 arr 数组上的 __ob__ (Observer 实例)
if (childOb) {
// 去收集依赖,调用数组的 dep 去收集。
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 的七个可以修改数组的方法。