资讯专栏INFORMATION COLUMN

vue计算属性Computed的小秘密

adie / 2121人阅读

摘要:中小秘密的发现之旅首先我们看一段代码请问会间隔的打印出来吗中去掉,再问会间隔的打印出来吗如果第二问没有打印出来,那么在第二问的基础上怎么修改才能再次打印出来呢我先来揭晓答案会打印出来不会打印出来可以用过添加监听,来打印请问为什么呢以下是我的

vue中computed小秘密的发现之旅 首先我们看一段代码

    
{{ count }}
请问

console.log(1)会间隔的打印出来吗?

html中去掉{{ count }},再问console.log(1)会间隔的打印出来吗?

如果第二问没有打印出来,那么在第二问的基础上怎么修改才能再次打印出来呢?

我先来揭晓答案

会打印出来

不会打印出来

可以用过添加watch监听count,来打印`console.log(1)

    watch: {
        count: function (oldValue, newValue) {

        }
    }
请问为什么呢?

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

一句话总结就是computed是惰性求值,在new watcher时是计算属性时,this.value=undefined所以一开始不会触发get进行依赖收集即仅仅定义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中,this.deps[i].depend()也会加,但之前已加入改id所以猜测会直接return掉

这样当num发生改变,触发set,触发其notify 方法即遍历dep.subDeps数组(subDeps中放的是各种依赖),触发依赖的update方法。但之前的update方法看了一下

update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}  

可以看出直接走queueWatcher(this)所以就算内容没有变化,也会走渲染流程,这就造成了浪费

新版本,发生了变化

第一个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()
  }

所以从上面的实现可以看出,对当前计算属性自身也生成一个dep列表进行收集;完全可以把一个computed的初始化看出data中数据的初始化,只不过该值又依赖多个依赖

第三个evaluate

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

关键的update也做了修改,

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)
      }
    }
  }

当触发update时首先通过getAndInvoke函数进行值得比较,看是否发生变化,即只有在变化时才会执行,执行的是this.dep.notify(),而这边打的this是当前watch实例对象;因为之前就添加了依赖this.dep.depend()所以接着触发cb.call(this.vm, value, oldValue)cb是:this.dep.notify()但this指向了vm用来触发渲染更新

总结

计算属性的观察者是惰性求值,需要手动通过get

怎么手动get,所以有了问题的第二问,和第三问

触发了get,也就是触发了createComputedGetter函数,就会去取值this.value = this.get(),进行第一次渲染或取值;同时watcher.depend(),将计算属性的依赖添加至dep中,

值发送变化时,输出watch.update,首先判断是否存在依赖,存在则只需watcher.getAndInvoke(cb),

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/109508.html

相关文章

  • 从 VantComponent 谈 小程序维护

    摘要:不多废话,先说结论小程序组件写法这里就不再介绍。在官方文档中,我们可以看到使用构造器构造页面事实上,小程序的页面也可以视为自定义组件。经过一番测试,得出结果为为了简便。毕竟官方标准,不用担心其他一系列后续问题。 在开发小程序的时候,我们总是期望用以往的技术规范和语法特点来书写当前的小程序,所以才会有各色的小程序框架,例如 mpvue、taro 等这些编译型框架。当然这些框架本身对于新开...

    worldligang 评论0 收藏0
  • Vue.js-计算属性和class与style绑定

    摘要:每一个计算属性都包含一个和一个。使用计算属性的原因在于它的依赖缓存。及与绑定的主要用法是动态更新元素上的属性。测试文字当的表达式过长或逻辑复杂时,还可以绑定一个计算属性。 学习笔记:前端开发文档 计算属性 所有的计算属性都以函数的形式写在Vue实例中的computed选项内,最终返回计算后的结果。 计算属性的用法 在一个计算属性中可以完成各种复杂的逻辑,包括运算、函数调用等,只要最终...

    Shihira 评论0 收藏0
  • vue.js的小知识

    摘要:对于最终的结果,两种方式确实是相同的。然而,不同的是计算属性是基于它们的依赖进行缓存的。这就意味着只要还没有发生改变,多次访问计算属性会立即返回之前的计算结果,而不必再次执行函数。 vue.js vue.js 的构造 new Vue({}) new new MyComponent() 属性与方法 vue会代理其data对象里所有的属性 例如 data.a=vn.a vm.$i...

    light 评论0 收藏0
  • 美团小程序框架mpvue蹲坑指南

    摘要:这个是我们约定的额外的配置这个字段下的数据会被填充到顶部栏的统一配置美团汽车票同时,这个时候,我们会根据的页面数据,自动填充到中的字段。 美团小程序框架mpvue(花名:没朋友)蹲坑指南 第一次接触小程序大概是17年初,当时小程序刚刚内侧,当时就被各种限制折腾的死去活来的,单向绑定,没有promise,请求数限制,包大小限制,各种反人类,...反正我是感受到了满满的恶意.最近接到一个工...

    AprilJ 评论0 收藏0
  • 浅谈Vue计算属性computed的实现原理

    摘要:虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。当某个属性发生变化,触发拦截函数,然后调用自身消息订阅器的方法,遍历当前中保存着所有订阅者的数组,并逐个调用的方法,完成响应更新。 虽然目前的技术栈已由Vue转到了React,但从之前使用Vue开发的多个项目实际经历来看还是非常愉悦的,Vue文档清晰规范,api设计简洁高效,对前端开发人员友好,上手快,甚至个人认为在很多...

    laznrbfe 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<