Computed 简单讲解

上一回说了Vue的第二种watch也就是$Watch 接下来讲讲第三种 wachter也就是 computed(计算属性)

计算属性的特点
  • 调用后执行
  • 依赖的数据改变会重新计算
  • 具有缓存性

调用后执行,调用后执行,我们可以想想我们定义data的时候,一样是调用后执行,所以我们联想到老朋友

Object.defineProperty(target, key, sharedPropertyDefinition)

而且依赖的数据发生改变,我们可以想象到我们可以在每个依赖的数据的Dep绑定Watcher,然后数据变动时候通过notify不就可以让数据去更新了吗

  • 先列出大概的图,然后我们跟着源代码讲解下

在这里插入图片描述

  • 第一步初始化initComputed这个方法,主要是为了定义一个lazy=trueWatcher
// initComputed
const computedWatcherOptions = { lazy: true } // 注意这个时候 lazy 是为 true 的
function initComputed (vm: Component, computed: Object) {
  // 省略部分跟开发环境相关的提示
  const watchers = vm._computedWatchers = Object.create(null)
  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (!isSSR) {
      // 定义computed的Watcher
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      // 判断computed的值和data或者props不能一致
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}
  • 第二步 我们要为这个数据添加 Object.defineProperty() 这个属性
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  // 省略一些服务端渲染以及其他的东西 ...
  sharedPropertyDefinition.get  =  createComputedGetter(key)
  sharedPropertyDefinition.set = noop
  // 如果试图改变computed的值 直接警告
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
// Object.defineProperty() 的 get属性
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}
  • 好 准备阶段我们已经完毕,现在是数据改变的阶段

  • 数据调用的时候,会执行dep中的 subs[0].notify() 从而执行watcher的 run()

  • 普通Watcher的run主要是调用render等操作

  • computed的Watcher会因为 lazy=true 导致不进行下一步

 update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
}
  • 如果调用了computed,那么它将执行 Watcher.evaluate()方法
evaluate () {
    this.value = this.get()
    this.dirty = false
}
  • this.get() 方法会通过 this.getter.call 去调用相关computed定义的方法了
  • 如果 this.getter.call 如果调用 this.xxx 等 data的数据 ,这个时候的Dep.target是computed的Watcher
  • 那么 他将再去访问data中的 get(),然后通过 get()depend将data的 Dep和computed的Watcher绑定
  • 整个流程大致走完了,我们来说说对conputed的一些思考
问题
  1. 看看下面这段代码,既然evaluate()执行的时候会去绑定依赖数据的Watcher,那我们为什么还有在去 wacher.depend() 主动再去绑定一次吗,如果我们删掉这段代码会出现什么情况呢
if (watcher.dirty) {
    watcher.evaluate()
}
// 下面这段代码 猜猜这个时候的Dep.target是computed还是data的
if (Dep.target) {
    watcher.depend()
}
//watcher.js

我们注释掉代码,随便写点代码

<div id="demo">
        {{color}}
        <button @click='onclick'>点击我改变数据</button>
</div>
data:{
    foo: 1,
    foo1:3
},
 methods:{
    onclick(){
        this.foo = 5
    }
},
computed:{
    color() {
        return this.foo + this.foo1
    }
},

打开页面并且点击按钮,发color并没有发生改变,这是为什么,其实这dep.target用的其实是data的Wacher,为的就是让data绑定两个Watcher。这里大家可以去打个断点试试

我给出下面两张图,大家可以自己去试试并理解下,过程太复杂,我就不说了

添加了代码
添加了代码
未添加代码

请添加图片描述

  1. watch能监听computed属性吗,答案是可以的,前提是依赖的数据发生改变
总结
  1. computed 执行需要两个条件 (1)数据被调用(触发get) (2)依赖的数据改变(lazy = true) [缓存性,依赖数据发生改变才改变]
  2. computed 在触发get属性的时候会去绑定他引用data的Dep
  3. computed和data的Watcher一样,通过 get() 来绑定依赖,$watch的expOrFn则是字符串通过parsePath(expOrFn) 来绑定依赖
简单实现过程
<!DOCTYPE html>
<html lang="ch">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div> o.b <div id='text'></div>
    </div>
    <div> c <div id='Computedtext1'></div></div>
    <div> o.b + c = n<div id="Computedtext"></div>
    </div>
    <script>
        // Watcher
        var _this = this
        var targetStack = []
        class Watcher {
            constructor(expOrFn, cb, user, deep, lazy) {
                this.newDepIds = new Set()
                this.newDeps = []
                this.depIds = new Set()
                this.getter = expOrFn;
                this.lazy ? undefined : this.value = this.get()
                this.cb = cb
                this.user = user
                this.deep = deep
                this.lazy = lazy
            }
            addDep(dep) {
                const id = dep.id
                if (!this.newDepIds.has(id)) {
                    // watcher保存和它有关的dep
                    this.newDepIds.add(id)
                    this.newDeps.push(dep)
                    // 反过来
                    if (!this.depIds.has(id)) {
                        dep.addSub(this)
                    }
                }
            }
            get() {
                var val = ''
                setTarget(this)
                val = this.getter.call()
                if (this.deep) {
                    _traverse(val)
                }
                removeTarget(this)
                return val
            }
            update() {
                if (this.lazy) {
                    this.dirty = true
                } else {
                    this.run()
                }
            }
            run() {
                if (this.user) {
                    const oldValue = this.value
                    this.value = this.get()
                    this.cb(this.value, oldValue)
                } else {
                    this.get()
                }
            }
            evaluate() {
                this.value = this.get()
                this.dirty = false
            }
            depend() {
                let i = this.newDeps.length
                while (i--) {
                    this.newDeps[i].depend()
                }
            }
        }
        function $watch(func, data) {
            // 调用一下这个方法,已添加这个方法的Watcher
            new Watcher(data, func, true, true)
        }
        function $computed(func, data) {
            var computedWatcher = new Watcher(func, null, false, false, true)
            Object.defineProperty(_this.data, data, {
                set: () => {
                    throw 'computed的值不能更改'
                },
                get: () => {
                    debugger
                    if (computedWatcher) {
                        if (computedWatcher.dirty) {
                            computedWatcher.evaluate()
                        }
                        if (Dep.target) {
                            computedWatcher.depend()
                        }
                        return computedWatcher.value
                    }
                }
            })
        }
        //Dep
        var uid = 0
        class Dep {
            constructor() {
                this.id = uid++;
                this.subs = []
            }
            addSub(Watcher) {
                this.subs.push(Watcher)
            }
            depend() {
                Dep.target.addDep(this)
            }
            notify() {
                for (let i = 0, l = this.subs.length; i < l; i++) {
                    this.subs[i].update()
                }
            }
        }
        function setTarget(Watcher) {
            targetStack.push(Watcher)
            Dep.target = Watcher
        }
        function removeTarget(Watcher) {
            targetStack.pop()
            Dep.target = targetStack[targetStack.length - 1]
        }
        // Observer
        var data = {
            o: { b: '1' },
            c: '2'
        }
        function _traverse(val) {
            let i, keys
            const isA = Array.isArray(val)
            if (!(isA || val instanceof Object)) {
                return
            }
            keys = Object.keys(val)
            i = keys.length
            while (i--) _traverse(val[keys[i]])
        }

        class Observer {
            constructor(value) {
                this.value = value
                this.dep = new Dep()
                if (value instanceof Object) {
                    this.walk(value);
                }
            }
            observe(value) {
                // 初始化时创建一个
                let ob;
                if (value instanceof Object) {
                    ob = new Observer(value)
                }
                return ob
            }

            defineReactive(obj, key, value) {
                const dep = new Dep();
                if (arguments.length == 2) {
                    value = obj[key]
                    // console.log(value)
                }
                this.observe(obj[key])
                // 递归遍历
                Object.defineProperty(obj, key, {
                    enumerable: true,
                    configurable: true,
                    get: function reactiveGetter() {
                        if (Dep.target) {
                            dep.depend()
                        }
                        // console.log(obj, key, value)
                        return value
                    },
                    set: function reactiveSetter(newVal) {
                        value = newVal
                        dep.notify()
                    }
                })
            }
            walk(obj) {
                const keys = Object.keys(obj)
                for (let i = 0; i < keys.length; i++) {
                    this.defineReactive(obj, keys[i])
                }
            }
        }
        //模拟Vue执行的过程 暂时用setTimeout函数
        setTimeout(() => {
            // initData
            new Observer(data)
            // initWatch
            this.$watch((val, oldValue) => {
                console.log('发生变化', 'newValue:', val, 'oldValue:', oldValue)
            }, () => {
                // 这个其实相当于parsePath, parsePath 相当于解析字符串然后调用每个数据的get,这个方法直接去调用get,简化了过程
                // 源码在lang.js parsePath()方法
                // 这是有个bug,如果没有另提出一份空间,会导致对象新旧值一直一样
                return JSON.parse(JSON.stringify(this.data.o))
            })
            // initComputed 这里n的值为computed的值 之后
            this.$computed(() => {
                return Number(this.data.o.b) + Number(this.data.c)
            }, "n")
            new Watcher(() => {
                // .. render 渲染
                console.log('...', '发生改变继续调用')
                document.getElementById('Computedtext').innerText = this.data.n
                document.getElementById('Computedtext1').innerText = this.data.c
                document.getElementById('text').innerText = this.data.o.b
            })
        })
        setTimeout(() => {
            this.data.o.b = 2
            this.data.c = 3
        }, 5000)
    </script>
</body>
</html>
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐