preface

The subscriber collection process is covered in detail in this column, but it leaves out how to collect subscribers when the publisher’s value is an object or array, which can cause performance problems. Of course, this is not a problem with Vue per se.

Review the core process of subscriber collection

As described in this column, the getter function is triggered when data is read, and subscribers are collected in the getter function. The getter function is defined in the defineReactive function.

function defineReactive(obj, key, val, customSetter, shallow) { var dep = new Dep(); var getter = property && property.get; var setter = property && property.set; if ((! getter || setter) && arguments.length === 2) { val = obj[key]; } var childOb = ! shallow && observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { var value = getter ? getter.call(obj) : val; if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); } } } return value }, set: function reactiveSetter(newVal) { } }); }Copy the code

In the defineReactive function. Instantiate the Dep class and assign the instantiated object to the constant Dep. Run var childOb =! Shallow && Observe (val), observe(val) if the value of the monitored data is an object or data, and assign the return value to the constant childOb.

The getter function is defined in defineReactive and references the constants DEp and childOb to form a closure. The constants DEP and childOb are always in memory and can be used by the getter function triggered later when the publisher is read.

Where is childOb’s DEp assigned? Go to the observe function to find the answer.

function observe(value, asRootData) { if (! isObject(value) || value instanceof VNode) { return } var ob; if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__; } else if ( shouldObserve && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && ! value._isVue ) { ob = new Observer(value); } if (asRootData && ob) { ob.vmCount++; } return ob }Copy the code

Observe that the observe function returns a constant ob, which is assigned to the new Observer(value) instantiation object for the first time.

var Observer = function Observer(value) { this.value = value; this.dep = new Dep(); this.vmCount = 0; def(value, '__ob__', this); if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); } else { this.walk(value); }}Copy the code

Inside this.dep = new dep () is assigned to this.dep, so dep on ob, that is, childOb, is a DEP class instantiation object.

When the publisher is read, the getter function is triggered, executing dep.depend() triggers the publisher to collect the subscriber itself, and if the publisher’s value is an object or array, then childOb exists, executing childob.dep.depend () triggers the publisher’s value to collect the subscriber itself.

DependArray (value) if (array.isarray (value)) if the publisher value is an Array, the dependArray(value) function must be executed.

function dependArray(value) { for (var e = (void 0), i = 0, l = value.length; i < l; i++) { e = value[i]; e && e.__ob__ && e.__ob__.dep.depend(); if (Array.isArray(e)) { dependArray(e); }}}Copy the code

Execute value[I] to get the subitem assigned to e, e && E.__ob__ && E.__ob__.dep.depend (), E. __ob__.dep.depend() triggers the array subitems themselves to collect subscribers.

Either childob.dep.Depend () or dependArray(value) triggers its own collection of subscribers.

$set = Vue; $set = Vue; $set = Vue; $set = Vue

At the end of the vm.$set instance method, ob.dep.notify() is executed to notify the subscribers that the object or array type data itself collects to update the response.

So in the getter for the publisher, if the publisher’s value is an object or array you collect the subscribers of the value itself. A dependArray function is recursively invoked to collect the subscribers of an object or array.

How does the collection process cause performance problems

The key to the performance problem is the implementation of dependArray(value). Is caused by reading data of array type (publisher) in a V-for loop. This is illustrated with a real business scenario.

In an exam simulation function, the question bank back-end interface provides an array of more than 3000 questions, each time 100 questions are randomly selected to form a set of papers.

Questions is defined to store the question bank returned by the back end, which is an array set with data structure as shown in the figure below

Questions: [{tid: 1, // title id content: 'XXXX ', // title description options: [' option 1',' option 2', 'option 3',' option 4'], // description of each option}]Copy the code

QuestionIds is defined to store 100 integers randomly obtained from 0 to 2999, and these numbers are used as the subscript of the array to get the corresponding questions in questions.

Display the contents of an exam paper on the page. The implementation code is as follows:

<template> <div> <div v-for="item in questionIds"> <div class="p-question-content">{{questions[item].content}}</div> <div class="p-question-options"> <span v-for = "a in questions[item].options">{{a}}</span> </div> </div> </div> </template> <script> export default {data(){return {questionIds: [0,1,4,5... 100] questions: [{tid: 1, // topic id content: 'XXXX ', // topic description options: [' option 1',' option 2',' option 3',' option 4'], // description of each option}, {tid: 2, // topic id content:' XXXX ', // topic id content: 'XXXX', / / title describes the options: [' option 1 ', 'option 2', 'option 3', 'option 4'], / / the description of each option}, / /... , } } } </script>Copy the code

The above code looks fine, and the page displays normally. Let’s count the number of cycles in the rendering process, 100 topics, each topic has 4 options tentatively, so there should be only 400 cycles.

Is there really only 400 cycles? In fact, the internal experience at least 3000 * 5 * 100 a total of 1.5 million cycles. Isn’t that a bit of an exaggeration, where there are so many loops. This is the loop generated by collecting subscribers in the getter function. Let’s look at the logic of collecting subscribers in the getter function:

if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); }}}Copy the code

When questions is accessed in the template, the following logic is triggered:

  • In this scenario it isDep.targetTo render Watcher (render subscriber), executedep.depend()The triggerquestionsOwn collection of render subscribers.
  • becausequestionsIs an array, sochildObIf yes, runchildOb.dep.depend()The triggerquestionsThe values themselves are collected by render subscribers.
  • becausequestionsIs an array, so executeif (Array.isArray(value))If the conditions are met, run the commanddependArray(value).

A dependArray function is used to create multiple loops.

function dependArray(value) { for (var e = (void 0), i = 0, l = value.length; i < l; i++) { e = value[i]; e && e.__ob__ && e.__ob__.dep.depend(); if (Array.isArray(e)) { dependArray(e); }}}Copy the code

The dependArray function circulates the value of questions. If the child of questions is also an array, invoke the dependArray function again.

A dependArray(value) will be repeated at least 3000 times if “questions” is set to 3000.

Questions in the template have been read for 5 times in a V-for =”item in questionIds” cycle, that is, the cycle has been at least 15000 times. Given the size of the questionIds array is 100, rendering an exam paper must loop at least 1.5 million times.

Increasing from 400 cycles on the surface to 1.5 million cycles on the inside can cause significant performance problems.

How to solve this performance problem

Solving this performance problem is simple. Do not use the array type in the template’s V-for loop. If you must, freeze the array type with object.freeze (). The implementation code is as follows:

<template> <div> <div v-for="item in questionIds"> <div class="p-question-content">{{questions[item].content}}</div> <div class="p-question-options"> <span v-for = "a in questions[item].options">{{a}}</span> </div> </div> </div> </template> <script> export default {data(){return {questionIds: [0,1,4,5... 100] [],}}, mounted(){const arr = [{tid: 1, // [' option 1 ', 'option 2', 'option 3', 4 'option'], / / the description of each option}, {dar: 2, id / / subject content: 'XXXX', / / title describes the options: [' option 1 ', 'option 2', 'option 3', 4 'option'], / / the description of each option},] this. Questions = Object. Freeze (arr)}} < / script >Copy the code