简言
这次我们来浅析下vue3的响应性原理。
话不多说,不玩虚的,let’s go! ——ZSK666
观察者模式
在探究vue3的响应性原理之前,我们来了解一种设计模式:观察者模式。有人也会叫它发布-订阅者模式,反正设计原则都一样,是一对多的处理方式,当多个事物(观察者)依赖一个事物(被观察者)时,它们就建立了一对多的关系,那一个事物(被观察者)改变,依赖它的多个事物(观察者)也得发生改变,如果观察者很多的时候,会比较混乱,需要一个有人对它们的依赖关系进行管理调度(管理中心)。
下面是简单实现:
<template>
<div>
<h1>{{ a }}</h1>
<div>{{ a }} + {{ b }} = {{ result }}</div>
</div>
</template>
<script lang="ts">
interface watch {
data: Array<Function>
push: Function
update: Function
}
class center implements watch {
// 存储要触发的事件
data = [] as Array<Function>
constructor() {}
push(fn: Function) {
this.data.push(fn)
}
update() {
this.data.forEach((fn) => fn())
}
}
// 简单使用
let watchCenter = new center()
let a = 1
let b = 1
let result = a + b
//先改变a
watchCenter.push(() => {
a = 2
})
// 然后将result重新算一遍
watchCenter.push(() => {
result = a + b
})
// 更新
watchCenter.update()
vue2的响应性
上面的设计模式好用是好用,但是总是自己调用太麻烦了。有没有更改值后自己调用的?哎,正好有,那就是Object.defineProperty()。Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。这个方法支持修改对象的get和set方法,如果我们在get方法,推送到数组中,set时更新数组;就达到修改值,依赖它的值自动更新的效果,这就是数据劫持(Observe)。如果有多个地方依赖这个数据变化而变化,就需要将观察者模式细分优化一下,一分为二,一个只做监听和操作,有几个依赖数据变化的地方添加几个实例(watcher),另一个当作中转管理中心(Dep),管理依赖这个数据的所有实例,一旦数据改变统一调用所有实例。
<!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>dom响应式</title>
</head>
<body>
<div>
<div id="a"></div>
<div id="b"></div>
</div>
<script>
// watcher
class Watcher {
constructor(vm, key, cb) {
this.cb = cb
this.vm = vm
this.key = key
// 创建的watcher实例存到Dep实例的subs数组中
Dep.target = this
console.log(this);
}
update() {
// 我们定义一个cb函数,这个函数用来模拟视图更新,调用它即代表更新视图
console.log('更新数据');
this.cb()
}
}
// dep
class Dep {
constructor() {
// 存放watcher
this.subs = []
}
// 添加
addSub(sub) {
console.log('添加watcher');
this.subs.push(sub)
}
// 更新
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
// 数据劫持
function observe(obj) {
console.log(obj);
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach((key) => {
let value = obj[key]
observe(value)
// 数据劫持
// 实例化一个Dep对象
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log(key, '获取数据');
if (Dep.target) {
// 添加dep管理watcher
dep.addSub(Dep.target)
Dep.target = null
}
return value
},
set(newVal) {
if (newVal === value) return null
// 赋新值
value = newVal
observe(value)
console.log('重新赋值', newVal, dep);
// 触发更新
dep.notify()
}
})
})
}
const data = {
a: 5,
b: 0
}
observe(data)
const Doma = document.querySelector('#a')
const Domb = document.querySelector('#b')
// 赋初始值
Doma.innerHTML = data.a
Domb.innerHTML = data.b
// new 观察者a
new Watcher(Doma, 'a', () => {
Doma.innerHTML = data.a
console.log('DomA更新');
})
// 打印一下,添加监听
console.log(data.a);
console.log(data.b);
// 更新a
data.a = 10
// new 观察者b
new Watcher(Domb, 'b', () => {
Domb.innerHTML = data.a + data.b
console.log('DomB更新');
})
// 改变值模拟改变
setTimeout(() => {
// 改变b
data.b = data.a + 100
}, 1000)
setTimeout(() => {
// 再次改变a,然后Domb也会改吧
data.a = 100
}, 2000)
</script>
</body>
</html>
vue3响应性
为啥要vue2和vue3响应性要单独说呢?因为vue3的响应性原理依赖的方法变了。vue2使用的不是挺好的嘛,为啥变了呢?因为vue2使用的Object.defineProperty()有局限性:1.只能让对象做响应性,且对象半路如果添加新属性或者删除就属性,无法监听到变化(因为更新视图是根据属性变化来监听的)2。js数组的方法也不能监听到,只有部分方法可以重写方法才可监听,如果使用数组下标方式改变值或使用length修改数组长度也无法更新。
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。jsES6的Proxy正好可以弥补老方法的缺陷,而且方式还比较精简。
<!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>Vue3响应式</title>
</head>
<body>
<div id="a"></div>
<div id="b"></div>
<button class="btn">a+1</button>
<script>
function reactive(obj) {
const proxy = new Proxy(obj, {
get(target, prototype, receiver) {
console.log('获取', target[prototype]);
return target[prototype]
},
set(target, prototype, value, receiver) {
console.log('设置', value);
target[prototype] = value
update()
return true
}
})
return proxy
}
const obj = reactive(
{
a: 1
}
)
//设置触发更新
obj.a = 10
// 更新函数
function update() {
const Doma = document.getElementById('a')
const Domb = document.getElementById('b')
Doma.innerHTML = obj.a
Domb.innerHTML = obj.a + 'hello World!'
}
const btn = document.querySelector('.btn')
btn.addEventListener('click', function () {
// 加一触发更新
obj.a += 1
})
</script>
</body>
</html>
proxy也有缺陷:1.比较新,有一定的兼容问题。2.代理返回的proxy对象和原对象不全等。
vue3响应性 + 观察者模式
上面的代码当然傻眼,,,我特啊油弄啥嘞?你搞笑呢?上面的代码还是自己调用更新啊,那个update函数都跟reactive函数强耦合啥样了,赶紧给我改,改个,一调用reative函数就可以的!
哎嘿,别急,知道你很着急,但是别先急,上面的代码确实没法看,但是有了响应性雏形,如果加上观察模式,直接原地起飞。
<!--
* @Date: 2023-10-27 14:51:55
* @LastEditors: zhangsk
* @LastEditTime: 2023-10-27 17:56:31
* @FilePath: \js\aaa.html
* @Label: Do not edit
-->
<!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>Vue3响应式</title>
</head>
<body>
<div id="a"></div>
<div id="b"></div>
<button class="btn">a+1</button>
<script>
/*
观察者模式
*/
let activeEffect;
const reactiveMap = new WeakMap()
function reactiveEffect(fn) {
return {
deps: [],
run: function () {
//
if (!Object.is(this, activeEffect)) {
activeEffect = this
} else {
// 清掉
activeEffect = undefined
}
return fn()
}
}
}
// 存储相关依赖函数
const targetMap = new WeakMap()
// 收集依赖函数
function track(target, key) {
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
if (activeEffect && !dep.has(activeEffect)) {
// 添加
dep.add(activeEffect)
// 新建好加到里面
activeEffect.deps.push(dep)
}
}
// 触发依赖函数
function trigger(target, type, key, newValue, oldValue) {
const depsMap = targetMap.get(target)
if (!depsMap) return
// set
// 获取收集到得依赖
let effects = depsMap.get(key)
//遍历set
for (let effect of effects) {
// 触发运行函数
effect.run()
}
}
// 更新函数在这
function effectFn(fn) {
const _effect = reactiveEffect(fn)
_effect.run()
}
/**
* 响应式函数
* */
function reactive(obj) {
// ...
// 啥边界都不管(假装obj是普通对象),直接开造
const proxy = new Proxy(obj, {
// 获取值
get(target, prototype, receiver) {
// Reflect.get() 方法与从 对象 (target[propertyKey]) 中读取属性类似,但它是通过一个函数执行来操作的。
const res = Reflect.get(target, prototype, receiver)
// 收集
track(target, prototype)
// 添加好后及时清理初始化,不然有bug
if (activeEffect && activeEffect.deps[activeEffect.deps.length - 1].has(activeEffect)) {
activeEffect = undefined
}
return res
},
// 设置值
set(target, prototype, value, receiver) {
// 旧值
const oldValue = target[prototype]
const result = Reflect.set(target, prototype, value, receiver)
// 这个不行啊,不能直接调,
// update()
// 间接调用,从 target为key得依赖map值上调用update函数
if (value !== oldValue)
trigger(target, 'set', prototype, value, oldValue)
return result
}
})
// 加一个
reactiveMap.set(obj, proxy)
return proxy
}
const obj = reactive(
{
a: 1
}
)
//设置触发更新
obj.a = 10
// 更新函数
function update() {
const Doma = document.getElementById('a')
const Domb = document.getElementById('b')
Doma.innerHTML = obj.a
Domb.innerHTML = obj.a + 'hello World!'
}
effectFn(update)
const btn = document.querySelector('.btn')
btn.addEventListener('click', function () {
// 加一触发更新
obj.a += 1
})
effectFn(() => {
console.log('a更新了', obj.a);
})
const obj2 = reactive({
b: 3
})
effectFn(() => {
console.log('obj2有更新', obj2.b);
})
setTimeout(() => {
obj2.b = 5
}, 1000)
effectFn(() => {
console.log('没有使用响应性数据,假装什么都没发生1');
})
effectFn(() => {
console.log('没有使用响应性数据,假装什么都没发生2');
})
</script>
</body>
</html>
结果
芜湖起飞,简单数值响应性属性运行良好