jingzhiMo.github.io
jingzhiMo.github.io copied to clipboard
动手实现简单版 vue 计算属性computed
在使用 vue 的时候,了解到计算属性很好用,可以延迟计算直到调用才返回真实的数据,而且计算属性依赖的值没有发生改变的情况,就不会重新执行函数计算;比较好奇是怎么实现的,但是没有去了解原理性相关,最近去看一下源码实现,大概直到具体的实现。下面就是根据自己的了解,手动实现一个简单的计算属性:
思考
我们知道 vue2.x 是基于Object.defineProperty
来劫持数据的,那么挂载到vm.data
的属性值就很好理解,在getter
与setter
的函数里面做一层简单的代理,那么计算属性为啥可以从一个函数变成一个数值,而且可以知道依赖的数据值?大概是因为计算属性的函数执行的时候,会触发到data
属性的getter
,那么我们就可以在这里做手脚,就知道当前的计算属性依赖了多少data
数据了。
v1.0
我们来看一段的代码,声明data
与computed
数据,劫持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版本修好这种情况。现在我们大概看到computed
与data
的观察者关系:
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 // 这个属性的观察者
}
dirty
为true
的情况主要是两种,一种初始化的时候,另外一个种是依赖的数据已经发生了改变。为了验证这种情况,我们在计算属性的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.
太感谢了,看了好多文章都没看懂。看的脑子嗡嗡的。 直到看到了这个文章,循序渐进,我终于理解了。