@Bios
2021-02-03T03:05:43.000000Z
字数 6057
阅读 865
Vue
一直以来我对vue中的watch和computed都一知半解的,知道一点(例如:watch和computed的本质都是new Watcher,computed有缓存,只有调用的时候才会执行,也只有当依赖的数据变化了,才会再次触发...),然后就没有然后了。
也看了很多大佬写的文章,一大段一大段的源码列出来,着实让我这个菜鸡看的头大,自然也就不想看了。最近,我又开始学习vue源码,才真正理解了它们的实现原理。
data() {return {msg: 'hello guys',info: {age:'18'},name: 'FinGet'}}
watcher 是什么?侦听器?它就是个类class!
class Watcher{constructor(vm,exprOrFn,callback,options,isRenderWatcher){}}
vm vue实例exprOrFn 可能是字符串或者回调函数(有点懵就往后看,现在它不重要)options 各种配置项(配置啥,往后看)isRenderWatcher 是否是渲染WathcerVue 初始化中 会执行一个 initState方法,其中有大家最熟悉的initData,就是Object.defineProperty数据劫持。
export function initState(vm) {const opts = vm.$options;// vue 的数据来源 属性 方法 数据 计算属性 watchif(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: {msg(newVal, oldVal){console.log('my watch',newVal, oldVal)}// ormsg: {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// 核心就是创建个watcherconst 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方法,然后存下这个watcherpopTarget(); // 再置空 当执行到这一步的时候 所以的依赖收集都完成了,都是同一个watcherreturn 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 也就是 handlerthis.callback(newValue, oldValue)}}
computed: {c_msg() {return this.msg + 'computed'}// orc_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为truewatcher.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 中都是字符串。文章中的代码是简略版的,还有很多细枝末节的东西没说,不重要也只是针对本文不重要,大家可以去阅读源码更深入的理解。