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

点击查看相关代码


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

查看所有标签

猜你喜欢:

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

Python网络编程攻略

Python网络编程攻略

萨卡尔 (Dr.M.O.Faruque Sarker) / 安道 / 人民邮电出版社 / 2014-12-1 / 45.00元

开发TCP/IP网络客户端和服务器应用 管理本地设备的IPv4/IPv6网络接口 使用HTTP和HTTPS协议编写用途多、效率高的Web客户端 编写可使用常见电子邮件协议的电子邮件客户端 通过Telnet和SSH连接执行远程系统管理任务 使用Web服务与流行的网站交互 监控并分析重要的常见网络安全漏洞一起来看看 《Python网络编程攻略》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

URL 编码/解码
URL 编码/解码

URL 编码/解码

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具