目录
前言
原因分析
解决方案
总结
前言
在 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" ,则虽然也会到这一步,但是没有依赖收集的过程,就无法去更新子组件了。