[关闭]
@Bios 2021-02-03T03:05:43.000000Z 字数 6057 阅读 647

我想用大白话讲清楚watch和computed

Vue


背景

一直以来我对vue中的watchcomputed都一知半解的,知道一点(例如:watchcomputed的本质都是new Watcher,computed有缓存,只有调用的时候才会执行,也只有当依赖的数据变化了,才会再次触发...),然后就没有然后了。

也看了很多大佬写的文章,一大段一大段的源码列出来,着实让我这个菜鸡看的头大,自然也就不想看了。最近,我又开始学习vue源码,才真正理解了它们的实现原理。

  1. data() {
  2. return {
  3. msg: 'hello guys',
  4. info: {age:'18'},
  5. name: 'FinGet'
  6. }
  7. }

watcher

watcher 是什么?侦听器?它就是个类class!

  1. class Watcher{
  2. constructor(vm,exprOrFn,callback,options,isRenderWatcher){
  3. }
  4. }

initState

Vue 初始化中 会执行一个 initState方法,其中有大家最熟悉的initData,就是Object.defineProperty数据劫持。

  1. export function initState(vm) {
  2. const opts = vm.$options;
  3. // vue 的数据来源 属性 方法 数据 计算属性 watch
  4. if(opts.props) {
  5. initProps(vm);
  6. }
  7. if(opts.methods) {
  8. initMethod(vm);
  9. }
  10. if(opts.data) {
  11. initData(vm);
  12. }
  13. if(opts.computed){
  14. initComputed(vm);
  15. }
  16. if(opts.watch) {
  17. initWatch(vm, opts.watch);
  18. }
  19. }

在数据劫持中,Watcher的好基友Dep出现了,Dep就是为了把Watcher存起来。

d61e55104ed04f9c9652a38b23522779~tplv-k3u1fbpfcp-watermark.image未知大小

  1. function defineReactive(data, key, val) {
  2. let dep = new Dep();
  3. Object.defineProperty(data, key, {
  4. get(){
  5. if(Dep.target) {
  6. dep.depend(); // 收集依赖
  7. }
  8. return val;
  9. },
  10. set(newVal) {
  11. if(newVal === val) return;
  12. val = newVal;
  13. dep.notify(); // 通知执行
  14. }
  15. })
  16. }

initData的时候,Dep.target啥也不是,所以收集了个寂寞。target是绑在Dep这个类上的,不是实例上的。

但是当$mount之后,就不一样了。至于$mount中执行的什么compilegeneraterenderpatchdiff都不是本文关注的,不重要,绕过!

你只需要知道一件事:会执行下面的代码

  1. new Watcher(vm, updateComponent, () => {}, {}, true); // true 表示他是一个渲染watcher

updateComponent就是更新哈,不计较具体执行,它现在就是个会更新页面的回调函数,它会被存在Watchergetter中。它对应的就是最开始那个exprOrFn参数。

嘿嘿嘿,这个时候就不一样了:

  1. 渲染页面就是调用了你定义的数据(别杠,定义了没调用),就会走get
  2. new Watcher 就会调用一个方法把这个实例放到Dep.target上。
  1. pushTarget(watcher) {
  2. Dep.target = watcher;
  3. }

这两件事正好凑到一起,那么 dep.depend()就干活了。

所以到这里可以明白一件事,所有的data中定义的数据,只要被调用,它都会收集一个渲染watcher,也就是数据改变,执行set中的dep.notify就会执行渲染watcher

下图就是定义了msginfoname三个数据,它们都有个渲染Watcher
2aa7c7ae7f304678b5dfe9f7229d24c1~tplv-k3u1fbpfcp-watermark.image未知大小

眼尖的小伙伴应该看到了msg中还有两个watcher,一个是用户定义的watch,另一个也是用户定义的watch。啊,当然不是啦,vue是做了去重的,不会有重复的watcher,正如你所料,另一个是computed watcher

用户watch

我们一般是这样使用watch的:

  1. watch: {
  2. msg(newVal, oldVal){
  3. console.log('my watch',newVal, oldVal)
  4. }
  5. // or
  6. msg: {
  7. handler(newVal, oldVal) {
  8. console.log('my watch',newVal, oldVal)
  9. },
  10. immediate: true
  11. }
  12. }

这里会执行一个initWatch,一顿操作之后,就是提取出exprOrFn(这个时候它就是个字符串了)、handleroptions,这就和Watcher莫名的契合了,然后就顺理成章的调用了vm.$watch方法。

  1. Vue.prototype.$watch = function(exprOrFn, cb, options = {}) {
  2. options.user = true; // 标记为用户watcher
  3. // 核心就是创建个watcher
  4. const watcher = new Watcher(this, exprOrFn, cb, options);
  5. if(options.immediate){
  6. cb.call(vm,watcher.value)
  7. }
  8. }

来吧,避免不了看看这段代码(本来粘贴了好长一段,但说了大白话,我就把和这段关系不大的给删减了):

  1. class Watcher{
  2. constructor(vm,exprOrFn,callback,options,isRenderWatcher){
  3. this.vm = vm;
  4. this.callback = callback;
  5. this.options = options;
  6. if(options) {
  7. this.user = !!options.user;
  8. }
  9. this.id = id ++;
  10. if (typeof exprOrFn == 'function') {
  11. this.getter = exprOrFn; // 将内部传过来的回调函数 放到getter属性上
  12. } else {
  13. this.getter = parsePath(exprOrFn);
  14. if (!this.getter) {
  15. this.getter = (() => {});
  16. }
  17. }
  18. this.value = this.get();
  19. }
  20. get(){
  21. pushTarget(this); // 把当前watcher 存入dep中
  22. let result = this.getter.call(this.vm, this.vm); // 渲染watcher的执行 这里会走到observe的get方法,然后存下这个watcher
  23. popTarget(); // 再置空 当执行到这一步的时候 所以的依赖收集都完成了,都是同一个watcher
  24. return result;
  25. }
  26. }
  1. // 这个就是拿来把msg的值取到,取到的就是oldVal
  2. function parsePath(path) {
  3. if (!path) {
  4. return
  5. }
  6. var segments = path.split('.');
  7. return function(obj) {
  8. for (var i = 0; i < segments.length; i++) {
  9. if (!obj) { return }
  10. obj = obj[segments[i]];
  11. }
  12. return obj
  13. }
  14. }

大家可以看到,new Watcher会执行一下get方法,当是渲染Watcher就会渲染页面,执行一次updateComponent,当它是用户Watcher就是执行parsePath中的返回的方法,然后得到一个值this.value也就是oldVal

嘿嘿嘿,既然取值了,那又走到了msgget里面,这个时候dep.depend()又干活了,用户Watcher就存进去了。

msg改变的时候,这过程中还有一些骚操作,不重要哈,最后会执行一个run方法,调用回调函数,把newValueoldValue传进去:

  1. run(){
  2. let oldValue = this.value;
  3. // 再执行一次就拿到了现在的值,会去重哈,watcher不会重复添加
  4. let newValue = this.get();
  5. this.value = newValue;
  6. if(this.user && oldValue != newValue) {
  7. // 是用户watcher, 就调用callback 也就是 handler
  8. this.callback(newValue, oldValue)
  9. }
  10. }

computed

  1. computed: {
  2. c_msg() {
  3. return this.msg + 'computed'
  4. }
  5. // or
  6. c_msg: {
  7. get() {
  8. return this.msg + 'computed'
  9. },
  10. set() {}
  11. }
  12. },

computed有什么特点:

  1. 调用的时候才会执行
  2. 有缓存
  3. 依赖改变时会重新计算

调用的时候执行,我怎么知道它在调用?嘿嘿嘿,Object.defineProperty不就是干这事的嘛,巧了不是。

依赖的数据改变时会重新计算,那就需要收集依赖了。还是那个逻辑,调用了this.msg -> get -> dep.depend()

  1. function initComputed(vm) {
  2. let computed = vm.$options.computed;
  3. const watchers = vm._computedWatchers = {};
  4. for(let key in computed) {
  5. const userDef = computed[key];
  6. // 获取get方法
  7. const getter = typeof userDef === 'function' ? userDef : userDef.get;
  8. // 创建计算属性watcher lazy就是第一次不调用
  9. watchers[key] = new Watcher(vm, userDef, () => {}, { lazy: true });
  10. defineComputed(vm, key, userDef)
  11. }
  12. }
  1. const sharedPropertyDefinition = {
  2. enumerable: true,
  3. configurable: true,
  4. get: () => {},
  5. set: () => {}
  6. }
  7. function defineComputed(target, key, userDef) {
  8. if (typeof userDef === 'function') {
  9. sharedPropertyDefinition.get = createComputedGetter(key)
  10. } else {
  11. sharedPropertyDefinition.get = createComputedGetter(userDef.get);
  12. sharedPropertyDefinition.set = userDef.set;
  13. }
  14. // 使用defineProperty定义 这样才能做到使用才计算
  15. Object.defineProperty(target, key, sharedPropertyDefinition)
  16. }

下面这一段最重要,上面的看一眼就好,上面做的就是把get方法找出来,用Object.defineProperty绑定一下。

  1. class Watcher{
  2. constructor(vm,exprOrFn,callback,options,isRenderWatcher){
  3. ...
  4. this.dirty = this.lazy;
  5. // lazy 第一次不执行
  6. this.value = this.lazy ? undefined : this.get();
  7. ...
  8. }
  9. update(){
  10. if (this.lazy) {
  11. // 计算属性 需要更新
  12. this.dirty = true;
  13. } else if (this.sync) {
  14. this.run();
  15. } else {
  16. queueWatcher(this); // 这就是个陪衬 现在不管它
  17. }
  18. }
  19. evaluate() {
  20. this.value = this.get();
  21. this.dirty = false;
  22. }
  23. }

缓存就在这里,执行get方法会拿到一个返回值this.value就是缓存的值,在用户Watcher中,它就是oldValue,写到这里的时候,对尤大神的佩服,又加深一层。🐂🍺plus!

  1. function createComputedGetter(key) {
  2. return function computedGetter() {
  3. // this 指向vue 实例
  4. const watcher = this._computedWatchers[key];
  5. if (watcher) {
  6. if (watcher.dirty) { // 如果dirty为true
  7. watcher.evaluate();// 计算出新值,并将dirty 更新为false
  8. }
  9. // 如果依赖的值不发生变化,则返回上次计算的结果
  10. return watcher.value
  11. }
  12. }
  13. }

watcherupdate是什么时候调用的?也就是数据更新调用dep.notify()dirty就需要变成true,但是计算属性还是不能马上计算,还是需要在调用的时候才计算,所以在update的时候只是改了dirty的状态!然后下次调用的时候就会重新计算。

  1. class Dep {
  2. constructor() {
  3. this.id = id ++;
  4. this.subs = [];
  5. }
  6. addSub(watcher) {
  7. this.subs.push(watcher);
  8. }
  9. depend() {
  10. Dep.target.addDep(this);
  11. }
  12. notify() {
  13. this.subs.forEach(watcher => watcher.update())
  14. }
  15. }

总结

  1. watchcomputed 本质都是Watcher,都被存放在Dep中,当数据改变时,就执行dep.notify把当前对应Dep实例中存的Watcherrun一下,这样执行了渲染Watcher 页面就刷新了;
  2. 每一个数据都有自己的Dep,如果他在模版中被调用,那它一定有一个渲染Watcher
  3. initData时,是没有 Watcher 可以收集的;
  4. 发现没有,渲染WatcherComputed 中,exprOrFn都是函数,用户Watcher 中都是字符串。

671328671b324688becf834458abfdb7~tplv-k3u1fbpfcp-watermark.image未知大小

文章中的代码是简略版的,还有很多细枝末节的东西没说,不重要也只是针对本文不重要,大家可以去阅读源码更深入的理解。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注