zoukankan      html  css  js  c++  java
  • Vue2.x计算属性为什么能依赖于另一个计算属性

    概述

    说到 computed 和 watch 有什么不同,也许大多数人都知道:computed 是用现有数据生成一个新数据,并且能够被缓存;而 watch 是根据数据变化,执行一些回调函数,它有很多配置比如 deep、immediate 等。

    大家也都知道,watch 只是源码里面 watcher 的一个实例,computed 属性也用到了 watcher,但是 computed 属性为什么能够相互依赖变化呢?明显 watcher 自己是做不到这一点的,因为 watcher 并不能 update 其它 watcher。我为了弄懂其中的原理根据 vue2.x 的源码写了一个简易的 computed 属性,供以后工作时参考,相信对其他人也有用。

    部分代码来源于Vue2.x是怎么收集依赖的

    简易的 computed

    为了简便,暂不考虑 computed 的 setter 的情况,我实现了一个简易的 computed,代码如下:

    function defineReactive(obj, key, val) {
        const dep = new Dep();
    
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                if (Dep.target) {
                    dep.depend();
                }
                return val;
            },
            set(newVal) {
                val = newVal;
                dep.notify();
            }
        });
    }
    
    class Dep {
        constructor() {
            this.subs = [];
        }
    
        addSub(sub) {
            this.subs.push(sub);
        }
    
        removeSub() {
            const index = this.subs.indexOf(sub);
            if (index > -1) {
                this.subs.splice(index, 1);
            }
        }
    
        depend() {
            if (Dep.target) {
                Dep.target.addDep(this);
            }
        }
    
        notify() {
            const subs = this.subs.slice();
            for (let i = 0, l = subs.length; i < l; i++) {
                subs[i].update();
            }
        }
    }
    
    Dep.target = null;
    const targetStack = []
    
    function pushTarget (target) {
      targetStack.push(target)
      Dep.target = target
    }
    
    function popTarget () {
      targetStack.pop()
      Dep.target = targetStack[targetStack.length - 1]
    }
    
    class Watcher {
        constructor(cb, dirty = false) {
            this.getter = cb;
            this.deps = [];
            this.newDeps = [];
            this.value = this.get();
            this.dirty = dirty;
        }
    
        get() {
            pushTarget(this);
            const value = this.getter();
            popTarget(this);
            this.deps = [...this.newDeps];
            this.newDeps = [];
            return value;
        }
    
        addDep(dep) {
            this.newDeps.push(dep);
            dep.addSub(this);
        }
    
        update() {
            this.dirty = true;
            this.value = this.get();
        }
    
        evaluate() {
            this.value = this.get();
            this.dirty = false;
        }
    
        depend() {
            let i = this.deps.length;
    
            while (i--) {
                this.deps[i].depend();
            }
        }
    }
    
    const obj = {};
    defineReactive(obj, 'text', 'Hello World!');
    
    const vm = {};
    const computed = {
        text1() {
            return `${obj.text}-text1`;
        },
        text2() {
            return `${vm.text1}-text2`;
        }
    };
    
    function createComputedGetter(key) {
        return function computedGetter() {
            const watcher = vm.computedWatchers[key];
    
            if (watcher) {
                if (watcher.dirty) {
                    watcher.evaluate();
                }
    
                if (Dep.target) {
                    watcher.depend();
                }
    
                return watcher.value;
            }
        }
    }
    
    function defineCompute(target) {
        const watchers = vm.computedWatchers = Object.create(null);
    
        for (key in target) {
            const cb = target[key];
            watchers[key] = new Watcher(cb, true);
    
            //defineComputed
            Object.defineProperty(vm, key, {
                get: createComputedGetter(key),
                set(a) {
                    return a;
                }
            });
        }
    }
    
    defineCompute(computed);
    
    const watcher = new Watcher(() => {
        document.querySelector('body').innerHTML = vm.text2;
    });
    

    把上面的代码复制到浏览器的控制台运行,就可以看到浏览器里面出现了Hello World-text1-text2,然后我们继续在控制台输入obj.text = 'Define Reactive',可以看到浏览器里面的Hello World-text1-text2就变成了Define Reactive-text1-text2

    显然,由于我们改变了obj.text的值,然后自动的导致了vm.text1vm.text2的值发生了响应式变化。

    而其中的原理是,假如计算属性 A 依赖计算属性 B,而计算属性 B 又依赖响应式数据 C,那么最一开始先把计算属性 AB 都转化为 watcher,然后在把计算属性 AB 挂载到 vm 上面的时候,插入了一段 getter,而计算属性 B 的这个 getter 在这个计算属性 B 被读取的时候会把计算属性 A 的 watcher 添加到响应式数据 C 的依赖里面,所以响应式数据 C 在改变的时候会先后导致计算属性 B 和 A 执行 update,从而发生改变。

    而其中关键的那段代码就是这段:

    function createComputedGetter(key) {
        return function computedGetter() {
            const watcher = vm.computedWatchers[key];
    
            if (watcher) {
                if (watcher.dirty) {
                    watcher.evaluate();
                }
    
                // 这里非常关键
                if (Dep.target) {
                    watcher.depend();
                }
    
                return watcher.value;
            }
        }
    }
    

    为什么在计算属性 B 的 getter 函数里面会添加计算属性 A 的 watcher 呢?这是因为计算属性 B 在求值完成后,会自动把Dep.target出栈,从而暴露出计算属性 A 的 watcher。代码如下:

    class Watcher {
        get() {
            // 这里把自己的 watcher 入栈
            pushTarget(this);
            const value = this.getter();
            // 这里把自己的 watcher 出栈
            popTarget(this);
            this.deps = [...this.newDeps];
            this.newDeps = [];
            return value;
        }
    }
    

    这就是 pushTarget 和 popTarget 调度 watchers 的美丽之处~~

    其它

    需要注意以下两点:

    1.在给计算属性生成 getter 的时候,不能直接使用 Object.defineProperty,而是使用闭包把 key 值储存了起来。

    2.为什么不直接使用 defineReactive 把计算属性变成响应式的。因为当把计算属性用 setter 挂载到 vm 上面的时候,计算属性这里确实变成了一个具体的值,但是如果使用 defineReactive 把计算属性变成响应式的话,计算属性会执行自己的依赖,从而和响应式数据的依赖重复了。其实这也是把非数据变成响应式的一种方法。

  • 相关阅读:
    linux之uniq
    shell之sort
    tr命令的使用
    AWK 简明教程
    Word Frequency
    10亿个数中选出最大的100个数
    代理模式(也称委托模式)
    java内存相关
    Two Sum
    Java的集合
  • 原文地址:https://www.cnblogs.com/yangzhou33/p/13809534.html
Copyright © 2011-2022 走看看