jingzhiMo.github.io icon indicating copy to clipboard operation
jingzhiMo.github.io copied to clipboard

动手实现简单版 vue 计算属性computed

Open jingzhiMo opened this issue 5 years ago • 1 comments

在使用 vue 的时候,了解到计算属性很好用,可以延迟计算直到调用才返回真实的数据,而且计算属性依赖的值没有发生改变的情况,就不会重新执行函数计算;比较好奇是怎么实现的,但是没有去了解原理性相关,最近去看一下源码实现,大概直到具体的实现。下面就是根据自己的了解,手动实现一个简单的计算属性:

思考

我们知道 vue2.x 是基于Object.defineProperty来劫持数据的,那么挂载到vm.data的属性值就很好理解,在gettersetter的函数里面做一层简单的代理,那么计算属性为啥可以从一个函数变成一个数值,而且可以知道依赖的数据值?大概是因为计算属性的函数执行的时候,会触发到data属性的getter,那么我们就可以在这里做手脚,就知道当前的计算属性依赖了多少data数据了。

v1.0

我们来看一段的代码,声明datacomputed数据,劫持data数据方法,初始化计算属性方法等

// data 数据
var data = {
    foo: 123,
    bar: 'bar'
}

// data 的代理对象
var _data = {}

// 计算属性数据
var computedData = {
    fooMap () {
        return data.foo + 1
    },
    barMap () {
        return data.bar + ' baz'
    }
}

// 是否在收集数据
var isDep = false
// 当前收集的回调函数
var notify

// foo 的回调函数列表
var fooNotify = []

// 回调函数对应的字段
var notifyProp


// 劫持数据方法
function defineProperty (obj) {
    for (let key in obj) {
        // 缓存原有的数据
        _data[key] = obj[key]

        Object.defineProperty(obj, key, {
            get () {
                // 判断当前调用方法是否在收集当中
                if (isDep) {
                    // 计算属性对应的方法与计算属性对应的key值加入到缓存
                    fooNotify.push([notify, notifyProp])
                }
                return _data[key]
            },
            set (value) {
                // 更改缓存的数值
                _data[key] = value
                // 计算属性对应的方法重新计算,重新赋值
                fooNotify.forEach(item => {
                    computedData[item[1]] = item[0]()
                })
            }
        })
    }
    return obj
}

// 初始化计算属性
function initComputed (computed) {
    // 依赖收集开始
    isDep = true

    for (let key in computed) {
        let method = computed[key]
    
        // 把当前的计算属性方法赋值到全局变量
        notify = method
        notifyProp = key
        // 通过函数计算获取数据,获得计算属性的值
        computed[key] = method()
    }

    // 依赖收集结束
    isDep = false
}

定义好方法与数据,我们可以尝试着使用:

// 1. 劫持数据
defineProperty(data)
// 2. 初始化计算属性
initComputed(computedData) 
// 执行完这一步,computedData 的数据就变成了:{ fooMap: 124, barMap: 'bar baz' }
// 3. 更改 data.foo 的值
data.foo = 1234
// 执行完这一步,computedData 为: {fooMap: 1235, barMap: 'bar baz'}

从上面的结果得到,可以实现计算属性一个很重要的一个特点:依赖数据发生改变,则计算属性发生改变;但是缺点也是很明显的,变量都是全局变量;依赖数据发生改变的回调的方法也是放到全局的数组;我们在接下来的v2版本修好这种情况。现在我们大概看到computeddata的观察者关系:

v2.0

这一个版本我们主要 fix 部分全局变量,把计算属性与数据归类到同一个对象,这个版本改动不多:

// 是否在收集数据
var isDep = false
// 当前收集的回调函数
var notify

// foo 的回调函数列表
var fooNotify = []

// 回调函数对应的字段
var notifyProp

var instance = {
    data: {
        foo: 123,
        bar: 'bar'
    },
    computed: {
        fooMap () {
            return instance.data.foo + 1
        },
        barMap () {
            return instance.data.bar + ' baz'
        }
    }
}

// 劫持对象
function defineProperty (vm) {
    let data = vm.data

    vm._data = {}

    for (let key in data) {
        // 缓存原有的数据
        vm._data[key] = data[key]

        Object.defineProperty(data, key, {
            get () {
                if (isDep) {
                    fooNotify.push([notify, notifyProp])
                }
                return vm._data[key]
            },
            set (value) {
                vm._data[key] = value
                fooNotify.forEach(item => {
                    vm.computed[item[1]] = item[0]()
                })
            }
        })
    }
    return data
}

// 初始化计算属性
function initComputed (vm) {
    // 依赖收集开始
    isDep = true

    for (let key in vm.computed) {
        let method = vm.computed[key]

        notify = method
        notifyProp = key
        // 通过函数计算获取数据
        vm.computed[key] = method()
    }

    // 依赖收集结束
    isDep = false
}

// 劫持 data 数据
defineProperty(instance)
// 初始化计算属性
initComputed(instance)

在完成v2.0版本之后,我们把计算属性与数据合成到一个对象;但是只能实现一个计算属性的应用,如果有多个计算属性的话,就控制不了,因为存放计算属性的数组只有一个,在v3版本,需要处理这种情况。

v3.0

// 观察者列表
class ObserverList {
    constructor () {
        this.list = []
    }
    add (item) {
        this.list.push(item)
    }
    count () {
        return this.list.length
    }
    getByIndex (index) {
        return this.list[index]
    }
}

// 被观察者
class Watcher {
    constructor () {
        this.observer = new ObserverList()
    }
    addObserver (observer) {
        this.observer.add(observer)
    }
    notify () {
        let len = this.observer.count()

        for (let i = 0; i < len; i++) {
            this.observer.getByIndex(i).update()
        }
    }
}

// 观察者
class Observer {
    constructor (update) {
        this.update = update
    }
}

// 数据依赖
class Dep {}

Dep.target = ''

// 劫持对象
function defineProperty (vm) {
    let data = vm.data

    vm._data = {}

    for (let key in data) {
        // 缓存原有的数据
        vm._data[key] = data[key]

        Object.defineProperty(data, key, {
            get () {
                // 只有在依赖收集的时候,才需要添加 watcher,普通数据调用不需要添加 watcher
                if (Dep.target) {
                    // 当前数据字段还没有 watcher ,则新建一个
                    let watcher = vm._watcher[key] || new Watcher()

                    // 加入到依赖数组
                    watcher.addObserver(Dep.target)
                    vm._watcher[key] = watcher
                }
                return vm._data[key]
            },
            set (value) {
                vm._data[key] = value

                // 有对应的 watcher 那么则提示更新,调用watcher的notify方法
                if (vm._watcher[key]) {
                    vm._watcher[key].notify()
                }
            }
        })
    }
    return data
}

// 初始化计算属性
function initComputed (vm) {
    vm._watcher = {}

    for (let key in vm.computed) {
        let method = vm.computed[key]

        // 新建 observer,并标识收集依赖开始
        Dep.target = new Observer(() => {
            vm.computed[key] = method()
        })

        // 通过函数初始化计算数据,并且获取到所有依赖
        vm.computed[key] = method()
        // 依赖收集结束
        Dep.target = undefined
    }
}

var instance = {
    data: {
        foo: 123,
        bar: 'bar',
        baz: 'bazbaz'
    },
    computed: {
        fooMap () {
            let num = instance.data.foo + 1
            let str = instance.data.bar + '...str'

            return num + str
        },
        barMap () {
            return instance.data.bar + ' baz'
        }
    }
}

// 劫持 data 数据
defineProperty(instance)
// 初始化计算属性
initComputed(instance)

// 验证处理:
console.log(instance.computed.fooMap) // 124bar...str
console.log(instance.computed.barMap) // bar baz
// 赋值数据
instance.data.bar = 'new bar value'
console.log(instance.computed.fooMap) // 124new bar value...str
console.log(instance.computed.barMap) // new bar value baz  
// done.

这个版本更改的地方比较多,加入观察者模式的处理;

在初始化计算属性的时候,为每个计算属性新建一个观察者,新建一个观察者传入的参数是一个函数,这个函数会在依赖的数据发生改变的时候执行;函数的内容就是为计算属性的值重新计算:

 // 新建 observer,并标识收集依赖开始
Dep.target = new Observer(() => {
    vm.computed[key] = method() // method 是指计算属性对应的方法
})

在劫持数据的时候,数据的get触发的时候,如果是在依赖收集的过程中(也就是数据被计算属性调用),那么就会为这个数据添加watcher;并且把当前正在收集依赖的计算属性对应的observer实例加入到watcher中

if (Dep.target) {
    // 当前数据字段还没有 watcher ,则新建一个
    let watcher = vm._watcher[key] || new Watcher()
    
    // 加入到依赖数组
    watcher.addObserver(Dep.target)
    vm._watcher[key] = watcher
}

数据的set触发的时候,那么就需要通知对应观察者,计算属性对应的值就可以更新。

// 有对应的 watcher 那么则提示更新,调用watcher的notify方法
if (vm._watcher[key]) {
    vm._watcher[key].notify()
}

通过引入观察者的类别,处理多个计算属性;现在我们基本完善好v1.0版本全局变量的问题;除此之外,计算属性也有一个比较重要的特点是:惰性求值。当没有调用计算属性的时候,是不会触发计算;而且如果单个计算属性调用数据多次的时候,会存在watcher添加多次observer,这些下一个版本继续增加或优化。

v4.0

// 观察者列表
class ObserverList {
    constructor () {
        this.list = []
    }
    add (item) {
        this.list.push(item)
    }
    count () {
        return this.list.length
    }
    getByIndex (index) {
        return this.list[index]
    }
}

// 被观察者
class Watcher {
    constructor () {
        this.dep = new Set()
        this.observer = new ObserverList()
    }
    addObserver (observer) {
        // 已经加入了到依赖,返回,不做处理
        if (!this.dep.has(observer.id)) {
            this.observer.add(observer)
            this.dep.add(observer.id)
        }
    }
    notify () {
        let len = this.observer.count()

        for (let i = 0; i < len; i++) {
            let ob = this.observer.getByIndex(i)
            ob.dirty = true
            ob.update()
        }
    }
}

// 观察者
let _uid = 0
class Observer {
    constructor (update) {
        this.id = _uid++
        this.update = update
    }
}

// 数据依赖
class Dep {}

Dep.target = ''

// 劫持对象
function defineProperty (vm) {
    let data = vm.data

    vm._data = {}
    vm._watcher = {}

    for (let key in data) {
        // 缓存原有的数据
        vm._data[key] = data[key]

        Object.defineProperty(data, key, {
            get () {
                if (Dep.target) {
                    // 当前数据字段还没有 watcher ,则新建一个
                    let watcher = vm._watcher[key] || new Watcher()

                    // 加入到依赖数组
                    watcher.addObserver(Dep.target)
                    vm._watcher[key] = watcher
                }
                return vm._data[key]
            },
            set (value) {
                vm._data[key] = value

                // 有 watcher 那么则提示更新
                if (vm._watcher[key]) {
                    vm._watcher[key].notify()
                }
            }
        })
    }
    return data
}

// 初始化计算属性
function initComputed (vm) {
    vm._computedWatcher = {}

    for (let key in vm.computed) {
        let method = vm.computed[key]

        vm._computedWatcher[key] = {
            dirty: true,
            value: undefined,
            getter: method,
            // 这个属性的观察者
            ob: undefined
        }

        Object.defineProperty(vm.computed, key, {
            get () {
                let cache = vm._computedWatcher[key]

                if (!cache.dirty) {
                    return cache.value
                } else {
                    // 该属性没有指定的观察者,则新建
                    if (!cache.ob) {
                        // 新建 observer,并标识收集依赖开始
                        Dep.target = cache.ob = new Observer(() => {
                            cache.value = cache.getter()
                        })
                    }
                    cache.dirty = false
                    cache.value = cache.getter()
                }
                
                console.log('calc new cache')
                return cache.value
            }
        })
        // 通过函数初始化计算数据,并且获取到所有依赖
        vm.computed[key] = method()
        // 依赖收集结束
        Dep.target = undefined
    }
}

var instance = {
    data: {
        foo: 123,
        bar: 'bar',
        baz: 'bazbaz'
    },
    computed: {
        fooMap () {
            let num = instance.data.foo + 1
            let str = instance.data.bar + '...str'

            return num + str
        },
        barMap () {
            return instance.data.bar + ' baz'
        }
    }
}

在这一个版本,主要新增了,vm._computedWatcher,缓存每一个计算属性的一些记录,结构如下:

vm._computedWatcher[key] = {
    dirty: true, // 表示当前数据是否为“脏”,当为“脏”的时候,则需要重新计算
    value: undefined, // 缓存计算属性的返回值
    getter: method, // 计算属性对应的计算方法
    ob: undefined // 这个属性的观察者
}

dirtytrue的情况主要是两种,一种初始化的时候,另外一个种是依赖的数据已经发生了改变。为了验证这种情况,我们在计算属性的get方法打log,如果被调用的时候就会log出来:

// 劫持 data 数据
defineProperty(instance)
// 初始化计算属性
initComputed(instance) // 这个时候并没有 log:calc new cache

// 获取计算属性
instance.computed.fooMap
// calc new cache
// return 124bar...str

由此可以看出,惰性求值是可以的。另外可以注意到为每个观察者的实例添加一个id,在watcher添加观察者的时候判断观察者列表是否已经包含当前观察者,可以实现简单的观察者去重。

总结

至此,一个简单的计算属性就可以实现起来,虽然使用起来与vue有区别,例如数据与计算属性都挂载到vm对象;并且例子的健壮性也需要提高,没有考虑到一些特殊的情况,例如如何监听数组的变化,这些也需要实现;还有一些例如sync特性没有实现;但是大部分常用功能都能够实现,而且思路上理解清晰就完成了部分任务;这个时候再去看 vue 的源码应该会理解起来更加快。end.

jingzhiMo avatar Jun 01 '19 06:06 jingzhiMo

太感谢了,看了好多文章都没看懂。看的脑子嗡嗡的。 直到看到了这个文章,循序渐进,我终于理解了。

Shadowzzh avatar Mar 16 '22 08:03 Shadowzzh