资讯专栏INFORMATION COLUMN

VUE - MVVM - part10 - Computed

callmewhy / 2735人阅读

摘要:了解之后我们来实现它,同样的为了方便理解我写成了一个类这里的一般是的实例将属性代理到实例下的构造函数我们实现了代理属性和更新计算属性的值,同时依赖没变化时,也是不会触发的更新,解决了以上的个问题。

看这篇之前,如果没有看过之前的文章,移步拉到文章末尾查看之前的文章。

回顾

先捋一下,之前我们实现的 Vue 类,主要有一下的功能:

属性和方法的代理 proxy

监听属性 watcher

事件

对于比与现在的 Vue 中的数据处理,我们还有一些东西没有实现:Computedpropsprovied/inject

由于后两者和子父组件有关,先放一放,我们先来实现 Computed

Computed

在官方文档中有这么一句话:

计算属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算。

这也是计算属性性能比使用方法来的好的原因所在。

ok 现在我们来实现它,我们先规定一下一个计算属性的形式:

{
    get: Function,
    set: Function
}

官方给了我们两种形式来写 Computed ,看了一眼源码,发现最终是处理成这种形式,所以我们先直接使用这种形式,之后再做统一化处理。

惯例我们通过测试代码来看我们要实现什么功能:

let test = new Vue({
    data() {
        return {
            firstName: "aco",
            lastName: "Yang"
        }
    },
    computed: {
        computedValue: {
            get() {
                console.log("测试缓存")
                return this.firstName + " " + this.lastName
            }
        },
        computedSet: {
            get() {
                return this.firstName + " " + this.lastName
            },
            set(value) {
                let names = value.split(" ")
                this.firstName = names[0]
                this.lastName = names[1]
            }
        }
    }
})

console.log(test.computedValue)
// 测试缓存
// aco Yang
console.log(test.computedValue)
// acoYang (缓存成功,并没有调用 get 函数)
test.computedSet = "accco Yang"
console.log(test.computedValue)
// 测试缓存 (通过 set 使得依赖发生了变化)
// accco Yang

我们可以发现:

计算属性是代理到 Vue 实例上的一个属性

第一次调用时,调用了 get 方法(有 ‘测试缓存’ 输出),而第二次没有输出

当依赖发生改变时,再次调用了 get 方法

解决

第一点很好解决,使用 Object.defineProperty 代理一下就 ok。
接下来看第二点和第三点,当依赖发生改变时,值就会变化,这点和我们之前实现 Watcher 很像,计算属性的值就是 get 函数的返回值,在 Watcher 中我们同样保存了监听的值(watcher.value),而这个值是会根据依赖的变化而变化的(如果没看过 Watcher 实现的同学,去看下 step3step4),所以计算属性的 get 就是 Watchergetter

那么 Watchercallback 是啥?其实这里根本不需要 callback ,计算属性仅仅需要当依赖发生变化时,保存的值发生变化。

ok 了解之后我们来实现它,同样的为了方便理解我写成了一个类:

function noop() {
}

let uid = 0

export default class Computed {
    constructor(key, option, ctx) {
        // 这里的 ctx 一般是 Vue 的实例
        this.uid = uid++
        this.key = key
        this.option = option
        this.ctx = ctx
        this._init()
    }

    _init() {
        let watcher = new Watcher(
            this.ctx,
            this.option.get || noop,
            noop
        )

        // 将属性代理到 Vue 实例下
        Object.defineProperty(this.ctx, this.key, {
            enumerable: true,
            configurable: true,
            set: this.option.set || noop,
            get() {
                return watcher.value
            }
        })
    }
}

// Vue 的构造函数
export class Vue extends Event {
    constructor(options) {
        super()
        this.uid = uid++
        this._init(options)
    }

    _init(options) {
        let vm = this
        ...
        for (let key in options.computed) {
            new Computed(vm, key, options.computed[key])
        }

    }
}

我们实现了代理属性 Object.defineProperty 和更新计算属性的值,同时依赖没变化时,也是不会触发 Watcher 的更新,解决了以上的 3 个问题。

但是,试想一下,计算属性真的需要实时去更新对应的值吗?

首先我们知道,依赖的属性发生了变化会导致计算属性的变化,换句话说就是,当计算属性发生变化了,data 下的属性一定有一部分发生了变化,而 data 下属性发生变化,会导致视图的改变,所以计算属性发生变化在去触发视图的变化是不必要的。

其次,我们不能确保计算属性一定会用到。

而基于第一点,计算属性是不必要去触发视图的变化的,所以计算属性其实只要在获取的时候更新对应的值即可。

Watcher 的脏检查机制

根据我们上面的分析,而 ComputedWatcher 的一种实现,所以我们要实现一个不实时更新的 Watcher

Watcher 中我们实现值的更新是通过下面这段代码:

update() {
    const value = this.getter.call(this.obj)
    const oldValue = this.value
    this.value = value
    this.cb.call(this.obj, value, oldValue)
}

当依赖更新的时候,会去触发这个函数,这个函数变更了 Watcher 实例保存的 value ,所以我们需要在这里做出改变,先看下伪代码:

update() {
    if(/* 判断这个 Watcher 需不需要实时更新 */){
        // doSomething
        // 跳出 update
        return
    }
    const value = this.getter.call(this.obj)
    const oldValue = this.value
    this.value = value
    this.cb.call(this.obj, value, oldValue)
}

这里的判断是需要我们一开始就告诉 Watcher 的,所以同样的我们需要修改 Watcher 的构造函数

constructor(object, getter, callback, options) {
    ···
    if (options) {
        this.lazy = !!options.lazy
    } else {
        this.lazy = false
    }
    this.dirty = this.lazy
}

我们给 Watcher 多传递一个 options 来传递一些配置信息。这里我们把不需要实时更新的 Watcher 叫做 lazy Watcher。同时设置一个标志(dirty)来标志这个 Watcher 是否需要更新,换个专业点的名称是否需要进行脏检查。

ok 接下来我们把上面的伪代码实现下:

update() {
    // 如果是 lazy Watcher
    if (this.lazy) {
        // 需要进行脏检查
        this.dirty = true
        return
    }
    const value = this.getter.call(this.obj)
    const oldValue = this.value
    this.value = value
    this.cb.call(this.obj, value, oldValue)
}

如果代码走到 update 也就说明这个 Watcher 的依赖发生了变化,同时这是个 lazy Watcher ,那这个 Watcher 就需要进行脏检查。

但是,上面代码虽然标志了这个 Watcher ,但是 value 并没有发生变化,我们需要专门写一个函数去触发变化。

/**
 * 脏检查机制手动触发更新函数
 */
evaluate() {
    this.value = this.getter.call(this.obj)
    // 脏检查机制触发后,重置 dirty
    this.dirty = false
}

查看完整的 Watcher 代码

ok 接着我们来修改 Computed 的实现:

class Computed {
    constructor(ctx, key, option,) {
        this.uid = uid++
        this.key = key
        this.option = option
        this.ctx = ctx
        this._init()
    }

    _init() {
        let watcher = new Watcher(
            this.ctx,
            this.option.get || noop,
            noop,
            // 告诉 Wather 来一个 lazy Watcher
            {lazy: true}
        )

        Object.defineProperty(this.ctx, this.key, {
            enumerable: true,
            configurable: true,
            set: this.option.set || noop,
            get() {
                // 如果是 dirty watch 那就触发脏检查机制,更新值
                if (watcher.dirty) {
                    watcher.evaluate()
                }
                return watcher.value
            }
        })
    }
}

ok 测试一下

let test = new Vue({
    data() {
        return {
            firstName: "aco",
            lastName: "Yang"
        }
    },
    computed: {
        computedValue: {
            get() {
                console.log("测试缓存")
                return this.firstName + " " + this.lastName
            }
        },
        computedSet: {
            get() {
                return this.firstName + " " + this.lastName
            },
            set(value) {
                let names = value.split(" ")
                this.firstName = names[0]
                this.lastName = names[1]
            }
        }
    }
})
// 测试缓存 (刚绑定 watcher 时会调用一次 get 进行依赖绑定)
console.log("-------------")
console.log(test.computedValue)
// 测试缓存
// aco Yang
console.log(test.computedValue)
// acoYang (缓存成功,并没有调用 get 函数)

test.firstName = "acco"
console.log(test.computedValue)
// 测试缓存 (当依赖发生变化时,就会调用 get 函数)
// acco Yang

test.computedSet = "accco Yang"
console.log(test.computedValue)
// 测试缓存 (通过 set 使得依赖发生了变化)
// accco Yang

到目前为止,单个 Vue 下的数据相关的内容就差不多了,在实现 propsprovied/inject 机制前,我们需要先实现父子组件,这也是下一步的内容。

点击查看相关代码

系列文章地址

VUE - MVVM - part1 - defineProperty

VUE - MVVM - part2 - Dep

VUE - MVVM - part3 - Watcher

VUE - MVVM - part4 - 优化Watcher

VUE - MVVM - part5 - Observe

VUE - MVVM - part6 - Array

VUE - MVVM - part7 - Event

VUE - MVVM - part8 - 优化Event

VUE - MVVM - part9 - Vue

VUE - MVVM - part10 - Computed

VUE - MVVM - part11 - Extend

VUE - MVVM - part12 - props

VUE - MVVM - part13 - inject & 总结

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/107764.html

相关文章

  • VUE - MVVM - part11 - Extend

    摘要:所以方法,是对默认进行扩展,从而实现扩展。这里我用了这个库提供的合并方法,用来合并两个对象,并不会修改原对象的内容。测试符合我们的预期,方法也就实现了,下一步,实现父子组件。系列文章地址优化优化总结 看这篇之前,如果没有看过之前的文章,移步拉到文章末尾查看之前的文章。 组件的扩展 在 Vue 中有 extend 方法可以扩展 Vue 的实例,在上一步中,有一些实现是必须要通过子父组件才...

    cartoon 评论0 收藏0
  • VUE - MVVM - part13 - inject & 总结

    摘要:通过装作这些变化,我们实现了从而到达了数据变化触发函数的过程。于此同时,我们还实现了来扩展这个可响应的结构,让这个对象拥有了触发和响应事件的能力。最后,根据我们的实现,这是最终的产出,一个框架,了解一下系列文章地址优化优化总结 看这篇之前,如果没有看过之前的文章,移步拉到文章末尾查看之前的文章。 provide / inject 在上一步我们实现了,父子组件,和 props 一样 pr...

    niuxiaowei111 评论0 收藏0
  • VUE - MVVM - part1 - defineProperty

    摘要:在中关于如何实现在网上可以搜出不少,在看了部分源码后,梳理一下内容。换个说法,当我们取值的时候,函数自动帮我们添加了针对当前值的依赖,当这个值发生变化的时候,处理了这些依赖,比如说节点的变化。 在 VUE 中关于如何实现在网上可以搜出不少,在看了部分源码后,梳理一下内容。 首先,我们需要了解一下 js 中的一个 API :Object.defineProperty(obj, prop,...

    liukai90 评论0 收藏0
  • VUE - MVVM - part7 - Event

    摘要:事件是什么在标准浏览器中,我们经常使用来为一个添加一个事件等。仔细看这些情况,归结到代码中,无非就是一个行为或情况的名称,和一些列的动作,而在中动作就是,一系列的动作就是一个函数的集合。 看这篇之前,如果没有看过之前的文章,可拉到文章末尾查看之前的文章。 事件是什么? 在标准浏览器中,我们经常使用:addEventListener 来为一个 DOM 添加一个事件(click、mouse...

    xialong 评论0 收藏0
  • VUE - MVVM - part2 - Dep

    摘要:看这篇之前,如果没看过先移步看实现中。同样的,在取值时收集依赖,在设置值当值发生变化时触发依赖。中实现了一个的类来处理以上两个问题,之后再说。以下语法下的,源码中差不多就这样点击查看相关代码系列文章地址优化优化总结 看这篇之前,如果没看过 step1 先移步看 实现 VUE 中 MVVM - step1 - defineProperty。 在上一篇我们大概实现了,Vue 中的依赖收集和...

    hover_lew 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<