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 相关文章推荐
ASP中Sub和Function的区别说明
Aug 30 Javascript
基于jQuery的简单的列表导航菜单
Mar 02 Javascript
JavaScript中两个感叹号的作用说明
Dec 28 Javascript
用js判断输入是否为中文的函数
Mar 10 Javascript
javascript写的一个模拟阅读小说的程序
Apr 04 Javascript
Javascript实现获取窗口的大小和位置代码分享
Dec 04 Javascript
推荐一个自己用的封装好的javascript插件
Jan 29 Javascript
基于Node.js实现nodemailer邮件发送
Jan 26 Javascript
基于JavaScript实现购物网站商品放大镜效果
Sep 06 Javascript
Angular JS 生成动态二维码的方法
Feb 23 Javascript
ES6 Symbol在对象中的作用实例分析
Jun 06 Javascript
js实现弹幕飞机效果
Aug 27 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编程开发“虚拟域名”系统
2006/10/09 PHP
重新封装zend_soap实现http连接安全认证的php代码
2011/01/12 PHP
php递归删除目录下的文件但保留的实例分享
2014/05/10 PHP
thinkphp四种url访问方式详解
2014/11/28 PHP
深入解析PHP中foreach语句控制数组循环的用法
2015/11/30 PHP
PHP在innodb引擎下快速代建全文搜索功能简明教程【基于xunsearch】
2016/10/14 PHP
php生成二维码图片方法汇总
2016/12/17 PHP
Jquery颜色选择器ColorPicker实现代码
2012/11/14 Javascript
Javascript算符的优先级介绍
2013/03/20 Javascript
jQuery中RadioButtonList的功能及用法实例介绍
2013/08/23 Javascript
采用自执行的匿名函数解决for循环使用闭包的问题
2014/09/11 Javascript
jQuery子属性过滤选择器用法分析
2015/02/10 Javascript
jQuery给多个不同元素添加class样式的方法
2015/03/26 Javascript
Jquery判断radio、selelct、checkbox是否选中及获取选中值方法总结
2015/04/15 Javascript
JavaScript实现鼠标滑过处生成气泡的方法
2015/05/16 Javascript
js+html5实现的自由落体运动效果代码
2016/01/28 Javascript
jQuery判断邮箱格式对错实例代码讲解
2017/04/12 jQuery
JavaScript实现一个带AI的井字棋游戏源码
2018/05/21 Javascript
Vue高版本中一些新特性的使用详解
2018/09/25 Javascript
vue实现购物车的监听
2020/04/20 Javascript
[55:16]Mski vs VGJ.S Supermajor小组赛C组 BO3 第二场 6.3
2018/06/04 DOTA
Python实现把json格式转换成文本或sql文件
2015/07/10 Python
使用Python脚本将文字转换为图片的实例分享
2015/08/29 Python
Python处理菜单消息操作示例【基于win32ui模块】
2018/05/09 Python
tensorflow更改变量的值实例
2018/07/30 Python
python3利用Socket实现通信的方法示例
2019/05/06 Python
Django如何简单快速实现PUT、DELETE方法
2019/07/24 Python
opencv-python 读取图像并转换颜色空间实例
2019/12/09 Python
python pycharm最新版本激活码(永久有效)附python安装教程
2020/09/18 Python
Myprotein比利时官方网站:欧洲第一运动营养品牌
2020/10/04 全球购物
人力资源管理专业自荐信
2014/06/24 职场文书
合作协议书格式
2014/08/19 职场文书
报效祖国演讲稿
2014/09/15 职场文书
2014年十一国庆节活动方案
2014/09/16 职场文书
2015婚礼主持词开场白
2015/05/28 职场文书
解决springboot druid数据库连接失败后一直重连的方法
2022/04/19 Java/Android