精华内容
下载资源
问答
  • 浅谈 Vue computed 实现原理

    千次阅读 2018-10-10 19:05:26
    虽然目前技术栈已由 Vue 转到了 React,但从之前使用 Vue 开发...之前也有断断续续研读过 Vue 源码,但一直没有梳理总结,所以在此做一些技术归纳同时也加深自己对 Vue 理解,那么今天要写便是 Vue 最常...

    虽然目前的技术栈已由 Vue 转到了 React,但从之前使用 Vue 开发的多个项目实际经历来看还是非常愉悦的,Vue 文档清晰规范,api 设计简洁高效,对前端开发人员友好,上手快,甚至个人认为在很多场景使用 Vue 比React 开发效率更高,之前也有断断续续研读过 Vue 的源码,但一直没有梳理总结,所以在此做一些技术归纳同时也加深自己对 Vue 的理解,那么今天要写的便是 Vue 中最常用到的 API 之一 computed 的实现原理。

    基本介绍

    话不多说,一个最基本的例子如下:

    {{fullName}}
    
    new Vue({
        data: {
            firstName: 'Xiao',
            lastName: 'Ming'
        },
        computed: {
            fullName: function () {
                return this.firstName + ' ' + this.lastName
            }
        }
    })
    

    Vue 中我们不需要在 template 里面直接计算 {{this.firstName + ‘ ‘ + this.lastName}},因为在模版中放入太多声明式的逻辑会让模板本身过重,尤其当在页面中使用大量复杂的逻辑表达式处理数据时,会对页面的可维护性造成很大的影响,而 computed 的设计初衷也正是用于解决此类问题。

    对比侦听器 watch

    当然很多时候我们使用 computed 时往往会与 Vue 中另一个 API 也就是侦听器 watch 相比较,因为在某些方面它们是一致的,都是以 Vue 的依赖追踪机制为基础,当某个依赖数据发生变化时,所有依赖这个数据的相关数据或函数都会自动发生变化或调用。

    虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch
    选项提供了一个更通用的方法来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。

    从 Vue 官方文档对 watch 的解释我们可以了解到,使用 watch 选项允许我们执行异步操作(访问一个 API)或高消耗性能的操作,限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态,而这些都是计算属性无法做到的。

    下面还另外总结了几点关于 computed 和 watch 的差异:

    • computed 是计算一个新的属性,并将该属性挂载到 vm(Vue 实例)上,而 watch 是监听已经存在且已挂载到 vm
      上的数据,所以用 watch 同样可以监听 computed 计算属性的变化(其它还有 data、props)
    • computed 本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值,而
      watch 则是当数据发生变化便会调用执行函数
    • 从使用场景上说,computed 适用一个数据被多个数据影响,而 watch 适用一个数据影响多个数据;

    以上我们了解了 computed 和 watch 之间的一些差异和使用场景的区别,当然某些时候两者并没有那么明确严格的限制,最后还是要具体到不同的业务进行分析。

    原理分析

    言归正传,回到文章的主题 computed 身上,为了更深层次地了解计算属性的内在机制,接下来就让我们一步步探索 Vue 源码中关于它的实现原理吧。

    在分析 computed 源码之前我们先得对 Vue 的响应式系统有一个基本的了解,Vue 称其为非侵入性的响应式系统,数据模型仅仅是普通的 JavaScript 对象,而当你修改它们时,视图便会进行自动更新。
    在这里插入图片描述

    当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项时,Vue 将遍历此对象所有的属性,并使用
    Object.defineProperty 把这些属性全部转为 getter/setter,这些 getter/setter
    对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化,每个组件实例都有相应的 watcher
    实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher
    重新计算,从而致使它关联的组件得以更新。

    Vue 响应系统,其核心有三点:observe、watcher、dep:

    • observe:遍历 data 中的属性,使用 Object.defineProperty 的 get/set 方法对其进行数据劫持;
    • dep:每个属性拥有自己的消息订阅器 dep,用于存放所有订阅了该属性的观察者对象;
    • watcher:观察者(对象),通过 dep 实现对响应属性的监听,监听到结果后,主动触发自己的回调进行响应。

    对响应式系统有一个初步了解后,我们再来分析计算属性。 首先我们找到计算属性的初始化是在 src/core/instance/state.js 文件中的 initState 函数中完成的

    export function initState (vm: Component) {
      vm._watchers = []
      const opts = vm.$options
      if (opts.props) initProps(vm, opts.props)
      if (opts.methods) initMethods(vm, opts.methods)
      if (opts.data) {
        initData(vm)
      } else {
        observe(vm._data = {}, true /* asRootData */)
      }
      // computed初始化
      if (opts.computed) initComputed(vm, opts.computed)
      if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch)
      }
    }
    

    调用了 initComputed 函数(其前后也分别初始化了 initData 和 initWatch )并传入两个参数 vm 实例和 opt.computed 开发者定义的 computed 选项,转到 initComputed 函数:

    const computedWatcherOptions = { computed: true }
    function initComputed (vm: Component, computed: Object) {
      // $flow-disable-line
      const watchers = vm._computedWatchers = Object.create(null)
      // computed properties are just getters during SSR
      const isSSR = isServerRendering()
    
      for (const key in computed) {
        const userDef = computed[key]
        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) {
          // create internal watcher for the computed property.
          watchers[key] = new Watcher(
            vm,
            getter || noop,
            noop,
            computedWatcherOptions      )
        }
    
        // component-defined computed properties are already defined on the
        // component prototype. We only need to define computed properties defined
        // at instantiation here.
        if (!(key in vm)) {
          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)
          }
        }
      }
    }
    

    从这段代码开始我们观察这几部分:

    获取计算属性的定义 userDef 和 getter 求值函数

       const userDef = computed[key]
        const getter = typeof userDef === 'function' ? userDef : userDef.get
    

    定义一个计算属性有两种写法,一种是直接跟一个函数,另一种是添加 set 和 get 方法的对象形式,所以这里首先获取计算属性的定义 userDef,再根据 userDef 的类型获取相应的 getter 求值函数。

    计算属性的观察者 watcher 和消息订阅器 dep

    watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions)
    

    这里的 watchers 也就是 vm._computedWatchers 对象的引用,存放了每个计算属性的观察者 watcher 实例(注:后文中提到的“计算属性的观察者”、“订阅者”和 watcher 均指代同一个意思但注意和 Watcher 构造函数区分),Watcher 构造函数在实例化时传入了 4 个参数:vm 实例、getter求值函数、noop 空函数、computedWatcherOptions 常量对象(在这里提供给 Watcher 一个标识 {computed:true} 项,表明这是一个计算属性而不是非计算属性的观察者,我们来到 Watcher 构造函数的定义:

    class Watcher {
      constructor (
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
      ) {
        if (options) {
          this.computed = !!options.computed
        } 
    
        if (this.computed) {
          this.value = undefined
          this.dep = new Dep()
        } else {
          this.value = this.get()
        }
      }
    
      get () {
        pushTarget(this)
        let value
        const vm = this.vm
        try {
          value = this.getter.call(vm, vm)
        } catch (e) {
    
        } finally {
          popTarget()
        }
        return value
      }
    
      update () {
        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)
        }
      }
    
      evaluate () {
        if (this.dirty) {
          this.value = this.get()
          this.dirty = false
        }
        return this.value
      }
    
      depend () {
        if (this.dep && Dep.target) {
          this.dep.depend()
        }
      }
    }
    

    为了简洁突出重点,这里我手动去掉了我们暂时不需要关心的代码片段。 观察 Watcher 的 constructor ,结合刚才讲到的 new Watcher 传入的第四个参数 {computed:true} 知道,对于计算属性而言 watcher 会执行 if 条件成立的代码 this.dep = new Dep(),而 dep 也就是创建了该属性的消息订阅器。

     export default class Dep {
          static target: ?Watcher;
          subs: Array<Watcher>;
        
          constructor () {
            this.id = uid++
            this.subs = []
          }
        
          addSub (sub: Watcher) {
            this.subs.push(sub)
          }
        
          depend () {
            if (Dep.target) {
              Dep.target.addDep(this)
            }
          }
        
          notify () {
            const subs = this.subs.slice()
            for (let i = 0, l = subs.length; i < l; i++) {
              subs[i].update()
            }
          }
        }
        Dep.target = null
    

    Dep 同样精简了部分代码,我们观察 Watcher 和 Dep 的关系,用一句话总结

    watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者,dep 通过 notify 遍历了 dep.subs 通知每个
    watcher 更新。

    defineComputed 定义计算属性

    if (!(key in vm)) {
      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)
      }
    }
    

    因为 computed 属性是直接挂载到实例对象中的,所以在定义之前需要判断对象中是否已经存在重名的属性,defineComputed 传入了三个参数:vm实例、计算属性的 key 以及 userDef 计算属性的定义(对象或函数)。 然后继续找到 defineComputed 定义处:

    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
      }
      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 方法,其中传入的第三个参数是属性描述符sharedPropertyDefinition,初始化为:

    const sharedPropertyDefinition = {
      enumerable: true,
      configurable: true,
      get: noop,
      set: noop
    }
    

    随后根据 Object.defineProperty 前面的代码可以看到 sharedPropertyDefinition 的 get/set 方法在经过 userDef 和 shouldCache 等多重判断后被重写,当非服务端渲染时,sharedPropertyDefinition 的 get 函数也就是 createComputedGetter(key) 的结果,我们找到 createComputedGetter 函数调用结果并最终改写 sharedPropertyDefinition 大致呈现如下:

    sharedPropertyDefinition = {
        enumerable: true,
        configurable: true,
        get: function computedGetter () {
            const watcher = this._computedWatchers && this._computedWatchers[key]
            if (watcher) {
                watcher.depend()
                return watcher.evaluate()
            }
        },
        set: userDef.set || noop
    }
    

    当计算属性被调用时便会执行 get 访问函数,从而关联上观察者对象 watcher 然后执行 wather.depend() 收集依赖和 watcher.evaluate() 计算求值。

    分析完所有步骤,我们再来总结下整个流程:

    • 当组件初始化的时候,computed 和 data 会分别建立各自的响应系统,Observer遍历 data 中每个属性设置
      get/set 数据拦截
    • 初始化 computed 会调用 initComputed 函数
      • 注册一个 watcher 实例,并在内实例化一个 Dep 消息订阅器用作后续收集依赖(比如渲染函数的 watcher
        或者其他观察该计算属性变化的 watcher )
      • 调用计算属性时会触发其Object.defineProperty的get访问器函数
      • 调用 watcher.depend() 方法向自身的消息订阅器 dep 的 subs 中添加其他属性的 watcher
      • 调用 watcher 的 evaluate 方法(进而调用 watcher 的 get 方法)让自身成为其他 watcher
        的消息订阅器的订阅者,首先将 watcher 赋给 Dep.target,然后执行 getter
        求值函数,当访问求值函数里面的属性(比如来自 data、props 或其他 computed)时,会同样触发它们的 get访问器函数从而将该计算属性的 watcher 添加到求值函数中属性的 watcher 的消息订阅器 dep 中,当这些操作完成,最后关闭Dep.target 赋为 null 并返回求值函数结果。
    • 当某个属性发生变化,触发 set 拦截函数,然后调用自身消息订阅器 dep 的 notify 方法,遍历当前 dep 中保存着所有订阅者wathcer 的 subs 数组,并逐个调用 watcher 的 update 方法,完成响应更新。

    原文链接:https://mp.weixin.qq.com/s/igkif-J_BHd1q5mZ7TewCw

    展开全文
  • 1.初始化 data 和 computed,分别代理其 set 和 get 方法,对 data 中的所有属性生成唯一 dep 实例 2.对 computed 中的 属性生成唯一 watcher,并保存在 vm._computedWatchers 3.访问计算属性时,设置 Dep....

    1.初始化 data 和 computed,分别代理其 set 和 get 方法,对 data 中的所有属性生成唯一的 dep 实例

    2.对 computed 中的 属性生成唯一的 watcher,并保存在 vm._computedWatchers 中

    3.访问计算属性时,设置 Dep.target 指向 计算属性的 watcher,调用该属性具体方法

    4.方法中访问 data 的属性,即会调用 data 属性的 get 方法,将 data 属性的 dep 加入到 计算属性的 watcher , 同时该 dep 中的 subs 添加这个 watcher

    5.设置 data 的这个属性时,调用该属性代理的 set 方法,触发 dep 的 notify 方法

    6.因为时 computed 属性,只是将 watcher 中的 dirty 设置为 true

    7.最后,访问计算属性的 get 方法时,得知该属性的 watcher.dirty 为 true,则调用 watcher.evaluate() 方法获取新的值

    综合以上:也可以解释了为什么有些时候当computed没有被访问(或者没有被模板依赖),当修改了this.data值后,通过vue-tools发现其computed中的值没有变化的原因,因为没有触发到其get方法。

    computed vs methods
    计算属性跟方法都能打到同样的方法,那么他们之间有什么不同呢?我们还是看一段代码理解一下

    // methods 每次render都会重新计算
    <template>
      <p>{{getName()}}</p>
    </template>
    methods:{
      getName:function(){
        return this.name.split(' ').reverse().join(' ')
      }
    }
    // 使用computed ,computed是基于他们的依赖进行缓存的,也就是说下面这段代码只要this.name不改变,每次访问都会立即返回结果
    <template>
      <p>{{getName}}</p>
    </template>
    computed:{
      getName:function(){
        return this.name.split(' ').reverse().join(' ')
      },
    }

    总结:methods是实时的,在重新渲染时,函数总会重新调用执行,不会缓存,(多次输出时间不同)
    而computed只有在属性值发生改变时才会触发,因此 性能会更好,但是如果你不希望缓存,你可以使用 methods 属性。
    但是在利用实时信息时,比如显示当前进入页面的时间,必须用methods方式

    methods: {
        now: function () {
          return Date.now()
        }
    }

    如果用computed计算属性的话,每次进入页面将一直沿用第一次的信息,不会再触发now。

    展开全文
  • 虽然目前技术栈已由 Vue 转到了 React,但从之前使用 Vue 开发多个项目实际经历来看还是非常愉悦Vue 文档清晰规范,api 设计简洁高效,对前端开发人员友好,上手快,甚至个人认为在很多场景使用 Vue 比 ...

    编者按:我们会不时邀请工程师谈谈有意思的技术细节,希望知其所以然能让大家在面试有更出色表现。也给面试官提供更多思路。

    虽然目前的技术栈已由 Vue 转到了 React,但从之前使用 Vue 开发的多个项目实际经历来看还是非常愉悦的,Vue 文档清晰规范,api 设计简洁高效,对前端开发人员友好,上手快,甚至个人认为在很多场景使用 Vue 比 React 开发效率更高,之前也有断断续续研读过 Vue 的源码,但一直没有梳理总结,所以在此做一些技术归纳同时也加深自己对 Vue 的理解,那么今天要写的便是 Vue 中最常用到的 API 之一 computed 的实现原理。

    基本介绍

    话不多说,一个最基本的例子如下:

    <div id="app">
        <p>{{fullName}}</p>
    </div>
    复制代码
    new Vue({
        data: {
            firstName: 'Xiao',
            lastName: 'Ming'
        },
        computed: {
            fullName: function () {
                return this.firstName + ' ' + this.lastName
            }
        }
    })
    复制代码

    Vue 中我们不需要在 template 里面直接计算 {{this.firstName + ' ' + this.lastName}},因为在模版中放入太多声明式的逻辑会让模板本身过重,尤其当在页面中使用大量复杂的逻辑表达式处理数据时,会对页面的可维护性造成很大的影响,而 computed 的设计初衷也正是用于解决此类问题。

    对比侦听器 watch

    当然很多时候我们使用 computed 时往往会与 Vue 中另一个 API 也就是侦听器 watch 相比较,因为在某些方面它们是一致的,都是以 Vue 的依赖追踪机制为基础,当某个依赖数据发生变化时,所有依赖这个数据的相关数据或函数都会自动发生变化或调用。

    虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。

    从 Vue 官方文档对 watch 的解释我们可以了解到,使用 watch 选项允许我们执行异步操作(访问一个 API)或高消耗性能的操作,限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态,而这些都是计算属性无法做到的。

    下面还另外总结了几点关于 computedwatch 的差异:

    1. computed 是计算一个新的属性,并将该属性挂载到 vm(Vue 实例)上,而 watch 是监听已经存在且已挂载到 vm 上的数据,所以用 watch 同样可以监听 computed 计算属性的变化(其它还有 dataprops
    2. computed 本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值,而 watch 则是当数据发生变化便会调用执行函数
    3. 从使用场景上说,computed 适用一个数据被多个数据影响,而 watch 适用一个数据影响多个数据;

    以上我们了解了 computedwatch 之间的一些差异和使用场景的区别,当然某些时候两者并没有那么明确严格的限制,最后还是要具体到不同的业务进行分析。

    原理分析

    言归正传,回到文章的主题 computed 身上,为了更深层次地了解计算属性的内在机制,接下来就让我们一步步探索 Vue 源码中关于它的实现原理吧。

    在分析 computed 源码之前我们先得对 Vue 的响应式系统有一个基本的了解,Vue 称其为非侵入性的响应式系统,数据模型仅仅是普通的 JavaScript 对象,而当你修改它们时,视图便会进行自动更新。

    当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项时,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter,这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化,每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

    Vue 响应系统,其核心有三点:observewatcherdep

    1. observe:遍历 data 中的属性,使用 Object.definePropertyget/set 方法对其进行数据劫持;
    2. dep:每个属性拥有自己的消息订阅器 dep,用于存放所有订阅了该属性的观察者对象;
    3. watcher:观察者(对象),通过 dep 实现对响应属性的监听,监听到结果后,主动触发自己的回调进行响应。

    对响应式系统有一个初步了解后,我们再来分析计算属性。 首先我们找到计算属性的初始化是在 src/core/instance/state.js 文件中的 initState 函数中完成的

    export function initState (vm: Component) {
      vm._watchers = []
      const opts = vm.$options
      if (opts.props) initProps(vm, opts.props)
      if (opts.methods) initMethods(vm, opts.methods)
      if (opts.data) {
        initData(vm)
      } else {
        observe(vm._data = {}, true /* asRootData */)
      }
      // computed初始化
      if (opts.computed) initComputed(vm, opts.computed)
      if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch)
      }
    }
    复制代码

    调用了 initComputed 函数(其前后也分别初始化了 initDatainitWatch )并传入两个参数 vm 实例和 opt.computed 开发者定义的 computed 选项,转到 initComputed 函数:

    const computedWatcherOptions = { computed: true }
    
    function initComputed (vm: Component, computed: Object) {
      // $flow-disable-line
      const watchers = vm._computedWatchers = Object.create(null)
      // computed properties are just getters during SSR
      const isSSR = isServerRendering()
    
      for (const key in computed) {
        const userDef = computed[key]
        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) {
          // create internal watcher for the computed property.
          watchers[key] = new Watcher(
            vm,
            getter || noop,
            noop,
            computedWatcherOptions
          )
        }
    
        // component-defined computed properties are already defined on the
        // component prototype. We only need to define computed properties defined
        // at instantiation here.
        if (!(key in vm)) {
          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)
          }
        }
      }
    }
    复制代码

    从这段代码开始我们观察这几部分:

    1. 获取计算属性的定义 userDefgetter 求值函数

      const userDef = computed[key]
      const getter = typeof userDef === 'function' ? userDef : userDef.get
      复制代码

      定义一个计算属性有两种写法,一种是直接跟一个函数,另一种是添加 setget 方法的对象形式,所以这里首先获取计算属性的定义 userDef,再根据 userDef 的类型获取相应的 getter 求值函数。

    2. 计算属性的观察者 watcher 和消息订阅器 dep

      watchers[key] = new Watcher(
          vm,
          getter || noop,
          noop,
          computedWatcherOptions
      )
      复制代码

      这里的 watchers 也就是 vm._computedWatchers 对象的引用,存放了每个计算属性的观察者 watcher 实例(注:后文中提到的“计算属性的观察者”、“订阅者”和 watcher 均指代同一个意思但注意和 Watcher 构造函数区分),Watcher 构造函数在实例化时传入了 4 个参数:vm 实例、getter求值函数、noop 空函数、computedWatcherOptions 常量对象(在这里提供给 Watcher 一个标识 {computed:true} 项,表明这是一个计算属性而不是非计算属性的观察者,我们来到 Watcher 构造函数的定义:

      class Watcher {
        constructor (
          vm: Component,
          expOrFn: string | Function,
          cb: Function,
          options?: ?Object,
          isRenderWatcher?: boolean
        ) {
          if (options) {
            this.computed = !!options.computed
          } 
      
          if (this.computed) {
            this.value = undefined
            this.dep = new Dep()
          } else {
            this.value = this.get()
          }
        }
        
        get () {
          pushTarget(this)
          let value
          const vm = this.vm
          try {
            value = this.getter.call(vm, vm)
          } catch (e) {
            
          } finally {
            popTarget()
          }
          return value
        }
        
        update () {
          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)
          }
        }
      
        evaluate () {
          if (this.dirty) {
            this.value = this.get()
            this.dirty = false
          }
          return this.value
        }
      
        depend () {
          if (this.dep && Dep.target) {
            this.dep.depend()
          }
        }
      }
      复制代码

      为了简洁突出重点,这里我手动去掉了我们暂时不需要关心的代码片段。 观察 Watcherconstructor ,结合刚才讲到的 new Watcher 传入的第四个参数 {computed:true} 知道,对于计算属性而言 watcher 会执行 if 条件成立的代码 this.dep = new Dep(),而 dep 也就是创建了该属性的消息订阅器。

      export default class Dep {
        static target: ?Watcher;
        subs: Array<Watcher>;
      
        constructor () {
          this.id = uid++
          this.subs = []
        }
      
        addSub (sub: Watcher) {
          this.subs.push(sub)
        }
      
        depend () {
          if (Dep.target) {
            Dep.target.addDep(this)
          }
        }
      
        notify () {
          const subs = this.subs.slice()
          for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update()
          }
        }
      }
      
      Dep.target = null
        
      复制代码

      Dep 同样精简了部分代码,我们观察 WatcherDep 的关系,用一句话总结

      watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者,dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新。

    3. defineComputed 定义计算属性

      if (!(key in vm)) {
        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)
        }
      }
      复制代码

      因为 computed 属性是直接挂载到实例对象中的,所以在定义之前需要判断对象中是否已经存在重名的属性,defineComputed 传入了三个参数:vm实例、计算属性的 key 以及 userDef 计算属性的定义(对象或函数)。 然后继续找到 defineComputed 定义处:

      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
        }
        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 方法,其中传入的第三个参数是属性描述符sharedPropertyDefinition,初始化为:

      const sharedPropertyDefinition = {
        enumerable: true,
        configurable: true,
        get: noop,
        set: noop
      }
      复制代码

      随后根据 Object.defineProperty 前面的代码可以看到 sharedPropertyDefinitionget/set 方法在经过 userDefshouldCache 等多重判断后被重写,当非服务端渲染时,sharedPropertyDefinitionget 函数也就是 createComputedGetter(key) 的结果,我们找到 createComputedGetter 函数调用结果并最终改写 sharedPropertyDefinition 大致呈现如下:

      sharedPropertyDefinition = {
          enumerable: true,
          configurable: true,
          get: function computedGetter () {
              const watcher = this._computedWatchers && this._computedWatchers[key]
              if (watcher) {
                  watcher.depend()
                  return watcher.evaluate()
              }
          },
          set: userDef.set || noop
      }
      复制代码

      当计算属性被调用时便会执行 get 访问函数,从而关联上观察者对象 watcher 然后执行 wather.depend() 收集依赖和 watcher.evaluate() 计算求值。

    分析完所有步骤,我们再来总结下整个流程:

    1. 当组件初始化的时候,computeddata 会分别建立各自的响应系统,Observer遍历 data 中每个属性设置 get/set 数据拦截
    2. 初始化 computed 会调用 initComputed 函数
      1. 注册一个 watcher 实例,并在内实例化一个 Dep 消息订阅器用作后续收集依赖(比如渲染函数的 watcher 或者其他观察该计算属性变化的 watcher
      2. 调用计算属性时会触发其Object.definePropertyget访问器函数
      3. 调用 watcher.depend() 方法向自身的消息订阅器 depsubs 中添加其他属性的 watcher
      4. 调用 watcherevaluate 方法(进而调用 watcherget 方法)让自身成为其他 watcher 的消息订阅器的订阅者,首先将 watcher 赋给 Dep.target,然后执行 getter 求值函数,当访问求值函数里面的属性(比如来自 dataprops 或其他 computed)时,会同样触发它们的 get 访问器函数从而将该计算属性的 watcher 添加到求值函数中属性的 watcher 的消息订阅器 dep 中,当这些操作完成,最后关闭 Dep.target 赋为 null 并返回求值函数结果。
    3. 当某个属性发生变化,触发 set 拦截函数,然后调用自身消息订阅器 depnotify 方法,遍历当前 dep 中保存着所有订阅者 wathcersubs 数组,并逐个调用 watcherupdate 方法,完成响应更新。

    文 / 亦然

    一枚向往诗与远方的 coder

    编 / 荧声

    本文已由作者授权发布,版权属于创宇前端。欢迎注明出处转载本文。本文链接:knownsec-fed.com/2018-09-12-…

    想要订阅更多来自知道创宇开发一线的分享,请搜索关注我们的微信公众号:创宇前端(KnownsecFED)。欢迎留言讨论,我们会尽可能回复。

    感谢您的阅读。

    展开全文
  • 继上一篇:深入理解Vue的watch实现原理及其实现方式 继续讲解Vue的...如果你还不是很理解推荐你先看此文章:彻底搞懂Vue针对数组和双向绑定(MVVM)的处理方式首先来看一波Vue中computed的使用方式:var vm = new Vue(...

    继上一篇:深入理解Vue的watch实现原理及其实现方式 继续讲解

    Vue的computed实现相对于watch和data来说比较难以理解,要真正的理解computed的工作方式,你需要深入理解Vue的双向数据绑定原理和实现方式。

    如果你还不是很理解推荐你先看此文章:

    彻底搞懂Vue针对数组和双向绑定(MVVM)的处理方式


    首先来看一波Vue中computed的使用方式:

    var vm = new Vue({
      data: { a: 1 },
      computed: {
        // 仅读取
        aDouble: function () {
          return this.a * 2
        },
        // 读取和设置
        aPlus: {
          get: function () {
            return this.a + 1
          },
          set: function (v) {
            this.a = v - 1
          }
        }
      }
    })
    vm.aPlus   // => 2
    vm.aPlus = 3
    vm.a       // => 2
    vm.aDouble // => 4复制代码

    计算属性的主要应用场景是代替模板内的表达式,或者data值的任何复杂逻辑都应该使用computed来计算,它有两大优势:

    1、逻辑清晰,方便于管理

    2、计算值会被缓存,依赖的data值改变时才会从新计算

    此文我们需要核心理解的是:

    1、computed是如何初始化,初始化之后干了些什么

    2、为何触发data值改变时computed会从新计算

    3、computed值为什么说是被缓存的呢,如何做的


    如果以上三个问题你都已知,你可以忽略下文了,若未知或一知半解,请抱着学习的态度看看别人的理解。

    备注:以下只是我的个人理解,并不保证绝对的正确性,若有问题欢迎指正

    以下大部分代码摘自Vue源码。


    如果你看到了这里,就当做你已经深入理解了Vue的MVVM原理及其实现方式。相关Vue的MVVM实现直接取自上一篇文章。

    Dep代码的实现:

    //标识当前的Dep id
    let uidep = 0
    class Dep{
    	constructor () {
    		this.id = uidep++
    		// 存放所有的监听watcher
        	this.subs = []
      	}
    
      	//添加一个观察者对象
      	addSub (Watcher) {
        	this.subs.push(Watcher)
      	}
      	//依赖收集
    	depend () {
    		//Dep.target 作用只有需要的才会收集依赖
    	    if (Dep.target) {
    	      Dep.target.addDep(this)
    	    }
    	}
    
    	// 调用依赖收集的Watcher更新
        notify () {
    	    const subs = this.subs.slice()
    	    for (let i = 0, l = subs.length; i < l; i++) {
    	      subs[i].update()
    	    }
      	}
    }
    
    Dep.target = null
    const targetStack = []
    
    // 为Dep.target 赋值
    function pushTarget (Watcher) {
    	if (Dep.target) targetStack.push(Dep.target)
      	Dep.target = Watcher
    }
    function popTarget () {
      Dep.target = targetStack.pop()
    }复制代码


    Watcher代码的实现:

    //去重 防止重复收集
    let uid = 0
    class Watcher{
    	constructor(vm,expOrFn,cb,options){
    		//传进来的对象 例如Vue
    		this.vm = vm
    		if (options) {
    	      this.deep = !!options.deep
    	      this.user = !!options.user
    	      this.lazy = !!options.lazy
    	    }else{
    	    	this.deep = this.user = this.lazy = false
    	    }
    	    this.dirty = this.lazy
    		//在Vue中cb是更新视图的核心,调用diff并更新视图的过程
    		this.cb = cb
    		this.id = ++uid
    		this.deps = []
    	    this.newDeps = []
    	    this.depIds = new Set()
    	    this.newDepIds = new Set()
    		if (typeof expOrFn === 'function') {
    			//data依赖收集走此处
    	      	this.getter = expOrFn
    	    } else {
    	    	//watch依赖走此处
    	      	this.getter = this.parsePath(expOrFn)
    	    }
    		//设置Dep.target的值,依赖收集时的watcher对象
    		this.value = this.lazy ? undefined : this.get()
    	}
    
    	get(){
    		//设置Dep.target值,用以依赖收集
    	    pushTarget(this)
    	    const vm = this.vm
    	    //此处会进行依赖收集 会调用data数据的 get
    	    let value = this.getter.call(vm, vm)
    	    popTarget()
    	    return value
    	}
    
    	//添加依赖
      	addDep (dep) {
      		//去重
      		const id = dep.id
    	    if (!this.newDepIds.has(id)) {
    	      	this.newDepIds.add(id)
    	      	this.newDeps.push(dep)
    	      	if (!this.depIds.has(id)) {
    	      		//收集watcher 每次data数据 set
    	      		//时会遍历收集的watcher依赖进行相应视图更新或执行watch监听函数等操作
    	        	dep.addSub(this)
    	      	}
    	    }
      	}
    
      	//更新
      	update () {
      		if (this.lazy) {
          		this.dirty = true
        	}else{
        		this.run()
        	}
    	}
    
    	//更新视图
    	run(){
    		console.log(`这里会去执行Vue的diff相关方法,进而更新数据`)
    		const value = this.get()
    		const oldValue = this.value
            this.value = value
    		if (this.user) {
    			//watch 监听走此处
                this.cb.call(this.vm, value, oldValue)
            }else{
            	//data 监听走此处
            	//这里只做简单的console.log 处理,在Vue中会调用diff过程从而更新视图
    			this.cb.call(this.vm, value, oldValue)
            }
    	}
    
        //如果计算熟悉依赖的data值发生变化时会调用
        //案例中 当data.name值发生变化时会执行此方法
    	evaluate () {
    	    this.value = this.get()
    	    this.dirty = false
    	}
    	//收集依赖
    	depend () {
    	    let i = this.deps.length
    	    while (i--) {
    	      this.deps[i].depend()
    	    }
    	}
    
    	// 此方法获得每个watch中key在data中对应的value值
    	//使用split('.')是为了得到 像'a.b.c' 这样的监听值
    	parsePath (path){
    		const bailRE = /[^w.$]/
    	  if (bailRE.test(path)) return
    	  	const segments = path.split('.')
    	  	return function (obj) {
    		    for (let i = 0; i < segments.length; i++) {
    		      	if (!obj) return
    		      	//此处为了兼容我的代码做了一点修改	 
    		        //此处使用新获得的值覆盖传入的值 因此能够处理 'a.b.c'这样的监听方式
    		        if(i==0){
    		        	obj = obj.data[segments[i]]
    		        }else{
    		        	obj = obj[segments[i]]
    		        }
    		    }
    		    return obj
    		 }
    	}
    }复制代码

    在Watcher中对于computed来说核心注意点是以下方法:

    //如果计算熟悉依赖的data值发生变化时会调用
    //案例中 当data.name值发生变化时会执行此方法
    evaluate () {
        this.value = this.get()
        this.dirty = false
    }复制代码

    当computed中用到的data值发生变化时,视图更新调用computed值时会从新执行,获得新的计算属性值。


    Observer代码实现

    class Observer{
    	constructor (value) {
    	    this.value = value
    	    // 增加dep属性(处理数组时可以直接调用)
    	    this.dep = new Dep()
    	    //将Observer实例绑定到data的__ob__属性上面去,后期如果oberve时直接使用,不需要从新Observer,
    	    //处理数组是也可直接获取Observer对象
    	    def(value, '__ob__', this)
    	    if (Array.isArray(value)) {
    	    	//这里只测试对象
    	    } else {
    	    	//处理对象
    	      	this.walk(value)
    	    }
    	}
    
    	walk (obj) {
        	const keys = Object.keys(obj)
        	for (let i = 0; i < keys.length; i++) {
        		//此处我做了拦截处理,防止死循环,Vue中在oberve函数中进行的处理
        		if(keys[i]=='__ob__') return;
          		defineReactive(obj, keys[i], obj[keys[i]])
        	}
      	}
    }
    //数据重复Observer
    function observe(value){
    	if(typeof(value) != 'object' ) return;
    	let ob = new Observer(value)
      	return ob;
    }
    // 把对象属性改为getter/setter,并收集依赖
    function defineReactive (obj,key,val) {
      	const dep = new Dep()
      	//处理children
      	let childOb = observe(val)
      	Object.defineProperty(obj, key, {
        	enumerable: true,
        	configurable: true,
        	get: function reactiveGetter () {
        		console.log(`调用get获取值,值为${val}`)
          		const value = val
          		if (Dep.target) {
    	        	dep.depend()
    		        if (childOb) {
    		          	childOb.dep.depend()
    		        }
    	      	}
          		return value
    	    },
    	    set: function reactiveSetter (newVal) {
    	    	console.log(`调用了set,值为${newVal}`)
    	      	const value = val
    	       	val = newVal
    	       	//对新值进行observe
    	      	childOb = observe(newVal)
    	      	//通知dep调用,循环调用手机的Watcher依赖,进行视图的更新
    	      	dep.notify()
    	    }
      })
    }
    //辅助方法
    function def (obj, key, val) {
      Object.defineProperty(obj, key, {
        value: val,
        enumerable: true,
        writable: true,
        configurable: true
      })
    }复制代码


    此文的重点Computed代码实现:

    //空函数
    const noop = ()=>{}
    // computed初始化的Watcher传入lazy: true就会触发Watcher中的dirty值为true
    const computedWatcherOptions = { lazy: true }
    //Object.defineProperty 默认value参数
    const sharedPropertyDefinition = {
      enumerable: true,
      configurable: true,
      get: noop,
      set: noop
    }
    // 初始化computed
    class initComputed {
    	constructor(vm, computed){
    		//新建存储watcher对象,挂载在vm对象执行
    		const watchers = vm._computedWatchers = Object.create(null)
    		//遍历computed
    		for (const key in computed) {
    		    const userDef = computed[key]
    		    //getter值为computed中key的监听函数或对象的get值
    		    let getter = typeof userDef === 'function' ? userDef : userDef.get
    		    //新建computed的 watcher
    		    watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)
    		    if (!(key in vm)) {
    		      	/*定义计算属性*/
    		      	this.defineComputed(vm, key, userDef)
    		    }
    		}
    	}
        //把计算属性的key挂载到vm对象下,并使用Object.defineProperty进行处理
        //因此调用vm.somecomputed 就会触发get函数
    	defineComputed (target, key, userDef) {
    	  if (typeof userDef === 'function') {
    	    sharedPropertyDefinition.get = this.createComputedGetter(key)
    	    sharedPropertyDefinition.set = noop
    	  } else {
    	    sharedPropertyDefinition.get = userDef.get
    	      ? userDef.cache !== false
    	        ? this.createComputedGetter(key)
    	        : userDef.get
    	      : noop
    	      //如果有设置set方法则直接使用,否则赋值空函数
    	    	sharedPropertyDefinition.set = userDef.set
    	      	? userDef.set
    	      	: noop
    	  }
    	  Object.defineProperty(target, key, sharedPropertyDefinition)
    	}
    
    	//计算属性的getter 获取计算属性的值时会调用
    	createComputedGetter (key) {
    	  return function computedGetter () {
    	  	//获取到相应的watcher
    	    const watcher = this._computedWatchers && this._computedWatchers[key]
    	    if (watcher) {
    	    	//watcher.dirty 参数决定了计算属性值是否需要重新计算,默认值为true,即第一次时会调用一次
    	      	if (watcher.dirty) {
    	      		/*每次执行之后watcher.dirty会设置为false,只要依赖的data值改变时才会触发
    	      		watcher.dirty为true,从而获取值时从新计算*/
    	        	watcher.evaluate()
    	      	}
    	      	//获取依赖
    	      	if (Dep.target) {
    	        	watcher.depend()
    	      	}
    	      	//返回计算属性的值
    	      	return watcher.value
    	    }
    	  }
    	}
    }复制代码


    代码已经写完,完整代码如下:

    //标识当前的Dep id
    let uidep = 0
    class Dep{
    	constructor () {
    		this.id = uidep++
    		// 存放所有的监听watcher
        	this.subs = []
      	}
    
      	//添加一个观察者对象
      	addSub (Watcher) {
        	this.subs.push(Watcher)
      	}
      	//依赖收集
    	depend () {
    		//Dep.target 作用只有需要的才会收集依赖
    	    if (Dep.target) {
    	      Dep.target.addDep(this)
    	    }
    	}
    
    	// 调用依赖收集的Watcher更新
        notify () {
    	    const subs = this.subs.slice()
    	    for (let i = 0, l = subs.length; i < l; i++) {
    	      subs[i].update()
    	    }
      	}
    }
    
    Dep.target = null
    const targetStack = []
    
    // 为Dep.target 赋值
    function pushTarget (Watcher) {
    	if (Dep.target) targetStack.push(Dep.target)
      	Dep.target = Watcher
    }
    function popTarget () {
      Dep.target = targetStack.pop()
    }
    /*----------------------------------------Watcher------------------------------------*/
    //去重 防止重复收集
    let uid = 0
    class Watcher{
    	constructor(vm,expOrFn,cb,options){
    		//传进来的对象 例如Vue
    		this.vm = vm
    		if (options) {
    	      this.deep = !!options.deep
    	      this.user = !!options.user
    	      this.lazy = !!options.lazy
    	    }else{
    	    	this.deep = this.user = this.lazy = false
    	    }
    	    this.dirty = this.lazy
    		//在Vue中cb是更新视图的核心,调用diff并更新视图的过程
    		this.cb = cb
    		this.id = ++uid
    		this.deps = []
    	    this.newDeps = []
    	    this.depIds = new Set()
    	    this.newDepIds = new Set()
    		if (typeof expOrFn === 'function') {
    			//data依赖收集走此处
    	      	this.getter = expOrFn
    	    } else {
    	    	//watch依赖走此处
    	      	this.getter = this.parsePath(expOrFn)
    	    }
    		//设置Dep.target的值,依赖收集时的watcher对象
    		this.value = this.lazy ? undefined : this.get()
    	}
    
    	get(){
    		//设置Dep.target值,用以依赖收集
    	    pushTarget(this)
    	    const vm = this.vm
    	    //此处会进行依赖收集 会调用data数据的 get
    	    let value = this.getter.call(vm, vm)
    	    popTarget()
    	    return value
    	}
    
    	//添加依赖
      	addDep (dep) {
      		//去重
      		const id = dep.id
    	    if (!this.newDepIds.has(id)) {
    	      	this.newDepIds.add(id)
    	      	this.newDeps.push(dep)
    	      	if (!this.depIds.has(id)) {
    	      		//收集watcher 每次data数据 set
    	      		//时会遍历收集的watcher依赖进行相应视图更新或执行watch监听函数等操作
    	        	dep.addSub(this)
    	      	}
    	    }
      	}
    
      	//更新
      	update () {
      		if (this.lazy) {
          		this.dirty = true
        	}else{
        		this.run()
        	}
    	}
    
    	//更新视图
    	run(){
    		console.log(`这里会去执行Vue的diff相关方法,进而更新数据`)
    		const value = this.get()
    		const oldValue = this.value
            this.value = value
    		if (this.user) {
    			//watch 监听走此处
                this.cb.call(this.vm, value, oldValue)
            }else{
            	//data 监听走此处
            	//这里只做简单的console.log 处理,在Vue中会调用diff过程从而更新视图
    			this.cb.call(this.vm, value, oldValue)
            }
    	}
    
        //如果计算熟悉依赖的data值发生变化时会调用
        //案例中 当data.name值发生变化时会执行此方法
    	evaluate () {
    	    this.value = this.get()
    	    this.dirty = false
    	}
    	//收集依赖
    	depend () {
    	    let i = this.deps.length
    	    while (i--) {
    	      this.deps[i].depend()
    	    }
    	}
    
    	// 此方法获得每个watch中key在data中对应的value值
    	//使用split('.')是为了得到 像'a.b.c' 这样的监听值
    	parsePath (path){
    		const bailRE = /[^w.$]/
    	  if (bailRE.test(path)) return
    	  	const segments = path.split('.')
    	  	return function (obj) {
    		    for (let i = 0; i < segments.length; i++) {
    		      	if (!obj) return
    		      	//此处为了兼容我的代码做了一点修改	 
    		        //此处使用新获得的值覆盖传入的值 因此能够处理 'a.b.c'这样的监听方式
    		        if(i==0){
    		        	obj = obj.data[segments[i]]
    		        }else{
    		        	obj = obj[segments[i]]
    		        }
    		    }
    		    return obj
    		 }
    	}
    }
    
    /*----------------------------------------Observer------------------------------------*/
    class Observer{
    	constructor (value) {
    	    this.value = value
    	    // 增加dep属性(处理数组时可以直接调用)
    	    this.dep = new Dep()
    	    //将Observer实例绑定到data的__ob__属性上面去,后期如果oberve时直接使用,不需要从新Observer,
    	    //处理数组是也可直接获取Observer对象
    	    def(value, '__ob__', this)
    	    if (Array.isArray(value)) {
    	    	//这里只测试对象
    	    } else {
    	    	//处理对象
    	      	this.walk(value)
    	    }
    	}
    
    	walk (obj) {
        	const keys = Object.keys(obj)
        	for (let i = 0; i < keys.length; i++) {
        		//此处我做了拦截处理,防止死循环,Vue中在oberve函数中进行的处理
        		if(keys[i]=='__ob__') return;
          		defineReactive(obj, keys[i], obj[keys[i]])
        	}
      	}
    }
    //数据重复Observer
    function observe(value){
    	if(typeof(value) != 'object' ) return;
    	let ob = new Observer(value)
      	return ob;
    }
    // 把对象属性改为getter/setter,并收集依赖
    function defineReactive (obj,key,val) {
      	const dep = new Dep()
      	//处理children
      	let childOb = observe(val)
      	Object.defineProperty(obj, key, {
        	enumerable: true,
        	configurable: true,
        	get: function reactiveGetter () {
        		console.log(`调用get获取值,值为${val}`)
          		const value = val
          		if (Dep.target) {
    	        	dep.depend()
    		        if (childOb) {
    		          	childOb.dep.depend()
    		        }
    	      	}
          		return value
    	    },
    	    set: function reactiveSetter (newVal) {
    	    	console.log(`调用了set,值为${newVal}`)
    	      	const value = val
    	       	val = newVal
    	       	//对新值进行observe
    	      	childOb = observe(newVal)
    	      	//通知dep调用,循环调用手机的Watcher依赖,进行视图的更新
    	      	dep.notify()
    	    }
      })
    }
    //辅助方法
    function def (obj, key, val) {
      Object.defineProperty(obj, key, {
        value: val,
        enumerable: true,
        writable: true,
        configurable: true
      })
    }
    /*----------------------------------------初始化watch------------------------------------*/
    //空函数
    const noop = ()=>{}
    // computed初始化的Watcher传入lazy: true就会触发Watcher中的dirty值为true
    const computedWatcherOptions = { lazy: true }
    //Object.defineProperty 默认value参数
    const sharedPropertyDefinition = {
      enumerable: true,
      configurable: true,
      get: noop,
      set: noop
    }
    // 初始化computed
    class initComputed {
    	constructor(vm, computed){
    		//新建存储watcher对象,挂载在vm对象执行
    		const watchers = vm._computedWatchers = Object.create(null)
    		//遍历computed
    		for (const key in computed) {
    		    const userDef = computed[key]
    		    //getter值为computed中key的监听函数或对象的get值
    		    let getter = typeof userDef === 'function' ? userDef : userDef.get
    		    //新建computed的 watcher
    		    watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)
    		    if (!(key in vm)) {
    		      	/*定义计算属性*/
    		      	this.defineComputed(vm, key, userDef)
    		    }
    		}
    	}
        //把计算属性的key挂载到vm对象下,并使用Object.defineProperty进行处理
        //因此调用vm.somecomputed 就会触发get函数
    	defineComputed (target, key, userDef) {
    	  if (typeof userDef === 'function') {
    	    sharedPropertyDefinition.get = this.createComputedGetter(key)
    	    sharedPropertyDefinition.set = noop
    	  } else {
    	    sharedPropertyDefinition.get = userDef.get
    	      ? userDef.cache !== false
    	        ? this.createComputedGetter(key)
    	        : userDef.get
    	      : noop
    	      //如果有设置set方法则直接使用,否则赋值空函数
    	    	sharedPropertyDefinition.set = userDef.set
    	      	? userDef.set
    	      	: noop
    	  }
    	  Object.defineProperty(target, key, sharedPropertyDefinition)
    	}
    
    	//计算属性的getter 获取计算属性的值时会调用
    	createComputedGetter (key) {
    	  return function computedGetter () {
    	  	//获取到相应的watcher
    	    const watcher = this._computedWatchers && this._computedWatchers[key]
    	    if (watcher) {
    	    	//watcher.dirty 参数决定了计算属性值是否需要重新计算,默认值为true,即第一次时会调用一次
    	      	if (watcher.dirty) {
    	      		/*每次执行之后watcher.dirty会设置为false,只要依赖的data值改变时才会触发
    	      		watcher.dirty为true,从而获取值时从新计算*/
    	        	watcher.evaluate()
    	      	}
    	      	//获取依赖
    	      	if (Dep.target) {
    	        	watcher.depend()
    	      	}
    	      	//返回计算属性的值
    	      	return watcher.value
    	    }
    	  }
    	}
    }复制代码


    computed测试:

    //1、首先来创建一个Vue构造函数:
    function Vue(){
    }
    //2、设置data和computed的值:
    let data={
        name:'Hello',
    }
    let computed={
    	getfullname:function(){
    		console.log('-----走了computed 之 getfullname------')
    		console.log('新的值为:'+data.name + ' - world')
    		return data.name + ' - world'
    	}
    }
    //3、实例化Vue并把data挂载到Vue上
    let vue 		= new Vue()
    vue.data 		= data
    //4、创建Watcher对象
    let updateComponent = (vm)=>{
    	// 收集依赖
    	data.name
    	
    }
    let watcher1 = new Watcher(vue,updateComponent,()=>{})
    //5、初始化Data并收集依赖
    observe(data)
    //6、初始化computed
    let watcher2 = new initComputed(vue,computed)复制代码


    在浏览器console中测试:

    //首先获得一次getfullname
    vue.getfullname
    
    //第二次调用getfullname看看会有什么变化呢
    vue.getfullname复制代码

    分析:调用vue.getfullname第一次会打印 '-----走了computed 之 getfullname------',即计算属性第一次计算了值,第二次调用时,不会再打印值

    即直接获取的缓存值,为什么第二次是获得的缓存值呢,因为第二次执行时watcher.dirty=true,就会直接返回watcher.value值。


    //为data.name赋值
    data.name = 'Hi'复制代码

    分析:执行data.name时会触发两个Watcher监听函数(为什么是两个Watcher自己去脑补一下额!),一个是全局的watcher,一个是computed的watcher,第一个Watcher会更新视图,第二个Watcher会触发watcher.dirty=true。


    //name值变更之后再次执行会是什么结果呢
    vue.getfullname
    
    //再执行一次
    vue.getfullname复制代码

    分析:运行vue.getfullname时会执行computedGetter函数,因为watcher.dirty=true因此会从新计算值,因此会打印 '-----走了computed 之 getfullname------',值为'HI world', 再次执行只会获得计算属性的缓存值。

    所有测试代码如下:

    /*----------------------------------------Vue------------------------------------*/
    function Vue(){
    }
    /*----------------------------------------测试代码------------------------------------*/
    // 调用
    let data={
        name:'Hello',
    }
    let computed={
    	getfullname:function(){
    		console.log('-----走了computed 之 getfullname------')
    		console.log('新的值为:'+data.name + ' - world')
    		return data.name + ' - world'
    	}
    }
    let vue 		= new Vue()
    vue.data 		= data
    let updateComponent = (vm)=>{
    	// 收集依赖
    	data.name
    }
    let watcher1 = new Watcher(vue,updateComponent,()=>{})
    observe(data)
    let watvher2 = new initComputed(vue,computed)
    
    //测试 浏览器console中相继运行一下代码测试
    vue.getfullname
    vue.getfullname
    data.name='Hi'
    vue.getfullname
    vue.getfullname复制代码


    若有疑问欢迎交流。


    展开全文
  • Vue中computed原理分析

    2020-09-10 14:40:24
    计算属性是有缓存,比如某个计算属性C,它依赖data中的A,如果没有缓存话,每次读取C时,C都回去读取A,从而触发Aget。多次触发Aget有时候是一个非常消耗性能操作。所以Computed必须要有缓存。 computed...
  • ,我们通过实现一个简单版Vue中computed具有相同功能函数来了解computed是如何工作。对Vue.js中computed工作原理感兴趣朋友跟随脚本之家小编一起学习吧
  • 我们通过实现一个简单版Vue中computed具有相同功能函数来了解computed是如何工作。 JS属性: JavaScript有一个特性是 Object.defineProperty ,它能做很多事,但我在这篇文章只专注于这个方法中一个: ...
  • vue中computed的实现原理---------需要建立数据依赖搜集,动态计算实现原理 1)问题:计算属性如何与属性建立依赖关系?属性发生变化又如何通知到计算属性重新计算? 如何建立依赖关系?----------利用 ...
  • computedVue 是很常用属性配置,它能够随着依赖属性变化而变化,为我们带来很大便利。那么本文就来带大家全面理解 computed 内部原理以及工作流程。 在这之前,希望你能够对响应式原理有一些理解,因为...
  • 1.Vue中computed 和 watch区别。 computed 计算属性 : 依赖其它属性值,并且 computed 值有缓存,只有它依赖属性值发生改变,下一次获取 computed 值时才会重新计算 computed 值。 watch 侦听器 : 更多是...
  • 基于上一篇(Vue中实现数组) 根据计算属性几个特点设计思路: 1.他值是一个函数运行结果; 2.函数里用到所有属性都会引起计算属性变化; 计算属性仍然属于Vue响应式实现一种,本质上还是一个watcher,但是又似乎...
  • 两个月前我曾在掘金翻译了一篇关于Vue中简单介绍computed是如何工作文章,翻译很一般所以我就不贴地址了。有位我非常敬佩前辈对文章做了评价,内容就是本文标题“感觉原文并没有讲清楚 computed 实现本质-...
  • Computed 计算属性是 Vue 常用一个功能,本篇文章主要介绍了Vue Computed 计算属性原理,小编觉得挺不错,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • 对于Vue的响应式,想必大家都有所了解,在Vue响应式数据,computed是比较特殊的响应式数据,它们可以监听使用到的数据,数据 改变computed的数据也会重新计算。 今天主要是讨论 computed 实现原理 。 computed在...
  • 我们通过实现一个简单版Vue中computed具有相同功能函数来了解computed是如何工作。写十分全面细致,具有一定参考价值,对此有需要朋友可以参考学习下。如有不足之处,欢迎批评指正。 JS属性: ...
  • 我们通过实现一个简单版Vue中computed具有相同功能函数来了解computed是如何工作。写十分全面细致,具有一定参考价值,对此有需要朋友可以参考学习下。如有不足之处,欢迎批评指正。 JS属性: ...
  • vue computed 原理

    2018-04-11 11:18:00
    vue computed 主要依靠数据依赖来更新,这里不展示computed源代码,只展示核心思想。...vue中如果b变化,a也会变化。这儿为了简单,不在展示computed.aset跟get 1、data中数据需要使用es5中O...
  • 浅谈Vue中computed

    2020-05-27 11:03:06
    1、computed特性: 计算属性在使用时候,要当做普通属性使用就好,不需要加() 只要计算属性这个function内部所用到data中的数据发生了变化,... 学习最常见听到一句话就是,computed就是...

空空如也

空空如也

1 2 3 4 5 ... 12
收藏数 236
精华内容 94
关键字:

vue中computed的原理

vue 订阅