vue3响应式系统流程分析与实现
vue3的代码实例
在写代码前,不妨来看看如何使用vue3吧,我们可以先去 github.com/vuejs/vue-n… clone一份代码,使用npm install && npm run dev后,会生成一个packages -> vue -> dist -> vue.global.js文件,这样我们就可以使用vue3了,在vue文件夹新建一个index.html文件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue3示例</title>
</head>
<body>
<div id="app"></div>
<button id="btn">按钮</button>
<script src="./dist/vue.global.js"></script>
<script>
const { reactive, computed, watchEffect } = Vue;
const app = document.querySelector('#app');
const btn = document.querySelector('#btn');
const year = new Date().getFullYear();
let person = reactive({
name: '烟花渲染离别',
age: 23
});
let birthYear = computed(() => year - person.age);
watchEffect(() => {
app.innerHTML = `<div>我叫${person.name},今年${person.age}岁,出生年是${birthYear.value}</div>`;
});
btn.addEventListener('click', () => {
person.age += 1;
});
</script>
</body>
</html>
复制代码

可以看到,我们每次点击一次按钮,触发person.age += 1;
,然后watchEffect
自动执行,计算属性也相应更新,现在我们的目标就很明确了,就是实现reactive
、watchEffect
、computed
方法。
reactive方法
我们知道vue3
是基于proxy
来实现响应式的,对proxy
不熟悉的可以去看看阮一峰老师的es6教程:es6.ruanyifeng.com/#docs/proxy reflect
也是es6
新提供的API,具体作用也可以参考阮一峰老师的es6教程:es6.ruanyifeng.com/#docs/refle… ,简单来说他提供了一个操作对象的新API
,将Object对象属于语言内部的方法放到Reflect
对象上,将老Object方法报错的情况改成返回false
值。 下面我们来看看具体的代码吧,它对对象的get
、set
、del
操作进行了代理。
function isObject(target) {
return typeof target === 'object' && target !== null;
}
function reactive() {
// 判断是否对象,proxy只对对象进行代理
if (!isObject(target)) {
return target;
}
const baseHandler = {
set(target, key, value, receiver) { // receiver:它总是指向原始的读操作所在的那个对象,一般情况下就是 Proxy 实例
trigger(); // 触发视图更新
return Reflect.set(target, key, value, receiver);
},
get(target, key, receiver) {
return Reflect.get(target, key, value, receiver);
},
del(target, key) {
return Reflect.deleteProperty(target, key);
}
};
let observed = new Proxy(target, baseHandler);
return observed;
}
复制代码
添加更新限制
上面的代码看上去好像没啥问题,但是在代理数组的时候,添加、删除数组的元素,除了能监听到数组本身要设置的元素变化,还会监听到数组长度length
属性修改的变化,如下图:
所以我们应该只在新增属性的时候去触发更新,我们添加hasOwnProperty
判断与老值和新值比较判断,只有修改自身对象的属性或者修改了自身属性并且值不同的时候才去更新视图。
set(target, key, value, receiver) {
const oldValue = target[key];
if (!target.hasOwnProperty(key) || oldValue !== value) { // 新增属性或者设置属性老值不等于新值
trigger(target, key); // 触发视图更新函数
}
return Reflect.set(target, key, value, receiver);
}
复制代码
深层级对象监听
上面我们只对对象进行了一层代理,如果对象的属性对应的值还是对象的话,它并没有被代理过,此时我们去操作该对象的时候,就不会触发set
,也就不会更新视图了。如下图:
那么我们应该怎么进行深层次的代理呢?
我们观察一下person.hair.push(4)
这个操作,当我们去取person.hair
的时候,会去调用person
的get
方法,拿到属性hair
的值,那么我们就可以再它拿到值之后判断是否是对象,再去进行深层次的监听。
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
return isObject(res) ? reactive(res) : res;
},
复制代码
缓存已代理对象
代理过的对象再去执行reactive
方法的时候,会去重新设置代理,我们应该避免这种情况,通过hashmap
缓存代理过的对象,这样在再次代理的时候,判断对象存在hashmap
中,直接返回该结果即可。
- 进行多次代理示例
let obj = {
name: '烟花渲染离别',
age: 23,
hair: [1,2,3]
}
let person = reactive(obj);
person = reactive(obj);
person = reactive(obj);
复制代码
- 定义
hashmap
缓存代理对象
我们使用WeakMap
缓存代理对象,它是一个弱引用对象,不会导致内存泄露。 es6.ruanyifeng.com/#docs/set-m…
const toProxy = new WeakMap(); // 代理后的对象
const toRaw = new WeakMap(); // 代理前的对象
function reactive(target) {
// 判断是否对象,proxy只对对象进行代理
if (!isObject(target)) {
return target;
}
let proxy = toProxy.get(target); // 当前对象在代理表中,直接返回该对象
if (proxy) {
return proxy;
}
if (toRaw.has(target)) { // 当前对象是代理过的对象
return target;
}
let observed = new Proxy(target, baseHandler);
toProxy.set(target, observed);
toRaw.set(observed, target);
return observed;
}
let obj = {
name: '烟花渲染离别',
age: 23,
hair: [1,2,3]
}
let person = reactive(obj);
person = reactive(obj); // 再去代理的时候返回的就是从缓存中取到的数据了
复制代码
这样reactive
方法就基本已经实现完了。
收集依赖,自动更新
我们先来瞅瞅之前是怎么渲染DOM的。
watchEffect(() => {
app.innerHTML = `<div>我叫${person.name},今年${person.age}岁,出生年是${birthYear.value}</div>`;
});
复制代码
在初始化默认执行一次watchEffect
函数后,渲染DOM数据,之后依赖的数据发生变化,会自动再次执行,也就会自动更新我们的DOM内容了,这就是我们常说的收集依赖,响应式更新。
那么我们在哪里进行依赖收集,什么时候通知依赖更新呢?
- 我们在用到数据进行展示的时候,它就会触发我们创建好的
proxy
对象的get
方法,这个时候我们就可以收集依赖了。 - 在数据发生变化的时候同样会触发我们的
set
方法,我们在set
中通知依赖更新。 这其实是一种设计模式叫做发布订阅。
我们在get
中收集依赖:
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
track(target, key); // 收集依赖,如果目标上的key变化,执行栈中的effect
return isObject(res) ? reactive(res) : res;
}
复制代码
在set
中通知依赖更新:
set(target, key, value, receiver) {
if (target.hasOwnProperty(key)) {
trigger(target, key); // 触发更新
}
return Reflect.set(target, key, value, receiver);
}
复制代码
可以看到我们在get
中执行了一个track
方法进行收集依赖,在set
中执行trigger
触发更新,这样我们知道了它的过程后,再来看看怎么实现watchEffect
方法。
watchEffect方法
我们传入到watchEffect
方法里的函数就是我们要收集的依赖,我们将收集到的依赖用栈保存起来,栈是一种先进后出的数据结构,具体我们看看下面代码实现:
let effectStack = []; // 存储依赖数据effect
function watchEffect(fn, options = {}) {
// 创建一个响应式的影响函数,往effectsStack push一个effect函数,执行fn
const effect = createReactiveEffect(fn, options);
return effect;
}
function createReactiveEffect(fn) {
const effect = function() {
if (!effectsStack.includes(effect)) { // 判断栈中是否已经有过该effecy,防止重复添加
try {
effectsStack.push(effect); // 将当前的effect推入栈中
return fn(); // 执行fn
} finally {
effectsStack.pop(effect); // 避免fn执行报错,在finally里执行,将当前effect出栈
}
}
}
effect(); // 默认执行一次
}
复制代码
关联effect和对应对象属性
上面我们只是收集了fn
存到effectsStack
中,但是我们还没将fn
和对应的对象属性关联,下面步我们要实现track
方法,将effect
和对应的属性关联。
let targetsMap = new WeakMap();
function track(target, key) { // 如果taeget中的key发生改变,执行栈中的effect方法
const effect = effectsStack[effectsStack.length - 1];
// 最新的effect,有才创建关联
if (effect) {
let depsMap = targetsMap.get(target);
if (!depsMap) { // 第一次渲染没有,设置对应的匹配值
targetsMap.set(target, depsMap = new Map());
}
let deps = depsMap.get(key);
if (!deps) { // 第一次渲染没有,设置对应的匹配值
depsMap.set(key, deps = new Set());
}
if (!deps.has(effect)) {
deps.add(effect); // 将effect添加到当前的targetsMap对应的target的存放的depsMap里key对应的deps
}
}
}
function trigger(target, key, type) {
// 触发更新,找到依赖effect
let depsMap = targetsMap.get(target);
if (depsMap) {
let deps = depsMap.get(key);
if (deps) {
deps.forEach(effect => {
effect();
});
}
}
}
复制代码
targetsMap
的数据结构较为复杂,它是一个WeakMap
对象,targetsMap
的key
就是我们target
对象,在targetsMap
中该target
对应的值是一个Map
对象,该Map
对象的key
是target
对象的属性,Map
对象对应的key
的值是一个Set
数据结构,存放了当前该target.key
对应的effect
依赖。看下面的代码可能会比较清晰点:
let person = reactive({
name: '烟花渲染离别',
});
targetsMap = {
person: {
'name': [effect]
}
}
// {
// target: {
// key: [dep1, dep2]
// }
// }
复制代码
执行流程
- 收集流程:执行
watchEffect
方法,将fn
也就是effect
push到effectStack
栈中,执行fn
,如果fn
中有用到reactive
代理过的对象,此时会触发该代理对象的get
方法,而我们在get
方法中使用了track
方法收集依赖,track
方法首先从effectStack
中取出最后一个effect
,也就是我们刚刚push到栈中的effect
,然后判断它是否存在,如果存在的话,我们从targetMap
取出对应的target
的depsMap
,如果depsMap
不存在,我们手动将当前的target
作为key
,depsMap = new Map()
作为值设置到targetMap
中,然后我们再从depsMap
中取出当前代理对象key
对应的依赖deps
,如果不存在则存放一个新Set
进去,然后将对应的effect
添加到该deps
中。 - 更新流程:修改代理后的对象,触发
set
方法,执行trigger
方法,通过传入的target
在targetsMap
中找到depsMap
,通过key
在depsMap
中找到对应的deps
,循环执行里面保存的effect
。
作者:烟花_渲染离别
链接:https://juejin.im/post/5edb93caf265da771526eeda
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。