vue响应式分析(三)计算属性与侦听属性

计算属性与侦听属性

计算属性

computed,通过监听某些响应式数据,从而实现动态返回一个变量,和react hooks中的effect有点类似,可以实现相同效果。

// vue中写法
export default{
	computed:{
        name(){
            return firstName + lastName
        }
    }
}

// react hooks写法
const App = () => {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    setFullName(firstName + lastName);
  }, [firstName, lastName]);

  const setname = (e, type) => {
    const value = e.target.value;
    if (type === 1) {
      setFirstName(value);
    } else {
      setLastName(value);
    }
  };
  return (
    <div>
      <input onChange={(e) => setname(e, 1)}></input>
      <input onChange={(e) => setname(e)}></input>
      <div>{fullName}</div>
    </div>
  );
};

简单对比之后,下面开始详细的分析,vue中的计算属性。

计算属性的调用方式

这部分逻辑在vue官网API上有,主要调用方式就是下面两种,可以在computed中定义一个函数,也可以使用一个设置了get和set的对象。

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
初始化

计算属性的初始化在本系列文章第一篇initState方法中就有提到,这部分逻辑定义在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 */)
  }
  // 注意,当传入的options中有计算属性时,则进行计算属性初始化
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

接下来,我们跳到initComputed中,查看相关的逻辑。

initComputed方法实现

相关代码

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  // 判断是不是ssr环境
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    // 尝试去拿到计算属性的每一个key值对应的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) {
      // create internal watcher for the computed property.
      // 给每一个计算属性创建watcher
      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.
    // key 值在组件中没有使用
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
      // key值在组件中已经被使用,如data、props
    } 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)
      }
    }
  }
}

可以看到,上面流程中,首先去获取计算属性上的getter方法,之后实例化计算属性的watcher,最后判断vm上没有对应key值的computed时,调用defineComputed方法,下面来看一下defineComputed的实现。

defineComputed方法实现

调用逻辑:

// vm:组件实例、key:计算属性中的一个key值,userDef:计算属性对应的value,可能是一个对象,也可能是一个函数
defineComputed(vm, key, userDef)

相关代码:

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

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  // ssr情况下不进行缓存
  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
  }
  // 开发环境下,在计算属性定义的是函数的时候,如果调用set方法,就会抛出这个错误
  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
      )
    }
  }
  // 转换为响应式的,挂载到vm实例上,组件内部就可以访问了
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

这一块逻辑还是比较清晰的,在设置getter时,有调用createComputedGetter这个函数,现在来查看一下它的定义:

createComputedGetter函数定义

调用方式:

// 传入参数实际上就是计算属性的key值
sharedPropertyDefinition.get = userDef.get
    ? shouldCache && userDef.cache !== false
    ? createComputedGetter(key)
: userDef.get
: noop

函数定义:

function createComputedGetter (key) {
  return function computedGetter () {
   	// 这个watcher就是在initComputed方法中实例化的watcher
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 调用watcher的depend方法,实际上也就是调用dep的depend方法,将watcher添加到deps中
      watcher.depend()
      return watcher.evaluate()
    }
  }
}
现在回到计算属性初始化watcher这一步

调用方式:

const computedWatcherOptions = { computed: true }
// 传入参数:组件实例,getter,回调传入空函数,以及上面这个变量
if (!isSSR) {
    // create internal watcher for the computed property.
    // 给每一个计算属性创建watcher
    watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
    )
}

watcher初始化构造函数接收参数:

// 其余部分省略
constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    
}

由于传入了options配置,所以就会重新赋值,最终实例化的watcher:

if (options) {
    this.deep = !!options.deep;
    this.user = !!options.user;
    this.computed = !!options.computed;
    this.sync = !!options.sync;
    this.before = options.before;
}
vm: Component;
cb: loop;
id: number;
deep: false;
user: false;
computed: true;
sync: false;
before: undefined;
active: true;

之后走到和渲染watcher相同的那部分逻辑:

if (this.computed) {
    this.value = undefined;
    this.dep = new Dep();
} else {
    this.value = this.get();
}

区别在于,渲染watcher立即求值,而computed watcher没有立即求值,同时实例化一个Dep实例。

执行流程

当页面中有引用计算属性时,就会走到先前watcher.depend()这一步,对计算属性进行求值:

// 执行
watcher.depend()
// 触发
/**
   * Depend on this watcher. Only for computed property watchers.
   */
depend() {
    if (this.dep && Dep.target) {
        this.dep.depend();
    }
}

因为这时在页面渲染过程中,Dep.target中存放的是渲染watcher,执行dep.depend函数:

depend () {
    // 渲染watcher存在情况下
    if (Dep.target) {
        // 渲染watcher将当前计算属性watcher添加到dep中,为什么是计算属性watcher,注意先前的调用方式,watcher.depend
        Dep.target.addDep(this)
    }
}

addDep方法执行:

/**
   * Add a dependency to this directive.
   */
addDep(dep: Dep) {
    const id = dep.id;
    if (!this.newDepIds.has(id)) {
        this.newDepIds.add(id);
        this.newDeps.push(dep);
        if (!this.depIds.has(id)) {
            dep.addSub(this);
        }
    }
}

上面流程执行完毕后,渲染watcher会将computed watcher添加到自身依赖当中,计算属性改变,即会触发渲染watcher的更新逻辑,从而更新到组件。

watcher.evaluate()方法定义

在computed watcher被渲染watcher添加到依赖当中后,有进行一个求值返回的操作,方法就是这个函数:

相关代码:

/**
   * Evaluate and return the value of the watcher.
   * This only gets called for computed property watchers.
   */
evaluate() {
    if (this.dirty) {
        this.value = this.get();
        this.dirty = false;
    }
    return this.value;
}

只有当this.dirty为true的情况下,才会进行重新求值,并修改标志位,否则直接返回this.value。

这个dirty标志位作用就是计算属性的缓存效果实现:

计算属性缓存

计算属性初始化阶段,标记位为true

// 此时为true 
this.dirty = this.computed; // for computed watchers

所以在获取计算属性的val时,可以走到读值的逻辑,之后再次修改标志位:

evaluate() {
    // 条件成立,进行读取操作
    if (this.dirty) {
        this.value = this.get();
        // 修改标志位,下次就直接返回,不进行读取操作
        this.dirty = false;
    }
    return this.value;
}

这个标志位在更新等过程中也会被刷新,下面会提到这一点:

计算属性更新

计算属性中引用响应式对象,那就会调用其getter触发依赖收集逻辑,computed watcher就会被收集到响应式对象的dep中,响应式对象发生变化,也就会通知到computed watcher。

// 逻辑实现
// 响应式对象
get(){
	// 触发依赖收集逻辑
    dep.depend()
}
Dep{
    depend(){
        // 此时,Dep.target 就是 computed watcher,this就是dep实例,属于响应式对象
        if (Dep.target) {
          Dep.target.addDep(this)
        }
    }
}
// computed watcher
// 收集对应的deo,保存到deps中,并进行记录,避免重复引用,同时在dep中添加
 addDep(dep: Dep) {
    const id = dep.id;
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id);
      this.newDeps.push(dep);
      if (!this.depIds.has(id)) {
        // 响应式对象中添加computed watcher
        dep.addSub(this);
      }
    }
  }

当计算属性依赖的数据发生变化,dep就会执行notify方法,通知到所有引用到当前数据的watcher,循环遍历subs数组,调用watcher的update方法,computed watcher也就这样被通知到:

相关代码:

// 响应式对象 setter触发
set: function reactiveSetter (newVal) {
    const value = getter ? getter.call(obj) : val
    /* eslint-disable no-self-compare */
    if (newVal === value || (newVal !== newVal && value !== value)) {
        return
    }
    /* eslint-enable no-self-compare */
    if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
    }
    if (setter) {
        setter.call(obj, newVal)
    } else {
        val = newVal
    }
    childOb = !shallow && observe(newVal)
    // 通知当前的依赖项更新
    dep.notify()
}
// dep的notify更新
 notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
 }

在notify中会调用每一个依赖项的update方法,computed watcher也就是这样被通知到,需要进行更新的,下面开始计算属性的更新逻辑:

computed watcher update流程

在响应式数据更新之后,进行依赖派发操作,就会调用相应watcher实例的update方法:

update() {
    /* istanbul ignore else */
    if (this.computed) {
        // A computed property watcher has two modes: lazy and activated.
        // It initializes as lazy by default, and only becomes activated when
        // it is depended on by at least one subscriber, which is typically
        // another computed property or a component's render function.
        // 这里的this.dep是只有计算属性才有的
        // 计算属性的watcher有两种模式,lazy 和 activated 这两种,默认为lazy
        // 当不存在依赖时,也就是没有被组件内部、其他函数引用时,修改标志位,只有在下一次访问这个值时,才会进行重新求值
        if (this.dep.subs.length === 0) {
            // In lazy mode, we don't want to perform computations until necessary,
            // so we simply mark the watcher as dirty. The actual computation is
            // performed just-in-time in this.evaluate() when the computed property
            // is accessed.
            this.dirty = true;
        } else {
            // In activated mode, we want to proactively perform the computation
            // but only notify our subscribers when the value has indeed changed.
            // 有引用的情况下,那它就是激活态,就会进行重新求值
            this.getAndInvoke(() => {
                this.dep.notify();
            });
        }
    } else if (this.sync) {
        this.run();
    } else {
        queueWatcher(this);
    }
}
总结

计算属性本身就是一个computed watcher,说它有缓存效果,实际上就是因为它有两种模式:lazy和activated这两种,默认为layz。

当存在引用时,他就是activated,计算属性依赖的值更新就会触发它的重新计算,不存在引用时,只是修改标志位,只有当下一次有引用时,才进行计算。

缓存的体现
  • 值的缓存,计算结果只计算一次,依赖项未改变,则不进行计算,通过标志位实现
  • 计算的缓存,惰性计算,不存在引用则不进行计算
侦听属性
侦听属性调用方式
var vm = new Vue({
  data: {
    a: 1,
    b: 2,
    c: 3,
    d: 4,
    e: {
      f: {
        g: 5
      }
    }
  },
  watch: {
    a: function (val, oldVal) {
      console.log('new: %s, old: %s', val, oldVal)
    },
    // 方法名
    b: 'someMethod',
    // 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
    c: {
      handler: function (val, oldVal) { /* ... */ },
      deep: true
    },
    // 该回调将会在侦听开始之后被立即调用
    d: {
      handler: 'someMethod',
      immediate: true
    },
    // 你可以传入回调数组,它们会被逐一调用
    e: [
      'handle1',
      function handle2 (val, oldVal) { /* ... */ },
      {
        handler: function handle3 (val, oldVal) { /* ... */ },
        /* ... */
      }
    ],
    // watch vm.e.f's value: {g: 5}
    'e.f': function (val, oldVal) { /* ... */ }
  }
})
vm.a = 2 // => new: 2, old: 1
初始化

侦听属性watch的初始化,和computed在同一个函数中定义:

export function initState (vm: Component) {
  // ...省略
  // 侦听属性初始化
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

可以看到,最后是调用initWatch方法,直接跳转initWatch方法实现:

initWatch方法定义

相关代码:

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    // 回调数组的处理逻辑,之前调用方式演示有提到
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

这段代码执行完成后,调用的是createWatcher这个方法,跳转到它的实现:

createWatcher方法定义

调用方式:

// vue组件实例,watcher key值,watcher对应的value
createWatcher(vm, key, handler)

相关代码:

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // 判断是不是对象类型,如果是,那说明传入的是一个{handler:func,deep?:boolean}的对象,实际的方法是对象的handler属性
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  // 如果是个str,那说明传入的是方法名,从组件实例上去取对应方法就行
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  // 返回一个方法
  return vm.$watch(expOrFn, handler, options)
}
vm.$watch方法实现

调用方式:

// watch key值、回调函数、配置参数
vm.$watch(expOrFn, handler, options)

这个方法是在执行startMixin时定义的,跳转到实现:

Vue.prototype.$watch = function (
expOrFn: string | Function,
 cb: any,
 options?: Object
): Function {
    const vm: Component = this
    // 如果传入的回调是个对象类型,返回一个watcher,跟之前区别就是将cb解构出来了
    if (isPlainObject(cb)) {
        return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    // 定义的是用户watch
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    // 如果设置了immediate 属性,则会直接调用回调函数,然后返回一个unwatchFn的方法进行解绑操作。
    if (options.immediate) {
        cb.call(vm, watcher.value)
    }
    return function unwatchFn () {
        watcher.teardown()
    }
}
不同watcher的实现

在watcher的构造函数中,分别针对不同的watcher做了不同的处理,传入不同的参数,就可以生成不同的watcher,之前的这些步骤也就是为了满足Watcher这个类,对参数的要求:

if (options) {
    // 用户定义时传入的配置参数
    this.deep = !!options.deep;
    // user watch
    this.user = !!options.user;
    this.computed = !!options.computed;
    // 另外一个
    this.sync = !!options.sync;
    this.before = options.before;
} else {
    // 如果没有传入,则赋默认值false
    this.deep = this.user = this.computed = this.sync = false;
}
传入deep的情况

在传入depp为true时,get方法逻辑会有些不同:

 get() {
    pushTarget(this);
    let value;
    const vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`);
      } else {
        throw e;
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        // 会执行这一步
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
    }
    return value;
  }

traverse方法实现

实际上就是对一个对象进行深度遍历,将每一个属性都变成getter和setter的形式,之后如果再次访问就可以进行相应的依赖收集逻辑。

export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}
userWatch

在用户自定义watch时,会默认传入一个user的选项,如果是用户自定义的watch,会默认添加一些错误处理,这部分逻辑处理如下:

 try {
      value = this.getter.call(vm, vm);
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`);
      } else {
        throw e;
      }
    } 

  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);
      }
sync watch

即传入一个sync true,表示是一个同步的,源码实现如下

 update() {
    /* istanbul ignore else */
    if (this.computed) {
      // A computed property watcher has two modes: lazy and activated.
      // It initializes as lazy by default, and only becomes activated when
      // it is depended on by at least one subscriber, which is typically
      // another computed property or a component's render function.
      if (this.dep.subs.length === 0) {
        // In lazy mode, we don't want to perform computations until necessary,
        // so we simply mark the watcher as dirty. The actual computation is
        // performed just-in-time in this.evaluate() when the computed property
        // is accessed.
        this.dirty = true;
      } else {
        // In activated mode, we want to proactively perform the computation
        // but only notify our subscribers when the value has indeed changed.
        this.getAndInvoke(() => {
          this.dep.notify();
        });
      }
      // 更新过程中,如果是同步的,则不添加进任务队列,直接执行run方法
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
  }
computed watch

这部分在计算属性这里已经进行了详细说明,不再讨论。

总结

watcher实际上就是再构造函数内部进行了一系列判断,通过传入的参数不同,从而生成不同用途的watch,watch的种类有:

  • 渲染watch
  • 计算属性computed watch
  • 深层 deep watch
  • 同步 sync watch
  • 用户 user watch

通过这些逻辑的判断,实现了watcher类的高度复用,$watch这个方法前面的逻辑也就是一系列判断,参数合并,最终传递正确的参数,生成相对应的watcher实例。

计算属性和侦听属性的区别
  • 两个都是共用一个watcher所实例化出的对象,传入参数不同所以有着不同的输出结果。
  • 计算属性有一个lazy惰性求值,同时通过一个标志位实现了缓存效果,而watch没有缓存。
  • 计算属性有一个实例化的dep用来收集依赖,从而实现惰性求值,没有依赖项,即不进行求值计算。
  • 计算属性更多的是凭借几个响应式数据生成新的数据,供其他函数、模板调用。
  • watch则是监听数据变化,从而进行业务逻辑处理,需要注意这些区别。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值