Vue中的计算属性、方法与侦听器源码分析

发布时间:2023-03-30 09:54:20 作者:iii
来源:亿速云 阅读:161

Vue中的计算属性、方法与侦听器源码分析

引言

Vue.js 是一个渐进式 JavaScript 框架,广泛应用于构建用户界面。在 Vue 中,计算属性(Computed Properties)、方法(Methods)和侦听器(Watchers)是三种常用的数据响应式处理方式。它们各自有不同的使用场景和特点,理解它们的底层实现原理对于深入掌握 Vue 的工作原理至关重要。

本文将深入分析 Vue 中计算属性、方法和侦听器的源码实现,帮助开发者更好地理解它们的内部机制。

1. 计算属性(Computed Properties)

1.1 计算属性的基本概念

计算属性是 Vue 中用于处理复杂逻辑的一种方式。它们是基于它们的依赖进行缓存的,只有在依赖发生变化时才会重新计算。这使得计算属性在处理复杂逻辑时非常高效。

1.2 计算属性的源码分析

在 Vue 的源码中,计算属性的实现主要位于 src/core/instance/state.js 文件中。计算属性的核心逻辑是通过 defineComputed 函数来实现的。

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

在这个函数中,createComputedGetter 是计算属性的核心逻辑。它会创建一个 getter 函数,这个 getter 函数会在计算属性被访问时执行。

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

在这个 getter 函数中,watcher.evaluate() 会执行计算属性的逻辑,并将结果缓存起来。如果依赖发生变化,watcher.dirty 会被设置为 true,从而在下一次访问时重新计算。

1.3 计算属性的缓存机制

计算属性的缓存机制是通过 Watcher 类来实现的。每个计算属性都会创建一个 Watcher 实例,这个 Watcher 实例会监听计算属性的依赖变化。

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.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
  }

  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}

Watcher 类中,lazy 属性用于标识是否是计算属性的 Watcher。如果是计算属性的 Watcherlazy 会被设置为 true,并且在初始化时不会立即执行 get 方法,只有在计算属性被访问时才会执行。

1.4 计算属性的依赖收集

计算属性的依赖收集是通过 Watcherget 方法来实现的。在 get 方法中,pushTarget(this) 会将当前的 Watcher 实例设置为 Dep.target,然后执行计算属性的逻辑。在执行过程中,所有被访问的响应式数据都会将当前的 Watcher 实例添加到它们的依赖列表中。

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

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

get 方法执行完毕后,popTarget() 会将 Dep.target 恢复到之前的状态。

2. 方法(Methods)

2.1 方法的基本概念

方法是 Vue 组件中定义的函数,通常用于处理用户交互或其他逻辑。与计算属性不同,方法不会缓存结果,每次调用时都会重新执行。

2.2 方法的源码分析

在 Vue 的源码中,方法的实现相对简单。方法的定义是通过 initMethods 函数来实现的,这个函数位于 src/core/instance/state.js 文件中。

function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      if (typeof methods[key] !== 'function') {
        warn(
          `Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
          `Did you reference the function correctly?`,
          vm
        )
      }
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      if ((key in vm) && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
          `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}

在这个函数中,bind(methods[key], vm) 会将方法绑定到 Vue 实例上,确保在方法中可以通过 this 访问 Vue 实例。

2.3 方法的执行

方法的执行是通过 Vue 实例直接调用的。由于方法不会缓存结果,每次调用时都会重新执行。

export function bind (fn: Function, ctx: Object): Function {
  return function boundFn (a) {
    const l = arguments.length
    return l
      ? l > 1
        ? fn.apply(ctx, arguments)
        : fn.call(ctx, a)
      : fn.call(ctx)
  }
}

bind 函数中,fn.apply(ctx, arguments) 会将方法的上下文绑定到 Vue 实例上。

3. 侦听器(Watchers)

3.1 侦听器的基本概念

侦听器是 Vue 中用于监听数据变化的一种方式。当监听的数据发生变化时,侦听器会执行相应的回调函数。侦听器通常用于处理异步操作或复杂逻辑。

3.2 侦听器的源码分析

在 Vue 的源码中,侦听器的实现也是通过 Watcher 类来实现的。侦听器的定义是通过 initWatch 函数来实现的,这个函数位于 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)
    }
  }
}

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

initWatch 函数中,createWatcher 函数会为每个侦听器创建一个 Watcher 实例。

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) {
    try {
      cb.call(vm, watcher.value)
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${expOrFn}"`)
    }
  }
  return function unwatchFn () {
    watcher.teardown()
  }
}

$watch 方法中,new Watcher(vm, expOrFn, cb, options) 会创建一个 Watcher 实例。这个 Watcher 实例会监听 expOrFn 的变化,并在变化时执行 cb 回调函数。

3.3 侦听器的依赖收集

侦听器的依赖收集也是通过 Watcherget 方法来实现的。在 get 方法中,pushTarget(this) 会将当前的 Watcher 实例设置为 Dep.target,然后执行侦听器的逻辑。在执行过程中,所有被访问的响应式数据都会将当前的 Watcher 实例添加到它们的依赖列表中。

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

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

get 方法执行完毕后,popTarget() 会将 Dep.target 恢复到之前的状态。

3.4 侦听器的回调执行

当侦听器监听的数据发生变化时,Watcherupdate 方法会被调用,从而触发回调函数的执行。

export default class Watcher {
  // ...

  update () {
    /* istanbul ignore else */
    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)
        }
      }
    }
  }
}

run 方法中,this.cb.call(this.vm, value, oldValue) 会执行侦听器的回调函数,并将新值和旧值作为参数传递给回调函数。

4. 总结

通过对 Vue 中计算属性、方法和侦听器的源码分析,我们可以更好地理解它们的内部机制。计算属性通过 Watcher 类实现了依赖收集和缓存机制,方法通过简单的函数绑定实现了逻辑处理,而侦听器则通过 Watcher 类实现了数据变化的监听和回调执行。

理解这些底层原理不仅有助于我们更好地使用 Vue,还能帮助我们在遇到问题时快速定位和解决问题。希望本文的分析能够帮助读者更深入地理解 Vue 的工作原理。

推荐阅读:
  1. vue更改数组中的值实例代码详解
  2. 怎么在Vue中一键清空表单

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

vue

上一篇:如何将文本数据从HTML或其他格式中提取出来

下一篇:删除gitee提交信息的方法是什么

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》