watcher和computed有何不同是面试中经常出现的一道题目,但是很少有文章从原理上剖析两者的差异
1. watch实现原理
watch和computed的实现都依赖了watcher类,通过options传入的标记判断是渲染watcher、用户watcher或者计算watcher。对于这两个方法源码的解读,应该关注何时调用watcher.get方法,以及update方法对这两个属性的判断逻辑。
- initWatch
vue在初始化阶段,调用initWatch,为每一个watch建立一个watcher实理,这个watcher实例接受4个参数, vm, 属性名,handler函数,以及options ;options内部传入user: true属性标记这是一个用户watcher
function initWatch(vm,watch) {
for(let key in watch) {
const handler = watch[key]
const options = {user: true,deep: false, immediate: true}
options.deep = watch[key][deep]
options.immediate = watch[key][immediate]
createWatcher(vm,key,handler,options)
}
}
function createWatcher(vm, key, handler, options) {
options.user = true
return vm.$watch(vm,key,handler,options)
}
Vue.prototype.$watch = function (vm,exporfn, cb, options) {
new Watcher(this, exporfn, cb, options)
}
- watcher依赖的值变化后,如何通知到这个监测watcher?
watcher类在初始化阶段会对传入的exporfn进行判断,如果是一个表达式则处理为函数,然后调用这个函数获取其对应的值,在读值时触发依赖收集逻辑,收集到这个用户watcher,当值更新时,触发更新逻辑,判断这是一个用户watcher, 通过run方法调用其传入的回调函数
class Watcher {
constuctor() {
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属性上
}
update() {
if(this.immediate) {
this.run()
}else {
queueWatcher(this)
}
}
run(){
let value = this.get(); // 获取新值
let oldValue = this.value; // 获取老值
this.value = value;
if(this.user){ // 如果是用户watcher 则调用用户传入的callback
this.callback.call(this.vm,value,oldValue)
}
}
}
- 如何实现immediate
if(this.immediate === true) {
this.run()
}
2. computed实现原理
每个计算属性也都是一个watcher,计算属性需要表示lazy:true,这样在初始化watcher时不会立即调用计算属性方法
- initWatch
function initComputed(vm, computed) {
// 存放计算属性的watcher
const watchers = vm._computedWatchers = {};
for (const key in computed) {
const userDef = computed[key];
// 获取get方法
const getter = typeof userDef === 'function' ? userDef : userDef.get;
// 创建计算属性watcher
watchers[key] = new Watcher(vm, userDef, () => {}, { lazy: true });
defineComputed(vm, key, userDef)
}
}
- 通过defineProperty把计算属性代理到vm上方便我们获取, 当读取值时,调用getter, 在getter中获取调用其对应的watcher.evaluate方法获取值,取值操作会触发依赖搜集逻辑
function defineComputed(target, key, userDef) {
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = createComputedGetter(key)
} else {
sharedPropertyDefinition.get = createComputedGetter(userDef.get);
sharedPropertyDefinition.set = userDef.set;
}
// 使用defineProperty定义
Object.defineProperty(target, key, sharedPropertyDefinition)
}
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) { // 如果dirty为true
watcher.evaluate();// 计算出新值,并将dirty 更新为false
}
// 如果依赖的值不发生变化,则返回上次计算的结果
return watcher.value
}
}
}
为了实现缓存,在get中注入额外的操作,通过判断dirty是否为true,决定是否需要更新,每次更新后把dirty置为false,由于evaluate调用了get方法,在这个过程中读取到了数据,触发依赖收集逻辑,把当前watcher收集起来了,当数据更新时,调用watcher的update方法,update方法中判断这个数据是一个computed watcher,就会把dirty置为true
class Watcher {
constructor(vm, exprOrFn, callback, options) {
this.vm = vm;
this.dirty = this.lazy
// ...
this.value = this.lazy ? undefined : this.get(); // 调用get方法 会让渲染watcher执行
}
get() {
pushTarget(this)
let value = this.getter.call(this.vm)
popTarget()
return value
}
evaluate() {
this.value = this.get()
this.dirty = false
}
update() {
if (this.lazy) {
this.dirty = true;
} else {
queueWatcher(this);
}
}
}
3. 结论
computed是一个数据,通过object.defineProperty代理到vm实例上,我们可以在模板中作为数据直接使用
computed属性具有缓存特性,只有其以依赖的数据变化了才会重新计算最新的值
watcher本质上是一个监测到数据变化后的执行的回调函数,当监测的数据变化后会执行