vue计算属性Computed的小秘密

5次阅读

共计 5503 个字符,预计需要花费 14 分钟才能阅读完成。

vue 中 computed 小秘密的发现之旅

首先我们看一段代码

<body>
    <div id="app">
        {{count}}
    </div>
</body>
<script>
    new Vue({
        el: '#app',
        data () {
            return {num: 66}
        },
        computed: {count () {console.log(1)
                return this.num
            }
        },
        methods: {add () {setInterval(() => {this.num ++}, 1000) 
            }
        },
        created () {this.add()
        }
    })
</script>

请问

  • console.log(1)会间隔的打印出来吗?
  • html 中去掉 {{count}},再问console.log(1) 会间隔的打印出来吗?
  • 如果第二问没有打印出来,那么在第二问的基础上怎么修改才能再次打印出来呢?

我先来揭晓答案

  • 会打印出来
  • 不会打印出来
  • 可以用过添加 watch 监听 count,来打印 `console.log(1)
    watch: {count: function (oldValue, newValue) {}}

请问为什么呢?

以下是我的理解,有误还请指出,共同进步

  • 一句话总结就是 computed 是惰性求值,即仅仅定义 computed 的话是没有进行计算属性 count 的依赖收集(可以类似看成 data 中的数值,仅仅进行了响应式 get,set 的定义,并没有触发dep.depend, 所以当值发生变化的时候,他并不知道要通知谁,也就不会执行相应的回调函数了)

源码中有这么一段:

depend () {if (this.dep && Dep.target) {  // 因为惰性求值,所以 Dep.target 为 false
    this.dep.depend()}
}

所以如果仅仅是 computed 的初始化的话并 Dep.target 就是 undefined, 所以实例化的watch 并不会加入 dep 的中

看看 Computed 的实现

  • computed 初始化
function initComputed (vm: Component, computed: Object) {const watchers = vm._computedWatchers = Object.create(null)  //(标记 1)新建一个没有原型链的对象,用来存 `computed` 对象每个值的 watch 实例对象
    const isSSR = isServerRendering()  // 与服务端渲染有关,暂时忽略
    for (const key in computed) {const userDef = computed[key]  // 取 key 的值,该值大部分是 function 类型
        // 下面主要作用就是在非生产环境中没有 getter,保警告
        const getter = typeof userDef === 'function' ? userDef : userDef.get
        if (process.env.NODE_ENV !== 'production' && getter == null) {
          warn(`Getter is missing for computed property "${key}".`,
            vm
          )
        }
    }
    if (!isSSR) {
      //computed 中不同的 key,也就是计算属性生成 watch 实例,//watch 作用:简单看就是当值发生变化时会触通知到 watch,触发更新,执行回调函数
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
    if (!(key in vm)) {// 作用是将 {key: userDef} 变成响应式,重写其 get 和 set
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {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)
      }
    }
}
  • defineComputed 先看这个函数做了什么
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {const shouldCache = !isServerRendering()
    if (typeof userDef === 'function') {
      sharedPropertyDefinition.get = shouldCache
        ? createComputedGetter(key)
        : userDef
      sharedPropertyDefinition.set = noop
    } else {
      sharedPropertyDefinition.get = userDef.get
        ? shouldCache && userDef.cache !== false
          ? createComputedGetter(key)
          : userDef.get
        : noop
      sharedPropertyDefinition.set = userDef.set
        ? userDef.set
        : noop
    }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

上面函数的作用就是改写 get 与 set,关键就是这个createComputedGetter 在做什么?
早版本 createComputedGetter 的实现是:

function createComputedGetter(){return function computedGetter () {
        // 这个就是之前用来收集 watch 实例的一个对象,可看注释:标记 1
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if(watcher) {if(watcher.dirty) {watcher.evaluate()
            }
            if(Dep.target){ // 这里也可以看出 Dep.target 为 false 时是不会触发 depend, 即添加依赖
                watcher.depend()}
            return watcher.value
        }
    }
}

重点看看 watch

export default class Watcher {

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // 进行初始化的定义,忽略无关代码
      if(options) {this.lazy = !!options.lazy}else {this.lazy = false}
      this.getter = parsePath(expOrFn) // 返回一个取 data 值得函数
      this.dirty = this.lazy   //true
      this.value = this.lazy ? undefined : this.get()  //undefined, 当不会执行 get 时也就不会触发 get 实例方法中的 depend 的了}

  get () {
    // 伪代码
    Dep.target = this
    // 取值也就是访问触发属性的 get,get 中又触发 dep.depend(), 而 dep.depend 内部触发的是 Dep.target.addDep(this), 这里的 this 其实是 Dep 实例
    let value = this.getter.call(vm, vm) 
    Dep.target = undefined
  }

  addDep (dep: Dep) {
    // 伪代码
    const id = dep.id
    if(!this.depIds.has(id)) {this.depIds.add(id)
        this.deps.push(dep)
        dep.addSub(this)  //this 是 watch 实例对象
    }
  }

  update () {// 省略...}

  getAndInvoke (cb: Function) {// 省略...}

  evaluate () {this.value = this.get()
    this.dirty = false
  }

  depend () {
    let i = this.deps.length
    while(i --) {this.deps[i].depend()}
  }
  ...

}

总结:1.watcher.dirty默认为 true,执行 watcher.evaluate() 所以 computed 第一次默认会渲染,与 watch 不同 ;2. 当默认渲染,触发了 get,Dep.target 就不是 false,就会执行watcher.depend()

watcher.depend() 早版的实现,它有什么问题

  • this.dep 这个数组中元素都是 Dep 的实例对象,watcher 所依赖的所有 Dep 实例化列表;
    举个例子:当计算属性中 return this.num + this.num1, 当读取计算属性时会分别触发num 与 num1 的 get,get 中又触发 dep.depend(), 而 dep.depend 内部触发的是 Dep.target.addDep(this), 这里的 this 其实是 Dep 实例,这样就会分别将不同编号的 num 与 num1 的 dep,加入到 deps 中,最后将计算属性的依赖加入到 num,num1 的 Dep 中,
  • 这样就会出现一个问题,当 num 发生改变,触发 set,触发其notify 方法即遍历 dep.subDeps 数组 (subDeps 中放的是各种依赖),触发依赖的 update 方法。这样就导致如果num,num1 发生值得变化,但其和不变, 也会造成渲染,这是不合理的

新版本,发生了变化

  • 第一个 createComputedGetter
function createComputedGetter (key) {return function computedGetter () {const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {watcher.depend()
      return watcher.evaluate()}
  }
}
  • 第二个 watcher.depend()
  if (this.dep && Dep.target) {this.dep.depend()
  }
}

上面这里的 dep 又是哪里来的呢?在 watch 类中加了下面代码

if (this.computed) {
    this.value = undefined
    this.dep = new Dep()   // 类似一个 Object 对象,进行 observer 设置 get,set 响应式时会进 let dep = new Dep, 来收集改值得依赖} else {this.value = this.get()
  }

所以从上面的实现,完全可以把一个 computed 的初始化看出 data 中数据的初始化,只不过该值又依赖多个依赖

  • 第三个 evaluate
evaluate () {if (this.dirty) {this.value = this.get()
    this.dirty = false
  }
  return this.value
}

总结

  • 计算属性的观察者是惰性求值,需要手动通过 get
  • 怎么手动 get, 所以有了问题的第二问,和第三问
  • 触发了 get,也就是触发了 createComputedGetter 函数,就会去取值 this.value = this.get(), 进行第一次渲染或取值;同时watcher.depend(), 将 计算属性的依赖 添加至 dep 中,
  • 值发送变化时,输出watch.update, 首先判断是否存在依赖,存在则只需watcher.getAndInvoke(cb),

相关代码如下:

update () {
  /* istanbul ignore else */
  if (this.computed) {if (this.dep.subs.length === 0) {this.dirty = true} else {this.getAndInvoke(() => {this.dep.notify()
      })
    }
  } else if (this.sync) {this.run()
  } else {queueWatcher(this)
  }
},// 当计算属性的值发生变化时,改触发回调函数或者进行渲染,而不是通过之前值 (例如 num 改变) 变化就触发回调
getAndInvoke (cb: Function) {const value = this.get()
    if (
      value !== this.value ||
      isObject(value) ||
      this.deep
    ) {
      const oldValue = this.value
      this.value = value
      this.dirty = false
      if (this.user) {
        try {cb.call(this.vm, value, oldValue)
        } catch (e) {handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {cb.call(this.vm, value, oldValue)
      }
    }
  }

正文完
 0