这篇文章我们我会用很简单的方法来实现类似计算属性的效果,以此学习Vue.js的计算属性的运行机制。
这个例子只说明运行机制,不支持对象、数组、watching/unwatching等Vue.js已实现的一大堆优化
看完源代码带着我有限的理解写的这篇文章,可能会有一些错误,如发现错误,请联系我
JS的属性
JS有Object.defineProperty
方法,它能做的事情很多,但我们先关注这一点:
var person = {};
Object.defineProperty (person, 'age', {
get: function () {
console.log ("Getting the age");
return 25;
}
});
console.log ("The age is ", person.age);
// 输出:
//
// Getting the age
// The age is 25
虽然看起来我们只是访问了对象的一个属性,但是其实我们是在运行一个函数。
基础的Vue.js Observable
Vue.js有一个基础结构,它可以帮你把一个常规的对象转换成一个“被观察”的值,这个值就叫做“observable”。以下是一个实现响应式属性的例子
function defineReactive (obj, key, val) {
Object.defineProperty (obj, key, {
get: function () {
return val;
},
set: function (newValue) {
val = newValue;
}
})
};
// 创建对象
var person = {};
// 添加两个响应式属性
defineReactive (person, 'age', 25);
defineReactive (person, 'country', 'Brazil');
// 现在你可以使用这两个属性
if (person.age < 18) {
return 'minor';
}
else {
return 'adult';
}
// 也可以给他们赋值
person.country = 'Russia';
有趣的是,25
和Brazil
仍是闭包变量val
,当你赋值时,它们的值也会被修改。这个值不是存在于person.country
,而是存在于getter函数闭包。
定义一个计算属性
创建一个计算属性函数defineComputed
defineComputed (
person, // the object to create computed property on
'status', // the name of the computed property
function () { // the function which actually computes the property
console.log ("status getter called")
if (person.age < 18) {
return 'minor';
}
else {
return 'adult';
}
},
function (newValue) {
// called when the computed value is updated
console.log ("status has changed to", newValue)
}
});
// We can use the computed property like a regular property
console.log ("The person's status is: ", person.status);
以下是defineComputed
的简单实现。这个函数支持调用计算函数,但是暂不支持updateCallback
function defineComputed (obj, key, computeFunc, updateCallback) {
Object.defineProperty (obj, key, {
get: function () {
// call the compute function and return the value
return computeFunc ();
},
set: function () {
// don't do anything. can't set computed funcs
}
})
}
这个函数有两个问题
每当属性被访问,计算函数都会运行
这个函数不知道何时应该更新
// 我们希望达到这个效果
person.age = 17;
// console: status has changed to: minor
person.age = 22;
// console: status has changed to: adult
添加一个依赖跟踪器
创建一个全局变量Dep
var Dep = {
target: null
};
这就是依赖跟踪器,现在我们把这个关键点融入defineComputed
function defineComputed (obj, key, computeFunc, updateCallback) {
var onDependencyUpdated = function () {
// TODO
}
Object.defineProperty (obj, key, {
get: function () {
// Set the dependency target as this function
Dep.target = onDependencyUpdated;
var value = computeFunc ();
Dep.target = null;
},
set: function () {
// don't do anything. can't set computed funcs
}
})
}
我们现在回到定义响应式对象的步骤
function defineReactive (obj, key, val) {
// all computed properties that depend on this
var deps = [];
Object.defineProperty (obj, key, {
get: function () {
// 检查是否有计算属性调用这个getter
// 顺便检查这个以来是否存在
if (Dep.target && ) {
// 添加依赖
deps.push (target);
}
return val;
},
set: function (newValue) {
val = newValue;
// 通知所有依赖这个值的计算属性
deps.forEach ((changeFunction) => {
// 请求重新计算
changeFunction ();
});
}
})
};
我们在计算属性的定义里更新onDependencyUpdated
函数,用以触发更新回调函数。
var onDependencyUpdated = function () {
// compute the value again
var value = computeFunc ();
updateCallback (value);
}
综合上述代码
从新看回我们之前定义的计算属性person.status
person.age = 22;
defineComputed (
person,
'status',
function () {
// compute function
if (person.age > 18) {
return 'adult';
}
},
function (newValue) {
console.log ("status has changed to", newValue)
}
});
console.log ("Status is ", person.status);
第一步:
person.status
被访问,触发了get()
函数,Dep.target
现在是onDependencyUpdated
第二步:
(计算属性的get()
函数第二行)调用了计算函数computeFunc
,而这个计算函数又调用了age属性,也就是触发了age属性的get()
第三步:
看回age属性的get()
,如果Dep.target
当前是有值的话,这个值就会被push到依赖列表(所以现在onDependencyUpdated
就在age属性的依赖列表里咯),之后Dep.target
会被赋值为null
第四步:
现在,每当person.age
被赋值,都会通知person.status
啦
某译者的胡说八道
如作者所说这个例子只是简化版,像官网说计算属性是基于它们的依赖进行缓存的这点没有表现出来,所以更多细节请研究Vue的源码
但是读了这篇文章我们可以知道计算属性更新是依赖data的属性通知的,所以必须调用了data的属性才会“重新计算”,否则永远不会更新
这就是为什么官网说
如果计算函数里面调用了多个属性,那么这些属性更新时都会通知这个计算函数。
其他参考 Vue 响应式原理探析
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。