This article contains about 2000 words and takes about 6 minutes to read

Understanding this article requires some knowledge of Vue2 responsive source code

background

The department wanted to develop a small program, and uni-App was finally selected as the framework of the small program when the technology stack was all Vue at that time

At this time, Vue3 was just released in beta and Taro3 was not yet out of the picture. In order to enjoy the benefits of composition-API, we targeted the Composition-API plugin

You can use this plugin to make Vue2 support some of the composition-API features

This part of the function is the root cause of the problem

The phenomenon of

In order to simplify the cognitive cost, two simple demos were written with Vue2 technology stack, respectively using composition-API plugin and optional-API to achieve similar functions

composition-api plugin

<template>
  <div>
    <div>Composition Api Plugin</div>
    <div>reactiveObj: {{ reactiveObj }}</div>
    <div>computedValue: {{ computedValue }}</div>
    <button @click="onClick">add a</button>
  </div>
</template>

<script>
import { reactive ,defineComponent,computed,set } from '@vue/composition-api'

export default defineComponent({
  setup(){
    const reactiveObj = reactive({})
    const computedValue = computed(() = > {
      return reactiveObj.a
    })
    return {
      reactiveObj,
      computedValue,
      onClick: () = > {
        set(reactiveObj,'a'.1)}}}})</script>
Copy the code

optional-api

<template>
  <div id="app">
    <div>OptionalApi</div>
    <div>reactiveObj: {{ reactiveObj }}</div>
    <div>computedValue: {{ computedValue }}</div>
    <button @click="onClick">add a</button>
  </div>
</template>

<script>
import Vue from "vue";

export default {
  props: {
    msg: String
  },
  data(){
    return {
      reactiveObj: {}}}.computed: {computedValue(){
      return this.reactiveObj.a
    }
  },
  methods: {onClick(){
      Vue.set(this.reactiveObj,'a'.1)}}}</script>
Copy the code

The results of

Define an empty reactive object reactiveObj, a compute property computedValue, whose value is Reactiveobj.a

Because Vue2 directly sets a non-existent key to the reactive object, the view cannot be updated, so vue. set actively notifys the reactiveObj subscription to trigger the update

Thus theoretically the value of the computedValue would be updated to eventually display the number 1 on the page

To see the results

All reactiveObj were successfully updated, while only the optional-API version of computedValue was updated. Why?

Analysis of the

Find the source of the problem

Open Vue DevTools to view the data source

You can see that the version of the Composition-API Plugin computedValue data level has not been updated either

Further investigation, there are no more than two reasons why the computedValue is not updated

  1. Vue.setreactiveObjAfter assignment, there is no notificationreactiveObjSubscription updates for
  2. Vue.setNotice thereactiveObjSubscription updates, howevercomputedValueNo update received

To see why, set a breakpoint on vue. set and look at the subscriptions collected in reactiveObj at this point (subs array below)

As you can see, reactiveObj only collects one subscription before calling vue.set, which is a render Watcher (the view-specific watcher, which is rerendered when the render Watcher is updated) from the value of the expression field.

The problem here is that because a computed function is called during setup to collect reactiveObj as a dependency, there should theoretically be a subscription in reactiveObj, a computational watcher called computedValue

The correct logic should be the following:

In optional-API debugging, it can be found that the reactiveObj does store two subscriptions, namely the rendering watcher and the calculation Watcher named computedValue

Missing subscriptions from reactiveObj can be pushed out, which should be the second case mentioned earlier, i.e

The calculated properties of the composition-API plugin versioncomputedValueNo collectionreactiveObjAs a dependency, so no matterreactiveObjHow to update,computedValueIt can’t be updated

Analyze the cause of the problem

Once you find the source of the problem, then analyze it

Why does computedValue not collect reactiveObj as a dependency?

Here’s how Vue relies on collecting updates

Vue2 returns a responsive Object by calling Object. DefineProperty underneath to intercept the getter/setter for the Object via its internal wrapper function defineReactive

When evaluating properties that access (depend on) reactive objects, the getter of the reactive object is fired, adding a subscription to __ob__.dep.subs of the object

When the value of a key is changed, all subscriptions in its subs are iterated and updated

We give the two versions of calculated properties break points to see the behavior of calculated properties dependent collection

composition-api plugin

You can see that when reactiveobj.a is accessed, the getter for reactiveObj is not fired, so no dependencies are collected, that is, the calculated property value is a constant

optional-api

As you can see from the figure, the optional-API version triggers the getter for reactiveObj

Analyzing reactive functions

Which brings us to the final question: why do reactive functions created in the composition-API plugin not trigger getters?

Here again, through the source code to see the underlying implementation, the core of the reactive implementation in composition – API plugin SRC/reactivity/reactive ts line 247

Aside from some edge judgment, the composition-API plugin provides reactive functions that are the syntax sugar of vue.Observable

Those familiar with Vue2 should know that the object returned by vue. Observable is not itself responsive, and this is clearly noted in the official documentation

That is, the reactiveObj created by the composition-API plugin (reactive) is not itself a reactive object, with no getters or setters.

In optional-API, the reactiveObj itself is reactive because the optional-API performs recursive reactive binding on the entire data when the component is initialized

Therefore, in the composition-API plugin, computedValue cannot collect dependencies from an ordinary object, whereas in the optional API, because reactiveObj is reactive, computedValue can collect dependencies normally

digression

In the example, reactiveObj is an empty object and the view is not updated because a nonexistent value was set

What if it’s a given key? Modify the code to pre-add a key named A to reactiveObj

<template>
  <div>
    <div>Composition Api Plugin</div>
    <div>reactiveObj: {{ reactiveObj }}</div>
    <div>computedValue: {{ computedValue }}</div>
    <button @click="onClick">add a</button>
  </div>
</template>

<script>
import { reactive ,defineComponent,computed } from '@vue/composition-api'

export default defineComponent({
  setup() {
    const reactiveObj = reactive({
      a: 1  // Pre-added key
    })
    const computedValue = computed(() = > {
      return reactiveObj.a
    })
    return {
      reactiveObj,
      computedValue,
      onClick: () = > {
        reactiveObj.a = 2}}}})</script>
Copy the code

Even successful updates!

The object returned by reactive is not reactive, but it is bound to a known key

This means that when the calculated property accesses reactiveObj. A, it will trigger the getter of A, and finally implement the dependency collection

However, if you pass an empty Object to Reactive, the composition-API plugin will still use object.defineProperty to intercept getters and setters for reactive binding and will not be able to handle non-existent keys

The solution is simple: use the Vue3 version reactive

Its underlying implementation is based on a Proxy, which proxies the entire object and can intercept almost any operation on the object, including the creation of a nonexistent key

<template>
  <div>
    <div>Vue3</div>
    <div>reactiveObj: {{ reactiveObj }}</div>
    <div>computedValue: {{ computedValue }}</div>
    <button @click="onClick">add a</button>
  </div>
</template>

<script>
import { reactive ,defineComponent,computed } from 'vue'

export default defineComponent({
  setup() {
    const reactiveObj = reactive({})
    const computedValue = computed(() = > {
      return reactiveObj.a
    })
    return {
      reactiveObj,
      computedValue,
      onClick: () = > {
        reactiveObj.a = 1}}}})</script>
Copy the code

conclusion

Computing attributes requires collecting dependencies for data updates, and the prerequisite for collecting dependencies must be responsive

Objects created using reactive functions in the composition-API plugin behave slightly differently from objects defined in data in the optional-API

  • The former creates an object that is not itself reactive, but has a recursive reactive binding on its known keys
  • The latter will bind all values in data recursively and responsively when the component is initialized, so it passesthis.reactiveObjThe getter collection dependency is triggered

The composition-API plugin’s reactive functions behave slightly differently from Vue3’s reactive functions

  • The former returns a source object and cannot bind a nonexistent key in a reactive manner
  • The latter returns a new proxy object that allows reactive binding of nonexistent keys