Vue2 响应式原理简单实现
浅谈Vue2响应式实现原理
其核心思想就是为每一个数据进行劫持和监听,具体怎么做的呢?就是利用
Object.defineProperty(obj, key, descriptor)
为对象中的每一个属性添加get和set方法来进行劫持和监听,当数据被读取时,触发get方法;当数据被修改时,触发set方法。
这仅仅只是把数据劫持监听了,至于如何将修改后的数据重新渲染到页面上,这个时候就需要使用DOM操作命令将数据重新渲染到页面上。
所以在触发set方法的时候,就需要去操作和这个属性相关的方法,即这个属性的依赖
已知,当数据被读取时,触发get方法;在页面上渲染数据的时候也是读取数据的过程,所以可以在set中搜集依赖。
搜集到依赖后,在set中依次调用即可
大致实现流程
- 使用Object.defineProperty()将数据对象劫持,添加get和set
- get中搜集依赖
- set中重新执行属性相关依赖
流程示例:
添加 get set
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div class="app">
<p id="name"></p>
</div>
<script>
let obj = {
name : '稀土掘金'
}
Object.defineProperty(obj, 'name', {
get: ()=>{
console.log(`name被读取了`)
},
set: (val)=>{
console.log(`name的值被修改为${val}`)
}
})
let str = obj.name // 将 obj.name 的值赋值给新的变量 str 则触发 get
// 执行到这 控制台打印 name被读取了
obj.name = '稀土掘金1' // 改变了 obj.name 的值
// 执行到这 控制台打印 name的值被修改为稀土掘金1
</script>
</body>
</html>

到这一步,仅仅只是在控制台看到了结果,那么如何将结果渲染到页面上呢?
js
// 先写一个方法,用来将数据渲染到页面
function showName() {
document.querySelector('#name').textContent = `渲染结果:${obj.name}`
}
showName()
理所应当,这个时候应该在屏幕上渲染出来 渲染结果:稀土掘金
但是渲染结果是这样的:

MDN
中关于Object.defineProperty()
的get方法是这样介绍的:get方法的返回值将被用做该属性的值 ,默认值为undefined
首先肯定不能在get内部直接返回 obj.name
,在内部返回renturn obj.name
仍然表示你在读取它,就导致get重复调用
完善 添加get返回值
所以代码可以改写成下面这个形式
js
let obj = {
name: '稀土掘金'
}
let internalValue = obj.name
Object.defineProperty(obj, 'name', {
get: ()=>{
console.log(`name被读取了`)
return internalValue
},
set: (val)=>{
internalValue = val // 下一次读取的时候就是读的新值
console.log(`name的值被修改为${val}`)
}
})
// let str = obj.name // 将 obj.name 的值赋值给新的变量 str 则触发 get
// 执行到这 控制台打印 name被读取了
// obj.name = '稀土掘金' // 改变了 obj.name 的值
// 执行到这 控制台打印 name的值被修改为稀土掘金1
function showName() {
document.querySelector('#name').textContent = `渲染结果:${obj.name}`
}
showName()
这个时候正常渲染,页面展示:渲染结果:稀土掘金
为对象多个属性添加set get
一般的 一个对象里面可能会有多个属性,那么可以代码改为如下:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div class="app">
<p id="name"></p>
<p id="time"></p>
</div>
<script>
let obj = {
name: "稀土掘金",
time: "2023.11.01",
}
observe(obj)
// 为对象每一个属性添加get set
function observe(obj) {
for (const key in obj) {
let internalValue = obj[key]
Object.defineProperty(obj, key, {
get: () => {
console.log(`${key}被读取了,读取的值为:${internalValue}`)
return internalValue
},
set: (val) => {
internalValue = val // 下一次读取的时候就是读的新值
console.log(`${key}的值被修改为${val}`)
},
})
}
}
function showName() {
document.querySelector("#name").textContent = `渲染结果:${obj.name}`
}
function showTime() {
document.querySelector("#time").textContent = `时间:${obj.time}`
}
showName()
showTime()
</script>
</body>
</html>
到这个时候,重新修改属性值obj.name = '稀土掘金2'
set触发 打印name的值被修改为稀土掘金2
然后再重新执行一次showName()
, 此时页面 渲染结果:稀土掘金2
,同时set处打印出name被读取了,读取的值为:稀土掘金2
js
obj.name = '稀土掘金2' // log name的值被修改为稀土掘金2
showName() // log name被读取了,读取的值为:稀土掘金2
当然,可以把showName()
放在set中执行
js
set: (val) => {
internalValue = val // 下一次读取的时候就是读的新值
showName()
console.log(`name的值被修改为${val}`)
},
依赖挂载全局
这样的话代码就写死了,所以这个时候就需要将依赖收集起来,也就是将和这个属性相关的方法收集起来
js
// 将依赖挂载到全局对象上, 比如挂载到window上
window.__func = showName()
// 执行依赖
showName() // 这一步的时候就已经触发了 get 所以可以在get中收集一下当前依赖
// 清空 __func
window.__func = null
逻辑有了,那么就封装一下该流程
js
function autorun(fn){
window.__func = fn
fn()
window.__func = null
}
// 后续执行就可以直接
autorun(showName)
autorun(...)
在get中搜集依赖 get中使用
接下来就需要在get中收集依赖、set中使用依赖
js
function observe(obj) {
for (const key in obj) {
let funs = new Set() // 创建一个数组用于收集依赖 一个方法可能被执行多次
let internalValue = obj[key]
Object.defineProperty(obj, key, {
get: () => {
if (window.__func) { // 避免 null 的影响
funs.add(window.__func)
}
console.log(`${key}被读取了,读取的值为:${internalValue}`)
return internalValue
},
set: (val) => {
internalValue = val // 下一次读取的时候就是读的新值
// 使用依赖
for (const key of funs.keys()) {
key()
}
console.log(`name的值被修改为${val}`)
},
})
}
}
说明
上述只是非常非常简单的演示了一下vue2的响应式原理,实际使用中,对于一些深层嵌套对象是,还需要递归去逐层添加get和set。有兴趣的可以去看一下源码,源码地址在下方。
附:源码+个人解读
这段地址是Vue2响应式核心实现
这里主要分为5个主要部分
Observer
类:Observer
主要结合defineReactive
将数据对象转化为响应式对象。使用Object.defineProperty
来劫持属性的读取和写入操作也是在defineReactive
方法中进行(index.ts)Dep
类: 用于管理依赖于数据的组件或表达式。Dep(dep.ts)getter和setter
: 也就是Object.defineProperty
的get
和set
用于在属性读取和写入时触发依赖的更新。当一个属性被访问/读取时,它会将当前Watcher
添加到该属性的依赖列表中。当属性被修改时,它会通知所有依赖于该属性的Watcher
执行更新操作。Watcher类
:观察者,它会订阅一个或多个数据的变化,当数据变化时,Watcher
会执行相应的回调函数。在Vue中,组件的渲染过程本质上就是一个Watcher
。(watcher.ts)- 还有一个数组响应式:通过重写数组的变异方法(例
push
、pop
、shift
、unshift
等)来实现数组的响应式。当这些方法被调用时,Vue就会派发更新。