현재 기술 스택이 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 的依赖追踪机制为基础,当某个依赖数据发生变化时,所有依赖这个数据的相关数据或函数都会自动发生变化或调用。
watch
选项提供了一个更通用的方法来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。从 Vue 官方文档对 watch
的解释我们可以了解到,使用 watch
选项允许我们执行异步操作(访问一个 API)或高消耗性能的操作,限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态,而这些都是计算属性无法做到的。
下面还另外总结了几点关于 computed
和 watch
的差异:
computed
是计算一个新的属性,并将该属性挂载到 vm(Vue 实例)上,而 watch
是监听已经存在且已挂载到 vm
上的数据,所以用 watch
同样可以监听 computed
计算属性的变化(其它还有 data
、props
)
computed
本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed
属性,才会计算新的值,而 watch
则是当数据发生变化便会调用执行函数
从使用场景上说,computed
适用一个数据被多个数据影响,而 watch
适用一个数据影响多个数据;
以上我们了解了 computed
和 watch
之间的一些差异和使用场景的区别,当然某些时候两者并没有那么明确严格的限制,最后还是要具体到不同的业务进行分析。
言归正传,回到文章的主题 computed
身上,为了更深层次地了解计算属性的内在机制,接下来就让我们一步步探索 Vue 源码中关于它的实现原理吧。
在分析 computed
源码之前我们先得对 Vue 的响应式系统有一个基本的了解,Vue 称其为非侵入性的响应式系统,数据模型仅仅是普通的 JavaScript 对象,而当你修改它们时,视图便会进行自动更新。
当你把一个普通的 JavaScript 对象传给 Vue 实例的
data
选项时,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty
把这些属性全部转为 getter/setter
,这些 getter/setter
对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化,每个组件实例都有相应的 watcher
实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter
被调用时,会通知 watcher
重新计算,从而致使它关联的组件得以更新。Vue 响应系统,其核心有三点:observe
、watcher
、dep
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 액세스) 또는 높은 수준의 작업을 수행할 수 있다는 것을 알 수 있습니다. -성능 작업, 작업 수행 빈도를 제한하고 계산된 속성이 수행할 수 없는 최종 결과를 얻기 전에 중간 상태를 설정합니다. #🎜🎜##🎜🎜#다음은 computed
와 watch
의 차이점을 요약한 것입니다.#🎜🎜#computed
는 새 속성을 계산하고 해당 속성을 vm(Vue 인스턴스)에 마운트하는 반면 watch
는 이미 존재하고 vm
에 마운트된 데이터를 모니터링하기 위해 watch
를 사용하면 computed
계산 속성의 변경 사항도 모니터링할 수 있습니다. code>data, props
) #🎜🎜#computed
는 본질적으로 게으른 검색입니다. 값의 관찰자 종속성이 변경되고 computed
속성에 처음으로 액세스할 때만 새 값이 계산되고, watch
는 데이터가 캐시될 때 새 값을 계산합니다. 변경 사항은 실행 함수를 호출합니다 #🎜🎜#계산
은 여러 데이터의 영향을 받는 하나의 데이터에 적용 가능하지만 watch
는 하나의 데이터를 적용하여 여러 데이터에 영향을 줍니다. #🎜🎜#계산된
과 watch 물론 사용 시나리오에는 약간의 차이점이 있습니다. 물론 때로는 두 가지가 명확하지 않고 엄격하지 않기 때문에 결국에는 여전히 다른 비즈니스를 기반으로 분석해야 합니다. #🎜🎜#<h2>원리 분석</h2>#🎜🎜# <code>계산
기사의 주제로 돌아가서 계산 속성의 내부 메커니즘을 더 깊이 이해하기 위해, Vue 소스 코드에서 구현 원리를 단계별로 살펴보겠습니다. #🎜🎜##🎜🎜# 계산된
소스 코드를 분석하기 전에 먼저 Vue의 반응형 시스템에 대한 기본적인 이해가 필요합니다. Vue는 이를 비침해적 반응형 시스템이라고 부릅니다. 일반적인 JavaScript 개체이며 수정하면 보기가 자동으로 업데이트됩니다. #🎜🎜##🎜🎜#일반 JavaScript 객체를 Vue 인스턴스의 데이터에 전달할 때
옵션을 사용하면 Vue는 이 객체의 모든 속성을 탐색하고 Object.defineProperty
를 사용하여 이러한 모든 속성을 getter/setter
로 변환합니다.
setter
가 호출되면 다시 계산하도록 watcher
에 알리고 관련 구성 요소가 업데이트됩니다. #🎜🎜#Vue 응답 시스템에는 세 가지 핵심 포인트가 있습니다: observe
, watcher
, dep
: #🎜🎜#observe
:遍历 data
中的属性,使用 Object.defineProperty 的 get/set
方法对其进行数据劫持;
dep
:每个属性拥有自己的消息订阅器 dep
,用于存放所有订阅了该属性的观察者对象;
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
函数(其前后也分别初始化了 initData
和 initWatch
)并传入两个参数 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) } } } }
从这段代码开始我们观察这几部分:
获取计算属性的定义 userDef
和 getter
求值函数
const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get
定义一个计算属性有两种写法,一种是直接跟一个函数,另一种是添加 set
和 get
方法的对象形式,所以这里首先获取计算属性的定义 userDef
,再根据 userDef
的类型获取相应的 getter
求值函数。
计算属性的观察者 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() } } }
为了简洁突出重点,这里我手动去掉了我们暂时不需要关心的代码片段。
观察 Watcher
的 constructor
,结合刚才讲到的 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>
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
前面的代码可以看到 sharedPropertyDefinition
的 get/set
方法在经过 userDef
和 shouldCache
等多重判断后被重写,当非服务端渲染时,sharedPropertyDefinition
的 get
函数也就是 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()
计算求值。
컴포넌트가 초기화되면 계산됩니다.
및 data
는 각각 자체 응답 시스템을 설정합니다. Observer
는 data
get/set /의 각 속성 설정을 순회합니다. code> 데이터 차단 <code>computed
和 data
会分别建立各自的响应系统,Observer
遍历 data
中每个属性设置 get/set
数据拦截
初始化 computed
会调用 initComputed
函数
注册一个 watcher
实例,并在内实例化一个 Dep
消息订阅器用作后续收集依赖(比如渲染函数的 watcher
或者其他观察该计算属性变化的 watcher
)
调用计算属性时会触发其Object.defineProperty
的get
访问器函数
调用 watcher.depend()
方法向自身的消息订阅器 dep
的 subs
中添加其他属性的 watcher
调用 watcher
的 evaluate
方法(进而调用 watcher
的 get
方法)让自身成为其他 watcher
的消息订阅器的订阅者,首先将 watcher
赋给 Dep.target
,然后执行 getter
求值函数,当访问求值函数里面的属性(比如来自 data
、props
或其他 computed
)时,会同样触发它们的 get
访问器函数从而将该计算属性的 watcher
添加到求值函数中属性的 watcher
的消息订阅器 dep
中,当这些操作完成,最后关闭 Dep.target
赋为 null
并返回求值函数结果。
当某个属性发生变化,触发 set
拦截函数,然后调用自身消息订阅器 dep
的 notify
方法,遍历当前 dep
中保存着所有订阅者 wathcer
的 subs
数组,并逐个调用 watcher
的 update
computed
는 initCompulated
함수를 호출합니다
계산된 속성이 호출되면 해당 Object.defineProperty
의 get
접근자 함수 #🎜🎜 ##이 트리거됩니다. 🎜🎜#watcher.dependent()
메서드를 호출하여 자체 메시지 구독자 의 <code>subs
에 다른 속성을 추가합니다. dep code>watcher
watcher
의 evaluate
메서드를 호출합니다(그리고 그런 다음 watcher
의 get
메소드를 호출하여 자신을 다른 watcher
메시지 구독자의 구독자로 만들고, 먼저 watcher
를 할당합니다. code>를 Dep .target
으로 변환한 다음 평가 함수의 속성(예: data
에서)에 액세스할 때 getter
평가 함수를 실행합니다. props
또는 기타 계산
) 시 해당 get
접근자 함수도 트리거되어 계산된 항목의 watcher
를 추가합니다. watcher
의 메시지 구독자 dep
에서 이러한 작업이 완료되면 마지막으로 Dep.target
을 닫고 null
에 할당하고 평가 함수 결과를 반환합니다. #🎜🎜##🎜🎜##🎜🎜##🎜🎜##🎜🎜#속성이 변경되면 set
차단 기능을 트리거한 다음 자체 메시지 구독자 dep의 <code>notify
메소드는 현재 dep
의 모든 구독자 wathcer
를 보유하는 subs
배열을 순회합니다. watcher
의 update
메소드를 하나씩 호출하여 응답 업데이트를 완료합니다. #🎜🎜##🎜🎜##🎜🎜##🎜🎜#관련 추천: #🎜🎜##🎜🎜##🎜🎜#javascript - 티켓 브러셔의 구현 원리#🎜🎜##🎜🎜##🎜 🎜# #🎜🎜##🎜🎜#thinkphp 컨트롤러에서 display() 단계를 구현하는 원리#🎜🎜##🎜🎜##🎜🎜##🎜🎜#위 내용은 Vue에서 계산의 구현 원리는 무엇입니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!