深入理解Vue中computed以及实现方式

190 阅读2分钟

理解computed

computed基本使用

  1. 当模板中存在复杂计算逻辑的时候使用
  2. 多个地方使用只会执行一次
  3. 只有当计算属性内部所依赖的数据发生了改变的话才会重新触发方法
  4. 计算属性会缓存上一次计算出来的值

computed中包含了getter 和 setter方法, 默认是getter

method

使用几次就会执行几次
下面先看一个computed基本使用例子:

    var App = {
        data() {
            return {
                studentName: '张三'
            }
        },
        template: `
            <div>
                <!-- 当多个地方需要使用时,会执行多次 -->
                <!--
                    <h1>{{ '我是一名学生,名叫:' + studentName }}</h1>
                    <h3>{{ '我是一名学生,名叫:' + studentName }}</h3>
                -->
                <h1>computed计算出来的值:{{ studentInfo }}</h1>
                <h3>computed计算出来的值:{{ studentInfo }}</h3>
                <h1>methods计算出来的值:{{ studentInfoFn() }}</h1>
                <h3>methods计算出来的值:{{ studentInfoFn() }}</h3>
                <button @click="changeStudent">CHANGE STUDENT</button>
            </div>
        `,
        computed: {
            // studentInfo() {
            //     console.log('computed')
            //     return this.studentName ? '我是一名学生,名叫:' + this.studentName
            //                             : '学生不存在'
            // }
            studentInfo: {
                get() {
                    console.log('computed')
                    return this.studentName ? '我是一名学生,名叫:' + this.studentName
                                            : '学生不存在'
                },
                set(newValue) {
                    this.studentName = newValue
                }
            }
        },
        methods: {
            changeStudent() {
                this.studentName = ''
            },
            studentInfoFn() {
                console.log('methods')
                return this.studentName ? '我是一名学生,名叫:' + this.studentName
                                        : '学生不存在'
            }
        }
    }

    var vm = Vue.createApp(App).mount('#app')
    vm.studentInfo = '李四'

从浏览器输出可以看出computed里面的属性调用了两次只打印了一次computed,而mthoeds里面的方法却打印了两次,由此可看出computed多个地方使用只会执行一次。

image.png

简单实现computed

app.js

    import Vue from './Vue'

    var vm = new Vue({
        el: '#app',
        data() {
            return {
                a: 1,
                b: 2
            }
        },
        template: `
            <span>{{ a }}</span>
            <span>+</span>
            <span>{{ b }}</span>
            <span>=</span>
            <span>{{ total }}</span>
        `,
        computed: {
            total() {
                console.log('computed')
                return this.a + this.b
            },
            // total: {
            //     get() {
            //         console.log('computed')
            //         return this.a + this.b
            //     }
            // }
        }
    })

    console.log(vm)

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

    vm.a = 100
    vm.b = 100

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

vue.js

    var Vue = (function() {
        /**
         * computedData
         * 存储计算属性的值 方法 依赖的数据 
         * {
         *  value: value,
         *  get: fn,
         *  dep: [a, b, ...]
         * }
         */
        var computedData = {},
            dataPool = {};

        function Vue(options) {
            this.$el = document.querySelector(options.el)
            this.$data = options.data()

            this._init(this, options.computed, options.template)
        }

        Vue.prototype._init = function(vm, computed, template) {
            dataReactive(vm)
            computedReactive(vm, computed)
            render(vm, template)
        }

        function dataReactive(vm) {
            var _data = vm.$data

            for(var key in _data) {
                (function(key) {
                    Object.defineProperty(vm, key, {
                        get() {
                            return _data[key]
                        },
                        set(newValue) {
                            _data[key] = newValue
                            update(vm, key)
                            _updateComputedData(vm, key, function(key) {
                                update(vm, key)
                            })
                        }
                    })
                })(key)
            }
        }

        function computedReactive(vm, computed) {
            _initComputedData(vm, computed)

            // 监听计算属性数据 将计算属性total...挂载到vm实例上
            for(var key in computedData) {
                (function(key) {
                    Object.defineProperty(vm, key, {
                        get() {
                            return computedData[key].value
                        },
                        set(newValue) {
                            computedData[key].value = newValue
                        }
                    })
                })(key)
            }
        }

        function render(vm, template) {
            // 创建容器
            var container = document.createElement('div'),
                _el = vm.$el;

            container.innerHTML = template

            // 替换模板内容
            var domTree = _compileTemplate(vm, container)
            // 插入到app中
            _el.appendChild(domTree)
        }

        function update(vm, key) {
            dataPool[key].textContent = vm[key]
        }

        function _compileTemplate(vm, container) {
            // 获取container里面所有的元素
            var allNodes = container.getElementsByTagName('*'),
                item = null,
                var_reg = /\{\{(.+?)\}\}/g; // 匹配模板插值 {{ xxx }}

            for(var i = 0; i < allNodes.length; i++) {
                item = allNodes[i]

                // 匹配出模板中所使用的变量 {{ a }}
                var matched = item.textContent.match(var_reg)

                if(matched) {
                    item.textContent = item.textContent.replace(var_reg, function(node, key) {
                        dataPool[key.trim()] = item

                        return vm[key.trim()]
                    })
                }
            }

            return container
        }

        function _updateComputedData(vm, key, update) {
            var _dep = null

            for(var _key in computedData) {
                _dep = computedData[_key].dep

                // 对比数据 更新
                for(var i = 0; i < _dep.length; i++) {
                    if(_dep[i] === key) {
                        vm[_key] = computedData[_key].get()
                        update(_key)
                    }
                }
            }
        }

        function _initComputedData(vm, computed) {
            for(var key in computed) {
                var descriptor = Object.getOwnPropertyDescriptor(computed, key), // 获取属性描述符 value configurable...
                    descriptorFn = descriptor.value.get || descriptor.value;

                computedData[key] = {}
                computedData[key].value = descriptorFn.call(vm) // 计算属性首次计算出来的值
                computedData[key].get = descriptorFn.bind(vm) // 计算属性的方法
                computedData[key].dep = _collectDep(descriptorFn) // 收集计算属性方法所依赖的数据
            }
        }

        function _collectDep(fn) {
            // 将计算属性方法转成字符串 收集其中this.xxx数据
            var _collection = fn.toString().match(/this.(.+?)/g)

            if(_collection.length) {
                for(var i = 0; i < _collection.length; i++) {
                    // 转成变量名 a b...
                    _collection[i] = _collection[i].split('.')[1]
                }
            }

            return _collection
        }

        return Vue
    })()

    export default Vue