• home > webfront > ECMAS > vue >

    vue中methods/watch/computed对比分析,watch及computed原理挖掘

    Author:zhoulujun Date:

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

    理解Vue中Watch的实现原理和方式之前,你需要深入的理解MVVM的实现原理

    vue computed用法

    直接跟函数

    computed: {  
        fullName: function () {  
            return this.firstName + ' ' + this.lastName  
        }  
    }

    需要注意的是,就算在data中没有直接声明出要计算的变量,也可以直接在computed中写入。

    也可跟对象

    计算属性默认只有getter,可以在需要的时候自己设定setter:

      fullName: {
        // getter
        get: function () {
          return this.firstName + ' ' + this.lastName
        },
        // setter
        set: function (newValue) {
          var names = newValue.split(' ')
          this.firstName = names[0]
          this.lastName = names[names.length - 1]
        }
      }
    }

    适用场景:

    computed适用场景


    computed原理分析

    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函数:

    // 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、获取计算属性的定义userDef和getter求值函数

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

    定义一个计算属性有两种写法,一种是直接跟一个函数,另一种是添加set和get方法的对象形式,所以这里首先获取计算属性的定义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()
        }
      }
    }

    为了简洁突出重点,这里我手动去掉了我们暂时不需要关心的代码片段。

    观察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更新。

    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前面的代码可以看到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。

    computed实现整个流程:

    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数组,并逐个调用watcher的 update方法,完成响应更新。

    vue watch 用法

    watch和computed很相似,watch用于观察和监听页面上的vue实例,当然在大部分情况下我们都会使用computed,但如果要在数据变化的同时进行异步操作或者是比较大的开销,那么watch为最佳选择。watch为一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。

    之前注解过《vue watch对象键值说明,immediate属性详解》,computed也是我们常用的监听函数

    如果在data中没有相应的属性的话,是不能watch的,这点和computed不一样。

    适用场景:

    watch适用场景

    watch实现原理

    watch的简单工作流程图

    具体阅读《这一次 彻底理解Vue的watch实现原理及其实现方式

    methods

    在某些时候methods和computed看不出来具体的差别,但是一旦在运算量比较复杂的页面中,就会体现出不一样。

    computed是具有缓存的,这就意味着只要计算属性的依赖没有进行相应的数据更新,那么computed会直接从缓存中获取值,多次访问都会返回之前的计算结果

    methond每次调用都重新计算。

    computed和watch比较

    一个是计算,一个是观察,在语义上是有区别的。

    计算是通过变量计算来得出数据。而观察是观察一个特定的值,根据被观察者的变动进行相应的变化,在特定的场景下不能相互混用,所以还是需要注意api运用的合理性和语义性。

    computed其实只是纯数据操作,需要返回数据结果

    watch就可以监测某个数据发生了变更进行一系列的回调操作

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

    • computed是计算一个新的属性,并将该属性挂载到vm(Vue实例)上,而watch是监听已经存在且已挂载到vm上的数据,所以用watch同样可以监听computed计算属性的变化(其它还有data、props)

    • computed本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值,而watch则是当数据发生变化便会调用执行函数

    • 从使用场景上说,computed适用一个数据被多个数据影响,而watch适用一个数据影响多个数据;

    参考文章:

    Vue中watch、computed和methods 之间的对比 https://my.oschina.net/rlqmy/blog/1930090

    浅谈Vue中计算属性computed的实现原理 https://segmentfault.com/a/1190000016368913?utm_source=tag-newest



    转载本站文章《vue中methods/watch/computed对比分析,watch及computed原理挖掘》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/vue/8447.html