实现 VUE 中 MVVM - step10 - Computed

栏目: 编程工具 · 发布时间: 5年前

内容简介:先捋一下,之前我们实现的对于比与现在的由于后两者和子父组件有关,先放一放,我们先来实现

先捋一下,之前我们实现的 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 机制前,我们需要先实现父子组件,这也是下一步的内容。

点击查看相关代码


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

软技能

软技能

John Sonmez / 王小刚 / 人民邮电出版社 / 2016-7 / 59.00元

这是一本真正从“人”(而非技术也非管理)的角度关注软件开发人员自身发展的书。书中论述的内容既涉及生活习惯,又包括思维方式,凸显技术中“人”的因素,全面讲解软件行业从业人员所需知道的所有“软技能”。本书聚焦于软件开发人员生活的方方面面,从揭秘面试的流程到精耕细作出一份杀手级简历,从创建大受欢迎的博客到打造你,从提高自己工作效率到与如何与“拖延症”做斗争,甚至包括如何投资不动产,如何关注自己的健康。本......一起来看看 《软技能》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具