目录
前言
原因分析
解决方案
总结
前言
在 Vue
开发过程中,如遇到祖先组件需要传值到孙子组件时,需要在儿子组件接收 props
,然后再传递给孙子组件,通过使用 v-bind="$attrs"
则会带来极大的便利,但同时也会有一些隐患在其中。
可以看到,当我们在输入框输入值的时候,只有修改到
input
字段,从而更新父组件,而子组件的 propstest
则是没有修改的,按照谁更新,更新谁
的标准来看,子组件是不应该更新触发updated
方法的,
原因分析
首先介绍一个前提,就是 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, null, true)
}
可以看到在 initRender
方法中,将 $attrs
属性绑定到了 this
上,并且设置成响应式对象
解决方案
方案一 判断值是否完全相等
<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>
2、通过props 接收info,并对info进行监听,就不存在这个问题
info:{
handler(newV,old) {
console.log('info改变了,触发')
},
deep: true,
immediate: true,
}
1、 基本数据类型
只有值改变时才触发watch
2、引用数据类型
同一个引用,内容改变,触发;
引用地址变化,触发;
watch内部的判断是:
不同的对象都不相等, 即使内部数据一样,对象(引用数据类型,如数组)只有引用地址一样才是相同的
比如:对象浅拷贝,内部数据是同一个引用
var obj1 = {
'a':{name: 123},
'b':{name: 45}
}
var obj2 = {...obj1}
console.log(obj1.a === obj2.a) // true
总结
在子组件有v-bind="$attrs",就会在 initRender 方法中,将 $attrs 属性绑定到了 this 上,并且设置成响应式对象。Vue 通过 Object.defineProperty 方法进行依赖收集, 我们在访问 $attrs 时,它( dep)会将 $attrs 所在的 Watcher 收集到 dep 的 subs 里面,从而在设置时进行派发更新notify(),通知视图渲染。
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()
}
})
$attrs的依赖收集发生在v-bind中, 通过vue-template-compiler 编译源代码即可发现。
所以当 input 中 v-model 的值更新时,触发 set 通知更新,而在更新组件时调用的 updateChildComponent 方法中会对 $attrs 进行赋值。
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 的 set ,导致它所在的 Watcher 进行更新,也就会导致子组件更新了。而如果没有绑定 v-bind="$attrs" ,则虽然也会到这一步,但是没有依赖收集的过程,就无法去更新子组件了。