前言
通过手写Vue2源码,更深入了解Vue;
在项目开发过程,一步一步实现Vue核心功能,我会将不同功能放到不同分支,方便查阅;
另外我会编写一些开发文档,阐述编码细节及实现思路;
源码地址:手写Vue2源码
计算属性的用法及功能
先看一下Vue中计算属性的用法及功能,然后我们一一实现这些功能。 用法:
new Vue({
data() {
return {
firstName: 'wu',
lastName: 'yanzu'
}
},
template: `<div class="home" id="main" style="font-size:12px;color:red">我的名字是:{{fullName}}</div>`,
computed: {
fullName() {
return this.firstName + this.lastName
},
fullName: {
get() {
return this.firstName + this.lastName
},
set(newValue) {
var names = newValue.split()
this.firstName = names[0]
this.lastName = names[1] + names[2]
}
}
}
})
computed特性:
- 可以同时设置多个计算属性
- 计算属性有两种写法:
- 第一种为函数,返回计算的值;
- 第二种为对象,里面有get和set两个函数,get函数返回计算的值,set用来更新依赖项;
- 模板中的计算属性没有定义在data中,但可以直接通过
vm.xxx
的方式获取 - 计算属性具有缓存功能,只有当依赖项发生改变时才重新计算
- 依赖项改变会触发重新计算,以及页面的重新渲染
根据这些特性,我们考虑一下实现思路:
- 使用
Object.defineProperty()
将computed中的计算属性直接代理到vm实例上 - 遍历计算属性创建计算属性watcher,对计算属性的依赖项收集相关计算属性watcher
- 当依赖项发生改变时通知计算属性watcher重新计算
- 当依赖项没有改变时,直接获取watcher.value(缓存的值)
- 依赖项收集完计算属性watcher后,还要收集渲染watcher,当依赖项发生改变时,通知渲染watcher更新视图
下面我们一步步实现这些功能。
计算属性初始化
// src/state.js
export function initState(vm) {
const opts = vm.$options;
// ...
// 初始化computed
if (opts.computed) {
initComputed(vm);
}
// ...
}
// 初始化computed
function initComputed(vm) {
const computed = vm.$options.computed
const watchers = (vm._computedWatchers = {}) // 用watchers和vm._computedWatchers 存放 computed watcher
// 遍历computed,每个计算属性创建一个watcher
for (let k in computed) {
const userDef = computed[k] // userDef可能是函数或对象(内部有get()、set()函数)
// 获取计算属性的getter函数
let getter = typeof userDef === 'function' ? userDef : userDef.get
// 创建watcher;lazy: true 表示 computed watcher
watchers[k] = new Watcher(vm, getter, () => {}, { lazy: true })
// 将computed中的属性直接代理到vm下,并对计算属性依赖项进行依赖收集相关操作
defineComputed(vm, k, userDef)
}
}
简而言之,先遍历computed,对每个计算属性创建一个computed watcher
,然后对计算属性进行代理及收集依赖操作 —— defineComputed(vm, k, userDef)
对计算属性进行代理
// src/state.js
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: () => {},
set: () => {},
};
function defineComputed(vm, key, userDef) {
if (typeof userDef === "function") {
sharedPropertyDefinition.get = createComputedGetter(key);
} else {
sharedPropertyDefinition.get = createComputedGetter(key);
sharedPropertyDefinition.set = userDef.set;
}
// 将计算属性直接代理到vm实例上
Object.defineProperty(vm, key, sharedPropertyDefinition);
}
代理的目的是为了能在vm上通过vm.xxx
直接访问到计算属性。
代理的过程中需要进行依赖收集、计算属性缓存等操作,核心方法是 —— createComputedGetter(key)
。
缓存与依赖收集
对于computed属性的getter,我们需要进行缓存;另外依赖项需要收集计算属性watcher,当依赖项改变时,通知计算属性watcher重新计算;还需要收集渲染watcher,当依赖项改变时,更新视图。
// src/state.js
function createComputedGetter(key) {
return function () {
const watcher = this._computedWatchers[key];
if(watcher) {
// 根据dirty属性,判断是否需要重新计算(脏就是要调用getter,不脏就是直接取watcher.value)
if (watcher.dirty) {
// 执行Watcher.get()重新计算 计算属性 的值,触发依赖项的收集依赖;完成之后将watcher.dirty设置为false
watcher.evaluate();
// 如果还存在Dep.target,我们对依赖项收集当前的watcher(一般为渲染watcher)
if (Dep.target) {
watcher.depend()
}
}
return watcher.value;
}
};
}
逻辑分析:
- 根据
watcher.dirty
判断是否有缓存,没缓存则:- 重新计算 计算属性 的值,触发依赖项收集
computed watcher
,完成之后将watcher.dirty
设为false;当依赖项更新时,将dirty设置为true; - 依赖项收集当前的渲染watcher (保证依赖项改变触发视图更新)
- 重新计算 计算属性 的值,触发依赖项收集
- 有缓存则直接获取缓存值 ——
watcher.value
这里我们需要了解一个事情,就是同一时间可能同时存在两个watcher:
- 执行$mount -> mountComponent时,会创建一个渲染watcher(此时Dep.target为渲染watcher),将它推入一个栈中
- 解析到计算属性时,会创建一个computed watcher(此时Dep.target为 computed watcher),将它推入同一栈中
- 解析完计算属性后,将computed watcher 移除栈,此时Dep.target 又是 渲染watcher
- 整个模板渲染结束时,栈为空,Dep.target为null
所以我们需要对Dep改造一下:
// src/observer/dep
// 栈结构用来存众多watcher
const targetStack = [];
// Dep.target 为 dep 当前所对应的watcher(即栈顶的watcher),默认为null
Dep.target = null;
export function pushTarget(watcher) {
targetStack.push(watcher);
Dep.target = watcher; // Dep.target指向当前watcher
}
export function popTarget() {
// targetStack可能同时存在多个watcher(比如渲染watcher处于栈底,上面有computed watcher)
targetStack.pop(); // 当前watcher出栈 拿到上一个watcher
Dep.target = targetStack[targetStack.length - 1];
}
针对computed watcher,我们还需要对watcher进行改造:
// src/observer/watcher.js
export default class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.lazy = !!options.lazy; // 表示是不是computed watcher
this.dirty = this.lazy; // dirty可变,默认为true;表示计算watcher是否需要重新计算-执行用户定义的方法。
// 当是渲染watcher 或 computed watcher时
if (typeof exprOrFn === "function") {
this.getter = exprOrFn;
} else {
// 当是用户自定义watcher时
// ...
}
// 如果是计算属性watcher,则创建watcher的时候,什么都不执行(计算属性的getter经过了代理,获取计算属性时调用它的getter进行计算)
this.value = this.lazy ? undefined : this.get();
}
get() {
pushTarget(this);
const res = this.getter.call(this.vm);
popTarget();
return res;
}
addDep(dep) {
let id = dep.id;
if (!this.depsId.has(id)) {
this.depsId.add(id);
this.deps.push(dep);
dep.addSub(this);
}
}
// 当计算属性的依赖项发生改变,会触发依赖项相关 watcher(一般依赖项会收集computed watcher和渲染watcher,所以下面if、else都会走) 的update方法
update() {
// 这里做个判断,如果是计算属性watcher,则将dirty设置成true,下次访问计算属性时就会重新计算(在computed代理中进行判断的)。
if (this.lazy) {
this.dirty = true;
} else {
// 每次watcher进行更新的时候,可以让他们先缓存起来,之后再一起调用
// 异步队列机制
queueWatcher(this);
}
}
// 在计算属性的代理中,当dirty为true时会执行evaluate
evaluate() {
this.value = this.get(); // 计算新值,并对依赖项收集computed watcher
this.dirty = false;
}
depend() {
// 计算属性的watcher存储了依赖项的dep
let i = this.deps.length;
while (i--) {
this.deps[i].depend(); // 调用依赖项的dep去收集渲染watcher
}
}
run() {
// 执行this.getter,更新视图/获取新值
// ...
}
}
两个核心方法:
evaluate()
:执行watcher.get()
,重新计算值,依赖项收集当前computed watcher
;最后将dirty设为falsedepend()
:遍历当前computed watcher
的deps(计算属性的依赖项),收集Dep.target
(渲染watcher)
小结
- 计算属性不是定义在data中的,不会进行依赖收集,收集的计算属性的依赖项
- 计算属性需要进行代理,以便通过this可以直接访问到
- 每个计算属性创建一个
computed watcher
- 计算属性需要缓存
- 计算属性的依赖项需要收集相关的
computed watcher
(watcher.evaluate
方法),以及渲染watcher(watcher.depend
方法)。 - 同一时间可能会存在多个watcher(例如渲染watcher及computed watcher),用栈存储watchers,
Dep.target
指向当前watcher
--
系列文章
- 手写Vue2源码(一)—— 环境搭建
- 手写Vue2源码(二)—— 数据劫持
- 手写Vue2源码(三)—— 模板编译
- 手写Vue2源码(四)—— 初次渲染
- 手写Vue2源码(五)—— 观察者模式
- 手写Vue2源码(六)—— 异步更新及nextTick
- 手写Vue2源码(七)—— 侦听属性
- 手写Vue2源码(八)—— 计算属性
- 手写Vue2源码(九)—— 混入原理与生命周期
- 手写Vue2源码(十)—— 组件原理
- 手写Vue2源码(十一)—— diff算法
- 手写Vue2源码(十二)—— keep-alive
- 手写Vue2源码(十三)—— 全局API
- vue-router原理解析
- vuex原理解析
- vue3原理解析