Vue为什么要谨慎使用$attrs与$listeners


Posted in Javascript onAugust 27, 2020

前言

Vue 开发过程中,如遇到祖先组件需要传值到孙子组件时,需要在儿子组件接收 props ,然后再传递给孙子组件,通过使用 v-bind="$attrs" 则会带来极大的便利,但同时也会有一些隐患在其中。

隐患

先来看一个例子:

Vue为什么要谨慎使用$attrs与$listeners

父组件:

{
 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 ,想着我也是参与过重大开源项目的人了,还不免一阵窃喜。事实很残酷,这么明显的问题怎么可能还没被发现...

Vue为什么要谨慎使用$attrs与$listeners

无情……,于是我打开看了看,尤大说了这么一番话我就好像明白了:

Vue为什么要谨慎使用$attrs与$listeners

那既然不是“bug”,那来看看是为什么吧。

前因

首先介绍一个前提,就是 Vue 在更新组件的时候是更新对应的 dataprops 触发 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)
}

噢,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 收集到 depsubs 里面,从而在设置时进行派发更新( 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()
 }
 })

很简单的一部分代码,就是在响应式数据被 set 时,调用 depnotify 方法,遍历每一个 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" 时,会多收集到一个依赖。

Vue为什么要谨慎使用$attrs与$listeners

会有一个 id8dep 里面收集了 $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();
 }

Vue为什么要谨慎使用$attrs与$listeners

这里可以明显看到也是 id8dep 正准备遍历 subs 通知 Watcher 来更新,也能看到 newValvalue

其实值并没有改变而进行了更新这个问题。

问题:$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),
//  [
//  _c('p', [_v('测试内容')])
//  ]
// );
// }

这就是最终访问 $attrs 的地方了,所以 $attrs 会被收集到依赖中,当 inputv-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;

所以会触发 $attrsset ,导致它所在的 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内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
用JQuery 实现的自定义对话框
Mar 24 Javascript
js编写trim()函数及正则表达式的运用
Oct 24 Javascript
jQuery自动添加表单项的方法
Jul 13 Javascript
原生js实现键盘控制div移动且解决停顿问题
Dec 05 Javascript
基于javascript实现数字英文验证码
Jan 25 Javascript
详解ES6之用let声明变量以及let loop机制
Jul 15 Javascript
老生常谈js数据类型
Aug 03 Javascript
Vue导出json数据到Excel电子表格的示例
Dec 04 Javascript
Vue中使用vee-validate表单验证的方法
May 09 Javascript
重新认识vue之事件阻止冒泡的实现
Aug 02 Javascript
js+html5 canvas实现ps钢笔抠图
Apr 28 Javascript
如何在postman测试用例中实现断言过程解析
Jul 09 Javascript
js实现批量删除功能
Aug 27 #Javascript
js利用拖放实现添加删除
Aug 27 #Javascript
基于jquery实现彩色投票进度条代码解析
Aug 26 #jQuery
Javascript call及apply应用场景及实例
Aug 26 #Javascript
Javascript类型判断相关例题及解析
Aug 26 #Javascript
Javascript基于OOP实实现探测器功能代码实例
Aug 26 #Javascript
Javascript如何实现扩充基本类型
Aug 26 #Javascript
You might like
PHP中调用SVN命令更新网站方法
2015/01/07 PHP
Laravel 5.1 on SAE环境开发教程【附项目demo源码】
2016/10/09 PHP
PHP预定义超全局数组变量小结
2018/08/20 PHP
用js实现控制内容的向上向下滚动效果
2007/06/26 Javascript
Ext JS Grid在IE6 下宽度的问题解决方法
2009/02/15 Javascript
30个最佳jQuery Lightbox效果插件分享
2011/04/11 Javascript
基于jquery实现的鼠标拖拽元素复制并写入效果
2011/08/23 Javascript
JS对img进行操作(换图片/切图/轮换/停止)
2013/04/17 Javascript
jQuery jcrop插件截图使用方法
2013/11/20 Javascript
JavaScript中对象介绍
2014/12/31 Javascript
JS+CSS实现滑动切换tab菜单效果
2015/08/25 Javascript
基于Arcgis for javascript实现百度地图ABCD marker的效果
2015/09/12 Javascript
【经验总结】编写JavaScript代码时应遵循的14条规律
2016/06/20 Javascript
CodeMirror js代码加亮使用总结
2017/03/25 Javascript
讲解vue-router之什么是嵌套路由
2018/05/28 Javascript
Bootstrap Table实现定时刷新数据的方法
2018/08/13 Javascript
浅谈在Vue.js中如何实现时间转换指令
2019/01/06 Javascript
Vue中Table组件行内右键菜单实现方法(基于 vue + AntDesign)
2019/11/21 Javascript
浅谈vue中$event理解和框架中在包含默认值外传参
2020/08/07 Javascript
vue项目在线上服务器访问失败原因分析
2020/08/14 Javascript
vue中后端做Excel导出功能返回数据流前端的处理操作
2020/09/08 Javascript
python实现封装得到virustotal扫描结果
2014/10/05 Python
python获取外网ip地址的方法总结
2015/07/02 Python
基于Python的接口自动化读写excel文件的方法
2021/01/15 Python
Python爬虫爬取ts碎片视频+验证码登录功能
2021/02/22 Python
CSS3之背景尺寸Background-size使用介绍
2013/10/14 HTML / CSS
CSS3 display知识详解
2015/11/25 HTML / CSS
HTML5 canvas基本绘图之文字渲染
2016/06/27 HTML / CSS
美国Rue La La闪购网站:奢侈品、中高档品牌限时折扣
2016/10/19 全球购物
Theory美国官网:后现代都市风时装品牌
2018/05/09 全球购物
自1926年以来就为冰岛保持温暖:66°North
2020/11/27 全球购物
JAVA的事件委托机制和垃圾回收机制
2014/09/07 面试题
类、抽象类、接口的差异
2016/06/13 面试题
商场总经理岗位职责
2014/02/03 职场文书
2015年电话客服工作总结
2015/05/18 职场文书
python plt.plot bar 如何设置绘图尺寸大小
2021/06/01 Python