vue源码剖析系列之相应式数据(三)

媒介

上一节偏重报告了initData中的代码,以及数据是怎样从data中到视图层的,以及data修改后怎样作用于视图。这一节重要纪录initComputed中的内容。

正文

前情回忆

在demo示例中,我们定义了一个盘算属性。

computed:{
  total(){
    return this.a + this.b
  }
}

本章节我们继承探讨这个盘算属性的相干流程。

initComputed

// initComputed(vm, opts.computed)
function initComputed (vm: Component, computed: Object) {
  // 定义盘算属性相干的watchers.
  const watchers = vm._computedWatchers = Object.create(null)
  // 是不是是服务端衬着,这里赞不斟酌。
  const isSSR = isServerRendering()

  for (const key in computed) {
    // 取得用户定义的盘算属性中的item,通常是一个要领
    // 在示例顺序中,唯一一个key为total的盘算a+b的要领。
    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.
      // 为盘算属性建立一个内部的watcher。
      // 个中computedWatcherOptions的值为lazy,意味着这个wacther内部的value,先不必盘算。
      // 只要在须要的状况下才盘算,这里重假如在后期页面衬着中,天生假造dom的时刻才会盘算。
      // 这时刻new Watcher只是走一遍watcher的组织函数,其内部value因为
      // lazy为true,先设置为了undefined.同时内部的dirty = lazy;
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions // 上文定义过,值为{lazy: true}
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    // 组件定义的属性只是定义在了组件上,这里只是把它翻译到实例中。即当前的vm对象。
    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)
      }
    }
  }
}

defineComputed

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
// defineComputed(vm, key, userDef)
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  // 是不是须要缓存。即非服务端衬着须要缓存。
  // 因为本案例用的demo非服务端衬着,这里效果是true
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    // userDef = total() {...}
    sharedPropertyDefinition.get = shouldCache
      // 依据key建立盘算属性的getter
      ? createComputedGetter(key)
      : userDef
    // 盘算属性是只读的,所以设置setter为noop.
    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)
}

createComputedGetter

// 依据key建立盘算属性的getter
// createComputedGetter(key)
function createComputedGetter (key) {
  return function computedGetter () {
    // 非服务端衬着的时刻,在上述的initComputed中定义了vm._computedWatchers = {},并依据组件中的设定watchers[key] = new Watcher(..),这里只是依据key掏出了当时new的watcher
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // watcher.dirty示意这个值是脏值,逾期了。所以须要从新盘算。
      // new Watcher的时刻,这个total的watcher中,内部的dirty已被置为
      // dirty = lazy = true;
      // 那末这个值什么时刻会逾期,会脏呢。就是内部的依靠更新时刻,
      // 比方我们的total依靠于this.a,this.b,当着两个值恣意一个变化时刻
      // 我们的total就已脏了。须要依据最新的a,b盘算。
      if (watcher.dirty) {
        // 盘算watcher中的值,即value属性.
        watcher.evaluate()
      }
      // 将依靠添加到watcher中。
      if (Dep.target) {
        watcher.depend()
      }
      // getter的效果就是返回getter中的值。
      return watcher.value
    }
  }
}

initComputed小结

继initComputed以后,一切组件中的computed都被赋值到了vm实例的属性上,并设置好了getter和setter。在非服务端衬着的状况下,getter会缓存盘算效果。并在须要的时刻,才盘算。setter则是一个什么都不做的函数,预示着盘算属性只能被get,不能被set。即只读的。

接下来的题目就是:

  1. 这个盘算属性什么时刻会盘算,前文{lazy:true}预示着当时new Watcher获得的值是undefined。还没最先盘算。
  2. 盘算属性是怎样晓得它本身依靠于哪些属性的。以便晓得其什么时刻更新。
  3. vue官方文档的缓存盘算效果怎样明白。

接下来我们继承理会背面的代码。处置惩罚这里提到的三个题目。

用来天生vnode的render函数

下次再见到这个盘算属性total的时刻,已经是在依据el选项或许template模板中,天生的render函数,render函数上一小节也提到过。长这个模样。

(function anonymous() {
    with (this) {
        return _c('div', {
            attrs: {
                "id": "demo"
            }
        }, [_c('div', [_c('p', [_v("a:" + _s(a))]), _v(" "), _c('p', [_v("b: " + _s(b))]), _v(" "), _c('p', [_v("a+b: " + _s(total))]), _v(" "), _c('button', {
            on: {
                "click": addA
            }
        }, [_v("a+1")])])])
    }
}
)

这里能够连系一下我们的html,看出一些特性。

<div id="demo">
  <div>
    <p>a:{{a}}</p>
    <p>b: {{b}}</p>
    <p>a+b: {{total}}</p>
    <button @click="addA">a+1</button>
  </div>
</div>

这里使用到盘算属性的重假如这一句

_v("a+b: " + _s(total))

那末关于我们来讲的症结就是_s(total)。因为这个函数的with(this)中,this被设置为vm实例,所以这里就能够明白为_s(vm.total)。那末这里就会触发之前定义的sharedPropertyDefinition.get

-> initComputed()
-> defineComputed()
-> Object.defineProperty(target, key, sharedPropertyDefinition)

也就是createComputedGetter返回的函数中的内容,也就是:

watcher细说

const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
  // 因为初始化的时刻这个dirty为true,所以会举行watcher.evaluate()的盘算。
  if (watcher.dirty) {
    watcher.evaluate()
  }
  if (Dep.target) {
    watcher.depend()
  }
  // getter的效果就是返回getter中的值。
  return watcher.value
}

这里我们看下watcher.evaluate的部份。

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

这里this.get即获得了value的值,这就是第一个题目的答案。
1.盘算属性什么时候会盘算。
即用到的时刻会盘算,准确的说,就是在盘算vnode的时刻会用到它,从而盘算它。
关于第二个题目,盘算属性是怎样晓得它本身依靠于哪些属性的?则是在这个
this.get内。

// Dep相干逻辑,Dep Class用来网络依靠某个值的watcher
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()
}

// Watcher class 相干逻辑
get () {
    // 将当前的watcher推到Dep.target中
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 这里的getter实际上就是对应total的函数体,
      // 而这个函数体内藏有很大的猫腻,接下来我们仔细剖析这一段。
      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
  }

当代码实行到this.getter.call,实际上实行的是盘算属性的函数,也就是
total() { return this.a + this.b};当代码实行到this.a时刻。就会触发上一节我们所讲的defineReactive内部的代码。

//// 这里我们以接见this.a为例
export function defineReactive (
  obj: Object,  // {a:1,b:1}
  key: string,  // 'a'
  val: any,     // 1
  customSetter?: ?Function,
  shallow?: boolean
) {
  
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // this.a会触发这里的代码。起首取得value,
      // 因为watcher内部this.get实行total盘算属性时刻,已将
      // total的watcher设置为Dep.target
      if (Dep.target) {
        // 所以这里最先网络依靠。
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    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()
    }
  })
}

上述代码中,this.a触发了dep.depend()。我们细看这里的代码。

class Dep {
  //省略代码...
  depend () {
    // 因为这里的Dep.target此时对应的是total的watcher。
    // 而这里的this.是指定义this.a时,天生的dep。
    // 所以这里是关照total依靠于this.a
    if (Dep.target) {
      // 经由过程挪用addDep.让total的watcher晓得total依靠this.a
      Dep.target.addDep(this)
    }
  }
}

class Watcher {
  // ...省略代码
  addDep (dep: Dep) {
    // 此时的this是total的watcher
    const id = dep.id
    // 防备反复网络
    if (!this.newDepIds.has(id)) {
      // 将依靠的可视察对象纪录。
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      // 假如这个可视察对象没有纪录当前watcher,
      if (!this.depIds.has(id)) {
        // 则将当前的watcher到场到可视察对象中
        // (轻易后续a变化后,示知total)
        dep.addSub(this)
      }
    }
  }
}

至此,上述的第二个题目,盘算属性是怎样晓得它本身依靠于哪些属性的?也有了答案。就是当天生假造dom的时刻,用到了total,因为获得total值的watcher是脏的,须要盘算一次,然后就将Dep.target的watcher设为total相干的watcher。并在watcher内实行了total函数,在函数内部,接见了this.a。this.a的getter中,经由过程dep.depend(),将this.a的getter上方的dep,到场到total的watcher.dep中,再经由过程watcher中的dep.addSub(this),将total的watcher到场到了this.a的getter上方中的dep中。至此total晓得了它依靠于this.a。this.a也晓得了,total须要this.a。

当盘算属性的依靠变动时发生了什么

当点击页面按钮的时刻,会实行我们案例中绑定的this.a += 1的代码。此时会走
this.a的setter函数。我们看看setter中所做的事变。

set: function reactiveSetter (newVal) {
  const value = getter ? getter.call(obj) : val
  // 假如旧值与新值相称,什么都不做。直接返回。
  if (newVal === value || (newVal !== newVal && value !== value)) {
    return
  }
  // 无关代码,pass
  if (process.env.NODE_ENV !== 'production' && customSetter) {
    customSetter()
  }
  // 有定义过setter的话经由过程setter设置新值
  if (setter) {
    setter.call(obj, newVal)
  } else {
    // 不然的话直接设置新值
    val = newVal
  }
  // 斟酌新值是对象的状况。
  childOb = !shallow && observe(newVal)

  // 关照视察了this.a的视察者。
  // 这里实际上是有两个视察a的视察者
  // 一个是上一篇讲的updateComponent。
  // 一个是这节讲的total。
  dep.notify()
}

这里我们看看dep.notify干了什么

class Dep {
  // **** 其他代码
  notify () {
    // 这里的subs实在就是上述的两个watcher。
    // 离别实行watcher的update
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

class Watcher{
  update () {
    // 第一个watcher,即关于updateComponent的。
    // 会实行queueWatcher。也就是会将处置惩罚放到守候行列里
    // 守候行列中,而第二个watcher因为lazy为true,
    // 所以只是将watcher标记为dirty。
    // 因为行列这个比较复杂,所以单开话题去讲
    // 这里我们只须要晓得它是一个异步的行列,末了效果就是
    // 挨个实行行列中watcher的run要领。
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
}

当触发了依靠更新时刻,第一个watcher(关于total的)会将本身的dirty标记为true,第二个则会实行run要领,在个中运转this.get致使updateComponent实行,进而再次盘算vnode,这时会再次盘算this.total。则会再次触发total的getter,这时刻我们再温习一下之前讲过的这个computed的getter:

const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
  // watcher.dirty示意这个值是脏值,逾期了。所以须要从新盘算。
  // new Watcher的时刻,这个total的watcher中,内部的dirty已被置为
  // dirty = lazy = true;
  // 那末这个值什么时刻会逾期,会脏呢。就是内部的依靠更新时刻,
  // 比方我们的total依靠于this.a,this.b,当着两个值恣意一个变化时刻
  // 我们的total就已脏了。须要依据最新的a,b盘算。
  if (watcher.dirty) {
    // 盘算watcher中的值,即value属性.
    watcher.evaluate()
  }
  // 将依靠添加到watcher中。
  if (Dep.target) {
    watcher.depend()
  }
  // getter的效果就是返回getter中的值。
  return watcher.value
}

至此,computed中total的更新流程也完毕了。
所以我们的第3个题目,vue官方文档的缓存盘算效果怎样明白?也就有了答案。也就是说盘算属性只要其依靠变动的时刻才会去盘算,依靠不更新的时刻,是不会盘算的。正文这一小节提到的,total的更新是因为this.a的更新致使其setter被触发,因而关照了其依靠,即total这个watcher。假如total的不依靠于this.a,则total相干的watcher的dirty就不会变成true,也就不会再次盘算了。

总结

本章节我们以示例顺序探讨了盘算属性,从initComputed中,盘算属性的初始化到盘算属性的变动,对着代码做了进一步的诠释。团体流程能够归结为:

initComputed定义了相干的盘算属性相干的watcher,以及watcher的getter。
在第一次盘算vnode的时刻趁便实行了盘算属性的盘算逻辑,趁便网络了依靠。本例中total网络到了依靠a,b;而且a,b也被示知total视察了他们。当a,b任何一个转变时的时刻,就会将total相干的watcher.dirty设置为true,下次须要更新界面时,盘算属性就会被从新盘算。固然,假如没有依靠于total的处所。那末total是不会盘算的,比方total基础没被界面或许js代码用到,就不会盘算total;假如total一切的依靠没有变动,其dirty为false,则也是无需盘算的。

文章链接

    原文作者:A_大白
    原文地址: https://segmentfault.com/a/1190000018179734
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞