本文的目的是掌握 Vue2 中的 响应式原理,学习的过程是手写一个简易版的
Vue。从数据劫持,到模板编译,再到依赖收集,完完整整的自己实现整套数据响应式的流程。
一个最基础的响应式
实现一个属性的响应式
我们首先封装一个响应式处理的方法 defineReactive
,通过 defineProperty
这个方法重新定义对象属性的 get
和 set
描述符,来实现对数据的劫持,每次 读取数据 的时候都会触发 get
,每次 更新数据 的时候都会触发 set
,所以我们可以在 set
中触发更新视图的方法 update
来实现一个基本的响应式处理。
/**
* @param {*} obj 目标对象
* @param {*} key 目标对象的一个属性
* @param {*} val 目标对象的一个属性的初始值
*/
function defineReactive(obj, key, val) {
// 通过该方法拦截数据
Object.defineProperty(obj, key, {
// 读取数据的时候会走这里
get() {
console.log('🚀🚀~ get:', key);
return val
},
// 更新数据的时候会走这里
set(newVal) {
// 只有当新值和旧值不同的时候 才会触发重新赋值操作
if (newVal !== val) {
console.log('🚀🚀~ set:', key);
val = newVal
// 这里是触发视图更新的地方
update()
}
}
})
}
我们写点代码来测试一下,每 1s 修改一次 obj.foo 的值 , 并定义一个 update 方法来修改 app 节点的内容。
// html
<div id='app'>123</div>
// js
// 劫持 obj.foo 属性
const obj = {}
defineReactive(obj, 'foo', '')
// 给 obj.foo 一个初始值
obj.foo = new Date().toLocaleTimeString()
// 定时器修改 obj.foo
setInterval(() => {
obj.foo = new Date().toLocaleTimeString()
}, 1000)
// 更新视图
function update() {
app.innerHTML = obj.foo
}
可以看到,每次修改 obj.foo 的时候,都会触发我们定义的 get 和 set ,并调用 update 方法更新了视图,到这里,一个最简单的响应式处理就完成了。
处理深层次的嵌套
一个对象通常情况下不止一个属性,所以当我们要给每个属性添加响应式的时候,就需要遍历这个对象的所有属性,给每个 key 调用 defineReactive 进行处理。
/**
* @param {*} obj 目标对象
*/
function observe(obj) {
// 先判断类型, 响应式处理的目标一定要是个对象类型
if (typeof obj !== 'object' || obj === null) {
return
}
// 遍历 obj, 对 obj 的每个属性进行响应式处理
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
// 定义对象 obj
const obj = {
foo: 'foo',
bar: 'bar',
friend: {
name: 'aa'
}
}
// 访问 obj 的属性 , foo 和 bar 都被劫持到,就不在浏览器演示了。
obj.bar = 'barrrrrrrr' // => 🚀🚀~ set: bar
obj.foo = 'fooooooooo' // => 🚀🚀~ set: foo
// 访问 obj 的属性 obj.friend.name
obj.friend.name = 'bb' // => 🚀🚀~ get: friend
当我们访问 obj.friend.name 的时候,也只是打印出来 get: friend ,而不是 friend.name , 所以我们还要进行个 递归,把 深层次的属性 同样也做响应式处理。
function defineReactive(obj, key, val) {
// 递归
observe(val)
// 继续执行 Object.defineProperty...
Object.defineProperty(obj, key, {
... ...
})
}
// 再次访问 obj.friend.name
obj.friend.name = 'bb' // => 🚀🚀~ set: name
递归的时机在 defineReactive 这个方法中,如果 value 是对象就进行递归,如果不是对象直接返回,继续执行下面的代码,保证 obj 中嵌套的属性都进行响应式的处理,所以当我们再次访问 obj.friend.name 的时候,就打印出了 set: name 。