Vue通过template模板来建立状态数据和dom之间的关系,将数据绑定在模板上,而模板又用于渲染生成dom。故,当状态数据发生变化时,可以通过更新模板来实现dom的自动更新。这中间存在三个过程:
- 侦测数据(对状态数据进行监测,以便当数据发生变化时,我们能知道数据变化了)
- 通知依赖(我们知道数据变化了以后,是不是要通知依赖了我们这个数据的东西去进行更新啊,其实这里应该还有一步,就是收集依赖,因为只有收集了依赖,我们才能知道我们需要通知哪些东西)
- 更新数据(依赖者收到通知后要进行对象的dom更新,这部分以后再说
变化侦测有两种类型:Push 和 Pull.
Vuejs属于Push,即当状态发生改变时,Vuejs 立刻就知道了,而且在一定程度上知道哪些状态发生了变化,因此Vue可以进行更细粒度的更新.Vue引入虚拟DOM,状态绑定的不再是具体DOM,而是一个组件.降低依赖追踪是所消耗的内存.
JS有两种变化侦测手段:
- Object.defineProperty() -VUE2
- Proxy: Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。-VUE3
这里我们主要介绍VUE2的方法:
1.defineReactive():用来将非响应式数据转换成响应式数据(被侦测的数据).
function defineReactive(data, key, val) {
Object.defineProperty(data, key, function() {
enumerable: true, // 该属性是否可被枚举
configurable: true, // 该属性的配置后续是否可进行更改
get: function () { // 用于获取属性值
return val
},
set: function (newVal) { // 设置属性值
if (newVal === val) return
// 用新值替换旧值
val = newVal
}
})
}
// 参数解释:
// data 表示定义属性的对象,key表示对象的属性,val是 要定义或修改的属性描述符
// 假如vue下有个这样的数据 cat: {name: kitty}
// 我们需要劫持cat下的name属性
// 那么只需要 defineReactive(cat, 'name', kitty) 就完成了对cat.name的数据侦测
// 访问cat.name,便会触发get函数
// 通过cat.name = 'bob'时, 就会触发set函数
如此一来 ,通过set和get函数,我们可以随时知道数据的变化了,然后通知依赖进行更新.
2.Watcher类:用于统一管理依赖.在VUE template模板中dom节点所依赖
export default class Watcher {
/*
* vm 是vue实例
* expr 是属性的key。如 data.a.b.c
* cb 是回调函数
*/
constructor (vm, expr, cb) {
this.vm = vm
this.value = this.get() // new 实例时,先存储一下oldVal
//getPathData用于解析属性路径
//data.a.b.c data 对象下的a对象下的b对象下的c属性
this.getter = getPathData(expr)
get () {
window.target = this
let value = this.getter(this.vm)
window.target = undefined
return value
}
update () {
const oldValue = this.value // 定义一个变量存储旧的值
this.value= this.getValue() // 重新获取新的值
this.cb(newVal, oldVal) // 触发回调函数,然后在回调函数中就可以做具体的更新了
}
}
}
// 这个函数主要用于解析路径,并返回一个获取属性值的函数,利用了闭包
export function getPathData(expr) {
//利用正则表达式判断是不是合法的路径
const reg = /[^\w.$/
if (!reg.test(expr)) return
//将data.a.b.c, 分解成['data', 'a', 'b', 'c']
const arr = expr.split('.')
return function (vm) {
if (!vm) return
let obj = vm // vm是vue实例
for (let i = 0; i < arr.length; i++) {
// 第一次循环,obj = vm.data 相当于vm就拿到了vue下的data对象
// 第二次循环 obj = obj['a'] 相当于就是vm.data.a的值
// 第三次循环 obj = obj['b'] 相当于vm.data.a.b的值
// 第四次循环 obj = obj['c'] 是不是就拿到了vm.data.a.b.c的值啊
obj = obj[arr[i]]
}
return obj // 返回获取到的值
}
}
Watcher类中,get方法把window.target设置成this,也就是当前的watcher实例,然后通过getter读取data.a.b.c的值,会相应的触发对应响应式数据属性的get()函数,该函数利用window.target获取依赖,并做相应的处理(详见下一部分) .当data.a.b.c发生变化时,会对依赖列表里所有的依赖循环执行update方法,通过回调函数来执行具体的更新操作.
3.收集依赖:每个数据都可能有多个依赖者,我们给每个数据都添加一个依赖的管理者Dep.这个类可以控制一个属性所对应的依赖的添加和删除,以及向依赖发送通知.
export default class Dep () {
constructor() {
this.sub = [] // 依赖者数组,用来存放依赖者watcher实例
}
addSub (sub) { // 添加依赖
this.sub.push(sub)
}
remove (sub) { // 删除依赖
remove(this.sub, sub)
}
// 通知方法,用于通知各个依赖者进行数据更新
notify () {
const arr = this.subs.slice() // 先拷贝一份
for (let i = 0; i < arr.length; i++) {
arr[i].update() // 让每一个watcher去调用自己的更新函数
}
}
}
export function remove (subs, sub) {
if (subs.length && subs.length > 0) {
const index = subs.indexOf(sub)
if (index > -1) {
return subs.splice(index, 1)
}
}
}
function defineReactive(data, key, val) {
let dep = new Dep()
Object.defineProperty(data, key, function() {
enumerable: true, // 该属性是否可被枚举
configurable: true, // 该属性的配置后续是否可进行更改
get: function () { // 用于获取属性值
// 收集依赖
if (window.target) {
dep.addSub(window.target)
}
return val
},
set: function (newVal) { // 设置属性值
if (newVal === val) return
// 用新值替换旧值
val = newVal
// 数据发生变化时,通知依赖者
dep.notify()
}
})
}
对defineReactive函数做出一些更改,在访问数据触发get方法时,获取window.target(当前watcher实例) ,将其加入Dep中进行管理.当修改数据触发set方法时,向所有和其相关的数据发送通知.
以上就是 侦测数据,收集依赖,向依赖发送通知进行更新的整条流程,但只能侦测data中的某一个属性,而我们需要将所有的属性(及其子属性)都侦测到,因此我们需要封装一个类用于将data中的所有属性都转换成响应式数据.即Observer.
export default class Observer {
// 参数就是需要被侦测的数据。首次进来参数肯定是vue的data对象
constructor (data) {
this.value = data
if (Array.isArray(this.value)) {
// 侦测数组,详见深入浅出VUEjs_变化侦测_Array
} else {
// 如果不是数组,必然就是对象
this.walk(this.value) // vue内部用来递归转换响应式数据
}
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
function defineReactive(data, key, val) {
// 新增判断当前属性值是否是对象,如果是对象,继续递归劫持
if (typeof val === 'object') {
new Obsever(val)
}
let dep = new Dep()
Object.defineProperty(data, key, function() {
enumerable: true, // 该属性是否可被枚举
configurable: true, // 该属性的配置后续是否可进行更改
get: function () { // 用于获取属性值
// 收集依赖
if (window.target) {
dep.addSub(window.target)
}
return val
},
set: function (newVal) { // 设置属性值
if (newVal === val) return
// 用新值替换旧值
val = newVal
// 数据发生变化时,通知依赖者
dep.notify()
}
})
}
定义好了Observer,要在defineReactive中新增一条判断,如果当前属性值还是object,那么我们继续new Observer(val)去递归进行侦测。
在每次new Vue()实例的时候都在其构造器里new Observer(this.$data)将其数据转换为响应式数据.
关于以上各模块关系的一个理解:
PS 关于Object的一些问题:
前面介绍了,对于Object的侦测是通过getter/setter来追踪的,而这是这一点,导致vue2无法识别一些数据变化.
当我们为对象添加或者是删除一些属性时,vue2无法侦测这个变化,因为这些操作不触发getter和setter.而ES6之前,js并没有提供元编程能力(ES6 可以通过Proxy解决),所以vue2提供了$set和$delete两个API.
本文章仅为本人学习总结,如果有不足还请各位指出!