Moment For Technology

Why are Vue's responsive updates accurate down to the component level? (In-depth analysis of principles)

Posted on Aug. 6, 2022, 1:49 a.m. by 葉惠如
Category: The front end Tag: vue.js The interview

preface

We all know that Vue updates reactive properties precisely to the current component that depends on the collection, and does not recursively update child components, which is one of the reasons for its great performance.

example

For example, a component like this:

template
   div
      {{ msg }}
      ChildComponent /
   /div
/template
Copy the code

When we trigger this. MSG = 'Hello, Changed~', the component will be updated and the view will be rerendered.

But the component is not actually re-rendered. Vue does this intentionally.

In the past, I used to think that because a component was a tree, its update was a matter of course going through the tree in depth, recursively updating it. This article will take you from the perspective of the source code, Vue is how to achieve accurate updates.

React update granularity

React updates recursively from top to bottom in a similar scenario. In other words, if there are ten nested child elements in the React ChildComponent, all levels are recursively rerender (without manual optimization), which is a performance disaster. (Hence React created Fiber and created asynchronous rendering, essentially making up for the performance they screwed up).

Can they use this system of collecting dependencies? No, because they are Immutable by design and never modify properties on the original Object, a responsive dependency collection mechanism based on Object.defineProperty or Proxy would be useless (you always return a new Object, how do I know what part of the old Object you changed?).

React recursively rerenders all subcomponents (except memo and shouldComponentUpdate), and then uses diff to determine which parts of the view to update. The recursive process is called reconciler, and it sounds cool, but the performance is catastrophic.

Update granularity of Vue

So how does this exact update of Vue work? Each component has its own rendering Watcher, which handles view updates for the current component, but not ChildComponent updates.

Specific to the source code, is how to achieve it?

In the process of patch, when the component is updated to ChildComponent, patchVnode will be moved to. What does this method roughly do?

patchVnode

performvnodeprepatchHook.

Note that only component VNodes have a prepatch lifecycle,

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

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    // Note that this child is the VM instance of the ChildComponent component
    const child = vnode.componentInstance = oldVnode.componentInstance
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children)},Copy the code

If the parameter is passed in, you can guess about it.

  1. Update props (more on this later)
  2. Update binding event
  3. Some updates to slot (more on that later)

Diff the child nodes, if any.

Like this scenario:

ul
  li1/li
  li2/li
  li3/li
ul
Copy the code

To use diff algorithm to update the three LI vnodes in UL, this article will skip.

At this point, patchVnode is finished and does not recursively update the subcomponent tree as usual.

This shows that Vue component updates are accurate to the component itself.

What if it's a child component?

Suppose the list looks like this:

ul
  component1/component
  component2/component
  component3/component
ul
Copy the code

During the diff process, only props, listeners, and other properties declared on the component are updated, but not updates to the components themselves.

Note: Updates are not made inside the component! (Highlight, which is the key to the granularity of updates described in this article.)

How does an update to props trigger a re-render?

If we pass MSG, a responsive element, to ChildComponent as props, how does it update if we don't recursively update subcomponents?

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

const props = vm._props = {}

 for (const key in propsOptions) {
    // After a series of processes to validate the props
    const value = validateProp(key, propsOptions, propsData, vm)
    // The props field is also defined as responsive
    defineReactive(props, key, value)
}
Copy the code

So far, the hijacking of field changes on _props has been implemented. If we do something like _props. MSG = 'Changed' (we don't do that, Vue does that internally), it will trigger the view update.

MSG is stored in the _props of the subcomponent instance and is defined as a responsive property. The access to MSG in the template of the subcomponent is actually propped to _props. MSG, so the dependency can be accurately collected. As long as ChildComponent also reads this property in the template.

When the parent component is re-rendered, the props of the child component are recalculated in the updateChildComponent:

  // update props
  if (propsData  vm.$options.props) {
    toggleObserving(false)
    // Notice that props is referenced to _props
    const props = vm._props
    const propKeys = vm.$options._propKeys || []
    for (let i = 0; i  propKeys.length; i++) {
      const key = propKeys[i]
      const propOptions: any = vm.$options.props // wtf flow?
      // This is what triggers the dependency update for _props. MSG.
      props[key] = validateProp(key, propOptions, propsData, vm)
    }
    toggleObserving(true)
    // keep a copy of raw propsData
    vm.$options.propsData = propsData
  }
Copy the code

So, thanks to the code in the comment above, the MSG change also re-renders the subcomponents through the responsiveness of _props. So far, only the components that actually use MSG have been re-rendered.

As stated in the API documentation on the official website:

Vm. $forceUpdate: Forces the Vue instance to be re-rendered. Note that it affects only the instance itself and the children that insert slot content, not all children. -- - forceUpdate vm documentation

One small thing to remember is that vm.$forceUpdate essentially triggers a re-execution of the render Watcher, in the same way that you would change a responsive property to trigger an update. It just calls vm._watcher.update() for you (just gives you a handy API called facade mode in design mode)

How is slot updated?

Note that one detail is also mentioned here, which is the child component that inserts the contents of the slot:

For instance,

Suppose we have a parent component parent-comp:

div
  slot-comp
     span{{ msg }}/span
  /slot-comp
/div
Copy the code

Subcomponent slot-comp:

div
   slot/slot
/div
Copy the code

A component that contains slot updates is a special case.

The MSG property collects parent-comp's 'render Watcher' when the dependency collection is performed. (Just look at the render context in which it is located to see why.)

So let's imagine that MSG is now updated,

div
  slot-comp
     span{{ msg }}/span
  /slot-comp
/div
Copy the code

When this component is updated, it encounters a child component, slot-comp, which is not re-rendered according to Vue's precise update policy.

In the source code, however, it makes a determination that when executing slot-comp's prepatch hook, the updateChildComponent logic will be executed and slot elements will be found inside the function.

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    // Note that this child is the VM instance of the slot-comp component
    const child = vnode.componentInstance = oldVnode.componentInstance
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children)},Copy the code

Inside the updateChildComponent

  consthasChildren = !! (// This is the slot element
    renderChildren ||               // has new static slots
    vm.$options._renderChildren ||  // has old static slots
    parentVnode.data.scopedSlots || // has new scoped slotsvm.$scopedSlots ! == emptyObject// has old scoped slots
  )
Copy the code

And then we make a judgment

  if (hasChildren) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context)
    vm.$forceUpdate()
  }
Copy the code

If $forceUpdate is called on the VM instance of the slot-comp component, the render Watcher it triggers is the render Watcher of slot-comp.

In summary, the MSG update not only triggered the re-rendering of parent-comp, but also triggered the re-rendering of slot-comp, the child component that owns slot.

It only triggers two layers of rendering, and if slot-comp renders another component slot-Child, it does not recursively update at this point. (Just don't slot slot-Child components anymore).

Isn't it still a lot better than the React recursive update?

Updates to parent and child components go through twonextTick?

The answer is no: note the logic in queueWatcher. When the parent component is updated, the global variable isFlushing is true, so it does not wait until the next tick. Instead, it pushes through the queue, updating in one tick at a time.

This is done in the nextTick update of the parent component, which loops through the Watcher in the queue

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  for (index = 0; index  queue.length; index++) {
     // Update the parent component
     watcher.run()
  }
}
Copy the code

And then when the parent updates it triggers the child's response update, which triggers queueWatcher, because isFlushing is true, it does this else logic, and because the child's ID is larger than the parent's ID, So after the parent component's watcher is inserted, the parent component's update function is executed, and the child component's Watcher is executed. This is in the same tick.

if(! flushing) { queue.push(watcher) }else {
  // if already flushing, splice the watcher based on its id
  // if already past its id, it will be run next immediately.
  let i = queue.length - 1
  while (i  index  queue[i].id  watcher.id) {
    i--
  }
  queue.splice(i + 1.0, watcher)
}
Copy the code

It just adds this to the queue and watcher executes it.

Optimization of Vue 2.6

Vue 2.6 further optimizes the above operation on slot, in short, using

slot-comp
  template v-slot:foo
    {{ msg }}
  /template
/slot-comp
Copy the code

The slots generated by this syntax are compiled into functions and executed in the context of the child component, so the parent component no longer collects its internal dependencies. If MSG is not used in the parent component, the update only affects the child component itself. Instead of notifying child components of updates by modifying _props from the parent component.

Gift a small issue

An issue has been proposed for Vue 2.4.2, and bugs can appear in the following scenarios.

let Child = {
  name: "child".template:
    'divspan{{ localMsg }}/spanbutton @click="change"click/button/div'.data: function() {
    return {
      localMsg: this.msg
    };
  },
  props: {
    msg: String
  },
  methods: {
    change() {
      this.$emit("update:msg"."world"); }}};new Vue({
  el: "#app".template: 'child :msg.sync="msg"child',
  beforeUpdate() {
    alert("update twice");
  },
  data() {
    return {
      msg: "hello"
    };
  },
  components: {
    Child
  }
});

Copy the code

Click the click button, and alert will appear twice update twice. This is because the subcomponent incorrectly collects the dep. target (i.e., render watcher) when executing the data function to initialize the component data.

Since the time of data initialization is beforeCreated - created, the dep. target is still the rendering watcher of the parent component because it has not entered the rendering stage of the child component.

This results in repeated collection of dependencies and repeated triggering of the same updates, which can be seen here: jsfiddle.net/sbmLobvr/9.

How did you solve it? Set dep. target to NULL before and after the data function is executed, and then restore in finally so that reactive data cannot be collected.

export function getData (data: Function, vm: Component): any {
  const prevTarget = Dep.target
+ Dep.target = null
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
+ } finally {
+ Dep.target = prevTarget}}Copy the code

Afterword.

If you don't understand the concepts of dep.Target and Watcher, you can read my article on how to implement Vue responsiveness in a minimalist way.

Learn Vue source code for Data, computed, and Watch by implementing a minimalist responsive system

This post is also hosted in my Github blog repository, subscribe and star.

Special thanks to

Thank Ji Zhi for correcting some details of this article.

❤️ thank you

1. If this article is helpful to you, please support it with a like. Your like is the motivation for my writing.

2. Follow the public account "front-end from advanced to hospital" to add my friends, I pull you into the "front-end advanced communication group", we communicate and progress together.

Search
About
mo4tech.com (Moment For Technology) is a global community with thousands techies from across the global hang out!Passionate technologists, be it gadget freaks, tech enthusiasts, coders, technopreneurs, or CIOs, you would find them all here.