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 相关文章推荐
javascript中将Object转换为String函数代码 (json str)
Apr 29 Javascript
js Map List 遍历使用示例
Jul 10 Javascript
jquery自定义滚动条插件示例分享
Feb 21 Javascript
js delete 用法(删除对象属性及变量)
Aug 24 Javascript
JavaScript中的类(Class)详细介绍
Dec 30 Javascript
javascript实现简单的进度条
Jul 02 Javascript
js验证手机号、密码、短信验证码代码工具类
Jun 24 Javascript
详解vue指令与$nextTick 操作DOM的不同之处
Aug 02 Javascript
Bootstrap Table列宽拖动的方法
Aug 15 Javascript
node.js学习笔记之koa框架和简单爬虫练习
Dec 13 Javascript
Js Snowflake(雪花算法)生成随机ID的实现方法
Aug 26 Javascript
解决vscode进行vue格式化,会自动补分号和双引号的问题
Oct 26 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
header()函数使用说明
2006/11/23 PHP
php中global和$GLOBALS[]的分析之一
2012/02/02 PHP
PHP判断是否为空的几个函数对比
2015/04/21 PHP
PHP7如何开启Opcode打造强悍性能详解
2018/05/11 PHP
ThinkPHP5.1验证码功能实现的示例代码
2020/06/08 PHP
{}与function(){}选用空对象{}来存放keyValue
2012/05/23 Javascript
javascript实现控制浏览器全屏
2015/03/30 Javascript
echart简介_动力节点Java学院整理
2017/08/11 Javascript
AngularJS实现的2048小游戏功能【附源码下载】
2018/01/03 Javascript
js自定义input文件上传样式
2018/10/26 Javascript
vue从一个页面跳转到另一个页面并携带参数的解决方法
2019/08/12 Javascript
JS实现利用闭包判断Dom元素和滚动条的方向示例
2019/08/26 Javascript
这15个Vue指令,让你的项目开发爽到爆
2019/10/11 Javascript
使用axios请求接口,几种content-type的区别详解
2019/10/29 Javascript
JS实现碰撞检测效果
2020/03/12 Javascript
《javascript设计模式》学习笔记四:Javascript面向对象程序设计链式调用实例分析
2020/04/07 Javascript
javascript实现获取中文汉字拼音首字母
2020/05/19 Javascript
解决Antd Table表头加Icon和气泡提示的坑
2020/11/17 Javascript
Python素数检测的方法
2015/05/11 Python
Python的时间模块datetime详解
2017/04/17 Python
Python连接Redis的基本配置方法
2018/09/13 Python
tensorflow 限制显存大小的实现
2020/02/03 Python
详解使用python3.7配置开发钉钉群自定义机器人(2020年新版攻略)
2020/04/01 Python
使用keras2.0 将Merge层改为函数式
2020/05/23 Python
Python坐标轴操作及设置代码实例
2020/06/04 Python
浅谈keras中的batch_dot,dot方法和TensorFlow的matmul
2020/06/18 Python
使用django自带的user做外键的方法
2020/11/30 Python
浅谈pc和移动端的响应式的使用
2019/01/03 HTML / CSS
Rosetta Stone官方网站:语言学习
2019/01/05 全球购物
营销与策划应届生求职信
2013/11/04 职场文书
广告学专业自荐信范文
2014/02/24 职场文书
公司财务流程之主管工作流程
2014/03/03 职场文书
田径运动会开幕式及主持词
2014/03/28 职场文书
党支部党的群众路线对照检查材料
2014/09/24 职场文书
北大自主招生自荐信
2015/03/04 职场文书
2019新员工试用期转正工作总结范文
2019/08/21 职场文书