SoFunction
Updated on 2025-04-07

Principle Deep Analysis Vue's responsive update is faster than React

Preface

We all know that Vue will only accurately update the current components that depend on collection for responsive properties, and will not recursively update the subcomponents, which is one of the reasons for its powerful performance.

example

For example, such a component:

<template>
  <div>
   {{ msg }}
   <ChildComponent />
  </div>
</template>

When we trigger = 'Hello, Changed~', the component update and the view re-rendering will be triggered.

However, the <ChildComponent /> component will not be re-rendered, which is deliberately done by Vue.

In the past, I once thought that because the component is a tree, its update was taken for granted to traverse the tree in depth and perform recursive updates. This article will analyze how Vue can be accurately updated from the source code perspective.

React's update granularity

React is updated recursively from top to bottom in similar scenarios. That is to say, if there are ten nested child elements in ChildComponent in React, then all levels will recursively re-repost (without manual optimization), which is a performance disaster. (So, React created Fiber, created asynchronous rendering, which is essentially making up for the performance that was screwed up by itself).

Can they use this system of collecting dependencies? No, because they follow the design idea of ​​Immutable and never modify properties on the original object, so the responsive dependency collection mechanism based on or Proxy will have no way to start (you always return a new object, how can I know which part of the old object you modified?)

At the same time, since there is no responsive collection dependency, React can only recursively render all subcomponents again (except for optimization methods such as memo and shouldComponentUpdate), and then use the diff algorithm to decide which part of the view to be updated. This recursive process is called reconciler, which sounds cool, but the performance is very disaster.

Vue's update granularity

So, how do you do this precise update of Vue? In fact, each component has its own rendering watcher, which controls the view update of the current component, but does not control the update of ChildComponent.

How is it implemented in the source code?

During the patch  process, when the component is updated to ChildComponent, it will go to patchVnode. So what does this method do roughly?

patchVnode

Execute the prepatch hook for vnode.

Note that only component vnode has the prepatch life cycle.

Here we will go to the updateChildComponent method. What does this child specifically refer to?

 prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
  const options = 
  // Note that this child is the vm instance of the ChildComponent component, which is the usual use of this  const child =  = 
  updateChildComponent(
   child,
   , // updated props
   , // updated listeners
   vnode, // new parent vnode
    // new children
  )
 },

In fact, you can guess the parameters passed in, just do it:

  • Update props (more on this later)
  • Update binding events
  • Do some updates to slot (more on this later)

If there are children, diff the children.

For example, such a scenario:

<ul>
 <li>1</li>
 <li>2</li>
 <li>3</li>
<ul>

To use the diff algorithm to update the three li child nodes in ul, this article is omitted.

Then until then, patchVnode ends and does not recursively update the subcomponent tree as in conventional thinking.

This also shows that Vue's component updates are indeed accurate to the component itself.

What if it is a child component?

Assume the list is like this:

<ul>
 <component>1</component>
 <component>2</component>
 <component>3</component>
<ul>

Then in the process of diff, only props, listeners and other properties declared on component will be updated, and will not be updated in the component.

Note: It will not go deep into the component for updates! (Graphics, this is also the key to updating the granularity mentioned in this article)

How to trigger re-rendering of props update?

Then some students may ask, if we do not recursively update the child component, if we pass the responsive element msg to ChildComponent through props, how can it be updated at this time?

First, when the component initializes props, it will go to the initProps method.

const props = vm._props = {}

 for (const key in propsOptions) {
  // After a series of processes to verify the legality of props  const value = validateProp(key, propsOptions, propsData, vm)
  // The fields in props are also defined as responsive  defineReactive(props, key, value)
}

So far, hijacking of field changes on _props has been implemented. That is, it becomes responsive data. When we do an operation similar to _props.msg = 'Changed' (of course we won't do this, Vue will do it internally), the view update will be triggered.

In fact, when msg is passed to a child component, it will be stored on the _props of the child component instance and defined as a responsive attribute. The access to msg in the child component template is actually proxied to _props.msg, so naturally, it can accurately collect dependencies, as long as ChildComponent reads this attribute in the template.

One detail should be noted here. In fact, when the parent component is re-rendered, the child component's props will be recalculated, specifically in updateChildComponent:

 // update props
 if (propsData &amp;&amp; vm.$) {
  toggleObserving(false)
  // Note that props are pointed to _props  const props = vm._props
  const propKeys = vm.$options._propKeys || []
  for (let i = 0; i &lt; ; i++) {
   const key = propKeys[i]
   const propOptions: any = vm.$ // wtf flow?
   // It is this sentence that triggers the dependency update for _props.msg.   props[key] = validateProp(key, propOptions, propsData, vm)
  }
  toggleObserving(true)
  // keep a copy of raw propsData
  vm.$ = propsData
 }

So, due to the code indicated in the above comment, the changes in msg have also re-rendered the child components through the responsiveness of _props. So far, only components that really use msg have been re-rendered.

As stated in the official API documentation:

vm.$forceUpdate: Forces the Vue instance to re-render. Note that it only affects the instance itself and the subcomponents that insert the contents of the slot, not all subcomponents.
—— vm-forceUpdate documentation

We need to know a little knowledge point. vm.$forceUpdate essentially triggers the re-execution of the rendering watcher, which is exactly the same as when you modify a responsive property to trigger updates. It just calls vm._watcher.update() for you (just provide you with a convenient API, called facade mode in design mode)

How is slot updated?

Note that a detail is also mentioned here, that is, the subcomponents that insert the contents of the slot:

For example

Suppose we have parent-comp:

<div>
 <slot-comp>
   <span>{{ msg }}</span>
 </slot-comp>
</div>

Subcomponent slot-comp:

<div>
  <slot></slot>
</div>

The component contains slot updates, which is a relatively special scenario.

When the msg attribute here is used for dependency collection, the parent-comp `render watcher is collected. (As for why, you can understand by looking at the rendering context it is located.)

Then we imagine that msg is updated at this time.

<div>
 <slot-comp>
   <span>{{ msg }}</span>
 </slot-comp>
</div>

When this component is updated, it encounters a subcomponent slot-comp. According to Vue's precise update strategy, the subcomponent will not be re-rendered.

However, inside the source code, it makes a judgment that when executing the slot-comp prepatch hook, it will execute the updateChildComponent logic, and it will be found that it has a slot element inside this function.

 prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
  const options = 
  // Note that this child is the vm instance of the slot-comp component, which is what we usually use  const child =  = 
  updateChildComponent(
   child,
   , // updated props
   , // updated listeners
   vnode, // new parent vnode
    // new children
  )
 },

Inside updateChildComponent

 const hasChildren = !!(
  // This thing is the slot element  renderChildren ||        // has new static slots
  vm.$options._renderChildren || // has old static slots
   || // has new scoped slots
  vm.$scopedSlots !== emptyObject // has old scoped slots
 )

Then go down to make a judgment

 if (hasChildren) {
  vm.$slots = resolveSlots(renderChildren, )
  vm.$forceUpdate()
 }

Here, $forceUpdate on the vm instance of the slot-comp component is called, and the rendering watcher it triggers is the rendering watcher belonging to the slot-comp.
In summary, this update of msg not only triggered the re-rendering of parent-comp, but also further triggered the re-rendering of slot-comp, a child component that owns slot.
It also only triggers two layers of rendering. If slot-comp is rendered inside other components slot-child, then it will not be updated recursively at this time. (As long as the slot-child component does not have slot anymore).

Isn't it much better than React's recursive update?

Gift a small issue

Someone mentioned a Vue version 2.4.2issue, there will be a bug in the following scenario.

let Child = {
 name: "child",
 template:
  '<div><span>{{ localMsg }}</span><button @click="change">click</button></div>',
 data: function() {
  return {
   localMsg: 
  };
 },
 props: {
  msg: String
 },
 methods: {
  change() {
   this.$emit("update:msg", "world");
  }
 }
};

new Vue({
 el: "#app",
 template: '<child :="msg"><child>',
 beforeUpdate() {
  alert("update twice");
 },
 data() {
  return {
   msg: "hello"
  };
 },
 components: {
  Child
 }
});

The specific manifestation is that click the click button, and update twice will be alerted. This is because when the child component executes the data function, it will collect the data again incorrectly (that is, rendering the watcher).

Since the time for data initialization is between beforeCreated -> created, since the child component has not yet entered the rendering stage, it is still the rendering watcher of the parent component.

This leads to repeated collection of dependencies, repeatedly triggering the same update

How to solve it? It's very simple. Before and after executing the data function, set null first and then restore it in finally, so that the responsive data cannot be collected.

export function getData (data: Function, vm: Component): any {
 const prevTarget = 
+  = null
 try {
  return (vm, vm)
 } catch (e) {
  handleError(e, vm, `data()`)
  return {}
+ } finally {
+   = prevTarget
 }
}

postscript

If you don’t understand the concepts of , rendering watcher, etc., you can read the article I wrote that implements Vue responsiveness. Welcome to read:
Step by step to implement the most streamlined responsive system to learn Vue's data, computed, and watch source code

This article is also stored in mineGithub Blog RepositoryWelcome to subscribe and star.

This is the article about the principle in-depth analysis of Vue's responsive updates faster than React. For more relevant content related to Vue's responsive updates faster than React, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!