一直以来我对vue中的watch
和computed
都只知其一;不知其二的,知道一点(例如:watch
和computed
的本质都是new Watcher
,computed
有缓存,只有调用的时候才会执行,也只有当依赖的数据变化了,才会再次触发...),而后就没有而后了。javascript
也看了不少大佬写的文章,一大段一大段的源码列出来,着实让我这个菜鸡看的头大,天然也就不想看了。最近,我又开始学习vue源码,才真正理解了它们的实现原理。vue
data() { return { msg: 'hello guys', info: {age:'18'}, name: 'FinGet' } }
watcher
是什么?侦听器?它就是个类class
!java
class Watcher{ constructor(vm,exprOrFn,callback,options,isRenderWatcher){ } }
vm
vue实例exprOrFn
多是字符串或者回调函数(有点懵就日后看,如今它不重要)options
各类配置项(配置啥,日后看)isRenderWatcher
是不是渲染Wathcer
Vue 初始化中 会执行一个 initState
方法,其中有你们最熟悉的initData
,就是Object.defineProperty
数据劫持。缓存
export function initState(vm) { const opts = vm.$options; // vue 的数据来源 属性 方法 数据 计算属性 watch if(opts.props) { initProps(vm); } if(opts.methods) { initMethod(vm); } if(opts.data) { initData(vm); } if(opts.computed){ initComputed(vm); } if(opts.watch) { initWatch(vm, opts.watch); } }
在数据劫持中,Watcher
的好基友Dep
出现了,Dep
就是为了把Watcher
存起来。函数
function defineReactive(data, key, val) { let dep = new Dep(); Object.defineProperty(data, key, { get(){ if(Dep.target) { dep.depend(); // 收集依赖 } return val; }, set(newVal) { if(newVal === val) return; val = newVal; dep.notify(); // 通知执行 } }) }
当initData
的时候,Dep.target
啥也不是,因此收集了个寂寞。target
是绑在Dep这个类上的(静态属性),不是实例上的。
可是当$mount
以后,就不同了。至于$mount
中执行的什么compile
、generate
、render
、patch
、diff
都不是本文关注的,不重要,绕过!学习
你只须要知道一件事:会执行下面的代码this
new Watcher(vm, updateComponent, () => {}, {}, true); // true 表示他是一个渲染watcher
updateComponent
就是更新哈,不计较具体执行,它如今就是个会更新页面的回调函数,它会被存在Watcher
的getter
中。它对应的就是最开始那个exprOrFn
参数。lua
嘿嘿嘿,这个时候就不同了:spa
get
。new Watcher
就会调用一个方法把这个实例放到Dep.target
上。pushTarget(watcher) { Dep.target = watcher; }
这两件事正好凑到一块儿,那么 dep.depend()
就干活了。prototype
因此到这里能够明白一件事,全部的data
中定义的数据,只要被调用,它都会收集一个渲染watcher
,也就是数据改变,执行set
中的dep.notify
就会执行渲染watcher
下图就是定义了msg
、info
、name
三个数据,它们都有个渲染Watcher
:
眼尖的小伙伴应该看到了msg
中还有两个watcher
,一个是用户定义的watch
,另外一个也是用户定义的watch
。啊,固然不是啦,vue
是作了去重的,不会有重复的watcher
,正如你所料,另外一个是computed watcher
;
咱们通常是这样使用watch的:
watch: { msg(newVal, oldVal){ console.log('my watch',newVal, oldVal) } // or msg: { handler(newVal, oldVal) { console.log('my watch',newVal, oldVal) }, immediate: true } }
这里会执行一个initWatch
,一顿操做以后,就是提取出exprOrFn
(这个时候它就是个字符串了)、handler
、options
,这就和Watcher
莫名的契合了,而后就瓜熟蒂落的调用了vm.$watch
方法。
Vue.prototype.$watch = function(exprOrFn, cb, options = {}) { options.user = true; // 标记为用户watcher // 核心就是建立个watcher const watcher = new Watcher(this, exprOrFn, cb, options); if(options.immediate){ cb.call(vm,watcher.value) } }
来吧,避免不了看看这段代码(原本粘贴了好长一段,但说了大白话,我就把和这段关系不大的给删减了):
class Watcher{ constructor(vm,exprOrFn,callback,options,isRenderWatcher){ this.vm = vm; this.callback = callback; this.options = options; if(options) { this.user = !!options.user; } this.id = id ++; if (typeof exprOrFn == 'function') { this.getter = exprOrFn; // 将内部传过来的回调函数 放到getter属性上 } else { this.getter = parsePath(exprOrFn); if (!this.getter) { this.getter = (() => {}); } } this.value = this.get(); } get(){ pushTarget(this); // 把当前watcher 存入dep中 let result = this.getter.call(this.vm, this.vm); // 渲染watcher的执行 这里会走到observe的get方法,而后存下这个watcher popTarget(); // 再置空 当执行到这一步的时候 因此的依赖收集都完成了,都是同一个watcher return result; } }
// 这个就是拿来把msg的值取到,取到的就是oldVal function parsePath(path) { if (!path) { return } var segments = path.split('.'); return function(obj) { for (var i = 0; i < segments.length; i++) { if (!obj) { return } obj = obj[segments[i]]; } return obj } }
你们能够看到,new Watcher
会执行一下get
方法,当是渲染Watcher就会渲染页面,执行一次updateComponent
,当它是用户Watcher就是执行parsePath
中的返回的方法,而后获得一个值this.value
也就是oldVal
。
嘿嘿嘿,既然取值了,那又走到了msg
的get
里面,这个时候dep.depend()
又干活了,用户Watcher就存进去了。
当msg
改变的时候,这过程当中还有一些骚操做,不重要哈,最后会执行一个run
方法,调用回调函数,把newValue
和oldValue
传进去:
run(){ let oldValue = this.value; // 再执行一次就拿到了如今的值,会去重哈,watcher不会重复添加 let newValue = this.get(); this.value = newValue; if(this.user && oldValue != newValue) { // 是用户watcher, 就调用callback 也就是 handler this.callback(newValue, oldValue) } }
computed: { c_msg() { return this.msg + 'computed' } // or c_msg: { get() { return this.msg + 'computed' }, set() {} } },
computed
有什么特色:
调用的时候执行,我怎么知道它在调用?嘿嘿嘿,Object.defineProperty
不就是干这事的嘛,巧了不是。
依赖的数据改变时会从新计算,那就须要收集依赖了。仍是那个逻辑,调用了this.msg
-> get
-> dep.depend()
。
function initComputed(vm) { let computed = vm.$options.computed; const watchers = vm._computedWatchers = {}; for(let key in computed) { const userDef = computed[key]; // 获取get方法 const getter = typeof userDef === 'function' ? userDef : userDef.get; // 建立计算属性watcher lazy就是第一次不调用 watchers[key] = new Watcher(vm, userDef, () => {}, { lazy: true }); defineComputed(vm, key, userDef) } }
const sharedPropertyDefinition = { enumerable: true, configurable: true, get: () => {}, set: () => {} } 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) }
下面这一段最重要,上面的看一眼就好,上面作的就是把get
方法找出来,用Object.defineProperty
绑定一下。
class Watcher{ constructor(vm,exprOrFn,callback,options,isRenderWatcher){ ... this.dirty = this.lazy; // lazy 第一次不执行 this.value = this.lazy ? undefined : this.get(); ... } update(){ if (this.lazy) { // 计算属性 须要更新 this.dirty = true; } else if (this.sync) { this.run(); } else { queueWatcher(this); // 这就是个衬托 如今无论它 } } evaluate() { this.value = this.get(); this.dirty = false; } }
缓存就在这里,执行get
方法会拿到一个返回值this.value
就是缓存的值,在用户Watcher中,它就是oldValue
,写到这里的时候,对尤大神的佩服,又加深一层。🐂🍺plus!
function createComputedGetter(key) { return function computedGetter() { // this 指向vue 实例 const watcher = this._computedWatchers[key]; if (watcher) { if (watcher.dirty) { // 若是dirty为true watcher.evaluate();// 计算出新值,并将dirty 更新为false } // 若是依赖的值不发生变化,则返回上次计算的结果 return watcher.value } } }
watcher
的update
是何时调用的?也就是数据更新调用dep.notify()
,dirty
就须要变成true
,可是计算属性仍是不能立刻计算,仍是须要在调用的时候才计算,因此在update
的时候只是改了dirty
的状态!而后下次调用的时候就会从新计算。
class Dep { constructor() { this.id = id ++; this.subs = []; } addSub(watcher) { this.subs.push(watcher); } depend() { Dep.target.addDep(this); } notify() { this.subs.forEach(watcher => watcher.update()) } }
watch
和 computed
本质都是Watcher
,都被存放在Dep
中,当数据改变时,就执行dep.notify
把当前对应Dep
实例中存的Watcher
都run
一下,这样执行了渲染Watcher
页面就刷新了;Dep
,若是他在模版中被调用,那它必定有一个渲染Watcher
;initData
时,是没有 Watcher
能够收集的;Watcher
和 Computed
中,exprOrFn
都是函数,用户Watcher
中都是字符串。文章中的代码是简略版的,还有不少细枝末节的东西没说,不重要也只是针对本文不重要,你们能够去阅读源码更深刻的理解。