Vue為什麼要謹慎使用$attrs與$listeners
前言
在 Vue
開發過程中,如遇到祖先元件需要傳值到孫子元件時,需要在兒子元件接收 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
則是沒有修改的,按照 誰更新,更新誰
的標準來看,子元件是不應該更新觸發 updated
方法的,那這是為什麼呢?
於是我發現這個“bug”,並迅速開啟 gayhub
提了個 issue
,想著我也是參與過重大開源專案的人了,還不免一陣竊喜。事實很殘酷,這麼明顯的問題怎麼可能還沒被發現...
無情……,於是我開啟看了看,尤大說了這麼一番話我就好像明白了:
那既然不是“bug”,那來看看是為什麼吧。
前因
首先介紹一個前提,就是 Vue
在更新元件的時候是更新對應的 data
和 props
觸發 Watcher
通知來更新渲染的。
每一個元件都有一個唯一對應的 Watcher
,所以在子元件上的 props
沒有更新的時候,是不會觸發子元件的更新的。當我們去掉子元件上的 v-bind="$attrs"
時可以發現, updated
鉤子不會再執行,所以可以發現問題就出現在這裡。
原因分析
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,true) }
噢,amazing!就是它。可以看到在 initRender
方法中,將 $attrs
屬性繫結到了 this
上,並且設定成響應式物件,離發現奧祕又近了一步。
依賴收集
我們知道 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()
),通知檢視渲染。
派發更新
下面是在改變響應式資料時派發更新的核心邏輯:
Object.defineProperty(obj,{ 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),[_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
產生了一些副作用,對於這兩者的使用,建議在沒有資料頻繁變化時可以使用,或者使用上面的奇淫技巧,以及……把產生頻繁變化的部分扔到一個單獨的元件中讓他自己自娛自樂去吧。
到此這篇關於Vue為什麼要謹慎使用$attrs與$listeners的文章就介紹到這了,更多相關Vue $attrs與$listeners內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!