Vue2源码之watch和computed(三)

475 阅读1分钟

1.watch实现

1.1.测试

<body>
    <div id="app" style="color:red;background:green">{{name}}{{obj}}{{age}}</div>
    <script src="./dist/vue.js"></script>
    <script>
        const vm=new Vue({
            el:'#app',
            data:{
                name:'zhangsan',
                obj:{
                    a:'1'
                },
                age:18
            },
            watch:{
                name(newVal,oldVal){
                    console.log(newVal,oldVal)
                },
                'obj.a'(newVal,oldVal){
                    console.log(newVal,oldVal)
                },
                age:[
                    function(newVal,oldVal){
                        console.log(newVal,oldVal)
                    },
                    function(newVal,oldVal){
                        console.log(newVal,oldVal)
                    }
                ]
            }
        })
        vm.$watch('name',function(newVal,oldVal){
            console.log(newVal,oldVal)
        })
        setTimeout(() => {
            vm.name='lisi';
            vm.age=2;
            vm.obj.a='4';
        }, 2000);
    </script>
</body>

1.2.index.js

stateMixin(Vue);

1.3.state.js

  • 初始化watch
  • 函数形式和数组形式的watch合并
/**
 * 初始化$watch
 * @param {*} Vue 
 */
export function stateMixin(Vue) {
    Vue.prototype.$watch = function (key, handler, options = {}) {
        //TODO:用来标记是用户watch
        options.user = true;
        new Watcher(this, key, handler, options);
    }
}
/**
 * 初始化状态
 * @param {*} vm 实例
 */
export function initState(vm) {
    //初始化watch
    if (opt.watch) {
        initWatch(vm, opt.watch)
    }
}


/**
 * 初始化watch
 * @param {*} vm 
 * @param {*} watch watch对象
 */
function initWatch(vm, watch) {
    for (const key in watch) {
        let handler = watch[key];
        if (Array.isArray(handler)) { //数组情况
            for (let i = 0; i < handler.length; i++) {
                createWatcher(vm, key, handler[i]);
            }
        } else {
            createWatcher(vm, key, handler)
        }
    }
}

/**
 * 创建watcher
 * @param {*} vm 
 * @param {*} key 
 * @param {*} handler 函数
 * @returns 
 */
function createWatcher(vm, key, handler) {
    return vm.$watch(key, handler)
}

1.4.observer/watcher.js

class Watcher {
    constructor(vm, exprOrFn, cb, options) {
        this.vm = vm;
        if (typeof exprOrFn === 'string') {
            this.exprOrFn = function () {
                //TODO:exprOrFn可能是'obj.a'
                const splits = exprOrFn.split('.')
                let obj = vm;
                for (let i = 0; i < splits.length; i++) {
                    obj = obj[splits[i]]
                }
                return obj; //vm[key]后,回调用getter方法
            }
        } else {
            this.exprOrFn = exprOrFn;
        }
        //后面用来判断是否是用户watcher用
        this.cb = cb;
        this.options = options;
        this.user = !!options.user;

        this.getter = this.exprOrFn;

        this.value = this.get(); //默认初始化要执行一次
    }

    get() {
        //Dep.target=watcher
        pushTarget(this);
        const value = this.getter(); //TODO:this.getter->render()执行,vm取值,会在get
        popTarget();
        return value;
    }


    run() {
        console.log('run')
        const newValue = this.get();
        const oldValue = this.value;
        this.value = newValue;
        if (this.user) {
            this.cb.call(this.vm, newValue, oldValue);
        }
    }
}

2.computed实现

  • 1.计算属性默认不会执行
  • 2.多次取值如果依赖的变量没有变化,不会重新执行
  • 3.依赖的值变化了,需要重新执行
  • 4.dirty表示这个是否需要重新执行

2.1.测试

  • 1.当在页面中直接写fullName是,fullName不会去收集渲染watcher,因为fullName没有dep,没有收集的功能,state的defineComputed中实现了此逻辑
  • 2.firstName是在计算属性中依赖,所以他会收集属性watcher,和渲染watcher
<body>
    <div id="app" style="color:red;background:green">{{fullName}}</div>
    <script src="./dist/vue.js"></script>
    <script>
        const vm=new Vue({
            el:'#app',
            data:{
                firstName:'zhang',
                lastName:'san'
            },
            computed:{
                fullName(){
                    console.log('get')
                    return this.firstName+this.lastName;
                }
                // fullName:{
                //     get(){
                //         console.log('get')
                //         return this.firstName+this.lastName;
                //     },
                //     set(newVal){
                //        console.log('set',newVal)
                //     }
                // }
            }
        })
        setTimeout(() => {
            vm.firstName='li'
        }, 2000);
    </script>
</body>

2.2.state.js

/**
 * 初始化$watch
 * @param {*} Vue 
 */
export function stateMixin(Vue) {
    Vue.prototype.$watch = function (key, handler, options = {}) {
        //TODO:用来标记是用户watch
        options.user = true;
        new Watcher(this, key, handler, options);
    }
}
/**
 * 初始化状态
 * @param {*} vm 实例
 */
export function initState(vm) {
    const opt = vm.$options;

    //初始化computed
    if (opt.computed) {
        initComputed(vm, opt.computed);
    }
}

/**
 * 改造get方法,是dirty为true时,才执行
 * @param {*} key 
 * @returns 
 */
function createComputedGetter(key) {
    return function () { //取计算属性的值
        const watcher = this._computedWatchers[key];
        //stack=[渲染watcher,计算watcher]
        if (watcher.dirty) { //根据dirty,判断是否需要重新求值
            watcher.evaluate();
        }
        //如果执行完成之后,Dep.target还有值,需要继续向上收集,处理 firstName,和lastName,使他们即有计算watcher,也有渲染watcher
        if (Dep.target) {
            watcher.depend();
        }
        return watcher.value;
    }
}

function defineComputed(vm, key, useDef) {
    let sharedProperty = {};
    if (typeof useDef === 'function') { //是方法,本身就是一个get方法
        sharedProperty.get = useDef;
    } else { //是对象
        sharedProperty.get = createComputedGetter(key);
        sharedProperty.set = useDef.set;
    }
    Object.defineProperty(vm, key, sharedProperty); //computed就是一个defineProperty
}

/**
 * 初始化computed
 * @param {*} vm 
 * @param {*} computed 计算对象
 */
function initComputed(vm, computed) {
    //将watcher和属性的映射挂载vm._computedWatchers上,方便后续使用
    const watchers = vm._computedWatchers = {};
    for (let key in computed) {
        const userDef = computed[key];
        //userDef可能是方法,也可能是对象
        const getter = typeof userDef === 'function' ? userDef : userDef.get;
        /**
         * TODO:
         * 1.每个计算属性其实就是一个计算watcher
         * 2.将watcher和属性做一个映射
         */
        watchers[key] = new Watcher(vm, getter, () => {}, {
            lazy: true //计算属性watcher默认不执行
        })
        //将计算属性挂载到vm上
        defineComputed(vm, key, userDef);
    }
}

2.3.observer/watcher.js

class Watcher {
    constructor(vm, exprOrFn, cb, options) {
        
        //计算属性watcher,lazy:true,dirty:true
        this.lazy = !!options.lazy;
        this.dirty = !!options.lazy;

        //计算属性,默认不会执行
        this.value = this.lazy ? undefined : this.get(); //默认初始化要执行一次
    }

    update() {
        if (this.dirty) { //计算属性改变时
            this.dirty = true;
        } else {
            //多次调用update,希望先将watcher缓存,一起更新
            queueWatcher(this);
        }
    }

    evaluate() {
        this.dirty = false; //false,已经取过值
        this.value = this.get(); //用户的getter执行
    }

    depend() {
        let i = this.deps.length;
        while (i--) { //firtName lastName收集渲染watcher
            this.deps[i].depend();
        }
    }
}

2.4.observer/dep.js

Dep.target = null;
let stack = [];
export function pushTarget(watcher) {
    Dep.target = watcher;
    stack.push(watcher);
}

export function popTarget() {
    stack.pop();
    Dep.target = stack[stack.length - 1];
}