背景
watch和computed在使用上有什么不同,相信大家都很清楚。那么让我们深入来看看,他们在实现原理上有什么不同吧?
watch实现原理
-
类型:
{ [key: string]: string | Function | Object | Array } -
详细:一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。Vue 实例将会在实例化时调用
$watch(),遍历watch对象的每一个property。
1. 初始化watch
export function initState(vm) {
//对vm上的watch做初始化
if (opts.watch) {
initWatch(vm, opts.watch);
}
function initWatch(vm, watch) {
//暂时不考虑值是方法名的情况
for (key in watch) {
let handler = watch[key];
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
createWatcher(vm, key, handler);
}
}
}
function createWatcher(vm, key, handler) {
return vm.$watch(key, handler);
}
//扩展Vue原型上的方法,都通过mixin的方式来进行添加的。
export function stateMixin(Vue) {
Vue.prototype.$watch = function(key, handler, options = {}) {
options.user = true;
// 标示 this.user 区分渲染 watcher 和用户 watcher
new Watcher(this, key, handler, options);
}
}
2. watcher实现
- 将表达式
key/exprOrFn转化为函数,方便后面调用this.getter this.getter取值调用Object.defineProperty的get方法里的dep.depend收集当前的watcher- 第一次
new Watcher的时候,调用了get方法保存初始this.value=this.get()。第二次用户更新run()又调用get方法 保存下新值newValue=this.get(),并执行回调函数。
class Watcher {
constructor(vm, exprOrFn, callback, options) {
// ...
this.user = !! options.user
if(typeof exprOrFn === 'function'){
this.getter = exprOrFn;
}else{
this.getter = function (){ // 将表达式转换成函数
let path = exprOrFn.split('.');
let obj = vm;
for(let i = 0; i < path.length;i++){
obj = obj[path[i]];
}
return obj;
}
}
this.value = this.get(); // 将初始值记录到value属性上
}
get() {
pushTarget(this); // 把用户定义的watcher存起来
const value = this.getter.call(this.vm); // 执行函数 (依赖收集)
popTarget(); // 移除watcher
return value;
}
run(){
let value = this.get(); // 获取新值
let oldValue = this.value; // 获取老值
this.value = value;
if(this.user){ // 如果是用户watcher 则调用用户传入的callback
this.callback.call(this.vm,value,oldValue)
}
}
}
computed实现原理
- 类型:
{ [key: string]: Function | { get: Function, set: Function } } - 详细:计算属性默认不执行。计算属性的结果会被缓存,除非依赖的响应式
property变化才会重新计算。注意,如果某个依赖 (比如非响应式 property) 在该实例范畴之外,则计算属性是不会被更新的。
1. 初始化computed
为
computed的每个属性key创建一个watcher
export function initState(vm) {
//对vm上的computed做初始化
if (opts.computed) {
initComputed(vm, opts.computed);
}
}
function initComputed(vm, computed) {
const watchers = vm._computedWatchers = {}
for (let key in computed) {
// 校验
const userDef = computed[key];
// 依赖的属性变化就重新取值 get
let getter = typeof userDef == 'function' ? userDef : userDef.get;
// 每个就算属性本质就是watcher
// 将watcher和 属性 做一个映射
watchers[key] = new Watcher(vm, getter, () => {}, { lazy: true }); // 默认不执行
// 将key 定义在vm上
defineComputed(vm, key, userDef);
}
}
将 key 定义在 vm 上 ,这样在页面上才能直接取到computed的值
this._computedWatchers包含着所有的计算属性- 通过
key可以拿到对应的watcher,这个watcher中包含了getter,如果dirty为ture则调用evaluate
function defineComputed(vm, key, userDef) {
let sharedProperty = {};
if (typeof userDef == 'function') {
sharedProperty.get = createComputedGetter(key)
} else {
sharedProperty.get = createComputedGetter(key);
sharedProperty.set = userDef.set ;
}
Object.defineProperty(vm, key, sharedProperty); // computed就是一个defineProperty
}
创建缓存
getter,取计算属性的值,走的是这个函数。
function createComputedGetter(key) {
return function computedGetter() {
// this._computedWatchers 包含着所有的计算属性
// 通过key 可以拿到对应watcher,这个watcher中包含了getter
let watcher = this._computedWatchers[key]
// 脏就是 要调用用户的getter 不脏就是不要调用getter
if(watcher.dirty){ // 根据dirty属性 来判断是否需要重新求职
watcher.evaluate();// this.get()
}
// 如果当前取完值后 Dep.target还有值 需要继续向上收集
if(Dep.target){
watcher.depend(); // watcher 里 对应了 多个dep
}
return watcher.value
}
}
2. watcher实现
class Watcher {
constructor(vm, exprOrFn, cb, options) {
//...
this.lazy = !!options.lazy;
this.dirty = options.lazy; // 如果是计算属性,那么默认值lazy:true,
this.getter = exprOrFn; // computed[key]/computed[key].get
this.value = this.lazy ? undefined : this.get();
}
get() {
pushTarget(this);
const value = this.getter.call(this.vm);
popTarget();
return value
}
update() {
if(this.lazy){
this.dirty = true;
}else{
queueWatcher(this);
}
}
evaluate(){
this.dirty = false; // 为false表示取过值了
this.value = this.get(); // 用户的getter执行
}
depend(){
let i = this.deps.length;
while(i--){
this.deps[i].depend(); //lastName,firstName 收集渲染watcher
}
}
}
computed的实现实在是太绕来绕去了😭😭😭😭😭
我们通过几个问题来理顺一下思路吧
QS1:计算属性如何实现缓存?
通过在watcher上定义了一个dirty属性。当dirty为true时,调用evaluate重新求值。
QS2:计算属性依赖的值变化,怎么让计算属性重新求值?
改变依赖值 --> 触发set --> 触发dep.notify --> watcher.update --> 是计算watcher --> this.dirty = true --> 当dirty为true时,调用evaluate重新求值。
QS3:计算属性依赖的值变化,怎么让视图更新?
目前依赖的值(没有在页面中使用的前提下) 的dep上只有一个计算属性wacher,想让视图更新那么,要将渲染wacher也放入依赖的数据的dep中,这样依赖的属性发生变化也可以让视图进行更新。
总结
watch和computed在实现原理上的不同:
watch实现是给watch 对象的每一个 key分配了一个watcher,this.get()取值,收集当前的用户watcher,并保存下初始值。当key变化的时候,触发watcher.run(),保存下新值,同时执行回调函数cb。
computed的实现通过给computed 对象的每一个 key分配了一个lazy Watcher,默认不执行,取值的时候才执行。Object.defineProperty在vm上定义了computed的每个key。通过key所依赖值收集当前的渲染watcher,来实现依赖值变化,视图更新。通过dirty属性来实现缓存效果。