Home  >  Article  >  Web Front-end  >  What is the implementation principle of computed in Vue?

What is the implementation principle of computed in Vue?

不言
不言Original
2018-09-14 16:10:063873browse
The content of this article is about the implementation principle of computed in Vue? It has certain reference value. Friends in need can refer to it. I hope it will be helpful to you.

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.

Basic introduction

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.

Comparing listenerswatch

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.

While computed properties are more appropriate in most cases, sometimes a custom listener is needed. That's why Vue provides a more general way to respond to data changes via the 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:

  1. 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)

  2. 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

  3. 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.

Principle Analysis

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.

What is the implementation principle of computed in Vue?

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:

  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() 计算求值。

After analyzing all the steps, let’s summarize the entire process:

  1. When the component is initialized, computed and data will establish their own response systems respectively, Observer Traverse each attribute setting in dataget/set Data interception

  2. Initializationcomputed will call the initComputed function

    1. 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)

    2. When a computed property is called, its Object.defineProperty's get accessor function

    3. is triggeredwatcher.depend() method adds the watcher of other attributes to the subs of its own message subscriber

      dep
    4. 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.

  3. 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.

Related recommendations:

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!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn