vue.js 设计与实现-vue3源码学习(6)-computed

129 阅读3分钟

前言

前面我们学习了如何实现一个简单的 reactive,effect ref 本篇我们学习 computed 函数

vue3源码学习系列 (1)源码了解 juejin.cn/post/722481…
vue3源码学习系列 (2)环境配置 juejin.cn/post/723229…
vue3源码学习系列 (3)reactive juejin.cn/post/723596…
vue3源码学习系列 (4)effect juejin.cn/post/723814…
vue3源码学习系列 (5)ref juejin.cn/post/724327…

computed 的实现

我们可以类比 ref 来进行 computed 的学习,computedref 一样也是通过 .value 方式进行数据的获取。废话不多说。创建 reactivity/src/computed.ts

//vue/shared/src/index.ts
export const isFunction=(value:unknown):value is Function=>typeof value==='function' 
 // computed.ts
 
import { isFunction } from "@vue/shared" //判断是否为函数
export function computed(getterOrOptions){  
//getterOrOptions 只做获取数据使用时为一个函数,如果为get 和set 则为一个配置项,我们暂时先以函数方式
  let getter
  if(isFunction(getterOrOptions)){
    getter=getterOrOptions
  }
  let cRef=new  ComputedRefImpl( getter)
  return cRef
}

主要逻辑在 ComputedRefImpl

import { Dep } from "./dep"
import { ReactiveEffect } from "./effect"
import { trackRefValue,tigggerRefValue } from "./ref"

export class ComputedRefImpl<T>{
    public dep?:Dep=undefined //收集fn函数

    private _value!:T //必然存在的数据

    public readonly effect:ReactiveEffect<T>

    public readonly __v_isRef=true

    public _dirty =true //脏值判断

    constructor(getter){
        this.effect=new ReactiveEffect(getter) // 我们在reactive 函数中已经实现的方法
        this.effect.computed=this //区别computed 与其他函数
    }
    // 我们需要进行页面数据展示,就会触发 ComputedRefImpl 中的 get  方法
    
    get value(){
        trackRefValue(this) //收集依赖
        if(this._dirty){
          this._dirty=false
          this._value=this.effect.run()
        }
        return this._value
    }
}

需要将computed 暴露出来 然后我们进行页面测试

<!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>reactive测试</title>
    <script src="../dist/vue.js"></script>
</head>
<body>
    <div id="app">
        <p id="p1"></p>
        <button onclick="changeObject()">修改name值</button>
    </div>
<script>
    const {reactive,effect,computed} = Vue
        const obj=reactive({
            name:'张三'
        })
        const computedObj=computed(()=>{
            return  obj.name
        })
        effect(()=>{
            document.getElementById("p1").innerText=computedObj.value
        })
        const changeObject=()=>{
            obj.name="李四"
        }
   
</script>
</body>
</html>

虽然页面数据展示出来了,但此时我们还是无法通过 changeObject 方法来动态修改页面,我们需要对代码进行修改

export class ComputedRefImpl<T>{
    public dep?:Dep=undefined //收集依赖

    private _value!:T //必然存在的数据

    public readonly effect:ReactiveEffect<T>

    public readonly __v_isRef=true

    public _dirty =true

    constructor(getter){
       console.log('执行constructor')
        this.effect=new ReactiveEffect(getter,()=>{
          if(!this._dirty){
            this._dirty=true
            tigggerRefValue(this)
          }
        })
        //该箭头函数为调度器,当响应式数据修改时会进行依赖触发,就会通过我们定义的调度器进行依赖触发
        this.effect.computed=this
    }
    get value(){
        trackRefValue(this)
        if(this._dirty){
          this._dirty=false
          this._value=this.effect.run()
        }
        return this._value
    }
}

effect.ts

type EffectScheduler=(...args:Array<any>)=>any
export class ReactiveEffect<T=any>{
  computed?:ComputedRefImpl<T>
  constructor(public fn:()=>T,public scheduler:EffectScheduler|null=null){}
  run(){
    activeEffect=this
    return this.fn()
  }
}
//当响应式数据变化,如果是调度器函数,就进行调度器内触发,多了一层 !this._dirty 的判断,再绕回来进行依赖更新
export function triggerEffects(dep:Dep){
  const effects=isArray(dep)?dep:[...dep]
  //依次触发依赖
  for (const effect of effects) {
    triggerEffect(effect)
  }
}
export function triggerEffect(effect){
  if(effect.scheduler){
    effect.scheduler()
  }
  else{
    effect.run()
  }
}

此时我们已经实现了对 computed 数据的响应式处理。下面我们来处理一下 computed 的缓存问题。而且此时会出现一个问题,当我们在effect 函数中进行两次修改时就会导致死循环的问题。这是因为第二次修改,会重复触发调度器导致的

effect(()=>{
   document.getElementById("p1").innerText=computedObj.value
   document.getElementById("p1").innerText=computedObj.value
})

effect.ts

export function triggerEffects(dep:Dep){
  const effects=isArray(dep)?dep:[...dep]
  //依次触发依赖
  for (const effect of effects) {
    if(effect.computed){
      triggerEffect(effect)
    }
  }
  for (const effect of effects) {
    if(!effect.computed){
      triggerEffect(effect)
    }
  }
}

最后

到这里 computed 的初始版本代码已经完成了,代码在 gitee.com/Provens/vue… 03-computed 分支中有兴趣的小伙伴可以把代码拉下来一起学习。