preface

During Vue development, if the parent component needs to pass values to the grandson component, the parent component needs to receive props from the child component, and then pass values to the grandson component, using v-bind=”$attrs” brings great convenience, but also has some risks.

Hidden trouble

Let’s start with an example:



The parent component:

{
  template: ` 
      
`
.data() { return { input: ' '.test: '1111'}; }},Copy the code

Child components:

{
  template: '<div v-bind="$attrs"></div>'.updated() {
    console.log('Why should I update? '); }},Copy the code

As you can see, when we enter the value in the input field, we only change toinputField to update the parent component, while the child component propstestIs not modified, according toWho updates, updates whoBy the standards, subcomponents should not be updated to triggerupdatedMethod. So why is that?

So I found this “bug” and quickly opened itgayhubTo mention aissue“, and I can’t help but laugh at the thought that I, too, have been involved in major open source projects. The fact is brutal, how could such an obvious problem have gone undetected…



Ruthless… “So I opened it and I kind of understood when Judah said,



Since it’s not a bug, let’s see why.

Before because of

When Vue updates a component, the data and props trigger the Watcher notification to update the render. Each component has a unique Watcher, so there is no need to trigger an update of the props on the child component if the props on the child component is not updated. When we remove v-bind=”$attrs” on the child component, the updated hook does not execute, so we can see the problem here.

Cause analysis,

Vue search source $attrs, find the SRC/core/instance/render. The js files:

export function initRender (vm: Component) {
  // ...
  defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null.true)
  defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null.true)}Copy the code

Oh, backpacks! Is it. You can see that in the initRender method, the $attrs attribute is bound to this and set as a responsive object, bringing the mystery one step closer.

Depend on the collection

We know that Vue uses the Object.defineProperty method for dependency collection, but since this section is also a bit more extensive, I’ll cover it briefly here.

  Object.defineProperty(obj, key, {
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend() // Dependency collection -- dep.target.adddep (Dep)
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    }
  })
Copy the code

By hijacking get, when we visit $attrs, it (DEP) collects the Watcher where $attrs is located into the SUBs of the DEP and sends out updates (notify()) to the render view when it is set up.

Distributed update

Here is the core logic for issuing updates when responsive data is changed:

  Object.defineProperty(obj, key, {
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if(newVal === value || (newVal ! == newVal && value ! == value)) {return
      }
      /* eslint-enable no-self-compare */
      if(process.env.NODE_ENV ! = ='production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else{ val = newVal } childOb = ! shallow && observe(newVal) dep.notify() } })Copy the code

A very simple part of the code is to call the notify method of the DEP when the responsive data is set, iterating through each Watcher for updates.

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
Copy the code

With that in mind, let’s go back to how $attrs triggers the updated method for the child component. To know that the child component will be updated, it must have accessed $attrs somewhere, and the dependency must have been collected in the subs to be notified of the update when it is dispatched. Add v-bind=”$attrs” and don’t add v-bind=”$attrs”

 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); }}}var a = dep; // See what the current deP is
    debugger; / / the debugger breakpoints
    return value
  }
Copy the code

When the bindingv-bind="$attrs", one more dependency will be collected.



There will be aid 为 8 的 depIt collected$attrsWhere theWatcherLet’s compare the presence and absencev-bind="$attrs"At the time of thesetUpdate status distributed:

  set: function reactiveSetter (newVal) {
    var value = getter ? getter.call(obj) : val;
    /* eslint-disable no-self-compare */
    if(newVal === value || (newVal ! == newVal && value ! == value)) {return
    }
    /* eslint-enable no-self-compare */
    if(process.env.NODE_ENV ! = ='production' && customSetter) {
      customSetter();
    }
    if (setter) {
      setter.call(obj, newVal);
    } else{ val = newVal; } childOb = ! shallow && observe(newVal);var a = dep; // View the current DEP
    debugger; / / the debugger breakpoints
    dep.notify();
  }
Copy the code



And you can see it clearly hereid 为 8 的 depGetting ready to traversesubsnoticeWatcherTo update, and you can see itnewVal 与 valueThe value has not changed and the problem has been updated.

Question: How are $attrs dependencies collected?

We know that dependency collection is done in GET, but we do not access data when we initialize, so how does this work? The answer lies in the vm._render() method, which generates vNodes and accesses data in the process, collecting dependencies. That still doesn’t answer the question, don’t get nervous, it’s still a setup, because you can’t find anywhere to call $attrs in vm._render()…

One good thing came out of

$attrs is not called in our code or vm._render(), for reasons that can only be found in v-bind.

const compiler = require('vue-template-compiler');

const result = compiler.compile(
  / / `
  // 
      
//

// / / ` '

Test contents

'
); console.log(result.render); // with (this) { // return _c( // 'div', // { attrs: { test: test } }, / / / // _c('p', [_v(' test content ')]) / /] / /); // } // with (this) { // return _c( // 'div', // _b({}, 'div', $attrs, false), / / / // _c('p', [_v(' test content ')]) / /] / /); // } Copy the code

This is where $attrs is finally accessed, so $attrs is collected into the dependency, and when the value of the V-model in the input is updated, the set notification is triggered to update, $attrs is assigned in the updateChildComponent method called when the component is updated:

  // update $attrs and $listeners hash
  // these are also reactive so they may trigger child update if the child
  // used them during render
  vm.$attrs = parentVnode.data.attrs || emptyObject;
  vm.$listeners = listeners || emptyObject;
Copy the code

So the set of $attrs is triggered, causing the Watcher on which it is located to update, which causes the child component to update. Without the v-bind=”$attrs” binding, you would have reached this point, but you would not have been able to update the child components without relying on the collection process.

Qi Yin skills

If you want to figure somebody else’s body, ah bah, figure somebody else’s convenience, and want to better performance how to do? Here’s a curvilinear way to save the country:

<template> <Child v-bind="attrsCopy" /> </template> <script> import _ from 'lodash'; import Child from './Child'; export default { name: 'Child', components: { Child, }, data() { return { attrsCopy: {}, }; }, watch: { $attrs: { handler(newVal, value) { if (! _.isEqual(newVal, value)) { this.attrsCopy = _.cloneDeep(newVal); } }, immediate: true, }, }, }; </script>Copy the code

conclusion

$attrs = $attrs; $attrs = $attrs; $attrs = $attrs

// attrs & listeners are exposed for easier HOC creation. // they need to be reactive so that HOCs using them are always updated

Started this design aims to HOC high-level components better create to use, easy to HOC components can always respond to data changes, but in the actual process with the v – model have some side effects, for the use of the two, suggest that we can use when no data changes frequently, or using the above her skills, and… Throw the parts that produce frequent changes into a separate component and let him entertain himself.