介绍
Vue 通过 Object.defineProperty 的 getter/setter 对收集的依赖项进行监听,在属性被访问和修改时通知变化,进而更新视图数据;
受现代JavaScript 的限制 (以及废弃 Object.observe),Vue不能检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化过程,所以属性必须在 options 的 data 对象上存在才能让Vue转换它,这样才能让它是响应的。
getter/setter
getter:
get语法将对象属性绑定到查询该属性时将被调用的函数。
setter: 当尝试设置属性时,
set语法将对象属性绑定到要调用的函数。
Object.defineProperty
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
Vue 做了哪些事情?
先来看一段代码:
let data0 = {
n: 0
}
// 需求一:用 Object.defineProperty 定义 n
let data1 = {}
Object.defineProperty(data1, 'n', {
value: 0
})
console.log(`需求一:${data1.n}`)
// 总结:这煞笔语法把事情搞复杂了?非也,继续看。
// 需求二:n 不能小于 0
// 即 data2.n = -1 应该无效,但 data2.n = 1 有效
let data2 = {}
data2._n = 0 // _n 用来偷偷存储 n 的值
Object.defineProperty(data2, 'n', {
get(){
return this._n
},
set(value){
if(value < 0) return
this._n = value
}
})
console.log(`需求二:${data2.n}`)
data2.n = -1
console.log(`需求二:${data2.n} 设置为 -1 失败`)
data2.n = 1
console.log(`需求二:${data2.n} 设置为 1 成功`)
// 抬杠:那如果对方直接使用 data2._n 呢?
// 算你狠
// 需求三:使用代理
let data3 = proxy({ data:{n:0} }) // 括号里是匿名对象,无法访问
function proxy({data}/* 解构赋值,别TM老问 */){
const obj = {}
// 这里的 'n' 写死了,理论上应该遍历 data 的所有 key,这里做了简化
// 因为我怕你们看不懂
Object.defineProperty(obj, 'n', {
get(){
return data.n
},
set(value){
if(value<0)return
data.n = value
}
})
return obj // obj 就是代理
}
// data3 就是 obj
console.log(`需求三:${data3.n}`)
data3.n = -1
console.log(`需求三:${data3.n},设置为 -1 失败`)
data3.n = 1
console.log(`需求三:${data3.n},设置为 1 成功`)
// 杠精你还有话说吗?
// 杠精说有!你看下面代码
// 需求四
let myData = {n:0}
let data4 = proxy({ data:myData }) // 括号里是匿名对象,无法访问
// data3 就是 obj
console.log(`杠精:${data4.n}`)
myData.n = -1
console.log(`杠精:${data4.n},设置为 -1 失败了吗!?`)
// 我现在改 myData,是不是还能改?!你奈我何
// 艹,算你狠
// 需求五:就算用户擅自修改 myData,也要拦截他
let myData5 = {n:0}
let data5 = proxy2({ data:myData5 }) // 括号里是匿名对象,无法访问
// vm = new Vue({data: {...}}) // 是不是很像
function proxy2({data}/* 解构赋值 */){
// 这里的 'n' 写死了,理论上应该遍历 data 的所有 key,这里做了简化
let value = data.n
Object.defineProperty(data, 'n', {
get(){
return value
},
set(newValue){
if(newValue<0)return
value = newValue
}
})
// 就加了上面几句,这几句话会监听 data
const obj = {}
Object.defineProperty(obj, 'n', {
get(){
return data.n
},
set(value){
if(value<0)return//这句话多余了
data.n = value
}
})
return obj
// 这段代码为 data 添加了代理功能,obj 就是代理
}
// data3 就是 obj
console.log(`需求五:${data5.n}`)
myData5.n = -1
console.log(`需求五:${data5.n},设置为 -1 失败了`)
myData5.n = 1
console.log(`需求五:${data5.n},设置为 1 成功了`)
// 这代码看着眼熟吗?
// let data5 = proxy2({ data:myData5 })
// let vm = new Vue({data: myData})
// 现在我们可以说说 new Vue 做了什么了
所以我们现在知道了 Vue 在初始化的时候做了两件事情:
- 让 vm 成为 data 的代理
- 对 data 的所有属性进行监听
至此,data 的任何改变 Vue 都可以响应了,而 Vue 帮我们在响应式数据改变的时候重新渲染UI,也就是执行 render 的过程。
Vue 监听 data 的一些问题
如果不声明就使用,Vue 会在控制台给出一个警告
new Vue({
data: {},
template: `
<div>{{n}}</div>
`
}).$mount("#app");
但是如果写成这样,就不会提示警告,可以看出 Vue 只会检查第一层属性
new Vue({
data: {
obj: {
a: 0
}
},
template: `
<div>
{{obj.b}}
<button @click="setB">set b</button>
</div>
`,
methods: {
setB() {
this.obj.b = 1;
}
}
}).$mount("#app");
而为了解决新增属性不会监听的问题,Vue 又提供 Vue.set 方法来添加属性监听
Vue.set(this.obj, 'b', 1)
// or this.$set
Vue 3 对响应式原理的实现
因为 Vue 2 不能监测到对象属性的添加或删除,所以 Vue 3 的响应式部分重新基于 Proxy 的 observer 实现,它可以提供覆盖语言 (JavaScript) 全范围的响应式能力,消除了当前 Vue 2 基于 Object.defineProperty 所存在的一些局限,这些局限包括:
- 对属性的添加、删除动作的监测;
- 对数组基于下标的修改、对 .length 修改的监测;
- 对 Map、Set、WeakMap 和 WeakSet 的支持;
Proxy
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
Vue 3 如何建立响应式
Vue 3 建立响应式的方法有两种:
- 第一个就是运用 Composition API 中的
reactive直接构建响应式 - 第二个就是用传统的
options: {data: return {}}形式