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+CSS 实现的超Sexy下拉菜单
Jan 17 Javascript
js取消单选按钮选中并判断对象是否为空
Nov 14 Javascript
JavaScript中的数组操作介绍
Dec 30 Javascript
JQuery实现动态添加删除评论的方法
May 18 Javascript
Node.js编程中客户端Session的使用详解
Jun 23 Javascript
jquery对象访问是什么及使用方法介绍
May 03 Javascript
JavaScript的模块化开发框架Sea.js上手指南
May 12 Javascript
Bootstrap学习笔记之环境配置(1)
Dec 07 Javascript
深入理解requireJS-实现一个简单的模块加载器
Jan 15 Javascript
JS脚本加载后执行相应回调函数的操作方法
Feb 28 Javascript
微信小程序选择图片控件
Jan 19 Javascript
React 并发功能体验(前端的并发模式)
Jul 01 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的FTP学习(三)
2006/10/09 PHP
php模拟asp中的XmlHttpRequest实现http请求的代码
2011/03/24 PHP
javascript基础第一章 JavaScript与用户端
2010/07/22 Javascript
jQuery实现可拖动的浮动层完整代码
2013/05/27 Javascript
JS不间断向上滚动效果代码
2013/12/25 Javascript
Javascript 按位取反运算符 (~)
2014/02/04 Javascript
Ubuntu系统下Angularjs开发环境安装
2016/09/01 Javascript
KnockoutJS 3.X API 第四章之数据控制流component绑定
2016/10/10 Javascript
简单实现jQuery级联菜单
2017/01/09 Javascript
如何快速上手Vuex
2017/02/14 Javascript
微信小程序中显示html格式内容的方法
2017/04/25 Javascript
解决jQuery ajax动态新增节点无法触发点击事件的问题
2017/05/24 jQuery
JS实现简单抖动效果
2017/06/01 Javascript
nodejs结合Socket.IO实现的即时通讯功能详解
2018/01/12 NodeJs
vue-scroller记录滚动位置的示例代码
2018/01/17 Javascript
Bootstrap 实现表格样式、表单布局的实例代码
2018/12/09 Javascript
微信小程序自定义弹窗实现详解(可通用)
2019/07/04 Javascript
Layer.js实现表格溢出内容省略号显示,悬停显示全部的方法
2019/09/16 Javascript
vue项目实现图片上传功能
2019/12/23 Javascript
[01:04:06]DOTA2上海特级锦标赛A组资格赛#2 Secret VS EHOME第一局
2016/02/26 DOTA
Python在图片中插入大量文字并且自动换行
2019/01/02 Python
python使用PIL模块获取图片像素点的方法
2019/01/08 Python
PyQt5内嵌浏览器注入JavaScript脚本实现自动化操作的代码实例
2019/02/13 Python
python数据预处理方式 :数据降维
2020/02/24 Python
使用python实现飞机大战游戏
2020/03/23 Python
Python实现初始化不同的变量类型为空值
2020/06/02 Python
HTML5 input placeholder 颜色修改示例
2014/05/30 HTML / CSS
后勤人员自我鉴定
2013/10/20 职场文书
酒店管理专业毕业生推荐信
2013/11/10 职场文书
房地产销售大学生自我评价分享
2013/11/11 职场文书
小学校园广播稿集锦
2014/10/04 职场文书
2014年城市管理工作总结
2014/12/02 职场文书
无工作证明怎么写
2015/06/15 职场文书
初中班主任工作随笔
2015/08/15 职场文书
SQL 窗口函数实现高效分页查询的案例分析
2021/05/21 SQL Server
SQL注入详解及防范方法
2021/12/06 MySQL