本篇主要剖析监听原理,没有详细的收集依赖逻辑,这个有待后续补充。
普通版
var data = {
name:'AC',
age: 18,
obj: { a: '111' },
arr: [1, 2, 3]
}
// 重新定义属性,监听起来
function defineReactive(target, key, value) {
Object.defineProperty(target, key, {
get() {
console.log('get了')
return value
},
set(newVal) {
// value 一直在闭包中,此处设置完成后,下次get能够获取最新设置的值
// 这里有个小优化,若相同则不触发更新
console.log('set了')
if (newVal !== value) {
value = newVal
// 触发更新
}
}
})
}
// 监听对象属性
function observe(target) {
if (typeof target !== 'object' || target === null) {
// 不是数组或对象不适合监听
return target
}
// 将对象的属性用 defineProperty 重新定义
for (let key in target) {
defineReactive(target, key, target[key])
}
}
observe(data)
对于普通属性来说正常
对于对象里面增添属性:可见并不是响应式
移除也是一样的:delete会把其getter与setter都删掉
可以看出对于增添的属性不具备响应式
对于嵌套对象与数组:
对其的修改setter根本没法捕获,都只触发外层的get
注意,defineProperty本身可以监听数组的变化,只不过由于性能问题,没采用罢了
实现对象深度监听
其实只用在定义响应式函数defineReactive中增添一个递归嵌套
var data = {
name:'AC',
age: 18,
obj: { a: '111' },
arr: [1, 2, 3]
}
// 重新定义属性,监听起来
function defineReactive(target, key, value) {
observe(value);//增加了对vulue的判断
Object.defineProperty(target, key, {
get() {
console.log('get了')
return value
},
set(newVal) {
// value 一直在闭包中,此处设置完成后,下次get能够获取最新设置的值
// 这里有个小优化,若相同则不触发更新
console.log('set了')
if (newVal !== value) {
value = newVal
// 触发更新
}
}
})
}
// 监听对象属性
function observe(target) {
if (typeof target !== 'object' || target === null) {
// 不是数组或对象不适合监听
return target
}
// 将对象的属性用 defineProperty 重新定义
for (let key in target) {
defineReactive(target, key, target[key])
}
}
observe(data)
可以看到这样就能解决嵌套对象的响应式问题(其实这样测试也能实现对数组的监视,但是vue中因为性能问题是不可以的)
实现后添加的属性具有响应式
Vue.set()或者this.$set()操作。但是**,Vue 不允许动态添加根级别的响应式 property**,因此只能用这些方法向嵌套对象添加响应式。
eg:
var vm = new Vue({
el: '#app',
data: {
product: {id:1, name:"uv", age:18} // 将变量赋值给Vue实例的 data 对象
}
});
vm.product.a='111'
console.log(vm);
但是用到vue.set:
var vm = new Vue({
el: '#app',
data: {
product: {id:1, name:"uv", age:18} // 将变量赋值给Vue实例的 data 对象
},
// created() {
// this.$set(this.product,'a',123)
// }
});
Vue.set(vm.product,'a',123)
console.log(vm);
实现数组监听
定义监听数组的原型
// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向oldArrayProperty
const arrProperty = Object.create(oldArrayProperty)
// 重写原型上的方法(可以所有都重写,这里只进行少量举例)
// arrProperty.push = function(){}
// arrProperty.pop = function(){}
// 优化写法
const methods = ['push','pop','shift','unshift','splice']
methods.forEach(method => {
arrProperty[method] = function(){
updateView() 加入触发更新的机制
Array.prototype[method].call(this, ...arguments) //执行原方法一次
}
})
将需要监听的数组原型 指向自定义的原型
对于vue里只有数组中7种API支持响应式变化:pop/push/shift/unshift/splice/sort/reverse
其原理就是对原本的Array.xxx方法进行包装,除了保留原功能,还添加了对页面的响应(重新解析模板、生成虚拟DOM、对比…)
// 监听对象属性
function observe(target){
if(typeof target !== 'object' || target === null) {
// 不是数组或对象
return target
}
// 如果是数组则修改该数组的原型
if(Array.isArray(target)){
target.__proto__ = arrProperty
return
}
// 重新定义属性
for(let key in target) {
defineReactive(target, key, target[key])
}
}
// 测试数据
const data = {
myCars: ['Bugatti','Koenigsegg']
}
// 监听数据
observe(data)
// 测试
data.myCars.push('AE86')
PS:想要改变数组实现响应式还可以用上面的Vue.set(xxx,index,‘想要改变的内容’)
图解Vue2响应式
上述都是仿vue响应式原理自己实现的伪响应式,实际vue实现原理肯定比这个复杂得多,那么vue2响应式到底是怎么实现的呢?
从源码的角度Vue2响应式分为这几步:
- 从 new Vue 开始,首先通过 get、set 监听 Data 中的数据变化,同时创建 Dep 用来搜集使用该 Data 的 Watcher。
- 编译模板,创建 Watcher,并将 Dep.target 标识为当前 Watcher。
- 编译模板时,如果使用到了 Data 中的数据,就会触发 Data 的 get 方法,然后调用 Dep.addSub 将 Watcher 搜集起来。
- 数据更新时,会触发 Data 的 set 方法,然后调用 Dep.notify 通知所有使用到该 Data 的 Watcher 去更新 DOM。