background
It’s been almost a year since the official release of Vue 3, and I believe many people are already using Vue 3 in production. Today, vue.js 3.2 has been released, and this minor update is mainly about source level optimization, but it doesn’t really change much at the user level. One of the things that appealed to me was the performance of responsiveness:
- More efficient ref implementation (~260% faster read / ~50% faster write)
- ~40% faster dependency tracking
- ~17% less memory usage
This translates to an increase in read efficiency of approximately 260%, write efficiency of approximately 50%, and dependent collection efficiency of approximately 40%, while reducing memory usage by approximately 17%.
This is a huge optimization, since reactive systems are one of the core implementations of Vue.js, and optimizing them means optimizing the performance of all apps developed using vue.js.
Moreover, this optimization was not implemented by Vue officials, but by @Basvanmeurs, a community leader. The relevant optimization code was submitted on October 9, 2020, but due to a big change to the internal implementation, the official waited until vue.js 3.2 was released. To put the code in.
Basvanmeurs’ responsive performance improvements were a real surprise. Not only did they improve the runtime performance of Vue 3, but the core code was contributed by the community, which meant that Vue 3 was getting more and more attention. With some strong developers involved in the core code contribution, Vue 3 can go a long way.
As we know, compared to Vue 2, Vue 3 has been optimized in many aspects, one part of which is that the data response implementation has been changed from Object. DefineProperty API to Proxy API.
When Vue 3 was initially advertised, it was officially announced that the performance of the responsive implementation had been optimized, so what are the aspects of the optimization? Some people think that the performance of Proxy API is better than that of Object. DefineProperty, but in fact, the performance of Proxy is worse than that of Object. DefineProperty. For details, please refer to Thoughts on ES6 Proxies Performance. I have also tested this and reached the same conclusion. Please refer to the Repo.
If Proxy is slow, why did Vue 3 choose it to implement data response? Because a Proxy is essentially hijacking an object, it can listen not only for changes in the value of a property of the object, but also for additions and deletions of properties. Object.defineproperty adds getters and setters to an existing property of an Object, so it can only listen for changes in the value of the property, not for new or deleted properties.
The performance optimization of responsiveness is actually reflected in turning objects nested at a deeper level into responsive scenarios. In the Vue 2 implementation, when the data is changed to responsive during the component initialization phase, if the child property is still an Object, the Object. DefineProperty defines the responsive of the child Object recursively. In the implementation of Vue 3, only when the object attribute is accessed, the type of the sub-attribute is judged to decide whether to implement reactive recursively. In fact, this is a delayed definition of reactive sub-object implementation, which will improve the performance to some extent.
Therefore, compared with Vue 2, Vue 3 does make some optimization in the responsive implementation, but the effect is actually limited. And vue.js 3.2 in the response performance optimization, is really a qualitative leap, next we will come to some hard dish, from the source code level analysis of specific optimization, as well as the technical level of thinking behind these optimization.
Reactive implementation principle
Reactive means that when we modify the data, we can automatically do something; The rendering of the corresponding component automatically triggers the rerendering of the component after modifying the data.
Vue 3 implements responsiveness, which essentially hijacks reading and writing data objects through the Proxy API. When we access the data, the getter is triggered to perform dependency collection. Setter dispatch notifications are triggered when data is modified.
Next, let’s briefly examine implementations (prior to vue.js 3.2) that rely on collecting and distributing notifications.
Depend on the collection
The core of the dependency collection process is to invoke the getter function when accessing the responsive data, and then execute the track function to collect the dependency:
let shouldTrack = true
// The current active effect
let activeEffect
// The original data object map
const targetMap = new WeakMap(a)function track(target, type, key) {
if(! shouldTrack || activeEffect ===undefined) {
return
}
let depsMap = targetMap.get(target)
if(! depsMap) {// Each target corresponds to a depsMap
targetMap.set(target, (depsMap = new Map()))}let dep = depsMap.get(key)
if(! dep) {// Each key corresponds to a deP set
depsMap.set(key, (dep = new Set()))}if(! dep.has(activeEffect)) {// Collect the currently active effect as a dependency
dep.add(activeEffect)
// The currently active effect collects deP sets as dependencies
activeEffect.deps.push(dep)
}
}
Copy the code
Before we look at the implementation of this function, let’s think about what dependencies we want to collect. Our goal is to implement responsiveness, which is to automatically do something as the data changes, such as executing certain functions. So the dependencies we collect are side effects that are executed after the data changes.
The track function takes three arguments, where target represents the original data; Type indicates the type of the dependency collection; Key indicates the accessed property.
The track function creates a global targetMap as the Map of the original data object, with the key being target and the value depsMap as the dependent Map. The key of the depsMap is the key of the target, and the value is the deP set, which stores the dependent side effects. For ease of understanding, the relationship between them can be shown in the following figure:
Therefore, each time the track function is executed, the currently activeEffect side effect function is collected as a dependency, and then the dependency set dep under the corresponding key of the depsMap related to the target is collected.
Distributed notification
Notification dispatch occurs in the stage of data update. The core is to trigger the setter function when modifying the responsive data, and then execute the trigger function to dispatch notification:
const targetMap = new WeakMap(a)function trigger(target, type, key) {
// Obtain the set of dependencies corresponding to the target from the targetMap
const depsMap = targetMap.get(target)
if(! depsMap) {// Return without dependencies
return
}
// Create a running effects collection
const effects = new Set(a)// Add the effects function
const add = (effectsToAdd) = > {
if (effectsToAdd) {
effectsToAdd.forEach(effect= > {
effects.add(effect)
})
}
}
/ / SET | ADD | DELETE operation, one of the corresponding effects
if(key ! = =void 0) {
add(depsMap.get(key))
}
const run = (effect) = > {
// Schedule execution
if (effect.options.scheduler) {
effect.options.scheduler(effect)
}
else {
// Run directly
effect()
}
}
// Iterate over the effects
effects.forEach(run)
}
Copy the code
The trigger function takes three arguments, where target represents the original object to be targeted; Type indicates the type of update. Key indicates the attribute to be modified.
The trigger function does four main things:
-
Select * from targetMap; select * from targetMap;
-
Create a running effects collection;
-
Select effect from depsMap by key and add it to effects set;
-
Iterating effects executes the associated side effect functions.
So each time you execute the trigger function, you iterate through the targetMap to find all the associated side effects based on the target and key.
In the process of describing dependency collection and dispatch notifications, we both mentioned the word side effect function. In dependency collection, we refer to the activeEffect function as a dependency collection. What is it? Let’s take a look at the side effect function.
Side effect function
So, what is a side effect function? Before introducing it, let’s go back to the original requirement of responsiveness, which is that we change the data to automatically do something. Here’s a simple example:
import { reactive } from 'vue'
const counter = reactive({
num: 0
})
function logCount() {
console.log(counter.num)
}
function count() {
counter.num++
}
logCount()
count()
Copy the code
We defined the responsive object counter, and then we accessed counter. Num in logCount. We wanted the logCount function to be automatically executed when the count function was executed to change the value of counter.
Based on our previous analysis of the dependency collection process, if logCount were an activeEffect, this would be possible, but it is not possible because when the code is executed on the console.log(counter.num) line, It doesn’t know anything about its run in the logCount function.
So what to do? Before we run the logCount function, we assign the value of logCount to the activeEffect:
activeEffect = logCount
logCount()
Copy the code
With this in mind, we can use the idea of higher-order functions to encapsulate logCount:
function wrapper(fn) {
const wrapped = function(. args) { activeEffect = fn fn(... args) }return wrapped
}
const wrappedLog = wrapper(logCount)
wrappedLog()
Copy the code
Wrapper itself is a function that accepts fn as an argument, returns a new function wrapped, and maintains a global variable Called activeEffect. When wrapped executes, set activeEffect to fn and execute fn.
After wrappedLog is executed and counter. Num is changed, the logCount function is automatically executed.
Vue 3 takes a similar approach and has an effect function inside it. Let’s look at its implementation:
// The global effect stack
const effectStack = []
// The current active effect
let activeEffect
function effect(fn, options = EMPTY_OBJ) {
if (isEffect(fn)) {
// If fn is already an effect function, it refers to the original function
fn = fn.raw
}
// Create a wrapper, which is a function that responds to side effects
const effect = createReactiveEffect(fn, options)
if(! options.lazy) {// In the lazy configuration, the calculated property is used. In the non-lazy configuration, the property is executed once
effect()
}
return effect
}
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect() {
if(! effect.active) {// If the function is not scheduled, then the original function is executed directly.
return options.scheduler ? undefined : fn()
}
if(! effectStack.includes(effect)) {// Clear the dependencies of the effect reference
cleanup(effect)
try {
// Enable global shouldTrack to allow dependency collection
enableTracking()
/ / pressure stack
effectStack.push(effect)
activeEffect = effect
// Execute the original function
return fn()
}
finally {
/ / out of the stack
effectStack.pop()
// Restore the state when shouldTrack was enabled
resetTracking()
// Point to the last effect on the stack
activeEffect = effectStack[effectStack.length - 1]
}
}
}
effect.id = uid++
// The id is an effect function
effect._isEffect = true
// Effect state
effect.active = true
// Wrap the original function
effect.raw = fn
// The dependency is a bidirectional pointer that contains a reference to effect, and effect contains a reference to the dependency
effect.deps = []
// Configure effect
effect.options = options
return effect
}
Copy the code
In combination with the above code, effect internally creates a new effect by executing the createReactiveEffect function. To distinguish it from the external effect function, we call it reactiveEffect. I’ve also added some additional attributes (which I’ve noted in the comments). In addition, the effect function supports passing in a configuration parameter to support more features, which I won’t expand here.
The reactiveEffect function is the reactive side effect function that is executed when the trigger process dispatches the notification.
According to our previous analysis, the reactiveEffect function only needs to do two things: let the global activeEffect point to it, and then execute the wrapped original function fn.
In fact, it’s a little more complicated to implement. First, it determines whether effect’s state is active, which is actually a control that allows the original function fn to be executed and returned in non-active state and unscheduled execution.
We then determine if effect is included in the effectStack, and if it is not, we push it onto the stack. As mentioned earlier, all you need to do is set activeEffect = effect, so why design a stack structure?
Consider the following nested effect scenario:
import { reactive} from 'vue'
import { effect } from '@vue/reactivity'
const counter = reactive({
num: 0.num2: 0
})
function logCount() {
effect(logCount2)
console.log('num:', counter.num)
}
function count() {
counter.num++
}
function logCount2() {
console.log('num2:', counter.num2)
}
effect(logCount)
count()
Copy the code
If we only assign reactiveEffect to activeEffect every time we execute effect, then for this nested scenario, after effect(logCount2), The activeEffect function is returned by effect(logCount2), so that subsequent visits to counter. Num rely on collecting the corresponding activeEffect function. Instead of executing logCount, we will execute logCount2, and the final output will be as follows:
num2: 0
num: 0
num2: 0
Copy the code
What we should expect is the following:
num2: 0
num: 0
num2: 0
num: 1
Copy the code
So for the nested effect scenario, instead of simply assigning the value activeEffect, we should consider that the execution of the function itself is a push-off operation, so we can also design an effectStack so that every time we enter the reactiveEffect function we push it first. And then activeEffect points to this reactiveEffect function, and then after fn executes, it goes off the stack, and then activeEffect points to the last element of the effectStack, Which is the reactiveEffect corresponding to the outer effect function.
Another detail we notice here is that a cleanup function is executed to clear the reactiveEffect dependencies before pushing the stack. When the track function is executed, in addition to collecting the currently activeEffect as a dependency, the activeeffect.deps.push (dep) is used to make dep as a dependency on the activeEffect. In this way, we can find the deP of effect at cleanup and remove effect from the deP. The code for the cleanup function looks like this:
function cleanup(effect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0}}Copy the code
Why cleanup? If you encounter this scenario:
<template> <div v-if="state.showMsg"> {{ state.msg }} </div> <div v-else> {{ Math.random()}} </div> <button @click="toggle">Toggle Msg</button> <button @click="switchView">Switch View</button> </template> <script> import { reactive } from 'vue' export default { setup() { const state = reactive({ msg: 'Hello World', showMsg: true }) function toggle() { state.msg = state.msg === 'Hello World' ? 'Hello Vue' : 'Hello World' } function switchView() { state.showMsg = ! state.showMsg } return { toggle, switchView, state } } } </script>Copy the code
This component’s View will display MSG or a random number based on the control of the showMsg variable, which will be changed when we click the Switch View button.
If there is no cleanup, activeEffect is the component’s side effect function when rendering the template for the first time. Because the template render accesses state.msg, the dependency collection will be performed, and the side effect function will be the dependency of state.msg. We call it the Render effect. Then we click the Switch View button, the View is switched to display random number, at this time we click the Toggle Msg button, because the state. Msg is modified, it will send a notification, find the Render effect and execute, it will trigger the component to re-render.
This behavior is actually not as expected, because when we click the Switch View button and the View switches to display random numbers, it also triggers a rerendering of the component, but the View does not render state. MSG at this time, so changes to it should not affect the rerendering of the component.
So if we cleanup dependencies by cleanup before the component’s render effect is executed, we can remove the render effect dependencies collected by state.msg. This way when we modify state. MSG, the component will not be rerendered because there are no dependencies, as expected.
Optimization of reactive implementation
After analyzing the responsive implementation principle, everything seems to be OK, so what else can be optimized?
Optimizations that rely on collection
Currently, every side effect function execution requires a cleanup of dependencies and then a re-collection of dependencies during side effect execution, which involves a lot of adding and removing of sets. In many scenarios, dependencies are rarely changed, so there is room for optimization.
To reduce collection additions and deletions, we need to identify the state of each dependent collection, such as whether it is newly collected or has already been collected.
So here we need to add two properties to the collection DEp:
export const createDep = (effects) = > {
const dep = new Set(effects)
dep.w = 0
dep.n = 0
return dep
}
Copy the code
W indicates whether the collection has been made, and n indicates whether the collection is new.
Then design several global variables, effectTrackDepth, trackOpBit, and maxMarkerBits.
Where effectTrackDepth represents the depth of recursively nested execution of effect function; TrackOpBit is used to identify the status of dependency collection; MaxMarkerBits Indicates the number of bits of the maximum token.
Here’s how they work:
function effect(fn, options) {
if (fn.effect) {
fn = fn.effect.fn
}
// Create the _effect instance
const _effect = new ReactiveEffect(fn)
if (options) {
// Copy the properties from options to _effect
extend(_effect, options)
if (options.scope)
// effectScope related processing logic
recordEffectScope(_effect, options.scope)
}
if(! options || ! options.lazy) {// Execute immediately
_effect.run()
}
// Bind the run function as the effect Runner
const runner = _effect.run.bind(_effect)
// Keep the reference to _effect in runner
runner.effect = _effect
return runner
}
class ReactiveEffect {
constructor(fn, scheduler = null, scope) {
this.fn = fn
this.scheduler = scheduler
this.active = true
// Effect stores the associated DEPS dependencies
this.deps = []
// effectScope related processing logic
recordEffectScope(this, scope)
}
run() {
if (!this.active) {
return this.fn()
}
if(! effectStack.includes(this)) {
try {
/ / pressure stack
effectStack.push((activeEffect = this))
enableTracking()
// Record bits according to the depth of the recursion
trackOpBit = 1 << ++effectTrackDepth
// If maxMarkerBits is exceeded, the trackOpBit computation will exceed the maximum integer number of digits, and demote to cleanupEffect
if (effectTrackDepth <= maxMarkerBits) {
// Mark dependencies
initDepMarkers(this)}else {
cleanupEffect(this)}return this.fn()
}
finally {
if (effectTrackDepth <= maxMarkerBits) {
// Finish the dependency tag
finalizeDepMarkers(this)}// Restore to the upper level
trackOpBit = 1 << --effectTrackDepth
resetTracking()
/ / out of the stack
effectStack.pop()
const n = effectStack.length
// Point to the last effect on the stack
activeEffect = n > 0 ? effectStack[n - 1] : undefined}}}stop() {
if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false}}}Copy the code
As you can see, the effect implementation has been modified to create an instance of _effect internally using the ReactiveEffect class, and the runner returned by the function refers to the run method of the ReactiveEffect class.
When the side effect function is executed, the run function is actually executed.
When the run function is executed, we notice that the cleanup function is no longer executed by default. TrackOpBit = 1 << ++effectTrackDepth is executed first before the fn function is executed. If it does (which it usually does not) then the old cleanup logic continues. If it does not run initDepMarkers to mark the dependencies and see how it works:
const initDepMarkers = ({ deps }) = > {
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].w |= trackOpBit // Tag dependencies have been collected}}}Copy the code
The initDepMarkers function is implemented simply by traversing the DEps attributes in the _effect instance, marking each DEP’s W attribute to the trackOpBit value.
The next step is to execute the fn function, which is the function encapsulated by the side effect function, such as for component rendering, fn is the component rendering function.
When the fn function executes, the reactive data is accessed, and their getters are triggered, and the track function executes the dependency collection. Accordingly, some adjustments have been made to the process of relying on collection:
function track(target, type, key) {
if(! isTracking()) {return
}
let depsMap = targetMap.get(target)
if(! depsMap) {// Each target corresponds to a depsMap
targetMap.set(target, (depsMap = new Map()))}let dep = depsMap.get(key)
if(! dep) {// Each key corresponds to a deP set
depsMap.set(key, (dep = createDep()))
}
consteventInfo = (process.env.NODE_ENV ! = ='production')? {effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
function trackEffects(dep, debuggerEventExtraInfo) {
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
if(! newTracked(dep)) {// Mark a new dependency
dep.n |= trackOpBit
// If dependencies have already been collected, there is no need to collect them againshouldTrack = ! wasTracked(dep) } }else {
/ / the cleanup modeshouldTrack = ! dep.has(activeEffect) }if (shouldTrack) {
// Collect the currently active effect as a dependency
dep.add(activeEffect)
// The currently active effect collects deP sets as dependencies
activeEffect.deps.push(dep)
if((process.env.NODE_ENV ! = ='production') && activeEffect.onTrack) {
activeEffect.onTrack(Object.assign({
effect: activeEffect
}, debuggerEventExtraInfo))
}
}
}
Copy the code
The createDep method is used to create the dep. In addition, the dep will determine whether the deP has been collected before the dep collects the previously activated effect as a dependency. If the deP has been collected, it does not need to be collected again. In addition, it will determine if the DEP is a new dependency, and if it is not, it will be marked as new.
Next, let’s look at the logic after fn is executed:
finally {
if (effectTrackDepth <= maxMarkerBits) {
// Finish the dependency tag
finalizeDepMarkers(this)}// Restore to the upper level
trackOpBit = 1 << --effectTrackDepth
resetTracking()
/ / out of the stack
effectStack.pop()
const n = effectStack.length
// Point to the last effect on the stack
activeEffect = n > 0 ? effectStack[n - 1] : undefined
}
Copy the code
FinalizeDepMarkers is used to complete the dependency tag if the dependency tag is satisfied. To see how it works, run finalizeDepMarkers:
const finalizeDepMarkers = (effect) = > {
const { deps } = effect
if (deps.length) {
let ptr = 0
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
// Dependencies that have been collected but are not new need to be removed
if(wasTracked(dep) && ! newTracked(dep)) { dep.delete(effect) }else {
deps[ptr++] = dep
}
// Clear the state
dep.w &= ~trackOpBit
dep.n &= ~trackOpBit
}
deps.length = ptr
}
}
Copy the code
The main thing finalizeDepMarkers does is identify dependencies that have been collected but are not collected ina new round of dependency collections and remove them from DEPS. This is actually the scenario to solve the aforementioned cleanup: a responsive object that is not accessed during the new component rendering should not trigger a rerendering of the component.
This is the optimization of dependency collection. You can see that compared with the previous process of clearing dependencies and adding dependencies each time the effect function is executed, the implementation will now mark the state of dependencies before each execution of the effect function. In the process, the collected dependencies will not be collected again. Executing effect also removes dependencies that have been collected but are not collected in a new round of dependency collection.
The optimization results in fewer operations on the set of DEP dependencies, which results in better performance.
Optimization of responsive APIS
The optimization of responsive APIS is mainly reflected in the optimization of REF and computed apis.
Take the REF API as an example, and look at its pre-optimized implementation:
function ref(value) {
return createRef(value)
}
const convert = (val) = > isObject(val) ? reactive(val) : val
function createRef(rawValue, shallow = false) {
if (isRef(rawValue)) {
// If a ref is passed in, then return itself to handle the nested ref case.
return rawValue
}
return new RefImpl(rawValue, shallow)
}
class RefImpl {
constructor(_rawValue, _shallow = false) {
this._rawValue = _rawValue
this._shallow = _shallow
this.__v_isRef = true
// In the non-shallow case, if its value is an object or an array, then the response is recursive
this._value = _shallow ? _rawValue : convert(_rawValue)
}
get value() {
// Add a getter to the value property and do a dependency collection
track(toRaw(this), 'get' /* GET */.'value')
return this._value
}
set value(newVal) {
// Add a setter for the value property
if (hasChanged(toRaw(newVal), this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
// Distribute notices
trigger(toRaw(this), 'set' /* SET */.'value', newVal)
}
}
}
Copy the code
The ref function returns the value that createRef executes. Inside createRef, it first handles the nesting of the ref. If the rawValue passed in is also a ref, then it returns the rawValue. The instance of the RefImpl object is then returned.
RefImpl’s internal implementation, on the other hand, hijacks the getters and setters of its instance value property.
When accessing the value property of a ref object, it triggers the getter to perform the track function to collect dependencies and return its value; When changing the value of a ref object, the setter is triggered to set the new value and the trigger function is executed to send the notification. If the new value newVal is an object or array type, it is converted to a reactive object.
Next, let’s look at the implementation changes in vue.js 3.2:
class RefImpl {
constructor(value, _shallow = false) {
this._shallow = _shallow
this.dep = undefined
this.__v_isRef = true
this._rawValue = _shallow ? value : toRaw(value)
this._value = _shallow ? value : convert(value)
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
newVal = this._shallow ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
triggerRefValue(this, newVal)
}
}
}
Copy the code
The main change is the logic for performing dependency collection and distribution notifications on the value attribute of the REF object.
The track function is changed to trackRefValue for dependency collection in vue.js 3.2 implementation.
function trackRefValue(ref) {
if (isTracking()) {
ref = toRaw(ref)
if(! ref.dep) { ref.dep = createDep() }if((process.env.NODE_ENV ! = ='production')) {
trackEffects(ref.dep, {
target: ref,
type: "get" /* GET */.key: 'value'})}else {
trackEffects(ref.dep)
}
}
}
Copy the code
You can see that the dependencies of ref are directly stored in the DEp attribute, whereas in the track implementation, the dependencies are stored in the global targetMap:
let depsMap = targetMap.get(target)
if(! depsMap) {// Each target corresponds to a depsMap
targetMap.set(target, (depsMap = new Map()))}let dep = depsMap.get(key)
if(! dep) {// Each key corresponds to a deP set
depsMap.set(key, (dep = createDep()))
}
Copy the code
Obviously, the track function may need to do a lot of judgment and setup logic internally, and saving the dependencies to the DEP attribute of the REF object eliminates this series of judgment and setup, thus optimizing performance.
Accordingly, the ref implementation has changed the triggerRefValue function from the original trigger function to the notification part of the distribution. Here’s the implementation:
function triggerRefValue(ref, newVal) {
ref = toRaw(ref)
if (ref.dep) {
if((process.env.NODE_ENV ! = ='production')) {
triggerEffects(ref.dep, {
target: ref,
type: "set" /* SET */.key: 'value'.newValue: newVal
})
}
else {
triggerEffects(ref.dep)
}
}
}
function triggerEffects(dep, debuggerEventExtraInfo) {
for (const effect of isArray(dep) ? dep : [...dep]) {
if(effect ! == activeEffect || effect.allowRecurse) {if((process.env.NODE_ENV ! = ='production') && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
if (effect.scheduler) {
effect.scheduler()
}
else {
effect.run()
}
}
}
}
Copy the code
There is also a performance boost because it picks up all of its dependencies directly from the ref attribute and iterates through the execution, without needing to execute the trigger function’s extra logic.
The design of the trackOpBit
If you are careful, you may notice that the trackOpBit that marks the dependency is calculated each time using the left-shifted operator trackOpBit = 1 << ++effectTrackDepth; And in the assignment, the or operation is used:
deps[i].w |= trackOpBit
dep.n |= trackOpBit
Copy the code
So why do they do this? Since effect’s execution can be recursive, this is the way to record the dependency markers at each level.
To determine if a deP has been collected by a dependency, use the wasTracked function:
const wasTracked = (dep) = > (dep.w & trackOpBit) > 0
Copy the code
This is determined by whether the result of the and operation is greater than zero, which requires that the nested hierarchy match when the dependency is collected. For example, if dep.w has a value of 2, which means it was created when effect was executed at the first level, but effect nested at the second level has already been executed, the trackOpBit moves left by two digits to 4, and the value of 2&4 is 0, The wasTracked function returns false, indicating that the dependency needs to be collected. Clearly, this is a reasonable requirement.
As you can see, without the trackOpBit bit operation design, it is difficult to deal with the dependency tags at different nested levels. This design also shows the basvanmeurs’ very solid computer foundation.
conclusion
In vue. js applications, reactive data is frequently accessed and modified. Therefore, optimizing the performance of this process will greatly improve the performance of the entire application.
Most people look at the vue. js responsive implementation, probably for the purpose of understanding the implementation principle, and rarely pay attention to whether the implementation is optimal. Basvanmeurs proposed the implementation of this series of optimizations and hand-wrote a Benchmark tool to verify their optimization, which is worth learning from.
I hope that after reading this article, in addition to liking, favoriting and forwarding the three links, you can also check out the original post and their discussion, I believe you will gain more.
Front-end performance optimization is always a good place to dig deep, and hopefully you’ll always be able to think about possible optimization points in future development, whether you’re writing frameworks or businesses.