Although the current technology stack has been transferred from Vue to React, the actual experience of developing multiple projects using Vue is still very pleasant. The Vue documentation is clear and standardized, the API design is simple and efficient, and it is friendly to front-end developers. It’s quick to get started, and I even personally think that using Vue in many scenarios is more efficient than React development. I have studied the source code of Vue intermittently before, but I have never summarized it, so I will make some technical summary here and deepen my understanding of Vue. So What I want to write about today is the implementation principle of computed
, one of the most commonly used APIs in Vue.
Without further ado, a basic example is as follows:
<div> <p>{{fullName}}</p> </div>
new Vue({ data: { firstName: 'Xiao', lastName: 'Ming' }, computed: { fullName: function () { return this.firstName + ' ' + this.lastName } } })
In Vue we don’t need to calculate directly in the template{{this. firstName ' ' this.lastName}}
, because putting too much declarative logic in the template will make the template itself overweight, especially when a large number of complex logical expressions are used to process data in the page, which will affect the page. It has a great impact on the maintainability, and computed
is designed to solve such problems.
watch
Of course, many times when we use computed
, we often compare it with another API in Vue, which is the listenerwatch
In comparison, because they are consistent in some aspects, both are based on Vue's dependency tracking mechanism. When a certain dependency data changes, all related data or functions that depend on this data will automatically changes or calls.
watch
option. This approach is most useful when you need to perform asynchronous or expensive operations when data changes. From the explanation of watch
in Vue official documentation, we can understand that using the watch
option allows us to perform asynchronous operations (accessing an API) or high-performance operations, limiting us How often to perform this operation and set up intermediate states before we get the final result, which is something that computed properties cannot do.
The following also summarizes a few additional points about the differences between computed
and watch
:
computed
is to calculate a new property and mount the property on the vm (Vue instance), while watch
is to monitor the existing and mounted to vm
data, so watch
can also be used to monitor changes in computed
calculated properties (others include data
, props
)
computed
It is essentially a lazy-evaluated observer with cacheability. Only when the dependency changes, the computed
attribute is accessed for the first time. , the new value will be calculated, and watch
will call the execution function when the data changes
In terms of usage scenarios, computed
applies to one data being affected by multiple data, and watch
applies to one data affecting multiple data;
We have learned about the abovecomputed
There are some differences and usage scenarios between watch
. Of course, sometimes the two are not so clear and strict. In the end, it is necessary to analyze different businesses specifically.
Let’s get back to the topic of the article computed
. In order to have a deeper understanding of the internal mechanism of computed properties, let us explore Vue step by step Let’s talk about its implementation principle in the source code.
Before analyzing the computed
source code, we must first have a basic understanding of Vue's responsive system. Vue calls it a non-intrusive responsive system, and the data model is just ordinary JavaScript objects, and when you modify them, the view automatically updates.
When you pass an ordinary JavaScript object to the
data
option of the Vue instance, Vue will traverse all the properties of the object. And use Object.defineProperty
to convert all these properties into getter/setter
. These getter/setter
are invisible to the user, but internally they Let Vue track dependencies and notify changes when properties are accessed and modified. Each component instance has a corresponding watcher
instance object. It will record the properties as dependencies during the component rendering process, and then when the dependencies When an item's setter
is called, it notifies the watcher
to recompute, causing its associated components to be updated. Vue response system has three core points: 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()
计算求值。
When the component is initialized, computed
and data
will establish their own response systems respectively, Observer
Traverse each attribute setting in data
get/set
Data interception
Initializationcomputed
will call the initComputed
function
to register a watcher
instance, and Instantiate a Dep
message subscriber internally for subsequent collection dependencies (such as the watcher
of the rendering function or other watcher
that observes changes in the calculated property)
When a computed property is called, its Object.defineProperty
's get
accessor function
is triggeredwatcher.depend()
method adds the watcher
of other attributes to the
subs
of its own message subscriber
Call the evaluate
method of watcher
(and then call the get
method of watcher
) to make itself the other Subscribers of watcher
's message subscribers first assign watcher
to Dep.target
, and then execute the getter
evaluation function. When accessing the evaluation When attributes inside the function (such as from data
, props
or other computed
), their get
accessor functions will also be triggered. Add the watcher
of the computed property to the message subscriber dep
of the watcher
of the property in the evaluation function, and finally close when these operations are completed Dep.target
is assigned to null
and returns the evaluation function result.
When a certain attribute changes, trigger the set
interception function, and then call the of its own message subscriber
dep notify
method, traverses the subs
array that holds all subscribers wathcer
in the current dep
, and calls watcher
one by one. ##update method to complete the response update.
javascript - The implementation principle of the ticket brusher
thinkphp controller The principle of display() step implementation
The above is the detailed content of What is the implementation principle of computed in Vue?. For more information, please follow other related articles on the PHP Chinese website!