Vue源码学习(十七):实现computed计算属性

好家伙,本章我们尝试实现computed属性

0.完整代码已开源

https://github.com/Fattiger4399/analytic-vue.git

1.分析

1.1computed的常见使用方法

复制代码
1. 计算依赖数据:当某个数据发生变化时,computed属性可以自动更新,并返回计算结果。例如:
复制代码
<template>
  <div>
    <p>用户姓名:{{ userName }}</p>
    <p>用户年龄:{{ age }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      userName: '张三',
      age: 25,
    };
  },
  computed: {
    // 计算用户年龄显示格式
    formattedAge() {
      return this.age.toString().padStart(2, '0');
    },
  },
};
</script>
复制代码
2. 数据过滤:利用computed属性对数据进行过滤处理,例如:
复制代码
<template>
  <div>
    <p>列表数据:{{ filteredList }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      list: [1, 2, 3, 4, 5],
      filterValue: 3,
    };
  },
  computed: {
    // 计算过滤后的列表数据
    filteredList() {
      return this.list.filter((item) => item === this.filterValue);
    },
  },
};
</script>
复制代码
而在computed定义中又分为两种写法:函数和属性
复制代码
computed: {
                //1.函数
                fullName() {
                    console.log('执行')
                    return this.firstName + this.lastName
                },
                //2.属性
                goodName: {
                    get() {
                        return this.firstName + this.lastName
                    },
                    set(value) {
                        // ....
                    }
                }
            }

1.2.Vue实现computed大概步骤

复制代码
1. 初始化阶段:当创建 Vue 实例时,Vue 会遍历 data 中的每个属性,并使用 Object.defineProperty 方法将它们转化为响应式。
这个过程会在 data 对象上创建一层 Watcher 对象,用于监听数据的变化。

2. 创建 Computed:当创建一个 Computed 属性时,Vue 会调用 initComputed 函数,该函数负责注册这个 Computed 属性。
注册过程中,会创建一个 Watcher 对象,并将其挂载到 Computed 属性的表达式上。
这样,当表达式依赖的数据发生变化时,Watcher 对象可以监听到这些变化,并更新 Computed 属性的值。

3. 更新 Computed 值:当 data 中的数据发生变化时,对应的 Watcher 对象会收到通知。
Watcher 对象会执行 Computed 属性的 get 方法,计算出新的 Computed 值。然后,Watcher 对象会通知视图层更新,使用新的 Computed 值。

4. 缓存 Computed 值:为了提高性能,Vue 会缓存 Computed 属性的计算结果。
   当 Computed 属性的依赖数据发生变化时,Vue 会先检查依赖数据的变化是否导致 Computed 值需要重新计算。
 如果需要重新计算,Vue 会清除缓存,并调用 Computed 属性的 get 方法计算新值。如果不需要重新计算,Vue 会直接使用缓存的旧值。

2、代码实现

//initState.js
** import Dep from "./observe/dep.js";**

复制代码
export function initState(vm) {
    // console.log(vm)
    //
    const opts = vm.$options
    if (opts.data) {
        initData(vm);
    }
    if (opts.watch) {
        initWatch(vm);
    }
    if (opts.props) {
        initProps(vm);
    }


    if (opts.computed) {
        initComputed(vm);
    }
    if (opts.methods) {
        initMethod(vm);
    }
}

function initComputed(vm) {
    let computed = vm.$options.computed
    console.log(computed)
    let watcher = vm.computedWatchers = {}

    for (let key in computed) {
        let userDef = computed[key]

        let getter = typeof userDef == 'function' ? userDef : userDef.get
        watcher[key] = new Watcher(vm, getter, () => {}, {
            //标记此为computed的watcher
            lazy: true
        })
        defineComputed(vm, key, userDef)
    }
}
let sharedPropDefinition = {}

function defineComputed(target, key, userDef, ) {
    sharedPropDefinition = {
        enumerable: true,
        configurable: true,
        get: () => {},
        set: () => {}
    }
    if (typeof userDef == 'function') {
        sharedPropDefinition.get = createComputedGetter(key)
    } else {
        sharedPropDefinition.get = createComputedGetter(key)
        sharedPropDefinition.set = userDef.set
    }
    Object.defineProperty(target, key, sharedPropDefinition)
}
//高阶函数
function createComputedGetter(key) {
    return function () {
        // if (dirty) {
        // }
        let watcher = this.computedWatchers[key]
        if (watcher) {
            if (watcher.dirty) {
                //执行 求值 
                watcher.evaluate() //
            }
            if(Dep.targer){ //说明

                watcher.depend()
            }
            return watcher.value
        }
    }
}

1.sharedPropDefinition: 这是一个空对象,用于定义计算属性的属性描述符(property descriptor)。属性配置
它包含了enumerable、configurable、get和set等属性配置,这些配置决定了计算属性在对象上的可枚举性、可配置性以及获取和设置属性时的行为。

2.computedWatchers: 该对象用于存储计算属性的观察者(watcher)。
每一个计算属性都会被创建一个对应的观察者对象,并将其存储在computedWatchers对象中。
观察者对象负责侦听计算属性的依赖变化,并在需要时更新计算结果。

3.createComputedGetter(): 该函数用于创建计算属性的 getter 。
getter 函数会在访问计算属性时被调用,它首先会检查观察者对象是否存在,如果存在则判断观察者对象是否需要重新计算计算属性的值,
如果需要则执行求值操作,并最终返回计算属性的值。
此外,通过Dep.target的判断,可以将计算属性的依赖添加到依赖收集器中,以便在依赖变化时及时更新计算属性的值。

watcher.js

复制代码
class Watcher {
    //vm 实例
    //exprOrfn vm._updata(vm._render()) 
    constructor(vm, exprOrfn, cb, options) {
        // 1.创建类第一步将选项放在实例上
        this.vm = vm;
        this.exprOrfn = exprOrfn;
        this.cb = cb;
        this.options = options;
        //for conputed
        this.lazy = options.lazy
        this.dirty = this.lazy
        // 2. 每一组件只有一个watcher 他是为标识
        this.id = id++
        this.user = !!options.user
        // 3.判断表达式是不是一个函数
        this.deps = [] //watcher 记录有多少dep 依赖
        this.depsId = new Set()
        if (typeof exprOrfn === 'function') {
            this.getter = exprOrfn
        } else { //{a,b,c}  字符串 变成函数 
            this.getter = function () { //属性 c.c.c
                let path = exprOrfn.split('.')
                let obj = vm
                for (let i = 0; i < path.length; i++) {
                    obj = obj[path[i]]
                }
                return obj //
            }
        }
        // 4.执行渲染页面
        // this.value =  this.get() //保存watch 初始值
        this.value = this.lazy ? void 0 : this.get()


    }
    addDep(dep) {
        //去重  判断一下 如果dep 相同我们是不用去处理的
        let id = dep.id
        //  console.log(dep.id)
        if (!this.depsId.has(id)) {
            this.deps.push(dep)
            this.depsId.add(id)
            //同时将watcher 放到 dep中
            // console.log(666)
            dep.addSub(this)

        }
        // 现在只需要记住  一个watcher 有多个dep,一个dep 有多个watcher
        //为后面的 component 
    }
    run() { //old new
        let value = this.get() //new
        let oldValue = this.value //old
        this.value = value
        //执行 hendler (cb) 这个用户wathcer
        if (this.user) {
            this.cb.call(this.vm, value, oldValue)
        }
    }
    get() {
        // Dep.target = watcher

        pushTarget(this) //当前的实例添加
        const value = this.getter.call(this.vm) // 渲染页面  render()   with(wm){_v(msg,_s(name))} ,取值(执行get这个方法) 走劫持方法
        popTarget(); //删除当前的实例 这两个方法放在 dep 中
        return value
    }
    //问题:要把属性和watcher 绑定在一起   去html页面
    // (1)是不是页面中调用的属性要和watcher 关联起来
    //方法
    //(1)创建一个dep 模块
    updata() { //三次
        //注意:不要数据更新后每次都调用 get 方法 ,get 方法回重新渲染
        //缓存
        // this.get() //重新
        
        // 渲染
        if(this.lazy){
            this.dirty = true
        }else{
            queueWatcher(this)
        }
    }
    evaluate() {
        this.value = this.get()
        this.dirty = false
    }
    depend(){
        let i = this.deps.length
        while(i--){
            this.deps[i].depend()
        }
    }
}

1.evaluate(): 该方法用于求值计算属性的结果。
它会调用计算属性的 getter 方法(也就是sharedPropDefinition.get或createComputedGetter()函数中返回的函数),
获取计算属性的最新值,并将该值保存在观察者对象的value属性中。
同时,将观察者对象的dirty属性设置为false,表示计算属性的值已经是最新的了。

2.depend():遍历所有的依赖对象,并调用它们的depend()方法,

dep.js

复制代码
class Dep {
    constructor() {
        this.subs = []
        this.id = id++
    }
    //收集watcher 
    depend() {
      
        //我们希望water 可以存放 dep
        //实现双休记忆的,让watcher 记住
        //dep同时,让dep也记住了我们的watcher
        Dep.targer.addDep(this)
        // this.subs.push(Dep.targer) // id:1 记住他的dep
    }
    addSub(watcher){
        this.subs.push(watcher)
    }
    //更新
    notify() {
        // console.log(Dep.targer)
        this.subs.forEach(watcher => {
            watcher.updata()
        })
    }
}

2.depend(): 该方法用于将计算属性的观察者对象添加到依赖收集器(Dependency)中。
在计算属性的 getter 方法执行过程中,如果访问了其他响应式属性(依赖),

那么这些响应式属性对应的观察者对象会将当前的计算属性的观察者对象添加到它们的依赖列表中。
这样,在依赖变化时,观察者对象会被通知到,并重新执行计算属性的 getter 方法来更新计算属性的值。 (在compoted的watcher执行完毕后,还有其他元素的wathcer等待执行)

3.预览效果