重学Vue【计算属性和监听属性】

简介: 重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。

网络异常,图片无法展示
|

重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈)github 上。


正文


计算属性

计算属性的初始化是在Vue初始化的 initState 方法中:

// ...
initState(vm)
// ...

具体实现是在 src/core/instance/state.js 里面:

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 */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

可以看到在里面初始化了 propsmethodsdata,另外也初始化了 computedwatch,这里就看看下 initComputed 的具体实现:

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)
      }
    }
  }
}

先定义了一个 watchers_computedWatchers 为空对象,然后判断是不是服务端渲染,这里肯定是false,后面遍历 computed,这个 computed 其实就是我们自定义写的 computed 属性,然后拿到每一个定义的值,它可以是函数也可以是对象,一般我们都会写一个函数,如果定义成对象的话,就必须定义一个 get 属性,相当于拿到 getter,接着判断 getter 有没有,如果没有就报错,接着实例化一个 Watcher,对应的值就是 watchers[key],这里就可以看出来 computed 里面的定义其实就是通过 watcher 来实现的,传入 watcher 的值分别是vm实例、getter(也就是定义的get)、noop回调函数和一个computedWatcherOptions(定义了一个对象: {computed: true})。接着判断 key 在不在 vm 中,如果不在就走一个 defineComputed 方法,如果在,那肯定在 data 或者 props 定义过,就报错,所以在 computed 里面定义的键值不可以在 data 或者 props 里出现。

来看下这个 defineComputed 方法:

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
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)
}

如果定义的每个计算属性是一个函数,就把对象的 getset 赋值,也就是说当访问我们自定义的 computed 属性的时候,就会执行对应的重新赋值的 get 方法,这里 shouldCache 是true,所以会走 createComputedGetter 方法,如果我们写的是一个对象,就走下面的 else 逻辑,也会走到 createComputedGetter,因为这个 computed 属性一般是计算而来的,所以一般我们不会用对象的方式去写。


计算属性的依赖收集

接着看下这个 createComputedGetter 做了什么:

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

单从名字来看,就是定义了在我们访问 computed 属性的时候的 getter,可以看到每次访问的时候就会执行 computedGetter,拿到刚才存起来的 _computedWatchers,走一个 depend 方法,返回执行 watcher.evaluate() 方法。

到这里计算属性的初始化流程就结束了,回到上面说到的访问 computed 的时候会 new 一个 watcher,它和之前说过的创建渲染watcher有哪些不一样,我们来重新看下 watcher 的构造函数,在 src/core/observer/watcher.js 里,这里只看关键点:

if (this.computed) {
  this.value = undefined
  this.dep = new Dep()
} else {
  this.value = this.get()
}
复制代码

前面传进来的对象 {computed: true} 在这里用到了,作为 computed watcher,会在当前实例定义一个 value,并且定义一个 depnew Dep(),然后就结束了,并不会像以前一样去求值。举个例子:

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

render 函数执行访问到 this.sum 的时候,就触发了计算属性的 getter,它就会拿到计算属性的 watcher,执行上面提到的 watcher.depend()

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

这个时候的 Dep.target 不是当前的 computed watcher ,而是 渲染watcher,所以this.dep.depend() 就相当于 渲染 watcher 订阅了这个 computed watcher 的变化(有关的订阅可以看这篇),接着按照上面 computedGetter 的逻辑就会执行 watcher.evaluate() 去求值:

/**
  * 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
}

evaluate 里判断 this.dirty(dirty 就是传入的 computed 布尔值),如果是true,就通过 this.get() 求值,然后把 dirty 设置为false,而在执行 get 的时候,里面有一个 value = this.getter.call(vm, vm),也就是执行了计算属性定义的 getter 函数,在上面的例子中就是执行了我们自定义的 return this.a + this.b,由于 ab 都是响应式对象,所以在执行它们的 getter 的时候,会将自身的 dep 添加到当前正在计算的 watcher 中,此时的 Dep.target 就是 computed watcher 了,到此求值结束。


计算属性的派发更新

一旦对计算属性依赖的数据做了修改,就会触发 setter 过程,然后通知所有订阅它变化的 watcher 进行派发更新,最终执行 watcher.update() 方法(有关派发更新可以看这篇):

/**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  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()
        })
      }
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

通过注释可以看出来 computed watcher 有两种模式:lazyactivated。如果 this.dep.subs.length === 0 为true,就说明没有人订阅这个 computed watcher 的变化,就只把 this.dirty 改成true,然后在下次再访问这个计算属性的时候才会去重新求值;否则如果订阅了,就执行:

this.getAndInvoke(() => {
  this.dep.notify()
})
getAndInvoke (cb: Function) {
  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
    this.dirty = false
    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)
    }
  }
}

这个在派发更新也分析过:

getAndInvoke 函数也比较简单,先通过 get 方法得到一个新值 value,然后让新值和之前的值做对比,如果值不相等,或者新值 value 是一个对象,或者它是一个 deep watcher 的话,就执行回调(user watcher 比 渲染watcher 多了一个报错提示而已),注意回调函数 cb 执行的时候会把第一个和第二个参数传入新值 value 和旧值 oldValue这就是当我们自定义 watcher 的时候可以拿到新值和旧值的原因。

getAndInvoke 会重新计算,对比新旧值,如果变化了就执行回调函数,这里的回调函数就是 this.dep.notify(),在求值 sum 的情况下,就是触发了 watcher 的重新渲染,对于这个 getAndInvoke 里面的 value !== this.value 其实算是做了一个优化,如果新值和旧值相等的话,就不做什么处理,也不会触发回调,这样就少走了一层渲染。


计算属性小总结

计算属性本质上就是一个 computed watcher,它在创建的过程中会实例一个 watcher,访问它的时候走定义的 computedGetter 进行依赖收集,在计算的时候触发依赖的更新,并且当计算属性最终计算的值更新发生变化才会触发 渲染 watcher 的重新渲染。


监听属性

最上面提到,在Vue实例初始化的时候会执行监听属性的初始化,它是在 computed 初始化之后的:

if (opts.watch && opts.watch !== nativeWatch) {
  initWatch(vm, opts.watch)
}

看下 initWatcher 的具体实现,它定义在 src/core/instance/state.js

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)
    }
  }
}

可以看到对 watcher 做了遍历,拿到每一个 handler,因为Vue是支持 watcher 的同一个 key 有多个 handler 的,所以如果 handler 是一个数组,就遍历数组并调用 createWatcher 方法,否则直接调用 createWatcher

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

createWatcher 中,对 handler 的类型做了判断,然后拿到它的回调函数,也就是我们自定义的函数,最后调用 vm.$watch 函数,这个 vm.$watch 是一个Vue原型上扩展的方法,它是在执行 stateMixin 的时候被加进去的:

Vue.prototype.$watch = function (
  expOrFn: string | Function,
   cb: any,
   options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    return function unwatchFn () {
      watcher.teardown()
    }
}

换句话说,监听属性最终执行的其实是 $watch,首先判断了 cb 如果是一个对象,就直接调用 createWatcher,因为 $watch 是可以直接外部调用的,它可以传一个对象,也可以传一个函数。接着实例化一个 watcher,注意这个 watcher 是一个 user watcher,因为是用户直接调用的,而且 options.user = true。这样通过实例化一个 watcher 的方式,一旦我们自定义的 watcher 发生了变化,最终会执行 watcherrun 方法(watcher 里的 update 调用的 run),从而调用我们的回调函数,也就是 cb,如果 options 上的 immediate 也是true,就直接执行回调函数,最终返回了一个 unwatchFn 方法,调用一个 teardown 来移除这个 watcher

/**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
run () {
  if (this.active) {
    this.getAndInvoke(this.cb)
  }
}
/**
   * Remove self from all dependencies' subscriber list.
   */
teardown () {
  if (this.active) {
    // remove self from vm's watcher list
    // this is a somewhat expensive operation so we skip it
    // if the vm is being destroyed.
    if (!this.vm._isBeingDestroyed) {
      remove(this.vm._watchers, this)
    }
    let i = this.deps.length
    while (i--) {
      this.deps[i].removeSub(this)
    }
    this.active = false
  }
}


到底有几种 watcher

Watcher 的构造函数里对 options 做了处理,代码如下:

if (options) {
  this.deep = !!options.deep
  this.user = !!options.user
  this.computed = !!options.computed
  this.sync = !!options.sync
  // ...
} else {
  this.deep = this.user = this.computed = this.sync = false
}

可以看出来一共有四种 watcher

deep watcher

这个一般在深度监听的时候会用到,我们一般会这样写:

var vm = new Vue({
  data() {
    a: {
      b: 1
    }
  },
  watch: {
    a: {
      handler(newVal) {
        console.log(newVal)
      }
    }
  }
})
vm.a.b = 2

这种情况下 handler 是不会监听到b的变化的,因为此时 watch 的是 a 对象,只触发了 agetter,并没有触发 a.bgetter,所以没有订阅它,所以在修改 a.b 的时候,虽然触发了 setter, 但是没有可通知的对象,所以也不会触发 watch 的回调函数。

此时如果想监听到 a.b,就得这样使用:

watch: {
  a: {
    deep: true,
    handler(newVal) {
      console.log(newVal)
    }
  }
}

这样就创建了一个 deep watcher,在 watcher 执行 get 求值的过程中有这么一段:

try {
  value = this.getter.call(vm, vm)
} catch (e) {
  //...
} finally {
  // "touch" every property so they are all tracked as
  // dependencies for deep watching
  if (this.deep) {
    traverse(value)
  }
  //...
}

在对 value 求值后,如果 this.deep 为true,就执行 traverse(value) 方法,它的定义在 src/core/observer/traverse.js

import { _Set as Set, isObject } from '../util/index'
import type { SimpleSet } from '../util/index'
import VNode from '../vdom/vnode'
const seenObjects = new Set()
/**
 * Recursively traverse an object to evoke all converted
 * getters, so that every nested property inside the object
 * is collected as a "deep" dependency.
 */
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)
  }
}

traverse 其实就是对一个对象做了深度递归遍历,遍历过程其实就是对一个子对象进行 getter 访问,这样就可以收集到依赖,也就是订阅它们的 watcher,在遍历过程中还会把子响应式对象通过它们的 dep id 记录到 seenObjects,避免以后重复访问。

在执行 traverse 之后,再对 watch 对象的内部任何一个属性进行监听就都可以调用 watcher 的回调函数。

user watcher

这个在上面有分析到,通过 vm.$watch 创建的 watcher 是一个 user watcher,其实源码涉及到它的地方就是 get 求值方法和 getAndInvoke 调用回调函数了:

get() {
  if (this.user) {
    handleError(e, vm, `getter for watcher "${this.expression}"`)
  } else {
    throw e
  }
},
getAndInvoke() {
  // ...
  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)
  }
}

handleError 在 Vue 中是一个错误捕获并且暴露给用户的一个利器。

computed watcher

computed watcher 几乎就是为计算属性量身定制的,上面已经对它做了详细的分析,这里就不再赘述了。

sync watcher

这个在 watchupdate 方法里:

update () {
  if (this.computed) {
    // ...
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

当响应式数据发生变化之后,就会触发 watch.update(),然后把这个 watcher 推到一个队列中,等下一个tick再进行 watcher 的回调函数,如果设置了 sync,就直接在当前tick中同步执行 watcher 回调函数了。

只有当我们需要 watch 的值的变化到执行 watcher 的回调函数是一个同步过程的时候才会去设置该属性为 true。

目录
相关文章
|
3天前
|
监控 JavaScript 前端开发
ry-vue-flowable-xg:震撼来袭!这款基于 Vue 和 Flowable 的企业级工程项目管理项目,你绝不能错过
基于 Vue 和 Flowable 的企业级工程项目管理平台,免费开源且高度定制化。它覆盖投标管理、进度控制、财务核算等全流程需求,提供流程设计、部署、监控和任务管理等功能,适用于企业办公、生产制造、金融服务等多个场景,助力企业提升效率与竞争力。
48 12
|
20天前
|
JavaScript 安全 API
iframe嵌入页面实现免登录思路(以vue为例)
通过上述步骤,可以在Vue.js项目中通过 `iframe`实现不同应用间的免登录功能。利用Token传递和消息传递机制,可以确保安全、高效地在主应用和子应用间共享登录状态。这种方法在实际项目中具有广泛的应用前景,能够显著提升用户体验。
48 8
|
21天前
|
存储 设计模式 JavaScript
Vue 组件化开发:构建高质量应用的核心
本文深入探讨了 Vue.js 组件化开发的核心概念与最佳实践。
59 1
|
2月前
|
JavaScript
vue使用iconfont图标
vue使用iconfont图标
141 1
|
3月前
|
JavaScript 前端开发 开发者
vue 数据驱动视图
总之,Vue 数据驱动视图是一种先进的理念和技术,它为前端开发带来了巨大的便利和优势。通过理解和应用这一特性,开发者能够构建出更加动态、高效、用户体验良好的前端应用。在不断发展的前端领域中,数据驱动视图将继续发挥重要作用,推动着应用界面的不断创新和进化。
108 58
|
2月前
|
JavaScript 关系型数据库 MySQL
基于VUE的校园二手交易平台系统设计与实现毕业设计论文模板
基于Vue的校园二手交易平台是一款专为校园用户设计的在线交易系统,提供简洁高效、安全可靠的二手商品买卖环境。平台利用Vue框架的响应式数据绑定和组件化特性,实现用户友好的界面,方便商品浏览、发布与管理。该系统采用Node.js、MySQL及B/S架构,确保稳定性和多功能模块设计,涵盖管理员和用户功能模块,促进物品循环使用,降低开销,提升环保意识,助力绿色校园文化建设。
|
3月前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱前端的大一学生,专注于JavaScript与Vue,正向全栈进发。博客分享Vue学习心得、命令式与声明式编程对比、列表展示及计数器案例等。关注我,持续更新中!🎉🎉🎉
64 1
vue学习第一章
|
3月前
|
JavaScript 前端开发 索引
vue学习第三章
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中的v-bind指令,包括基本使用、动态绑定class及style等,希望能为你的前端学习之路提供帮助。持续关注,更多精彩内容即将呈现!🎉🎉🎉
61 1
|
3月前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
54 1
vue学习第四章
|
3月前
|
JavaScript 前端开发 算法
vue学习第7章(循环)
欢迎来到瑞雨溪的博客,一名热爱JavaScript和Vue的大一学生。本文介绍了Vue中的v-for指令,包括遍历数组和对象、使用key以及数组的响应式方法等内容,并附有综合练习实例。关注我,将持续更新更多优质文章!🎉🎉🎉
44 1
vue学习第7章(循环)

热门文章

最新文章