背景
始终以来我对vue中的watch
和computed
都只知其一;不知其二的,晓得一点(例如:watch
和computed
的实质都是new Watcher
,computed
有缓存,只有调用的时候才会执行,也只有当依赖的数据变动了,才会再次触发...),而后就没有而后了。
也看了很多大佬写的文章,一大段一大段的源码列出来,着实让我这个菜鸡看的头大,天然也就不想看了。最近,我又开始学习vue源码,才真正了解了它们的实现原理。
data() { return { msg: 'hello guys', info: {age:'18'}, name: 'FinGet' }}
watcher
watcher
是什么?侦听器?它就是个类class
!
class Watcher{ constructor(vm,exprOrFn,callback,options,isRenderWatcher){ }}
vm
vue实例exprOrFn
可能是字符串或者回调函数(有点懵就往后看,当初它不重要)options
各种配置项(配置啥,往后看)isRenderWatcher
是否是渲染Wathcer
initState
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
都不是本文关注的,不重要,绕过!
你只须要晓得一件事:会执行上面的代码
new Watcher(vm, updateComponent, () => {}, {}, true); // true 示意他是一个渲染watcher
updateComponent
就是更新哈,不计较具体执行,它当初就是个会更新页面的回调函数,它会被存在Watcher
的getter
中。它对应的就是最开始那个exprOrFn
参数。
嘿嘿嘿,这个时候就不一样了:
- 渲染页面就是调用了你定义的数据(别杠,定义了没调用),就会走
get
。 new Watcher
就会调用一个办法把这个实例放到Dep.target
上。
pushTarget(watcher) { Dep.target = watcher;}
这两件事正好凑到一起,那么 dep.depend()
就干活了。
所以到这里能够明确一件事,所有的data
中定义的数据,只有被调用,它都会收集一个渲染watcher
,也就是数据扭转,执行set
中的dep.notify
就会执行渲染watcher
下图就是定义了msg
、info
、name
三个数据,它们都有个渲染Watcher
:
眼尖的小伙伴应该看到了msg
中还有两个watcher
,一个是用户定义的watch
,另一个也是用户定义的watch
。啊,当然不是啦,vue
是做了去重的,不会有反复的watcher
,正如你所料,另一个是computed watcher
;
用户watch
咱们个别是这样应用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的值取到,取到的就是oldValfunction 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
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
中都是字符串。
文章中的代码是简略版的,还有很多细枝末节的货色没说,不重要也只是针对本文不重要,大家能够去浏览源码更深刻的了解。