Vue中watch和computed的使用与实现

165 阅读2分钟

watch基本使用

computed
    关注点在模板
    场景:专注于模板中复杂的逻辑
    特点:当计算属性所依赖的数据发生了变化时,会重新执行该方法,会缓存上一次的结果
 
 watch
    关注点在数据更新
    场景:监听某个数据,给数据绑定侦听函数,当数据改变时,执行该函数
    特点:当数据更新时,需要完成什么样的逻辑
    可以监听computed里面的属性
    const App = {
        data() {
            return {
                num: 1
            }
        },
        template: `
            <div>
                <p>{{ num }}</p>
                <button @click="changeNum">CHANGE NUM</button>
            </div>
        `,
        watch: {
            // 第一次默认不执行
            // 可以获取到新值和旧值
            num(newValue, oldValue) {
                if(newValue !== oldValue) {
                    this.getData()
                }
            }
        },
        methods: {
            changeNum() {
                this.num += 1
            },
            getData() {
                console.log('当num变化时,获取数据')
            }
        }
    }

    Vue.createApp(App).mount('#app')

image.png

computed和watch实现

app.js

    import Vue from './Vue'

    const vm = new Vue({
        data() {
            return {
                a: 1,
                b: 2
            }
        },
        computed: {
            total() {
                console.log('computed', this)
                return this.a + this.b
            },
        },
        watch: {
            total(newValue, oldValue) {
                console.log('total', newValue, oldValue)
            },
            a(newValue, oldValue) {
                console.log('a', newValue, oldValue)
            },
            b(newValue, oldValue) {
                console.log('b', newValue, oldValue)
            }
        }
    })

    console.log(vm)

    console.log(vm.total)
    console.log(vm.total)
    console.log(vm.total)

    vm.a = 100

    console.log(vm.total)
    console.log(vm.total)
    console.log(vm.total)

    vm.b = 200

    console.log(vm.total)
    console.log(vm.total)
    console.log(vm.total)

Vue -> index.js

    import reactive from './reactive'
    import Computed from './Computed'
    import Watcher from './Watcher'

    class Vue {
        constructor(options) {
            const { data, computed, watch } = options

            this.$data = data()

            this.init(this, computed, watch)
        }

        init(vm, computed, watch) {
           this.initData(vm) 
           const computedIns = this.initComputed(vm, computed)
           const watcherIns = this.initWatcher(vm, watch)

           this.$update = computedIns.update.bind(computedIns)
           this.$watcher = watcherIns.invoke.bind(watcherIns)
        //    this.initWatcher(vm, watch)
        }

        initData(vm) {
            reactive.dataReactive(vm, (key, value) => {
                console.log(key, value)
            }, (key, newValue, oldValue) => {
                // 每次更改数据时 执行
                // 新值旧值相等直接reutrn
                if(newValue === oldValue) return
                // 在computed属性结果变化时 watcher 通过callback触发
                this.$update(key, this.$watcher)
                // 这里只能监听到data里面的数据  监听不到computed里面的数据挂载到vm实例里面的
                this.$watcher(key, newValue, oldValue)
            })
        }

        initComputed(vm, computed) {
            const computedIns = new Computed()

            for(let key in computed) {
                computedIns.addComputed(vm, computed, key)
            }

            return computedIns
        }

        initWatcher(vm, watch) {
            const watcherIns = new Watcher()

            for(let key in watch) {
                watcherIns.addWatcherData(vm, watch, key)
            }

            return watcherIns
        }
    }

    export default Vue

Vue -> reactive.js

    class Reactive {
        constructor(vm) {
        }

        dataReactive(vm, __get__, __set__) {
            const _data = vm.$data

            for(let key in _data) {
                Object.defineProperty(vm, key, {
                    get() {
                        __get__ && __get__(key, _data[key])
                        return _data[key]
                    },
                    set(newValue) {
                        const oldValue = _data[key]
                        _data[key] = newValue
                        __set__ && __set__(key, _data[key], oldValue)
                    }
                })
            }
        }
    }

    export default new Reactive()

Vue -> Computed.js

    class Computed {
        constructor() {
            /**
             * computedData []
             * {
             *  key: value,
             *  value: key get fn()
             *  get: fn
             *  dep: [a, b...] 依赖的数据
             * }
             */

            this.computedData = []
        }

        addComputed(vm, computed, key) {
            const descriptor = Object.getOwnPropertyDescriptor(computed, key), // 拿到对象里面某个自有属性的属性描述符 
                  descriptorFn = descriptor.value.get || descriptor.value, // 拿到某个自由属性的方法
                  value = descriptorFn.call(vm), // 执行该方法计算出conputed属性的值 需要更改this指向 谁调用指向谁 默认会指向computedData -> {}
                  get = descriptorFn.bind(vm), // 拿到某个自由属性的方法 get
                  dep = this._collectDep(descriptorFn); // 取出方法所依赖的数据

            this.addComputedProp({
                key,
                value,
                get,
                dep
            })

            // 找到计算属性里面的值
            const selectItem = this.computedData.find(item => item.key === key)

            // 存在时 赋值给vm 实例上
            if(selectItem) {
                Object.defineProperty(vm, selectItem.key, {
                    // 当每次获取计算属性里面的值的时候 直接从vm中拿
                    get() {
                        return selectItem.value
                    },
                    set(newValue) {
                        // 每次更改都需要重新调用get 不直接用newValue
                        selectItem.value = selectItem.get()
                    }
                })
            }
        }

        update(key, cb) {
            // 当计算属性里面所依赖的数据变化了的话,直接重新执行get
            this.computedData.map(item => {
                const dep = item.dep,
                      _key = dep.find(el => el === key);

                if(_key) {
                    const oldValue = item.value
                    item.value = item.get()
                    // 触发watcher
                    // key要使用item.key 其他的key是计算属性所依赖的key
                    cb && cb(item.key, item.value, oldValue)
                }
            })
        }

        addComputedProp(prop) {
            this.computedData.push(prop)
        }

        _collectDep(fn) {
            // 找到方法里面this.的数据
            const matched = fn.toString().match(/this\.(.+?)/g)

            return matched.map(item => item.split('.')[1])
        }
    }

    export default Computed

Vue -> Watcher.js

    class Watcher {
        constructor() {
            /**
             * watcherData = []
             * {
             *  key: 值,
             *  fn: key fn
             * }
             */
            this.watcherData = []
        }

        addWatcherData(vm, watch, key) {
            this.addWatcherDataProp({
                key,
                fn: watch[key].bind(vm)
            })
        }

        invoke(key, newValue, oldValue) {
            this.watcherData.map(item => {
                // key相同时 就执行wathcer fn
                if(item.key === key) {
                    item.fn(newValue, oldValue)
                }
            })
        }

        addWatcherDataProp(prop) {
            this.watcherData.push(prop)
        }
    }

    export default Watcher

image.png 源码地址:https://gitee.com/me_peng/review/tree/f57ecc02337329329e0f14774a2aed7e6ff55588/vue/5.watch