vue.js的响应式原理,理解为什么修改数据视图会自动更新
如何追踪变化
在js中,有两种方法可以侦测到数据的变化:Object.defineProperty和Es6的Proxy。这里讨论的是vue2的响应式原理,所以就说Object.defineProperty,在vue3中使用的是Proxy,还没有开始看呢。
那么Object.defineProperty是如何侦测到对象的变化呢?如下
function defineReactive(data, key, val) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
return val
},
set: function(newVal) {
if(val === newVal) {
return
}
val = newVal
}
})
}
上面将Object.defineProperty进行了封装,封装好之后,每当data中值被读取就会触发get;每当设置data中的数据时,set函数就会触发
那么在哪里收集呢
把用到数据的地方收集起来,等到数据变化的时候再将收集好的依赖循环触发一次。(在getter中收集依赖,在setter中触发依赖)
依赖被收集在哪里
在每个key上面设置一个数组,专门去收集使用到key的地方,即依赖。然后这个数组我们取名dep,用来存储被收集的依赖。假设依赖是一个函数,保存在window.target上。然后我们来改造一下defineReactive:
function defineReactive(data, key, val) {
let dep = [] //新增
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.push(window.target) //新增
return val
},
set: function(newVal) {
if(val === newVal) {
return
}
for(let i in dep) {
dep[i](newVal, val)
}
val = newVal
}
})
}
然后将dep封装成一个Dep类,专门管理依赖:
export default class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
removeSub(sub) {
remove(this.subs, sub)
}
depend() {
if(window.target) {
this.addSub(window.target)
}
}
notify() {
const subs = this.subs.slice()
for(let i in subs) {
subs[i].update()
}
}
}
function remove(arr, item) {
if(arr.length) {
const index = arr.indexOf(item)
if(index > -1) {
return arr.splice(index, 1)
}
}
}
依赖是什么?
上面说,依赖是window.target,那么它又是什么呢?我们究竟要收集什么东西?
收集谁,就是当属性发生变化时,通知谁。我们要通知使用数据的地方,但是使用数据的地方有很多,类型也不同,可能是模板也可以是其他地方,所以我们要封装一个能处理所有情况的类,就叫做Watcher。所以,依赖就是Watcher,我们收集的也是Watcher。
什么是Watcher
Watcher是一个中间角色,数据发生变化时通知它,它再通知其他地方。
关于Watcher,先来看一个经典的使用方式:
vm.$watch('a.b.c', function(newVal, oldVal) {
//do someting
})
当data.a.b.c发生变化时,触发第二个参数中的函数。
那么怎么去实现这个功能呢?首先要把这个watcher实例添加到data.a.b.c的Dep中去,然后当它改变的时候,通知watcher,watcher再执行里面的回调函数就行了。
那么实际代码如下:
export default class Watcher {
constructor (vm, expOrFn, cb) {
this.vm = vm
//执行一下this.getter就能读取到data.a.b.c的内容
this.getter = parsePath(expOrFn) //读取字符串的keyPath
this.cb = cb
}
get() {
window.target = this //将window.target设成当前watcher实例
let value = this.getter.call(this.vm, this.vm) //触发getter,将window.target添加到dep中
window.target = undefined
return value
}
update() {
const oldValue = this.value //老数据
this.value = this.get() //新数据
this.cb.call(this.vm, this.value, oldValue) //回调函数
}
}
const bailRE = /[^\w.$]/
function parsePath (path) {
if(bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for(let i in segments) {
if(!obj) return
obj = obj[segments]
}
return obj
}
}
递归侦测所有key
上面已经能实现变化侦测的功能了,但是只能侦测数据中的一个属性,我们希望能把数据中的所有属性都侦测到,所以需要封装一个Observer类。这个类的作用就是将数据中的所有属性都转换成getter/setter的形式,然后去追踪它们的变化。
export class Observer {
constructor (value) {
this.value = value
if(!Array.isArray(value)) { //判断是不是数组,数组需要单独进行特殊处理
this.walk(value)
}
}
//walk会将每一个属性转换成getter/setter,并且只有在数据类型是对象才会调用
walk(obj) {
const keys = Object.keys(obj)
for(let i in keys) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
然后再将defineReactive修改一下:
function defineReactive(data, key, val) {
if(typeof val === 'object') {
new Observer(val)
}
let dep = new Dep() //新增
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.depend()//新增
return val
},
set: function(newVal) {
if(val === newVal) {
return
}
val = newVal
dep.notify() //调用watcher中的回调函数去通知所有的依赖
}
})
}
现在就大功告成了!!!
关于Object的添加和删除问题
有一些语法中即使数据发生了变化,vue.js也追踪不到。
比如,向object添加属性:
var vm = new Vue({
el: '#el',
template: '#demo-template',
methods: {
action() {
this.obj.name = 'berwin' //不能实时监听
}
},
data: {
obj: {}
}
})
再比如,删除一个属性:
var vm = new Vue({
el: '#el',
template: '#demo-template',
methods: {
action() {
delete this.obj.name //不能实时监听
}
},
data: {
obj: {name: 'sifan'}
}
})
Object.defineProperty只能追踪一个数据是否被修改,无法追踪新增属性和删除属性,所以才会出现上面的问题。
但是也不用担心,vue.js提供了两个API——vm.\(set和vm.\)delete,之后再更它们两个的原理。
行百里者半九十