Vue為什么要謹慎使用$attrs與$listeners
前言
在 Vue 開發(fā)過程中,如遇到祖先組件需要傳值到孫子組件時,需要在兒子組件接收 props ,然后再傳遞給孫子組件,通過使用 v-bind="$attrs" 則會帶來極大的便利,但同時也會有一些隱患在其中。
隱患
先來看一個例子:

父組件:
{
template: `
<div>
<input
type="text"
v-model="input"
placeholder="please input">
<test :test="test" />
</div>
`,
data() {
return {
input: '',
test: '1111',
};
},
}
子組件:
{
template: '<div v-bind="$attrs"></div>',
updated() {
console.log('Why should I update?');
},
}
可以看到,當我們在輸入框輸入值的時候,只有修改到 input 字段,從而更新父組件,而子組件的 props test 則是沒有修改的,按照 誰更新,更新誰 的標準來看,子組件是不應該更新觸發(fā) updated 方法的,那這是為什么呢?
于是我發(fā)現(xiàn)這個“bug”,并迅速打開 gayhub 提了個 issue ,想著我也是參與過重大開源項目的人了,還不免一陣竊喜。事實很殘酷,這么明顯的問題怎么可能還沒被發(fā)現(xiàn)...

無情……,于是我打開看了看,尤大說了這么一番話我就好像明白了:

那既然不是“bug”,那來看看是為什么吧。
前因
首先介紹一個前提,就是 Vue 在更新組件的時候是更新對應的 data 和 props 觸發(fā) Watcher 通知來更新渲染的。
每一個組件都有一個唯一對應的 Watcher ,所以在子組件上的 props 沒有更新的時候,是不會觸發(fā)子組件的更新的。當我們去掉子組件上的 v-bind="$attrs" 時可以發(fā)現(xiàn), updated 鉤子不會再執(zhí)行,所以可以發(fā)現(xiàn)問題就出現(xiàn)在這里。
原因分析
Vue 源碼中搜索 $attrs ,找到 src/core/instance/render.js 文件:
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 上,并且設置成響應式對象,離發(fā)現(xiàn)奧秘又近了一步。
依賴收集
我們知道 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 里面,從而在設置時進行派發(fā)更新( notify() ),通知視圖渲染。
派發(fā)更新
下面是在改變響應式數(shù)據(jù)時派發(fā)更新的核心邏輯:
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()
}
})
很簡單的一部分代碼,就是在響應式數(shù)據(jù)被 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 是如何觸發(fā)子組件的 updated 方法的。
要知道子組件會被更新,肯定是在某個地方訪問到了 $attrs ,依賴被收集到 subs 里了,才會在派發(fā)時被通知需要更新。我們對比添加 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
派發(fā)更新狀態(tài):
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 中完成的,但是我們初始化的時候并沒有訪問數(shù)據(jù),那這是怎么實現(xiàn)的呢?
答案就在 vm._render() 這個方法會生成 Vnode 并在這個過程中會訪問到數(shù)據(jù),從而收集到了依賴。
那還是沒有解答出這個問題呀,別急,這還是一個鋪墊,因為你在 vm._render() 里也找不到在哪訪問到了 $attrs ...
柳暗花明
我們的代碼里和 vm._render() 都沒有對 $attrs 訪問,原因只可能出現(xiàn)在 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 的值更新時,觸發(fā) 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;
所以會觸發(fā) $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>
總結
到此為止,我們就已經(jīng)分析完了 $attrs 數(shù)據(jù)沒有變化,卻讓子組件更新的原因,源碼中有這樣一段話:
// $attrs & $listeners are exposed for easier HOC creation. // they need to be reactive so that HOCs using them are always updated
一開始這樣設計目的是為了 HOC 高階組件更好的創(chuàng)建使用,便于 HOC 組件總能對數(shù)據(jù)變化做出反應,但是在實際過程中與 v-model 產(chǎn)生了一些副作用,對于這兩者的使用,建議在沒有數(shù)據(jù)頻繁變化時可以使用,或者使用上面的奇淫技巧,以及……把產(chǎn)生頻繁變化的部分扔到一個單獨的組件中讓他自己自娛自樂去吧。
到此這篇關于Vue為什么要謹慎使用$attrs與$listeners的文章就介紹到這了,更多相關Vue $attrs與$listeners內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Element-ui設置el-table表頭全選框隱藏或禁用
這篇文章主要給大家介紹了關于Element-ui設置el-table表頭全選框隱藏或禁用的相關資料,文中手把手教你實現(xiàn)el-table實現(xiàn)跨表格禁用選項,需要的朋友可以參考下2023-07-07
vue+echarts動態(tài)更新數(shù)據(jù)及數(shù)據(jù)變化重新渲染方式
這篇文章主要介紹了vue+echarts動態(tài)更新數(shù)據(jù)及數(shù)據(jù)變化重新渲染方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-06-06
vue3在自定義hooks中使用useRouter報錯的解決方案
這篇文章主要介紹了vue3在自定義hooks中使用useRouter報錯的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08
Vue3?使用v-md-editor如何動態(tài)上傳圖片的方法實現(xiàn)
本文主要介紹了Vue3?使用v-md-editor如何動態(tài)上傳圖片,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-08-08
在Vue3中使用vue-qrcode庫實現(xiàn)二維碼生成的方法
在Vue3中實現(xiàn)二維碼生成需要使用第三方庫來處理生成二維碼的邏輯,常用的庫有 qrcode和 vue-qrcode,這篇文章主要介紹了在Vue3中使用vue-qrcode庫實現(xiàn)二維碼生成,需要的朋友可以參考下2023-12-12
vue cli 3.0下配置開發(fā)環(huán)境下的sourcemap問題
這篇文章主要介紹了vue cli 3.0下配置開發(fā)環(huán)境下的sourcemap問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-06-06
更強大的vue ssr實現(xiàn)預取數(shù)據(jù)的方式
這篇文章主要介紹了更強大的 vue ssr 預取數(shù)據(jù)的方式,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-07-07

