Manual implementation of Vue3 & principle analysis (2) - Refs related & Computed implementation

foreword

For this analysis, please refer to the vue3 source code, Cui Da's mini-vue, and Huo Chunyang's "Vuejs Design and Implementation" to record my Vue3 source code reading and learning process as much as possible. I will combine my own thinking, ask questions, find answers, and attach them to the bottom of each article. I hope that everyone can learn and make progress together in my article. If you have something, can you give the author a small like as encouragement? thank you all!

Handwritten simple vue3 Refs related & Computed related implementation

Refs

We all know that Proxy is used to proxy objects, and for primitive values ​​such as (Boolean, Number, BigInt, String, Symbol, undefined and null) we have no way to make it reactive, then there is spanref

Note: The ref here is different from the ref we commonly use in vue2

Initial realization

The definition of ref is as follows:

We need to have a ref method that can receive a value, this value

  1. is an object, use reactive to turn it into a reactive object
  2. is a primitive value, then returns a reactive ref object

This ref object has a value attribute, which means that the corresponding value ref.valuecan be

Let's look at the test case:

it('happy path', () => {
  // 这里用 const 是因为他是对象
  // 创建一个 ref 对象 value 是 1
  const a = ref(1)
  // 访问 a.value 他应该得到 1
  expect(a.value).toBe(1)
});
复制代码

The condition should be very clear, we can directly create a Ref instance:

class RefImpl {
  private _value: any
  constructor(value) {
    // 如果 value 是对象 需要处理成 reactive
    this._value = convert(value)
  }
  // 访问 value 属性就直接返回这个 _value
  get value() {
    return this._value
  }
}

// 判断新值是不是对象 是的话要 reactive 一下,否则直接给
function convert(value) {
  // isObject 其实就是 return value !== null && typeof value === 'object'
  return isObject(value) ? reactive(value) : value
}

export function ref(value) {
  // 调用 ref 方法我们直接生成一个 实例
  return new RefImpl(value)
}
复制代码

After the above implementation, we are just 实现了一个 .value 可以返回值的 ref对象, but this time还不是响应式的

ref object implements responsiveness

Next, we need to implement the responsive function of the ref object. The responsiveness must have other effects after we trigger a modification of .value

We can specify the functionality we want to implement simply by using the following test cases:

it('should be reactive', () => {
  // 创建一个 ref 对象 value 是 1
  const a = ref(1)
  let dummy
  let calls = 0
  // 创建一个副作用函数,他会自己执行一次,这时候 dummy 应该等于 1,calls由于函数被调用也会 + 1
  effect(() => {
    calls++
    dummy = a.value
  })
  expect(calls).toBe(1)
  expect(dummy).toBe(1)
  // 到这里 我们要操作 a.value 也即是触发 set value
  a.value = 2
  // 那 副作用函数就应该被 trigger 触发,两个变量都会更新
  expect(calls).toBe(2)
  expect(dummy).toBe(2)
  // 优化:赋值和之前的一样 不应该触发响应式
  a.value = 2
  expect(calls).toBe(2)
  expect(dummy).toBe(2)
});
复制代码

Let's summarize what points we need to pay attention to through the above test cases?

  1. a.value = 2 should trigger the set value of the ref instance
  2. set value to check if it is a new value
  3. set value 需要触发 trigger 来让副作用函数执行
  4. 有 trigger 那就肯定需要 track 来收集依赖

那么我们先对原有的 track 和 trigger 来针对 ref 做一下处理拿到以下这两个函数:

export function trackEffects(dep) {
  // 已经在 dep 中就不用 add 了
  if (dep.has(activeEffect)) return
  dep.add(activeEffect) // 把对应的 effect 实例加入 set 里
  activeEffect.deps.push(dep)
}
export function triggerEffects(dep) {
  for (const effect of dep) {
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}
复制代码

细心的你可以发现这两个方法中的代码 其实就是原有 tarck原有trigger 中的代码,一模一样没有任何变化,我们只是将这两段代码单独抽离出来复用。

我们两个新的方法只接收一个依赖 dep (set类型)

接下来我们修改刚才创建的 class RefImp,新增一个 trackRefValue 方法:

class RefImpl {
  // 保留原有的 value,需要用来比较 set 的新的值,因为_value有可能是响应式的对象所以不能用 _value 来比较
  private _rawValue: any
  private _value: any
  // 在 track 中收集依赖,在 trigger 中触发依赖
  public dep
  constructor(value) {
    this._rawValue = value
    // 如果 value 是对象 需要处理成 reactive
    this._value = convert(value)
    // 初始化 Set
    this.dep = new Set()
  }
  get value() {
    // 去收集依赖,但是在那之前要判断是否有 effect
    trackRefValue(this)
    return this._value
  }
  set value(newValue) {
    // 语义化 Obejct.js  即没有变化 直接 return
    if (!hasChanged(this._rawValue, newValue)) return
    // 一定是先修改 value 再去 调用 trigger 才能保证_value准确
    this._rawValue = newValue
    this._value = convert(newValue)
    triggerEffects(this.dep)
  }
}
// ref get 依赖收集
function trackRefValue(ref) {
  // isTraking 其实就是 shouldTrack && activeEffect !== undefined
  if (isTracking()) trackEffects(ref.dep)
}
复制代码

通过上面的一个实现,我们也顺带实现了对于对象的响应式,我们已经可以直接通过以下的测试用例了

it('should make nested properties reactive', () => {
  const a = ref({
    count: 1
  })
  let dummy
  // 调用 副作用函数 给 dummy 赋值 注意是 a.value.count
  effect(() => {
    dummy = a.value.count
  })
  expect(dummy).toBe(1)
  // 修改 a.value.count 也能触发响应式,因为这时候的 this._value 实际是通过 reactive() 创建的响应式对象
  a.value.count = 2
  expect(dummy).toBe(2)
});
复制代码

isRef 和 unRef(脱ref)

之前有 isReactive 、 isReadonly ,那现在要有 isRef 也不是很过分嘛!

isRef 的定义:判断当前的值是不是一个 ref 对象

实现方法非常简单:

class RefImpl {
  // ...
  // 定义一个 __v_isRef 属性即可
  public __v_isRef = true
  // constructor() ....
}

// 判断是不是一个 ref
export function isRef(ref) {
  // 如果传入的不是一个 ref对象 那他是没有__v_isRef属性的,那就是 undefined ,所以需要 !! 将它转成一个 boolean 类型
  return !!ref.__v_isRef
}
复制代码

unRef:也是我们常说的 脱ref,他的一个作用实际上就是当我们传入一个 ref对象 的时候直接返回他的value,如果不是 ref对象 就直接返回传入的值。

unRef(脱ref)的应用场景是什么?为什么设计这个东西?

// 是 ref 返回 ref.value 否则 直接返回
export function unRef(ref) {
  return isRef(ref) ? ref.value : ref
}
复制代码

可以看到这个 unRef 的实现实际上是非常的简单,就是利用上边热乎的 isRef 来实现的。

ProxyRefs (自动脱ref)

我们实现完了以上的内容后,我们发现每次都需要 .value 来拿到对应的值,真的很麻烦很麻烦,不可能我写 template 的时候也让我 .value 吧!!也太坑啦!!!然后呢我们就有了 ProxyRefs

我们先通过测试用例来看一下我们需要实现什么:

it('proxyRefs', () => {
  // 创建一个用户 age字段是 ref对象
  const user = {
    age: ref(18),
    name: "Ben"
  }
  // 通过一个 proxyRefs 方法创建 proxyUser
  const proxyUser = proxyRefs(user)
  // 不影响 user.age.value 的使用
  expect(user.age.value).toBe(18)
  // proxyUser 可以直接 .age 拿到数值而不用 .value 了
  expect(proxyUser.age).toBe(18)
  expect(proxyUser.name).toBe("Ben")
  // 设置 proxyUser.age 的值变成为 非ref的原始值
  proxyUser.age = 20
  // user 的 age 仍然是ref对象,能够从 .value 获取到更新
  expect(user.age.value).toBe(20)
  // 依旧能正常访问更新后的 age
  expect(proxyUser.age).toBe(20)
  // 又给 proxyUser.age 设置成一个 ref对象
  proxyUser.age = ref(25)
  // user 的 age 仍然是ref对象,能够从 .value 获取到更新
  expect(user.age.value).toBe(25)
  // 依旧能正常访问更新后的 age
  expect(proxyUser.age).toBe(25)
});
复制代码

通过上面的测试用例,我们不难发现,通过 ProxyRefs方法 获取到的对象不再需要通过 .value 来获取值,且修改对象的 ref对象 属性的类型也不会有影响。

我们来写一下 ProxyRefs:

export function proxyRefs(objectWithRefs) {
  return new Proxy(objectWithRefs, {
    // 触发 get 的时候我们帮它 脱ref
    get(target, key) {
      return unRef(Reflect.get(target, key))
    },
    set(target, key, newValue) {
      // 如果当前已经是 ref 而传入的 值不是 ref,那么要给 .value 赋值
      if (isRef(target[key]) && !isRef(newValue)) {
        return target[key].value = newValue
      } else {
        // 否则直接设置成 newValue即可
        return Reflect.set(target, key, newValue)
      }
    }
  })
}
复制代码

实现其实也是借助我们已经实现了的 unRef 来做脱ref的功能,要注意的是 set 可能有些绕,大家结合测试用例消化一下吧

toRefs

思来想去,我们还想实现一个 传入对象所有的属性都变成 ref对象,我们该怎么实现呢??

其实很简单,就是遍历对象的 key 然后挨个 ref 即可:

export function toRefs(obj) {
  const ret = {}
  // 用 for...in 遍历对象
  for (const key of obj) {
    ret[key] = ref(obj, key)
  }
  return ret
}
复制代码

这样就可以啦!!是不是很简单呢!!!

Computed

接下来到了我们比较好奇的 computed 的实现了!!我们知道 computed 他是具有缓存功能的,且他应该是懒执行的。

我们通过测试用例来看看我们需要做什么吧:

it('should compute lazily', () => {
  // lazy 懒执行 (不调用 cValue.value 我就不触发)
  const value = reactive({
    foo: 1
  })
  const getter = jest.fn(() => {
    return value.foo
  })
  // 创建 computed实例
  const cValue = computed(getter)
  // 懒执行 getter不应该被调用
  expect(getter).not.toHaveBeenCalled()
  // 访问 cValue.value 了要触发一次了
  expect(cValue.value).toBe(1)
  expect(getter).toHaveBeenCalledTimes(1)
  // 访问这个属性不应该再次触发 getter 缓存!!
  cValue.value
  expect(getter).toHaveBeenCalledTimes(1)
  // 修改属性 需要触发
  value.foo = 2
  expect(getter).toHaveBeenCalledTimes(1)
  // 访问这个属性不应该再次触发 getter 还是缓存!!
  cValue.value
  expect(getter).toHaveBeenCalledTimes(2)
});
复制代码

我们看到这个测试用例就已经很好的诠释了 computed 的两大特点:

  1. 懒执行
  2. 缓存

接下来我们来实现 computed :

class ComputedRefImpl {
  private _getter: any
  // 缓存变量 如果是 true 的时候说明有修改 需要更新 value
  private _dirty: boolean = true 
  private _value: any
  // 保存 ReactiveEffect 实例
  private _effect: any
  constructor(getter) {
    this._getter = getter
    // 利用第二个参数 scheduler 的特性,执行一次 getter 后触发 trigger 的时候只执行 scheduler
    this._effect = new ReactiveEffect(getter, () => {
      // 当 dirty 是 false 的时候 改为 true 即表示已更新过了
      if (!this._dirty) this._dirty = true
    })
  }
  get value() {
    // dirty 不是true 说明value没有改变 直接返回value
    if (this._dirty) {
      this._dirty = false
      // effect.run() 实际就是触发了 传入的 getter
      this._value = this._effect.run()
    }
    return this._value
  }
}
// 返回一个 ComputedRefImpl 实例
export function computed(getter) {
  return new ComputedRefImpl(getter)
}
复制代码

这样我们的 computed 就实现完成了,要注意两个细节:

  1. 我们是通过 dirty 这个 flag 来判断数据是否被更新了
  2. effect 的 scheduler 参数这时候就显得特别妙了,也就是说 computed 也是 scheduler 的重点应用场景

问题:Vue2 中的 ref 和 Vue3 中的 ref 是同一个东西吗?有什么区别?

实际上在 Vue2 中的 ref 是一个 attribute(属性),它的作用就是用来标记一个元素/子组件,让你可以在父组件中使用 $refs 来找到这个元素/组件,那么他是这么使用的:

<!-- `vm.$refs.p` 你就会得到 p 这个dom -->
<p ref="p">hello</p>

<!-- `vm.$refs.child` 你就会得到 chld-component 的实例 -->
<child-component ref="child"></child-component>
复制代码

而到了 Vue3 我们依然能够这样使用 ref。(来作为一个属性,用 $refs 拿到引用)

但 Vue3 还有一个 ref。这个 ref 实际上是由一个叫做 Refs 的响应式 api 携带的方法,通过 ref(xxx) 来让 xxx(原始值) 变成一个 ref对象具备响应式的功能。(如果 xxx 是一个对象,那么还是会调用 reactive 来变成一个代理对象)

这个 ref对象 仅有一个 .value property

我们是这样使用的:

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1
复制代码

Question: What is the application scenario of unRef (unref)? Why design this thing? What is automatic off-ref?

Because ref objects can only access the corresponding value through ref.value, this will bring us a very serious mental burden

When we use reactive objects generated by reactive:

const obj = reactive({ foo: 1, bar: 2 })
obj.foo // 1
obj.bar // 2
复制代码

And when we use the responsive data generated by ref:

const a = ref(1)
a.value // 1

const b = {...toRefs({ foo: 1, bar: 2 })}
b.value.foo // 1
b.value.bar // 2
复制代码

You can clearly feel a heavy pressure from that comparison, what? Not enough pressure? ?

That. . .

<!-- 正常的 -->
<p>{{ foo }} / {{ bar }}</p>
<!-- ref -->
<p>{{ foo.value }} / {{ bar.value }}</p>
复制代码

now what? ! !

Therefore, the existence of off-ref is to reduce a mental burden on users.

This is why there is an automatic off ref

With the ability to automatically remove refs, when users use responsive data in templates, they don't need to concern whether a value is a ref.

Guess you like

Origin juejin.im/post/7085393997342081055