数组怎么取值_Vue漫谈 -- 怎么个依赖法(二)

本文深入探讨Vue中计算属性的依赖收集机制,解析如何在未被引用时避免订阅Dep,以及在组件中如何处理间接依赖。通过分析源码,揭示Vue在计算属性和模板依赖之间的巧妙关联,帮助理解Vue数据响应系统的内部工作原理。
摘要由CSDN通过智能技术生成

900187d0753bd7dde78e13ec21c2a30c.png

上一篇我们聊了提了3个问题,解答一个watch是如何收集依赖的,还剩下2个:

computed如果定义了但是没被引用?为什么就不会订阅Dep?

如果template只依赖了计算属性A,A又依赖了state里的a属性,很明显组件没有直接对于a的依赖,但是为什么修改了a组件也能更新呢?

和watch一样,计算属性的初始化也在 src/core/instance/state.js 中可以找到:

const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null)

  for (const key in computed) {
    const userDef = computed[key]
    let getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production') {
      if (getter === undefined) {
        warn(
          `No getter function has been defined for computed property "${key}".`,
          vm
        )
        getter = noop
      }
    }
    // create internal watcher for the computed property.
    watchers[key] = new Watcher(vm, getter, 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)
      }
    }
  }
}

可以看到每个key对应的计算属性都新建了一个Watcher实例,并且最顶部有一个watcher option参数,lazy = true表示了这是个延迟计算的watcher。而在Watcher构造函数里(具体代码看上篇),lazy watcher有以下2句特殊的逻辑:

this.dirty = this.lazy // for lazy watchers
this.value = this.lazy ? undefined : this.get()

第一句直接对lazy watcher下了定义:你要是lazy,那你就是脏的,别人要用你,必须重新取值,也就是要调用watcher.evalute()方法,这样才能保证你是最新的:

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

第二句也就解释了为什么计算属性如果没被引用,就不会收集依赖。因为对于lazy watcher,第一次根本不会调用this.get()方法

还剩下最后一个问题,试想一下这样的组件:

Vue.component('rectangle', {
  data: function () {
    return {
      width: 0,
      height: 0
    };
  },
  computed: {
    area() {
      return this.width * this.height;
    }
  },
  template: `
    <span>area: {{ area }}</span>
  `
});

width和height没有直接被template引用,那为什么修改他们组件也会更新?之前我们说过,组件的watcher是普通watcher,他会绑定组件的render方法,在第一次调用render的时候收集所以引用的依赖,那这种间接的依赖是怎么收集的呢?

我在没看计算属性代码前,第一直觉就是为每个计算属性也new一个Dep实例,然后拦截对应的计算属性函数,通知计算属性Dep对应的watcher。这样想想好像说得通,但是Vue不是这样实现的。

先说结论,对于上面这种情况,Vue会直接收集到computed的2个dep,然后订阅他们,下面我们就来看看Vue的奇技淫巧。

首先,我们得回头看一下Dep的pushTarget和popTarget方法:

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null
const targetStack = []

export function pushTarget (_target: Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}

这里有一个targetStack数组,或者说是栈,这个栈的作用就是当watcher.get执行过程中,如果遇到了别的watcher,就先把当前的watcher入栈,先执行别的watcher。上面说的间接依赖收集就借助了这个方法。

上面讲initComputed的时候,最后还有一个方法没讲,就是defineComputed,它的主要作用就是把computed属性代理到组件实例上,并且,拦截了计算属性的getter,setter:

export function defineComputed (target: any, key: string, userDef: Object | Function) {
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = createComputedGetter(key)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.set
      ? userDef.set
      : noop
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

我们就主要关注第一个if,它的get被设置为了一个createComputedGetter的返回值,set则被设置成了noop,也就是空方法(所以如果直接修改一个computed是没啥用的)。继续看看这个方法:

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

先看第一个if(watcher.dirty) ,表明这是一个没有及时更新的lazy watcher,或者简单的说就是计算属性的watcher, 就会调用evalute()来取值,在这个取值过程中,依赖就会被收集了。所以只要引用,计算属性就能订阅依赖的变化。

重点来了,第二个if(Dep.target) 里,调用了watcher的depend。我最初看的时候,无数次忽略了这个watcher.depend,总是习惯性的以为是dep.depend,然后想了一遍又一遍,一直没想到哪里可以收集到间接依赖。直到我反复的打断点,才突然发现,这TM是watcher.depend,不是dep.depend,那他是怎么实现的呢?

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

这个里面,this指向的是当前计算属性的watcher,而他的deps就是在dep.depend里被添加到watcher里的。

这里可能不太好理解,我们来梳理一下。

首先,1个Dep可以被多个Watcher订阅,但是1个Watcher也可以订阅多个依赖,比如我的面积属性,就依赖了长和宽。所以这样一来,Dep和Watcher其实是多对多的关系。

那知道一个Watcher所依赖的Dep有什么用呢?没错,就是依赖转移(我自己取得名字<_<),回到我们的问题。watcher.depend里遍历每个Dep,并且调用他们的depend方法,乍一看摸不着头脑,这不还是把计算属性的watcher添加到了dep里吗?

No No No!

再来看一下Dep.depend实现:

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

回想一下,上上一步,我们调用了watcher.evalute(),它的里面又调用了watcher.get(),get的最后又调用了popTarget,而popTarget返回的是出栈后栈顶的元素。而这个元素就是组件的watcher,所以一切迎刃而解了。通过遍历计算属性watcher的deps,让组件watcher去订阅他们。

妙,真的是妙啊!

弄懂了这个问题,我甚至兴奋得去楼下超市买了一袋干拌面!

不知道尤大写得时候花了多久想到这种方式,反正我光理解就花了很久,差距!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值