引用官方的一段话:
Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。
这里的非侵入式与侵入式都是框架功能的设计模式。
什么是变化侦测?
Vue.js会自动通过状态生成DOM,并将其输出到页面上显示,这个过程叫渲染。整个渲染过程是声明式的,我们可以通过模板来描述状态(数据)与DOM之间的映射关系。但是,在运行时应用内部状态不断发生变化,对应页面就需要重新渲染。变化侦测就是来解决如何确定状态中发生了什么变化的问题。当状态发生变化时,vue.js立刻就知道了,而且在一定程度上知道哪些状态变了。因此它知道的信息更多,也就可以进行更细粒度的更新(假如有一个状态绑定着好多个依赖, 每个依赖表示一个具体的DOM节点,那么当这个状态发生变化时,向这个状态的所有依赖发送通知,让它们进行DOM更新操作)。
如何追踪数据变化?
我们先回答这个问题:在JavaScript中,如何侦测一个对象的变化?其实学过JS的人都知道,有两种方法可以侦测数据的变化:(1)使用Object.defineProperty (2)ES6的Proxy
由于ES6在浏览器中的支持度并不理想,Vue2.x采用Object.defineProperty来实现的。而到Vue3中,尤雨溪使用Proxy重写了这部分代码。
两者同宗同源,原理和思想是不会变的。
数据劫持
指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。
const obj = {}
let val = 1
Object.defineProperty(obj, 'a', {
get() { // getter
console.log('get property a')
return val
},
set(newVal) { // setter
if (val === newVal) return
console.log(`set property a -> ${newVal}`)
val = newVal
}
})
console.log(obj.a); // 1
obj.a = 999
console.log(obj.a); // 999
当我们访问obj.a
时,打印get property a
并返回1,obj.a = 999
设置新值时,打印set property a -> 999
。这相当于我们自定义了obj.a
取值和赋值的行为,使用自定义的getter
和setter
来重写了原有的行为,这也就是数据劫持
的含义。但不足的是需要一个全局变量来保存这个属性a的值,所以我们可以写一个封装函数:
// value使用了参数默认值
function defineReactive(data, key, value = data[key]) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
return value
},
set: function reactiveSetter(newValue) {
if (newValue === value) return
value = newValue
}
})
}
defineReactive(obj, a, 1)
属性描述符的两种主要形式:数据描述符和存取描述符。一个描述符只能是这两者其中之一;不能同时是两者。
上面的defineReactive函数作用是定义一个响应式数据,进行变化追踪。只需传入data,key就行,每当从data的key中读取数据时,get函数被触发;每当往data的key中设置数据时,set函数被触发。
如何收集依赖?
如果只是对Object.defineProperty进行封装,那其实并没有实际用处。我们思考一下,之所以有观察数据的需要,目的就是当数据的属性发生变化时,能够及时通知那些曾经使用了该数据的地方。
举个例子:
<template>
<h1>{{ name }}</h1>
</template>
该模板中使用了数据name,所以当它发生变化时,要向使用它的地方发消息。
在Vue2.0中,模板等同于组件,所以当数据发生变化时,会通知相应的组件,然后组件内部再通过虚拟DOM重新渲染。
针对这个问题,最重要的是先收集依赖,即把用到数据name的地方收集起来,然后等数据发生变化时,把之前收集好的依赖循环触发一遍就OK了。一句话就是,在getter中收集依赖,在setter中触发依赖。
依赖收集在哪里?
既然收集地点是getter中,那么该用什么器皿去盛呢?先分析一波,我们自然想到的是用一个数组,去存储当前对象key的依赖。假设依赖是一个函数,保存在window.target上,现在把defineReactive函数稍微加工一下:
function defineReactive(data, key, value = data[key]) {
let dep = [] // 新增
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
dep.push(window.target) // 新增
return value
},
set: function reactiveSetter(newValue) {
if (newValue === value) return
// 新增
for(let i = 0; i < dep.length; i++) {
dep[i](newValue, val)
}
value = newValue
}
})
}
这里我们新增了数组 dep ,用来存储被收集的依赖。 然后在set 被触发时,循环 dep 以触发收集到的依赖。但是这样写有点耦合,我们把依赖收集的代码封装成一个 Dep 类,它
未完待续。。。
参考:
Object.defineProperty() - JavaScript | MDN