前言
在看computed实现的源码之前,我是一直有些疑问的:
- computed是有缓存的,那么缓存是怎么实现的呢?
- 在computed里定义的计算属性是一个函数,怎么我们取值的时候,就变成直接取属性了(this.xxx)?
在看完源码之后,找到了答案。
先简单概括下computed的实现原理:
computed底层也是由Watcher类实现的,初始化时也会new一个Watcher,但是不取value值,接着使用Object.defineProperty把开发者在computed中定义的函数名定义到Vue/Vue组件的实例上作为属性,当取计算属性的值时,触发对应的属性描述符的getter函数,调用watcher执行回调函数,拿到最新的值。
思维导图
computed的全链路图:
分析源码
示例代码
分析源码前,我们先写一个示例代码,便于分析
// index.html
<section id="app">
<div>count:{{ count }}</div>
<div>countComputed:{{ countComputed }}</div>
<button @click="plus">+1</button>
</section>
<script src="../../dist/vue.js"></script>
<script src="app.js"></script>
// app.js
const app = new Vue({
name: 'SimpleDemoAPI_Computed',
data() {
return {
count: 0,
}
},
computed: {
countComputed() {
return this.count * 2;
}
},
created() {
console.log('alan->countComputed', this.countComputed);
},
methods: {
plus() {
this.count += 1;
}
}
})
computed源码
源码预览,源码对应的位置:src\core\instance\state.js
// src\core\instance\state.js
// ...
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef) // 定义computed,挂getter函数,将computed的方法名挂载到vm上,提供computedGettergetter函数
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(`The computed property "${key}" is already defined as a method.`, vm)
}
}
}
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
// 把computed的属性,通过Object.defineProperty的方式定义到Vue/Vue组件实例上,属性描述符为sharedPropertyDefinition
// 其中sharedPropertyDefinition的get就是createComputedGetter返回的一个取值函数computedGetter
Object.defineProperty(target, key, sharedPropertyDefinition)
}
// 创建函数computed取值函数
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
// 执行computed中的函数,把返回值保存到value中
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
// 返回computed中函数的返回值
return watcher.value
}
}
}
代码逻辑解析:
-
首先是
new Vue时初始化,调用this._init() -
_init()调用initState(),initState()调用initComputed()初始化computed -
重点看
initComputed(),创建watchers用于收集watcher,然后遍历组件中定义的computed对象 -
接着把用户定义的computed的函数保存到userDef和getter变量中
-
然后new一个Watcher,以用户定义的computed函数作为表达式,在第2个参数传给Watcher
- Watcher实例化时,标记computed对应的watcher的lazy为true(在外层定义的
computedWatcherOptions = { lazy: true }) - 把用户定义的computed函数赋值给watcher的getter
- watcher的
value置为undefined
- Watcher实例化时,标记computed对应的watcher的lazy为true(在外层定义的
-
initComputed()调用defineComputed(vm, key, userDef),主要是调用createComputedGetter(key)给sharedPropertyDefinition.get赋值 -
createComputedGetter(key)返回一个函数computedGetter -
然后使用
Object.defineProperty把computed的属性,通过Object.defineProperty的方式定义到Vue/Vue组件实例上,属性描述符为sharedPropertyDefinition,其中sharedPropertyDefinition的get就是createComputedGetter返回的一个取值函数computedGetter
到这里,前面我的疑问也有了答案:
- Q:在computed里定义的计算属性是一个函数,怎么我们取值的时候,就变成直接取属性了(this.xxx)?
- A:通过Object.defineProperty把computed的函数名绑定到Vue/Vue组件的实例中,所以可以再组件中通过this来访问计算属性
- 当我们取
countComputed的值时,触发Object.defineProperty定义的getter,执行computedGetter函数
computedGetter里调用watcher.evaluate(),evaluate()调用自己的get(),get()执行自己的getter(),这个getter就是用户定义computed的函数,如当前示例代码中的countComputed
执行完函数,取到最新的值。
到这里,第1个疑问也有了答案:
Q:computed是有缓存的,那么缓存是怎么实现的呢?
A:初始化时watcher标记为lazy,不立即计算值,value赋值为undefined,每次访问的时候都执行函数取到最新值
结合调用链路图,理解会更好一点
computed与watch的区别
经过源码分析,可以得出结论:
相同点:
- 都是使用了Watcher类创建了实例
不同点:
- computed的watcher标记为lazy
- computed初始化时不取值,值为undefined,后面触发取值时才追踪依赖;watch的watcher初始化时,会取一次值,立即追踪依赖
- computed通过Object.defineProperty把属性定义到组件实例上
- computed的取值是通过watcher同步调用getter,getter函数就是用户定义的computed的函数;watch是异步调用cb回调函数
动手实现computed
本次computed的实现,基于上一篇文章的代码来实现的
首先,在MiniVue函数中新增initComputed方法和defineComputed方法,初始化时调用initComputed
function MiniVue(options = {}) {
// ...
// 初始化computed
if(options.computed) initComputed(vm, options.computed);
// ...
function initComputed(vm, computed = {}) {
// 给vm新增_computedWatchers属性,收集computed的watchers
const watchers = vm._computedWatchers = Object.create(null);
for (const key in computed) {
const userDef = computed[key];
const getter = userDef;
const noop = () => {};
// new 一个Watcher,第2个参数为我们定义的computed的函数,传入lazy标记
watchers[key] = new MiniWatcher(vm, getter, noop, {lazy: true});
// 定义给vm绑上computed属性
defineComputed(vm, key);
}
}
function defineComputed(vm, key) {
// 定义getter函数
const computedGetter = () => {
const watcher = vm._computedWatchers[key]
watcher.evaluate()
return watcher.value;
}
// 定义属性描述符
const descriptor = {
get: computedGetter, // 传入getter函数
set: () => {}
}
// 定义computed的属性到vm上
Object.defineProperty(vm, key, descriptor);
}
}
加上了初始化computed的函数,然后我们还有改造一下Watcher类,增加对computed的支持
class MiniWatcher {
// ...
constructor(vm, expOrFn, cb, options = {}) {
this.vm = vm;
this.cb = cb;
this.expression = expOrFn;
// 对getter做区分处理
if(typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parseExpression(this.expression, vm, this);
}
this.user = options.user;
// 初始化lazy
this.lazy = !!options.lazy;
// 增加对computed的处理
this.value = this.lazy ? undefined :this.get();
}
get() {
const value = this.getter.call(vm);
return value;
}
// 新增computed用的计算值的函数
evaluate() {
this.value = this.get();
}
// ...
}
最后在改造一下组件代码,新增computed选项
const vm = new MiniVue({
data: {
count: 0
},
computed: {
countComputed() {
return this.count;
}
}
})
const btn = document.getElementById('btnPlus');
const res = document.getElementById('res');
btn.onclick = () => {
vm.data.count = vm.data.count + 1;
const count = vm.data.count
console.log('alan->count', count);
console.log('alan->countComputed', vm.countComputed);
res.innerHTML = count;
}
测试效果:
完整代码
mini-computed.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Mini Computed</title>
</head>
<body>
<section id="mini-vue-app">
<button id="btnPlus">+1</button>
<h1 id="res"></h1>
</section>
<script src="./mini-computed.js"></script>
</body>
</html>
mini-computed.js
class MiniWatcher {
vm = null; // 当前vue/vue组件实例
cb = () => {}; // 回调函数
getter = () => {}; // 取值函数
expression = ''; // watch的键名
user = false; // 是否是用户定义的watch
value; // 当前观察的属性的值
constructor(vm, expOrFn, cb, options = {}) {
this.vm = vm;
this.cb = cb;
this.expression = expOrFn;
// 对getter做区分处理
if(typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parseExpression(this.expression, vm, this);
}
this.user = options.user;
// 初始化lazy
this.lazy = !!options.lazy;
// 增加对computed的处理
this.value = this.lazy ? undefined :this.get();
}
get() {
const value = this.getter.call(vm);
return value;
}
update() {
nextTick(() => {
this.run();
})
}
run() {
// 获取新值和旧值
const newValue = this.get();
const oldValue = this.value;
this.value = newValue;
this.cb.call(this.vm, newValue, oldValue);
}
// 新增computed用的计算值的函数
evaluate() {
this.value = this.get();
}
}
class MiniDep {
static target = null;
subs = [];
depend(sub) {
if(sub && !this.subs.includes(sub)) {
this.subs.push(sub);
}
}
notify() {
this.subs.forEach(sub => {
sub && sub.update();
})
}
}
// 解析表达式,返回一个函数
function parseExpression(key, vm, watcher) {
return () => {
MiniDep.target = watcher;
// 取值,触发getter,取值前先把watcher实例放到target中
const value = vm.data[key];
// 取完值后,清空Dep.target
MiniDep.target = null;
return value;
}
}
function nextTick(cb) {
return Promise.resolve().then(cb);
}
function MiniVue(options = {}) {
const vm = this;
this.vm = this;
this.data = options.data;
this.watch = options.watch;
this.deps = new Set();
initData(vm, this.data); // 初始化data
initWatch(this.watch); // 初始化watch
// 初始化computed
if(options.computed) initComputed(vm, options.computed);
function observe(data) {
for (const key in data) {
defineReactive(data, key);
}
}
function defineReactive(data, key) {
const dep = new MiniDep();
vm.deps.add(dep);
const clonedData = JSON.parse(JSON.stringify(data));
Object.defineProperty(data, key, {
get: function reactiveGetter() {
// console.log('alan->', 'get', clonedData[key]);
dep.depend(MiniDep.target);
return clonedData[key];
},
set: function reactiveSetter(value) {
// console.log('alan->', 'set', key, value);
dep.notify();
clonedData[key] = value;
return value;
}
});
}
function initData(vm, data = {}) {
for (const key in data) {
Object.defineProperty(vm, key, {
configurable: true,
enumerable: true,
get() {
return vm['data'][key];
},
set(val) {
vm['data'][key] = val;
}
})
observe(vm.data);
}
}
function initWatch(watch = {}) {
for (const key in watch) {
new MiniWatcher(vm, key, watch[key], {user: true}); // user = true,标记这是用户定义的watch
}
}
function initComputed(vm, computed = {}) {
// 给vm新增_computedWatchers属性,收集computed的watchers
const watchers = vm._computedWatchers = Object.create(null);
for (const key in computed) {
const userDef = computed[key];
const getter = userDef;
const noop = () => {};
// new 一个Watcher,第2个参数为我们定义的computed的函数,传入lazy标记
watchers[key] = new MiniWatcher(vm, getter, noop, {lazy: true});
// 定义给vm绑上computed属性
defineComputed(vm, key);
}
}
function defineComputed(vm, key) {
// 定义getter函数
const computedGetter = () => {
const watcher = vm._computedWatchers[key]
watcher.evaluate()
return watcher.value;
}
// 定义属性描述符
const descriptor = {
get: computedGetter, // 传入getter函数
set: () => {}
}
// 定义computed的属性到vm上
Object.defineProperty(vm, key, descriptor);
}
}
const vm = new MiniVue({
data: {
count: 0
},
computed: {
countComputed() {
return this.count;
}
}
})
console.log('alan->', vm);
const btn = document.getElementById('btnPlus');
const res = document.getElementById('res');
btn.onclick = () => {
vm.data.count = vm.data.count + 1;
const count = vm.data.count;
console.log('alan->countComputed', vm.countComputed);
res.innerHTML = count;
}
总结
computed其实也是由Watcher实现的,但是和watch又有一些不同点。computed通过Object.defineProperty把属性定义到组件实例上,并且getter函数里调用watcher去取值,追踪依赖,再返回值,以实现计算属性的效果。