vue3响应式原理: reactive、computed、ref源码实现
简要描述了vue3的reactive响应式原理,包括effect、track和trigger函数在实现数据变化监听中的作用。此外,还给出了简易版的computed原理的实现。
2172 热度
724 浏览
1、首先我们要创建 effect.js 文件,在里面写上一个全局变量 effectSave 用来存储传入的函数,然后再写一个 effect 函数,里面用来接收传入的 fn 形参,再写一个闭包函数 _effect,在闭包函数内部自动执行传入的 fn,并且把闭包赋值给 effectSave 全局变量。
实现:effect.js。注:方法里有些没提到的是为computed准备的
js
// 生成全局变量
let effectSave;
// 创建 effect 函数,接收 fn 函数。
const effect = (fn, options) => {
const _effect = function () {
effectSave = _effect;
// 是为了 computed 的回调, 在 trigger 函数里,把 computed 里传入来的 options 里的方法调用更改脏值,已用来不用每次获取 computed 的参数都会去调用计算。
effectSave.options = options
let res = fn();
return res;
}
_effect(); // 要自走一下,因为要把传入的 fn 函数保存在全局变量 effectSave 里;
return _effect;
}
2、在写一个 tarck 函数,用来精准的把 effectSave 全局变量根据 target 存储到相应的 WeakMap 里。
实现:effect.js
js
// 全局变量用来根据传入的 target key 存储 effectSave 方法
let weakMap = new WeakMap();
const tarck = (target, key) = {
// 首先获取
let depsMap = weakMap.get(target);
// 如果没获取到,就是说没存储过这个 target 下面就来存储一下新值
if(!depsMap) {
depsMap = new Map();
weakMap.set(target, depsMap);
}
// 获取 key 对应的响应函数集, 没存过下面就存储一下新值
let deps = depsMap.get(key);
if(!deps) {
// 建立当前key 和依赖的关系,因为一个key 会有多个依赖
// 为了防止重复依赖,使用set
deps = new Set()
depsMap.set(key, deps)
}
// 把 effectSave 全局变量添加到 Set 里。
deps.add(effectSave)
}
3、再写一个 trigger 函数,用来调用 前面储存的 effectSave 函数。
实现:effect.js
js
// 同样接收一个 target 和 key 用来去 weakMap 里寻找 effectSave 函数。
const trigger = (target, key) => {
let depMap = weakMap.get(target);
let deps = depMap.get(key)
deps.forEach(effect = > {
// computed 用
if(effect?.options?.scheduler) {
effect?.options?.scheduler()
} else {
effect();
}
})
}
4、现在 effect 文件已经创建好了,就开始写 reactive 文件,要使用 proxy 来实现数据劫持来完成某些操作。
实现:reactive.js
js
const reactive = (target) => {
// 先判断一下类型
if(typeOf target != 'object') {
console.error('reactive 只能接收 object 类型的数据')
}
return new Proxt({
// 每次视图渲染的时候都会触发这个 get
get(target, key, r) {
let res = Reflect.get(target, key, r)
// 每次视图渲染的时候都会获取这个值,所以在这个时候把 target 存入对应的 target 的 weakMap 里; 需要调用 tarck 方法
tarck(target, key);
// 用于多层的 object
if(target != null && typeOf target == 'object') {
reactive(target)
}
// 把值返回出去
return res
},
// 每次修改视图数据的时候,都会触发这个 set
set(target, key, value, r) {
let res = Reflect.set(target, key, value, r); // set 需要返回布尔值这个 Reflect.set 的返回值就是布尔值
// 修改就触发, 那么视图也得更新, 就得调用之前存储的 effectSave 方法, 调用 trigger 方法去找到并调用
trigger(target, key); // 为什么不用传 value 是因为 Reflect.set 的时候已经把 target 的数据变为最新的数据了。
return res
}
})
}
5、reactive 文件已创建好,通过 html 文件验证
实现:index.html
html
<body>
<div id="app"></div>
</body>
js
let user = reactive({
name: '王搏',
age: 22
})
// effect 来挂载方法,传入一个函数,effect 方法那边形参接收到自走一下,把传入的函数赋值给 effectSave
effect(() => {
document.querySelect("#app").innerHtml = `${user.name}-${user.age}`;
})
setTimeout(_ => {
user.name = '王一搏'
}, 1000)
实现逻辑:
- 首先是调用 reactive 传入一个对象, 在函数内部创建一个 Proxy 把传入的对象实行 Proxy 数据劫持, 当使用 user 的时候会触发 Proxy 的 get 属性, 当修改 user 数据的时候会触发 Proxy 的 set 属性。
- 调用 effect 传入一个函数,传入的函数内部是挂载 dom 渲染数据到视图, effect 函数内部接收的 fn 形参函数自走(渲染视图)把它写在一个闭包里并且把闭包赋值给 effectSave 存储一下, 以便根据 target 存储到 WeekMap 里。
- effect 传入的函数里使用了
user.name的数据,所以会触发 Proxy 的 get 属性, get 属性内部调用了 tarck 方法并传入了 target 和 key, target 就是调用 reactive 时传入的一个对象, key 就是当前user.name要获取的 key(key=name)。tarck 里做了处理, 就是创建一个全局变量 new WeakMap 通过 target 去保存全局变量 effectSave(调用effect时传入的函数) 以便后面调用更新视图, 具体是先通过 weakMap 的 get 去获取 target 的数据, 获取不到就创建一个 new Map() 调用 WeakMap 的 set 传入 target 和 new Map 值存入 WeakMap, 然后接收刚才 set 的返回, 用 newMap 的 set 去存入 key 和 new Set() , 最后根据 newMap.get 接收到的值去把 effectSave(调用effect时传入的函数) 通过 .add() 添加进去。 - 更新:当
user.name='王一搏'修改的时候,会触发 Proxy 的 set 属性, set 属性内部修改好最新的 target 数据之后, 会调用 trigger 函数, 传入 target 和 key 去找到对应的 effectSave(调用effect时传入的函数) 调用。就实现了视图的更新。
6、实现 computed, 创建 computed.js 文件
实现: computed.js
js
const computed = (target) => {
let _value = effect(target, {
scheduler: () => _dirty = true;
});
let catchValue; // 存储值
let _dirty = true; // 为了避免每次获取都会去调用 _value 计算。
class ComputedRefImpl {
// 每次获取这个值的时候都会执行 get 方法
get() {
// 首次进来 _dirty 是 true, 如果下次数据有修改就需要把 _dirty 改为 true 所以 effect 里,还要接收一个函数用来回调把 _dirty 的值改为 true, 重新渲染视图的时候, 就能重新计算获取了。
if(_dirty) {
catchValue = _value();
// 计算过后就把 _dirty 改为 false;
_dirty = false;
}
return catchValue;
}
}
return ComputedRefImpl
}
7、computed 文件已创建成功, 通过 html 文件验证
实现:index.html
js
<body>
<div id="app"></div>
</body>
let user = reactive({
name: '王搏',
age: 22
})
let userInfo = computed(() => {
return `用户信息:姓名:${user.name}。年龄:${user.age}`
})
// effect 来挂载方法,传入一个函数,effect 方法那边形参接收到自走一下,把传入的函数赋值给 effectSave
effect(() => {
document.querySelect("#app").innerHtml = `
用户信息:${userInfo}
`;
})
实现逻辑:
- 调用 computed 传入的方法首先会自走一次,然后调用 effect 方法, 把调用 computed 里写的方法和在 computed 函数里定义的一个对象 scheduler 方法(options),传入赋值给 effectSave 里。这个 options 的作用是修改脏值,防止每次获取 computed 值的时候,都会重新计算。
- 首次自走的时候会访问 user, 然后 user 的 proxy get 触发去把当前访问的 target 存入 weakMap 里。 存入 weakMap 之后,再把 weakMap 对应的 target 的值里添加一个 new Map 根据当前传入的 key 去创建 new Set 再把这个 key 和 new Set 加入到通过 target 找到的 weakMap 里。再把 computed 函数里 effect 传入的方法赋值的 effectSave 加入到这个 Set 里。
- 每当 user 数据发生变化,就会调用 user 的 proxy set 方法,然后去 trigger 里通过当前修改的 target 找 weakMap 值, 找到后,根据当前修改的 key 去获取那个 Set 里存的方法(里面有 dom 挂载传入的方法,还有 computed 传入的方法)因为是 computed 创建在前所以先存的是 computed 里更改脏值 _dirty 的方法 scheduler 所以循环 Set 的时候就把脏值 _dirty 改为了 true。 然后才会调用挂载传入的 dom 函数, 当 dom 函数里的 userInfo 被调用了, 所以又会去调用 computed 方法,然后触发 computed 函数里的 ComputedRefImpl 类 get 属性,因为前面 scheduler 把 _dirty 脏值改为了 true, 所以获取的时候就会重新去调用 computed 传入的方法去更新最新的数据到页面。
8.ref 简述一下
ref 是由类创建的(扩展是 reactive),ref 需要 value 修改值的原因是类里面的 get value 和 set value 做了数据劫持,劫持到就会去更新视图。如果 ref 传的是 object 类的 constructor 会判断如果传的是 object 会去调用 reactive 来做响应式。
js
const isObject = (val) => val !== null && typeof val === 'object'
const toReactive = (value) => return isObject(value) ? reactive(value) : value
class RefImpl {
// 源码是接受俩参数,第二个主要是判断 shallowRef
constructor(value) {
this._rawValue = value;
this._value = toReactive(value); // 里面判断了是否为对象,如果是对象的话就去调用reactive不是就直接原样返回
}
get value() {
return this._value
}
set value(newValue) {
if(this.__rawValue != this._value) {
console.log('需要更新视图')
}
}
}

声明:Web前端小站 - 前端博客 - 王搏的个人博客|版权所有,违者必究|如未注明,均为原创
转载:转载请注明原文链接 - vue3响应式原理: reactive、computed、ref源码实现
评论 (0)
0/50
暂无评论,快来抢沙发吧~