>  기사  >  웹 프론트엔드  >  Vue에서 계산의 구현 원리는 무엇입니까?

Vue에서 계산의 구현 원리는 무엇입니까?

不言
不言원래의
2018-09-14 16:10:063860검색
이 글의 내용은 Vue?에서 계산되는 구현 원리에 관한 것입니다. 도움이 필요한 친구들이 참고할 수 있기를 바랍니다.

현재 기술 스택이 Vue에서 React로 이전되었지만 Vue를 사용하여 여러 프로젝트를 개발하는 실제 경험은 여전히 ​​매우 즐겁습니다. Vue 문서는 명확하고 표준화되어 있으며 API 디자인은 간단하고 효율적입니다. 프론트엔드 개발자들에게 친숙하고, 시작도 빠르고, 개인적으로 Vue를 여러 시나리오에서 사용하는 것이 React 개발보다 더 효율적이라고 생각합니다. 그래서 여기서는 기술적인 요약을 하고 Vue에 대한 이해를 깊게 하려고 합니다. 그래서 오늘 제가 작성하려는 내용은 Vue에서 가장 많이 사용되는 API 중 하나인 computed의 구현 원리입니다. computed 的实现原理。

基本介绍

话不多说,一个最基本的例子如下:

<div>
    <p>{{fullName}}</p>
</div>
new Vue({
    data: {
        firstName: 'Xiao',
        lastName: 'Ming'
    },
    computed: {
        fullName: function () {
            return this.firstName + ' ' + this.lastName
        }
    }
})

Vue 中我们不需要在 template 里面直接计算 {{this.firstName + ' ' + this.lastName}},因为在模版中放入太多声明式的逻辑会让模板本身过重,尤其当在页面中使用大量复杂的逻辑表达式处理数据时,会对页面的可维护性造成很大的影响,而 computed 的设计初衷也正是用于解决此类问题。

对比侦听器 watch

当然很多时候我们使用 computed 时往往会与 Vue 中另一个 API 也就是侦听器 watch 相比较,因为在某些方面它们是一致的,都是以 Vue 的依赖追踪机制为基础,当某个依赖数据发生变化时,所有依赖这个数据的相关数据或函数都会自动发生变化或调用。

虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。

从 Vue 官方文档对 watch 的解释我们可以了解到,使用  watch  选项允许我们执行异步操作(访问一个 API)或高消耗性能的操作,限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态,而这些都是计算属性无法做到的。

下面还另外总结了几点关于 computedwatch 的差异:

  1. computed 是计算一个新的属性,并将该属性挂载到 vm(Vue 实例)上,而 watch 是监听已经存在且已挂载到 vm 上的数据,所以用 watch 同样可以监听 computed 计算属性的变化(其它还有 dataprops

  2. computed 本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问  computed  属性,才会计算新的值,而 watch 则是当数据发生变化便会调用执行函数

  3. 从使用场景上说,computed 适用一个数据被多个数据影响,而 watch 适用一个数据影响多个数据;

以上我们了解了 computedwatch 之间的一些差异和使用场景的区别,当然某些时候两者并没有那么明确严格的限制,最后还是要具体到不同的业务进行分析。

原理分析

言归正传,回到文章的主题 computed 身上,为了更深层次地了解计算属性的内在机制,接下来就让我们一步步探索 Vue 源码中关于它的实现原理吧。

在分析 computed 源码之前我们先得对 Vue 的响应式系统有一个基本的了解,Vue 称其为非侵入性的响应式系统,数据模型仅仅是普通的 JavaScript 对象,而当你修改它们时,视图便会进行自动更新。

Vue에서 계산의 구현 원리는 무엇입니까?

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项时,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter,这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化,每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

Vue 响应系统,其核心有三点:observewatcherdep

기본 소개

말할 것은 많지 않지만 기본 예는 다음과 같습니다. #🎜🎜#
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 */)
  }
  // computed初始化
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
const computedWatcherOptions = { computed: true }

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)
      }
    }
  }
}
#🎜🎜#Vue에서는 직접 계산할 필요가 없습니다. template{{this.firstName + ' ' + this.lastName}}, 템플릿에 선언적 논리를 너무 많이 넣으면 특히 복잡한 논리 표현식을 많이 사용할 때 템플릿 자체가 과중해지기 때문입니다. in the page 이런 식으로 데이터를 처리하게 되면 페이지의 유지 관리에 큰 영향을 미치게 되는데, computed는 원래 이런 문제를 해결하기 위해 설계되었습니다. #🎜🎜#

비교 리스너watch

#🎜🎜#물론 computed를 사용할 때 Vue의 다른 API와 작동하는 경우가 많습니다. 리스너 watch와 비교됩니다. 일부 측면에서는 둘 다 Vue의 종속성 추적 메커니즘을 기반으로 하기 때문입니다. 특정 종속성 데이터가 변경되면 이 데이터에 종속되는 모든 종속성은 관련 데이터가 됩니다. 또는 함수가 자동으로 변경되거나 호출됩니다. #🎜🎜#대부분의 경우 계산된 속성이 더 적합하지만 때로는 사용자 정의 리스너가 필요할 수도 있습니다. 이것이 바로 Vue가 watch 옵션을 통해 데이터 변경에 응답하는 보다 일반적인 방법을 제공하는 이유입니다. 이 접근 방식은 데이터가 변경될 때 비동기식 또는 비용이 많이 드는 작업을 수행해야 할 때 가장 유용합니다. #🎜🎜#Vue 공식 문서의 watch 설명을 보면 watch 옵션을 사용하면 비동기 작업(API 액세스) 또는 높은 수준의 작업을 수행할 수 있다는 것을 알 수 있습니다. -성능 작업, 작업 수행 빈도를 제한하고 계산된 속성이 수행할 수 없는 최종 결과를 얻기 전에 중간 상태를 설정합니다. #🎜🎜##🎜🎜#다음은 computedwatch의 차이점을 요약한 것입니다.#🎜🎜#
  1. #🎜🎜#computed는 새 속성을 계산하고 해당 속성을 vm(Vue 인스턴스)에 마운트하는 반면 watch 는 이미 존재하고 vm에 마운트된 데이터를 모니터링하기 위해 watch를 사용하면 computed 계산 속성의 변경 사항도 모니터링할 수 있습니다. code>data, props) #🎜🎜#
  2. #🎜🎜#computed는 본질적으로 게으른 검색입니다. 값의 관찰자 종속성이 변경되고 computed 속성에 처음으로 액세스할 때만 새 값이 계산되고, watch는 데이터가 캐시될 때 새 값을 계산합니다. 변경 사항은 실행 함수를 호출합니다 #🎜🎜#
  3. #🎜🎜# 사용 시나리오에서 계산은 여러 데이터의 영향을 받는 하나의 데이터에 적용 가능하지만 watch는 하나의 데이터를 적용하여 여러 데이터에 영향을 줍니다. #🎜🎜#
#🎜🎜# 위에서 우리는 계산된watch 물론 사용 시나리오에는 약간의 차이점이 있습니다. 물론 때로는 두 가지가 명확하지 않고 엄격하지 않기 때문에 결국에는 여전히 다른 비즈니스를 기반으로 분석해야 합니다. #🎜🎜#<h2>원리 분석</h2>#🎜🎜# <code>계산 기사의 주제로 돌아가서 계산 속성의 내부 메커니즘을 더 깊이 이해하기 위해, Vue 소스 코드에서 구현 원리를 단계별로 살펴보겠습니다. #🎜🎜##🎜🎜# 계산된 소스 코드를 분석하기 전에 먼저 Vue의 반응형 시스템에 대한 기본적인 이해가 필요합니다. Vue는 이를 비침해적 반응형 시스템이라고 부릅니다. 일반적인 JavaScript 개체이며 수정하면 보기가 자동으로 업데이트됩니다. #🎜🎜#

Vue에서 계산의 구현 원리는 무엇입니까?#🎜🎜#일반 JavaScript 객체를 Vue 인스턴스의 데이터에 전달할 때 옵션을 사용하면 Vue는 이 객체의 모든 속성을 탐색하고 Object.defineProperty를 사용하여 이러한 모든 속성을 getter/setter로 변환합니다.

는 사용자에게 보이지 않지만 내부적으로 Vue는 종속성을 추적하고 속성에 액세스하고 수정될 때 변경 사항을 알릴 수 있습니다. 나중에 종속성의 setter가 호출되면 다시 계산하도록 watcher에 알리고 관련 구성 요소가 업데이트됩니다. #🎜🎜#Vue 응답 시스템에는 세 가지 핵심 포인트가 있습니다: observe, watcher, dep: #🎜🎜#
  1. observe:遍历 data 中的属性,使用 Object.defineProperty 的 get/set 方法对其进行数据劫持;

  2. dep:每个属性拥有自己的消息订阅器 dep,用于存放所有订阅了该属性的观察者对象;

  3. watcher:观察者(对象),通过 dep 实现对响应属性的监听,监听到结果后,主动触发自己的回调进行响应。

对响应式系统有一个初步了解后,我们再来分析计算属性。
首先我们找到计算属性的初始化是在 src/core/instance/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 */)
  }
  // computed初始化
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

调用了 initComputed 函数(其前后也分别初始化了 initDatainitWatch )并传入两个参数 vm 实例和 opt.computed 开发者定义的 computed 选项,转到 initComputed 函数:

const computedWatcherOptions = { computed: true }

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

从这段代码开始我们观察这几部分:

  1. 获取计算属性的定义 userDefgetter 求值函数

    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get

    定义一个计算属性有两种写法,一种是直接跟一个函数,另一种是添加 setget 方法的对象形式,所以这里首先获取计算属性的定义 userDef,再根据 userDef 的类型获取相应的 getter 求值函数。

  2. 计算属性的观察者 watcher 和消息订阅器 dep

    watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
    )

    这里的 watchers 也就是 vm._computedWatchers 对象的引用,存放了每个计算属性的观察者 watcher 实例(注:后文中提到的“计算属性的观察者”、“订阅者”和 watcher 均指代同一个意思但注意和 Watcher 构造函数区分),Watcher 构造函数在实例化时传入了 4 个参数:vm 实例、getter 求值函数、noop 空函数、computedWatcherOptions 常量对象(在这里提供给 Watcher 一个标识 {computed:true} 项,表明这是一个计算属性而不是非计算属性的观察者,我们来到 Watcher 构造函数的定义:

    class Watcher {
      constructor (
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
      ) {
        if (options) {
          this.computed = !!options.computed
        } 
    
        if (this.computed) {
          this.value = undefined
          this.dep = new Dep()
        } else {
          this.value = this.get()
        }
      }
      
      get () {
        pushTarget(this)
        let value
        const vm = this.vm
        try {
          value = this.getter.call(vm, vm)
        } catch (e) {
          
        } finally {
          popTarget()
        }
        return value
      }
      
      update () {
        if (this.computed) {
          if (this.dep.subs.length === 0) {
            this.dirty = true
          } else {
            this.getAndInvoke(() => {
              this.dep.notify()
            })
          }
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
      }
    
      evaluate () {
        if (this.dirty) {
          this.value = this.get()
          this.dirty = false
        }
        return this.value
      }
    
      depend () {
        if (this.dep && Dep.target) {
          this.dep.depend()
        }
      }
    }

    为了简洁突出重点,这里我手动去掉了我们暂时不需要关心的代码片段。
    观察 Watcherconstructor ,结合刚才讲到的 new Watcher 传入的第四个参数 {computed:true} 知道,对于计算属性而言 watcher 会执行 if 条件成立的代码 this.dep = new Dep(),而 dep 也就是创建了该属性的消息订阅器。

    export default class Dep {
      static target: ?Watcher;
      subs: Array<watcher>;
    
      constructor () {
        this.id = uid++
        this.subs = []
      }
    
      addSub (sub: Watcher) {
        this.subs.push(sub)
      }
    
      depend () {
        if (Dep.target) {
          Dep.target.addDep(this)
        }
      }
    
      notify () {
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i <p><code>Dep</code> 同样精简了部分代码,我们观察 <code>Watcher</code> 和 <code>Dep</code> 的关系,用一句话总结</p>
    <blockquote>
    <code>watcher</code> 中实例化了 <code>dep</code> 并向 <code>dep.subs</code> 中添加了订阅者,<code>dep</code> 通过 <code>notify</code> 遍历了 <code>dep.subs</code> 通知每个 <code>watcher</code> 更新。</blockquote></watcher>
  3. defineComputed 定义计算属性

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

    因为 computed 属性是直接挂载到实例对象中的,所以在定义之前需要判断对象中是否已经存在重名的属性,defineComputed 传入了三个参数:vm 实例、计算属性的 key 以及 userDef 计算属性的定义(对象或函数)。
    然后继续找到 defineComputed 定义处:

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

    在这段代码的最后调用了原生 Object.defineProperty 方法,其中传入的第三个参数是属性描述符sharedPropertyDefinition,初始化为:

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

    随后根据 Object.defineProperty 前面的代码可以看到 sharedPropertyDefinitionget/set 方法在经过 userDef 和  shouldCache 等多重判断后被重写,当非服务端渲染时,sharedPropertyDefinitionget 函数也就是 createComputedGetter(key) 的结果,我们找到 createComputedGetter 函数调用结果并最终改写  sharedPropertyDefinition 大致呈现如下:

    sharedPropertyDefinition = {
        enumerable: true,
        configurable: true,
        get: function computedGetter () {
            const watcher = this._computedWatchers && this._computedWatchers[key]
            if (watcher) {
                watcher.depend()
                return watcher.evaluate()
            }
        },
        set: userDef.set || noop
    }

    当计算属性被调用时便会执行 get 访问函数,从而关联上观察者对象 watcher 然后执行 wather.depend() 收集依赖和 watcher.evaluate() 计算求值。

모든 단계를 분석한 후 전체 프로세스를 요약해 보겠습니다.

  1. 컴포넌트가 초기화되면 계산됩니다. data는 각각 자체 응답 시스템을 설정합니다. Observer data get/set /의 각 속성 설정을 순회합니다. code> 데이터 차단 <code>computeddata 会分别建立各自的响应系统,Observer 遍历 data 中每个属性设置 get/set 数据拦截

  2. 初始化 computed 会调用 initComputed 函数

    1. 注册一个 watcher 实例,并在内实例化一个 Dep 消息订阅器用作后续收集依赖(比如渲染函数的 watcher 或者其他观察该计算属性变化的 watcher

    2. 调用计算属性时会触发其Object.definePropertyget访问器函数

    3. 调用 watcher.depend() 方法向自身的消息订阅器 depsubs 中添加其他属性的 watcher

    4. 调用 watcherevaluate 方法(进而调用 watcherget 方法)让自身成为其他 watcher 的消息订阅器的订阅者,首先将 watcher 赋给 Dep.target,然后执行 getter 求值函数,当访问求值函数里面的属性(比如来自 dataprops 或其他 computed)时,会同样触发它们的 get 访问器函数从而将该计算属性的 watcher 添加到求值函数中属性的 watcher 的消息订阅器 dep 中,当这些操作完成,最后关闭 Dep.target 赋为 null 并返回求值函数结果。

  3. 当某个属性发生变化,触发 set 拦截函数,然后调用自身消息订阅器 depnotify 方法,遍历当前 dep 中保存着所有订阅者 wathcersubs 数组,并逐个调用 watcher 的  update

초기화 computedinitCompulated 함수를 호출합니다

watcher 인스턴스를 등록하고 A Dep 메시지 구독자는 후속 종속성 수집에 사용됩니다(예: 렌더링 함수의 watcher 또는 계산된 속성의 변경 사항을 관찰하는 기타 watcher) #🎜🎜 #

계산된 속성이 호출되면 해당 Object.definePropertyget 접근자 함수

#🎜🎜 ##이 트리거됩니다. 🎜🎜#watcher.dependent() 메서드를 호출하여 자체 메시지 구독자 의 <code>subs에 다른 속성을 추가합니다. dep code>watcher

watcherevaluate 메서드를 호출합니다(그리고 그런 다음 watcherget 메소드를 호출하여 자신을 다른 watcher 메시지 구독자의 구독자로 만들고, 먼저 watcher를 할당합니다. code>를 Dep .target으로 변환한 다음 평가 함수의 속성(예: data에서)에 액세스할 때 getter 평가 함수를 실행합니다. props 또는 기타 계산) 시 해당 get 접근자 함수도 트리거되어 계산된 항목의 watcher를 추가합니다. watcher의 메시지 구독자 dep에서 이러한 작업이 완료되면 마지막으로 Dep.target을 닫고 null에 할당하고 평가 함수 결과를 반환합니다. #🎜🎜##🎜🎜##🎜🎜##🎜🎜##🎜🎜#속성이 변경되면 set 차단 기능을 트리거한 다음 자체 메시지 구독자 dep의 <code>notify 메소드는 현재 dep의 모든 구독자 wathcer를 보유하는 subs 배열을 순회합니다. watcherupdate 메소드를 하나씩 호출하여 응답 업데이트를 완료합니다. #🎜🎜##🎜🎜##🎜🎜##🎜🎜#관련 추천: #🎜🎜##🎜🎜##🎜🎜#javascript - 티켓 브러셔의 구현 원리#🎜🎜##🎜🎜##🎜 🎜# #🎜🎜##🎜🎜#thinkphp 컨트롤러에서 display() 단계를 구현하는 원리#🎜🎜##🎜🎜##🎜🎜##🎜🎜#

위 내용은 Vue에서 계산의 구현 원리는 무엇입니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.