Vue $set 分析
提问
Vue 2.x 里劫持对象仍用 Object.defineProperty() 方法,受此方法的限制,Vue 无法检测到直接数组更改(mobx 旧版本也有过这毛病,他对数组0到999项的内容都现实地 Object.defineProperty 监听了),和对象属性的添加或删除。
举个例子,运行下面代码,能看到点击 changeArr 按钮,页面没有变化,而点击 setArr 按钮,页面上显示的数据就变了。
<!DOCTYPE html>
<html>
<head>
<title>Form Demo</title>
</head>
<body>
<div id="app">
<div>{{arr[0]}}</div>
<button @click="changeArr">changeArr</button>
<button @click="setArr">setArr</button>
</div>
<!-- Vue.js v2.6.11 -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
var app = new Vue({
el: '#app',
data: {
arr: [1, 2, 3, 4]
},
methods: {
changeArr() {
this.arr[0] = 11;
},
setArr() {
Vue.set(this.arr, 0, 12);
}
}
})
console.log(app);
</script>
</body>
</html>
那么 Vue 内部是如何解决对象新增属性不能响应的问题的呢?
结论
- 如果目标是数组,使用 vue 实现的变异方法 splice 实现响应式
- 如果目标是对象,判断属性存在,即为响应式,直接赋值
- 如果 target 本身就不是响应式,直接赋值
- 如果属性不是响应式,则调用 defineReactive 方法进行响应式处理,并通知被观察者 ob.dep.notify()
附录
Vue 2.6.2 源码
/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
*/
export function set (target: Array<any> | Object, key: any, val: any): any {
if (Array.isArray(target) && typeof key === 'number') {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
/*如果是一个对象,并且已经存在了这个key则直接返回*/
if (hasOwn(target, key)) {
target[key] = val
return val
}
/*获得target的Oberver实例*/
const ob = (target : any).__ob__
/*
_isVue 一个防止vm实例自身被观察的标志位 ,_isVue为true则代表vm实例,也就是this
vmCount判断是否为根节点,存在则代表是data的根节点,Vue 不允许在已经创建的实例上动态添加新的根级响应式属性(root-level reactive property)
*/
if (target._isVue || (ob && ob.vmCount)) {
/*
Vue 不允许在已经创建的实例上动态添加新的根级响应式属性(root-level reactive property)。
https://cn.vuejs.org/v2/guide/reactivity.html#变化检测问题
*/
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if (!ob) {
target[key] = val
return val
}
/*为对象defineProperty上在变化时通知的属性*/
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}