Vue为什么要谨慎使用$attrs与$listeners
前言
在Vue开发过程中,如遇到祖先组件需要传值到孙子组件时,需要在儿子组件接收props,然后再传递给孙子组件,通过使用v-bind="$attrs"则会带来极大的便利,但同时也会有一些隐患在其中。
隐患
先来看一个例子:
父组件:
{
template:`
`,
data(){
return{
input:'',
test:'1111',
};
},
}
子组件:
{
template:'可以看到,当我们在输入框输入值的时候,只有修改到input字段,从而更新父组件,而子组件的propstest则是没有修改的,按照谁更新,更新谁的标准来看,子组件是不应该更新触发updated方法的,那这是为什么呢?
于是我发现这个“bug”,并迅速打开gayhub提了个issue,想着我也是参与过重大开源项目的人了,还不免一阵窃喜。事实很残酷,这么明显的问题怎么可能还没被发现...
无情……,于是我打开看了看,尤大说了这么一番话我就好像明白了:
那既然不是“bug”,那来看看是为什么吧。
前因
首先介绍一个前提,就是Vue在更新组件的时候是更新对应的data和props触发Watcher通知来更新渲染的。
每一个组件都有一个唯一对应的Watcher,所以在子组件上的props没有更新的时候,是不会触发子组件的更新的。当我们去掉子组件上的v-bind="$attrs"时可以发现,updated钩子不会再执行,所以可以发现问题就出现在这里。
原因分析
Vue源码中搜索$attrs,找到src/core/instance/render.js文件:
exportfunctioninitRender(vm:Component){
//...
defineReactive(vm,'$attrs',parentData&&parentData.attrs||emptyObject,null,true)
defineReactive(vm,'$listeners',options._parentListeners||emptyObject,null,true)
}
噢,amazing!就是它。可以看到在initRender方法中,将$attrs属性绑定到了this上,并且设置成响应式对象,离发现奥秘又近了一步。
依赖收集
我们知道Vue会通过Object.defineProperty方法来进行依赖收集,由于这部分内容也比较多,这里只进行一个简单了解。
Object.defineProperty(obj,key,{
get:functionreactiveGetter(){
constvalue=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)
}
}
}
returnvalue
}
})
通过对get的劫持,使得我们在访问$attrs时它(dep)会将$attrs所在的Watcher收集到dep的subs里面,从而在设置时进行派发更新(notify()),通知视图渲染。
派发更新
下面是在改变响应式数据时派发更新的核心逻辑:
Object.defineProperty(obj,key,{
set:functionreactiveSetter(newVal){
constvalue=getter?getter.call(obj):val
/*eslint-disableno-self-compare*/
if(newVal===value||(newVal!==newVal&&value!==value)){
return
}
/*eslint-enableno-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(){
//stabilizethesubscriberlistfirst
constsubs=this.subs.slice()
for(leti=0,l=subs.length;i
了解到这些基础后,我们再回头看看$attrs是如何触发子组件的updated方法的。
要知道子组件会被更新,肯定是在某个地方访问到了$attrs,依赖被收集到subs里了,才会在派发时被通知需要更新。我们对比添加v-bind="$attrs"和不添加v-bind="$attrs"调试一下源码可以看到:
get:functionreactiveGetter(){
varvalue=getter?getter.call(obj):val;
if(Dep.target){
dep.depend();
if(childOb){
childOb.dep.depend();
if(Array.isArray(value)){
dependArray(value);
}
}
}
vara=dep;//看看当前dep是啥
debugger;//debugger断点
returnvalue
}
当绑定了v-bind="$attrs"时,会多收集到一个依赖。
会有一个id为8的dep里面收集了$attrs所在的Watcher,我们再对比一下有无v-bind="$attrs"时的set
派发更新状态:
set:functionreactiveSetter(newVal){
varvalue=getter?getter.call(obj):val;
/*eslint-disableno-self-compare*/
if(newVal===value||(newVal!==newVal&&value!==value)){
return
}
/*eslint-enableno-self-compare*/
if(process.env.NODE_ENV!=='production'&&customSetter){
customSetter();
}
if(setter){
setter.call(obj,newVal);
}else{
val=newVal;
}
childOb=!shallow&&observe(newVal);
vara=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对模板进行编译看看:
constcompiler=require('vue-template-compiler');
constresult=compiler.compile(
//`
//
//测试内容
// 测试内容