响应式原理
vue2.x响应式,借助es5的Object.defineProperty(obj, key, {})实现响应式监听。
响应式:就是更改某个数据变量之后,使得视图也进行相应的重新渲染;这就需要监听数据的变化,拦截变化并做重新渲染处理等操作。
基本原理
let data = {}
let name = '张三'
Object.defineProperty(data, 'name', {
get: function(){
console.log('get')
return name
},
set: function(newValue){
console.log('set')
name = newValue
// 视图重新渲染
}
})
console.log(data.name) // 触发get拦截,打印张三
data.name = '李四' // 触发set拦截,在set中进行了视图重新渲染
console.log(data.name) // 触发get拦截,打印李四
响应式原理
let person = {
name: '张三',
age: 18,
habby: {
ball: '羽毛球',
sing: '唱歌'
},
colors: ['red', 'blue', 'pink']
}
//! 数组方法响应式思想:克隆数组原型上的方法,进行适当修改
const protoMethod = Array.prototype
const protoTemp = Object.create(protoMethod)
let methodList = ['push', 'pop', 'reserve', 'shift']
methodList.forEach(method => {
protoTemp[method] = function(){
protoMethod[method].call(this, ...arguments)
renderView()
}
})
observer(person)
function observer(target) {
// 如果是基本类型直接返回
if (typeof target !== 'object' || typeof target === null) {
return target
}
// 如果是数组
if (Array.isArray(target)){
target.__proto__ = protoTemp
}
// 如果是正常对象
for (let key in target) {
defineReactive(target, key, target[key])
}
}
function defineReactive(target, key, value) {
// 深度监听 直到不是一个对象为止(防止value还是个对象。如person.habby)
// 如果初始数据结构嵌套很深很复杂,则初次渲染可能出现卡死,因为这里一上来就直接进行递归深度监听下去,直到最深层的普通值为止(这个问题vue3中proxy解决,只有到使用到数据时才会做相应的监听)
observer(value)
// 响应式重新定义
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
// 深度监听(防止属性被外界又赋值为对象 person.age = {num:10})
observer(newValue)
if (value !== newValue) {
value = newValue
renderView()
}
}
})
}
function renderView(){
console.log('视图更新')
}
person.age = { num: 10 } //视图更新
person.age.num = 20 //视图更新
person.colors.push('white') //视图更新
对象响应式缺点:
-
无法监听对象属性的
新增和删除(vue中可以通过Vue.delete和Vue.set进行对象属性新增和删除的响应式。) -
如果person的数据结构嵌套层次很深,则首次渲染会很耗时,有可能卡死。(因为一上来就直接进行递归深度监听下去,直到最深层的普通值为止(这个问题vue3中proxy解决,只有到使用到数据时才会做相应的监听))
数组响应式缺点:
- 数组通过下标更改数据不具有响应式(vue中可以通过Vue.set进行强制响应式)
- 数组修改length不具有响应式
心得:重写函数时,外部函数给内部函数传参,可以直接通过具名参数传入,也可以通过绑定内部函数的this+arguments传入
依赖收集
虽然可以监听到数据的变化了,那我们怎么处理通知视图就更新呢?
Dep就是帮我们收集【究竟要通知到哪里的】。比如下面的代码案例,我们发现,虽然data中有text和message属性,但是只有message被渲染到页面上,至于text无论怎么变化都影响不到视图的展示,因此我们仅仅对message进行收集即可,可以避免一些无用的工作。
那这个时候message的Dep就收集到了一个依赖,这个依赖就是用来管理data中message变化的。
<div>
<p>{{message}}</p>
</div>
data: {
text: 'hello world',
message: 'hello vue',
}
当使用watch属性时,也就是开发者自定义的监听某个data中属性的变化。比如监听message的变化,message变化时我们就要通知到watch这个钩子,让它去执行回调函数。
这个时候message的Dep就收集到了两个依赖,第二个依赖就是用来管理watch中message变化的。
watch: {
message: function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
},
}
当开发者自定义computed计算属性时,如下messageT属性,是依赖message的变化的。因此message变化时我们也要通知到computed,让它去执行回调函数。
这个时候message的Dep就收集到了三个依赖,这个依赖就是用来管理computed中message变化的。
computed: {
messageT() {
return this.message + '!';
}
}
如何收集依赖?
我们如何知道data中的某个属性被使用了,答案就是Object.defineProperty,因为读取某个属性就会触发get方法。可以将代码进行如下改造:
function defineReactive (obj, key, val) {
let Dep; // 依赖
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
console.log('我被读了,我要不要做点什么好?');
// 被读取了,将这个依赖收集起来
Dep.depend(); // 本次新增
return val;
},
set: newVal => {
if (val === newVal) {
return;
}
val = newVal;
// 被改变了,通知依赖去更新
Dep.notify(); // 本次新增
console.log("数据被改变了,我要把新的值渲染到页面上去!");
}
})
}