vue3中ref的原理,ref功能实现
其实ref的实现也是很简单的,和reactive的思路是差不多的,但是要注意的点还是有的
1 ref 一般都针对单个值 怎么才能知道他被get 或 set 2 reactive 中的proxy又只是针对对象的,所以实现起来肯定还是不太一样的
解决方法:
通过一个对象进行包裹 对象就由RefImpl这个类来的 然后可以给类里加个value的 get 或 set,这样就可以监听到这个值什么时候被get和set了,就可以进行依赖的收集和触发
首先先给出测试用例:ref.spec.ts
import { effect, trigger } from "../effect"
import { ref } from "../ref"
describe('ref',()=>{
it('happy path',()=>{
const a = ref(1)
expect(a.value).toBe(1)
})
it('should be reactive',()=>{
const a = ref(1)
let dummy
let calls = 0
effect(()=>{
calls++
dummy = a.value
})
expect(calls).toBe(1)
expect(dummy).toBe(1)
a.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)
// same value should not trigger
a.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)
})
it('should make nested properties reactive',()=>{
const a = ref({
count:1
})
let dummy
effect(()=>{
dummy = a.value.count
})
expect(dummy).toBe(1)
a.value.count = 2
expect(dummy).toBe(2)
})
})
由于案例分成3部分,我们就先从happy path通过开始,happy path的通过就比较简单,只要实现get方法就可以通过了:
1 导出ref方法
2 由上面分析,我们要监听单个值,那我们就要通过对象包裹的方式去对value进行一个监听,所以ref函数中就要导出RefImpl实例
3 RefImpl中创建私有变量_value,get value时把_value变量return出去就可以实现了
代码截图如下
这时候happy path也就通过了
接着我们实现should be reactive测试用例,大致浏览实现功能如下
1 对ref类的set功能的追加 (就是给value私有变量进行一个更新)
2 对ref的依赖收集和触发进行实现
2.1 ref是一个单值的依赖收集和触发,是可以对reactive中的依赖收集和触发的代码做一个复用的
2.2 RefImpl中需要一个dep来对依赖进行收集
2.3 抽离effect.ts中track和trigger中收集和触发部分,分别为trackEffects函数和triggerEffects函数并导出给ref.ts使用
3 对重复设置相同值的条件进行判断: 使用Object.is对this._value和newValue的值进行判断,当对象改变的时候再去做set的操作和依赖触发的流程
想着内容也不算多,就直接把should make nested properties reactive的测试案例也一起分析了,再一块附上代码截图,这块测试大致要实现功能就是: 让ref支持传入对象,并且具有响应式
那这时候大家就会想到了,让对象响应式不就是之前写的reactive吗,直接导入进来使用就可以了,那其实也确实是这样的
这块我觉得直接截图,然后在截图中解释代码块内容会比较简单一些:
然后你以为这就结束了吗,其实才刚开始,你发现你包装完后,测试并没有完全通过,那是因为之前的对象对比就出现问题了
解决问题图片解释:
其他需要注意的还有优化:
接下来就出涉及到修改的几个代码
ref.ts:
import { hasChange, isObject } from "../shared";
import { trackEffects, triggerEffects } from "./effect";
import { reactive } from "./reactive";
// ref 一般都针对单个值 怎么才能知道他被get 或 set
// reactive 中的proxy又只是针对对象的
// 通过一个对象进行包裹 对象就由RefImpl这个类来的 然后可以给类里加个value的 get 或 set
class RefImpl {
private _value: any;
public dep
private _rawValue: any;
constructor(value){
// 如果是对象 要用reactive进行一层包装
this._rawValue = value
this._value = convert(value)
this.dep = new Set()
}
get value(){
trackEffects(this.dep)
return this._value
}
set value(newValue){
// hasChange
// 对比的时候 object的对比 但是this._value已经变成了proxy
if(hasChange(this._rawValue,newValue)) {
// 设置完值再去做依赖触发
this._rawValue = newValue
this._value = convert(newValue)
triggerEffects(this.dep)
}
}
}
function convert(value){
return isObject(value) ? reactive(value) : value
}
export function ref(value){
return new RefImpl(value)
}
effect.ts:
import { extend } from "../shared"
let activeEffect
let shouldTrack
class ReactiveEffect {
private _fn: any
deps=[]
active=true
onStop?:()=>void
constructor(fn,public scheduler?){
this._fn = fn
}
run(){
if(!this.active){
return this._fn()
}
// 依赖收集之前去给激活的effect赋值
activeEffect = this
shouldTrack = true
const res = this._fn()
shouldTrack = false
return res
}
stop(){
if(this.active){
cleanupEffect(this)
if(this.onStop){
this.onStop()
}
this.active = false
}
}
}
function cleanupEffect(effect){
effect.deps.forEach((dep:any) => {
dep.delete(effect)
})
effect.deps.length=0
}
const targetMap = new Map()
export function track(target,key){
if(!isTracking()) return
// target -> key -> dep
let depsMap = targetMap.get(target)
if(!depsMap){
depsMap = new Map()
// 没有depsMap时要加进去
targetMap.set(target,depsMap)
}
let dep = depsMap.get(key)
if(!dep){
dep = new Set()
depsMap.set(key,dep)
}
trackEffects(dep)
}
export function trackEffects(dep){
if(!isTracking()) return
if(dep.has(activeEffect)) return
dep.add(activeEffect)
// 这里需要反向收集一下dep
activeEffect.deps.push(dep)
}
function isTracking() {
return shouldTrack && activeEffect !== undefined
// if(!activeEffect) return
// if(!shouldTrack) return
}
export function trigger(target,key){
const depsMap = targetMap.get(target)
const dep = depsMap.get(key)
triggerEffects(dep)
}
export function triggerEffects(dep){
for (const effect of dep) {
if(effect.scheduler){
effect.scheduler()
} else {
effect.run()
}
}
}
export function effect(fn,options:any = {}){
const _effect = new ReactiveEffect(fn,options.scheduler)
extend(_effect,options)
_effect.run()
const runner:any = _effect.run.bind(_effect)
runner.effect = _effect
return runner
}
export function stop(runner) {
runner.effect.stop()
}
index.ts
export const extend = Object.assign
export const isObject = (value) => {
return value !== null && typeof value === 'object'
}
export const hasChange = (value,newVal) => {
return !Object.is(value,newVal)
}