【Vue】謹慎使用$attrs與$listeners

前言

Vue 開發過程當中,如遇到祖先組件須要傳值到孫子組件時,須要在兒子組件接收 props ,而後再傳遞給孫子組件,經過使用 v-bind="$attrs" 則會帶來極大的便利,但同時也會有一些隱患在其中。 javascript

隱患

先來看一個例子:
$attrs
父組件:vue

{
  template: ` <div> <input type="text" v-model="input" placeholder="please input"> <test :test="test" /> </div> `,
  data() {
    return {
      input: '',
      test: '1111',
    };
  },
}
複製代碼

子組件:java

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

能夠看到,當咱們在輸入框輸入值的時候,只有修改到 input 字段,從而更新父組件,而子組件的 props test 則是沒有修改的,按照 誰更新,更新誰 的標準來看,子組件是不該該更新觸發 updated 方法的,那這是爲何呢?
因而我發現這個「bug」,並迅速打開 gayhub 提了個 issue ,想着我也是參與太重大開源項目的人了,還難免一陣竊喜。事實很殘酷,這麼明顯的問題怎麼可能還沒被發現...

無情……,因而我打開看了看,尤大說了這麼一番話我就好像明白了:

那既然不是「bug」,那來看看是爲何吧。 node

前因

首先介紹一個前提,就是 Vue 在更新組件的時候是更新對應的 data 和 props 觸發 Watcher 通知來更新渲染的。
每個組件都有一個惟一對應的 Watcher ,因此在子組件上的 props 沒有更新的時候,是不會觸發子組件的更新的。當咱們去掉子組件上的v-bind="$attrs"時能夠發現, updated 鉤子不會再執行,因此能夠發現問題就出如今這裏。 react

緣由分析

Vue 源碼中搜索 $attrs ,找到 src/core/instance/render.js 文件:markdown

export function initRender (vm: Component) {
  // ...
  defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
  defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
複製代碼

噢,amazing!就是它。能夠看到在 initRender 方法中,將 $attrs 屬性綁定到了 this 上,而且設置成響應式對象,離發現奧祕又近了一步。 oop

依賴收集

咱們知道 Vue 會經過 Object.defineProperty 方法來進行依賴收集,因爲這部份內容也比較多,這裏只進行一個簡單瞭解。性能

Object.defineProperty(obj, key, {
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend() // 依賴收集 -- Dep.target.addDep(dep)
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    }
  })
複製代碼

經過對 get 的劫持,使得咱們在訪問 $attrs 時它( dep )會將 $attrs 所在的 Watcher 收集到 dep 的 subs 裏面,從而在設置時進行派發更新( notify() ),通知視圖渲染。 測試

派發更新

下面是在改變響應式數據時派發更新的核心邏輯:ui

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()
    }
  })
複製代碼

很簡單的一部分代碼,就是在響應式數據被 set 時,調用 dep 的 notify 方法,遍歷每個 Watcher 進行更新。

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

瞭解到這些基礎後,咱們再回頭看看 $attrs 是如何觸發子組件的 updated 方法的。
要知道子組件會被更新,確定是在某個地方訪問到了 $attrs ,依賴被收集到 subs 裏了,纔會在派發時被通知須要更新。咱們對比添加 v-bind="$attrs" 和不添加 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; // 看看當前 dep 是啥
    debugger; // debugger 斷點
    return value
  }
複製代碼

當綁定了 v-bind="$attrs" 時,會多收集到一個依賴。

會有一個 id 爲 8 的 dep 裏面收集了 $attrs 所在的 Watcher ,咱們再對比一下有無 v-bind="$attrs" 時的 set 派發更新狀態:

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; // 查看當前 dep
    debugger; // debugger 斷點
    dep.notify();
  }
複製代碼


這裏能夠明顯看到也是 id 爲 8 的 dep 正準備遍歷 subs 通知 Watcher 來更新,也能看到 newVal 與 value 其實值並無改變而進行了更新這個問題。

問題:$attrs 的依賴是如何被收集的呢?

咱們知道依賴收集是在 get 中完成的,可是咱們初始化的時候並無訪問數據,那這是怎麼實現的呢?
答案就在 vm._render() 這個方法會生成 Vnode 並在這個過程當中會訪問到數據,從而收集到了依賴。
那仍是沒有解答出這個問題呀,別急,這仍是一個鋪墊,由於你在 vm._render() 裏也找不到在哪訪問到了 $attrs ...

柳暗花明

咱們的代碼裏和 vm._render() 都沒有對 $attrs 訪問,緣由只可能出如今 v-bind 上了,咱們使用 vue-template-compiler 對模板進行編譯看看:

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

const result = compiler.compile(
  // `
  // <div :test="test">
  // <p>測試內容</p>
  // </div>
  // `
  ` <div v-bind="$attrs"> <p>測試內容</p> </div> `
);

console.log(result.render);

// with (this) {
// return _c(
// 'div',
// { attrs: { test: test } },
// [
// _c('p', [_v('測試內容')])
// ]
// );
// }

// with (this) {
// return _c(
// 'div',
// _b({}, 'div', $attrs, false),
// [
// _c('p', [_v('測試內容')])
// ]
// );
// }
複製代碼

這就是最終訪問 $attrs 的地方了,因此 $attrs 會被收集到依賴中,當 input 中 v-model 的值更新時,觸發 set 通知更新,而在更新組件時調用的 updateChildComponent 方法中會對 $attrs 進行賦值:

// 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;
複製代碼

因此會觸發 $attrs 的 set ,致使它所在的 Watcher 進行更新,也就會致使子組件更新了。而若是沒有綁定 v-bind="$attrs" ,則雖然也會到這一步,可是沒有依賴收集的過程,就沒法去更新子組件了。

奇淫技巧

若是又想圖人家身子,啊呸,圖人家方便,又想要好點的性能怎麼辦呢?這裏有一個曲線救國的方法:

<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>

複製代碼

總結

到此爲止,咱們就已經分析完了 $attrs 數據沒有變化,卻讓子組件更新的緣由,源碼中有這樣一段話:

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

一開始這樣設計目的是爲了 HOC 高階組件更好的建立使用,便於 HOC 組件總能對數據變化作出反應,可是在實際過程當中與 v-model 產生了一些反作用,對於這二者的使用,建議在沒有數據頻繁變化時可使用,或者使用上面的奇淫技巧,以及……把產生頻繁變化的部分扔到一個單獨的組件中讓他本身自娛自樂去吧。

相關文章
相關標籤/搜索