写在前面
vue2 的数据响应式已经非常成熟且过时了,但是相信很多人还是对原理的东西一知半解,甚至还是不知道他究竟是怎么实现的,今天我们就试着一步一步分析看看响应式需要解决哪些问题,具体的问题难点是什么?
数据响应式
数据响应式就做了两件事,第一就是数据变化通知函数,第二就是函数进行视图也就是页面的变化 所以数据响应式就是数据变化引起视图更新
实现一个数据响应式需要具备的条件
- 需要一个方法设置数据变化的时候映射到页面
- 需要一个方法数据变化要可以及时调对应的方法
难点是什么?
- 怎么知道数据变化了?
- 数据变化之后怎么知道是哪个方法该更新?
实现第一件事
const data = { name: 'tom', age: 16 }
// TODO: 设置数据变化映射到页面
function setName() {
const name = document.getElementById('name')
name.innerHTML = data.name
}
function setAge() {
const age = document.getElementById('age')
age.innerHTML = data.age
}
实现第二件事
setName()
setAge()
// TODO: 两秒之后继续执行 测试页面更新的情况
setTimeout(() => {
data.age = 22
data.name = "JIM"
setName()
setAge()
}, 2000)
这两件事单独做都很简单,甚至没有任何的技术难度,那么问题就是我们怎么知道数据变化了?
解决怎么知道数据变化了的问题
Object.defineProperty
这玩意不就可以监测到吗?这个东西不仅仅可以给对象设置新的属性和设置属性的一些属性,同时他也可以知道对象的变化,变化之后调用一下方法不就好了吗?有方法了事情就好办了,直接开整
let __FV = data.name
Object.defineProperty(data, 'name', {
get: function () {
console.log('🚀 调用了 get()')
return __FV
},
set: function (V) {
console.log(`🚀 数据及时更新,更新的结果是:${V}`)
__FV = V
// TODO: 及时更新页面数据
setName()
setAge()
}
})
和预期是一致的:
怎么知道是哪些方法需要调用呢?
现在是因为只有两个方法,所以是只需要分别调用一次就好了,但是如果方法很多的时候,或者是用户只更新了年龄,没有更新姓名,你再直接全部更新就不太好了,
我们现在怎么可以知道哪些属性对应的是哪些方法呢?细心一点就会发现,我们每次调用属性的时候,get的方法就一定会执行,那么既然他执行了,是不是他就可以知道是谁调用了他呢?
这个时候我们会发现我们即使知道了有方法调用他,也还是一样没有办法具体知道是哪一个方法调用了他,这个时候我们就像,是不是可以设置一个全局的方法,将所有的方法属性都挂载上去
那么调用之前挂载上去,然后他只要被调用,就给一个数组里面塞一条函数进去,这样的话,在 set 的时候将这些方法全部执行一遍不就好了吗?
startObserve() // TODO: 开始收集数据变化会用到的方法
// TODO: 将方法挂到全局的 window 上
window.__ADDFUNC = setName
setName()
window.__ADDFUNC = null
window.__ADDFUNC = setAge
setAge()
window.__ADDFUNC = null
function startObserve(){
for (const key in data) {
let __FV = data[key]
let dependentOns = new Set()
Object.defineProperty(data, key, {
get: function () {
console.log('🚀 调用了 get()')
if (window.__ADDFUNC) { dependentOns.add(window.__ADDFUNC) } // TODO: 依赖收集
return __FV
},
set: function (V) {
console.log(`🚀 数据及时更新,更新的结果是:${V}`)
__FV = V
// TODO: 及时更新页面数据
Array.from(dependentOns).forEach(dependentOn => { dependentOn() }) // TODO: 任务派发执行
}
})
}
}
这样的话 问题基本上就全部解决了,我们只需要将该封装的一部分数据封装起来就可以了,比如所有的方法应该统一进行处理运行,下面是封装之后的代码
// TODO: 实现一个数据响应式 源数据 FILENAME: observer.js
const data = { name: 'tom', age: 16 }
observer(data) // TODO: 观察数据
const methods = {
setName: () => {
const name = document.getElementById('name')
name.innerHTML = data.name
},
setAge: () => {
const age = document.getElementById('age')
age.innerHTML = data.age
}
}
// TODO: 将执行的方法使用全局的函数进行包装,依赖收集的时候可以直接使用
function collectionMethodRun(methods) {
for (const fn in methods) {
window.OVERALLSITUATION = methods[fn]
methods[fn]()
window.OVERALLSITUATION = null
}
}
collectionMethodRun(methods) // TODO: 收集方法并执行
// 观察当前的对象信息的变化
function observer(obj) {
for (const key in obj) {
let __FV = obj[key]
let dependentOns = new Set() // TODO: 依赖收集 避免收集到重复的依赖
Object.defineProperty(obj, key, {
get: function () {
if (window.OVERALLSITUATION) { dependentOns.add(window.OVERALLSITUATION) } // TODO: 依赖收集
return __FV
},
set: function (V) {
__FV = V
Array.from(dependentOns).forEach(dependentOn => { dependentOn() }) // TODO: 任务派发执行
}
})
}
}
测试一下
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<p id="name"></p>
<p id="age"></p>
<script src="./observer.js"></script>
<span>改变名字:</span>
<input type="text" oninput="data.name = this.value">
<span>改变年龄:</span>
<input type="number" min="0" oninput="data.age = this.value">
</body>
</html>
写在后面
通过上面的分析我们可以发现,其实很多看起来很复杂的问题,只要将问题分解成一步一步的,挨个击破即可,不过我还是觉得自己写一遍是比较重要的,不然你看懂了自己写的时候会发现会有很多自己不太理解的地方,今天就这样吧,拜拜