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将一个表单序列化为一个对象的方法
Dec 02 Javascript
详谈LABJS按需动态加载js文件
May 07 Javascript
jQuery实现复选框批量选择与反选的方法
Jun 17 Javascript
简单谈谈Javascript中类型的判断
Oct 19 Javascript
js+css实现回到顶部按钮(back to top)
Mar 02 Javascript
JSP基于Bootstrap分页显示实例解析
Jun 12 Javascript
layer弹出层框架alert与msg详解
Mar 14 Javascript
Vue.js学习记录之在元素与template中使用v-if指令实例
Jun 27 Javascript
Angular 2 ngForm中的ngModel、[ngModel]和[(ngModel)]的写法
Jun 29 Javascript
node前端模板引擎Jade之标签的基本写法
May 11 Javascript
vue-router启用history模式下的开发及非根目录部署方法
Dec 23 Javascript
35个最好用的Vue开源库(史上最全)
Jan 03 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中unlink()、mkdir()、rmdir()等方法的使用介绍
2012/12/21 PHP
PHP遍历某个目录下的所有文件和子文件夹的实现代码
2013/06/28 PHP
thinkphp3.2.3版本的数据库增删改查实现代码
2016/09/22 PHP
PDO::_construct讲解
2019/01/27 PHP
浅谈PHP中的那些魔术常量
2020/12/02 PHP
几个比较实用的JavaScript 测试及效验工具
2010/04/18 Javascript
JavaScript中的数学运算介绍
2014/12/29 Javascript
jQuery热气球动画半透明背景的后台登录界面代码分享
2015/08/28 Javascript
浏览器检测JS代码(兼容目前各大主流浏览器)
2016/02/21 Javascript
详解利用exif.js解决ios手机上传竖拍照片旋转90度问题
2016/11/04 Javascript
解析Javascript单例模式概念与实例
2016/12/05 Javascript
Bootstrap基本样式学习笔记之标签(5)
2016/12/07 Javascript
微信小程序表单验证插件WxValidate的二次封装功能(终极版)
2019/09/03 Javascript
使用PYTHON接收多播数据的代码
2012/03/01 Python
浅析Python装饰器以及装饰器模式
2018/05/28 Python
Python使用re模块正则提取字符串中括号内的内容示例
2018/06/01 Python
详解Python中的type和object
2018/08/15 Python
pandas 根据列的值选取所有行的示例
2018/11/07 Python
用python一行代码得到数组中某个元素的个数方法
2019/01/28 Python
django多对多表的创建,级联删除及手动创建第三张表
2019/07/25 Python
Pytorch自己加载单通道图片用作数据集训练的实例
2020/01/18 Python
django项目中新增app的2种实现方法
2020/04/01 Python
scrapy在python爬虫中搭建出错的解决方法
2020/11/22 Python
css3个性化字体_动力节点Java学院整理
2017/07/12 HTML / CSS
赫里福德的一家乡村零售商店:Philip Morris & Son
2017/06/25 全球购物
舞蹈教育学专业推荐信
2013/11/27 职场文书
家佳咖啡店创业计划书
2013/12/27 职场文书
服装设计专业毕业生求职信
2014/04/09 职场文书
房屋买卖协议书范本
2014/04/10 职场文书
捐款倡议书格式范文
2014/05/14 职场文书
2015年爱国卫生月活动总结
2015/03/26 职场文书
部队2015年终工作总结
2015/04/02 职场文书
2015年教师学期工作总结
2015/04/30 职场文书
张丽莉观后感
2015/06/16 职场文书
法定授权委托证明书
2015/06/18 职场文书
教师学习心得体会范文
2016/01/21 职场文书