从vue源码解析Vue.set()和this.$set()


Posted in Javascript onAugust 30, 2018

前言

最近死磕了一段时间vue源码,想想觉得还是要输出点东西,我们先来从Vue提供的Vue.set()和this.$set()这两个api看看它内部是怎么实现的。

Vue.set()和this.$set()应用的场景

平时做项目的时候难免不会对 数组或者对象 进行这样的骚操作操作,结果发现,咦~~,他喵的,怎么页面没有重新渲染。

const vueInstance = new Vue({
 data: {
  arr: [1, 2],
  obj1: {
    a: 3
  }
 }
});
vueInstance.$data.arr[0] = 3; // 这种骚操作页面不会重新渲染
vueInstance.$data.obj1.b = 3; // 这种骚操作页面不会重新渲染

查了一下官方文档,发现人家早就说过这种情况

Vue.set()向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新属性,因为 Vue 无法探测普通的新增属性 (比如 this.myObject.newProperty = 'hi')

所以按照官网的写法,我们应该使用下面这种写法:

Vue.set(vueInstance.$data.arr, 0, 3); // 这样操作数组可以让页面重新渲染
vueInstance.$set(vueInstance.$data.arr, 0, 3); // 这样操作数组也可以让页面重新渲染
Vue.set(vueInstance.$data.obj1, b, 3); // 这样操作对象可以让页面重新渲染
vueInstance.$set(vueInstance.$data.obj1, b, 3); // 这样操作对象也可以让页面重新渲染

Vue.set()和this.$set()实现原理

是时候看一波这两个api的源码了,我们先来看看Vue.set()的源码:

import { set } from '../observer/index'
...
Vue.set = set
...

再来看看this.$set()的源码:

import { set } from '../observer/index'
...
Vue.prototype.$set = set
...

结果我们发现Vue.set()和this.$set()这两个api的实现原理基本一模一样,都是使用了set函数。set函数是从 ../observer/index 文件中导出的,区别在于Vue.set()是将set函数绑定在Vue构造函数上,this.$set()是将set函数绑定在Vue原型上。

接下来我们根据 ../observer/index 中找出set函数:

function set (target: Array<any> | Object, key: any, val: any): any {
 if (process.env.NODE_ENV !== 'production' &&
  (isUndef(target) || isPrimitive(target))
 ) {
  warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
 }
 if (Array.isArray(target) && isValidArrayIndex(key)) {
  target.length = Math.max(target.length, key)
  target.splice(key, 1, val)
  return val
 }
 if (key in target && !(key in Object.prototype)) {
  target[key] = val
  return val
 }
 const ob = (target: any).__ob__
 if (target._isVue || (ob && ob.vmCount)) {
  process.env.NODE_ENV !== 'production' && warn(
   'Avoid adding reactive properties to a Vue instance or its root $data ' +
   'at runtime - declare it upfront in the data option.'
  )
  return val
 }
 if (!ob) {
  target[key] = val
  return val
 }
 defineReactive(ob.value, key, val)
 ob.dep.notify()
 return val
}

我们发现set函数接收三个参数分别为 target、key、val,其中target的值为数组或者对象,这正好和官网给出的调用Vue.set()方法时传入的参数参数对应上。如下图所示:

从vue源码解析Vue.set()和this.$set()

我们接着往下看:

if (process.env.NODE_ENV !== 'production' &&
  (isUndef(target) || isPrimitive(target))
 ) {
  warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
 }

我们先看isUndef和isPrimitive方法,从名字就可以看出,isUndef是判断target是不是等于undefined或者null。isPrimitive是判断target的数据类型是不是string、number、symbol、boolean中的一种。所以这里的意思是如果当前环境不是生产环境并且 isUndef(target) || isPrimitive(target) 为真的时候,那么就抛出错误警告。

数组的实现原理

接着向下看:

if (Array.isArray(target) && isValidArrayIndex(key)) {
  target.length = Math.max(target.length, key)
  target.splice(key, 1, val)
  return val
 }

这里实际就是修改数组时调用set方法时让我们能够触发响应的代码,不过在分析这段代码之前我们来看看vue中的数组实际上是长什么样的。 下图分别是vue中的数组和普通的js数组:

从vue源码解析Vue.set()和this.$set()

从vue源码解析Vue.set()和this.$set()

vue中的数组我们命名为arrVue,js中的普通数组命名为arrJs。其实我们平时调用普通数组的push、pop等方法是调用的Array原型上面定义的方法, 从图中我们可以看出arrJs的原型是指向Array.prototype,也就是说 arrJs.__proto__ == Array.prototype 。

但是在vue的数组中,我们发现arrVue的原型其实不是指向的Array.prototype,而是指向的一个对象(我们这里给这个对象命名为arrayMethods)。arrayMethods上面只有7个push、pop等方法,并且arrayMethods的原型才是指向的Array.prototype。所以我们在vue中调用数组的push、pop等方法时其实不是直接调用的数组原型给我们提供的push、pop等方法,而是调用的arrayMethods给我们提供的push、pop等方法。vue为什么要给数组的原型链上面加上这个arrayMethods呢?这里涉及到了vue的数据响应的原理,我们这篇文章暂时不谈论数据响应原理的具体实现。这里你可以理解成vue在arrayMethods对象中做过了特殊处理,如果你调用了arrayMethods提供的push、pop等7个方法,那么它会触发当前收集的依赖(这里收集的依赖可以暂时理解成渲染函数),导致页面重新渲染。换句话说,对于数组的操作,我们只有使用arrayMethods提供的那7个方法才会导致页面渲染,这也就解释了为什么我们使用 vueInstance.$data.arr[0] = 3; 时不会导致页面出现渲染。

搞清楚vue中的数组具体是怎么实现了之后,我们再来看上面的代码:

if (Array.isArray(target) && isValidArrayIndex(key)) {
  target.length = Math.max(target.length, key)
  target.splice(key, 1, val)
  return val
 }

首先if判断当前target是不是数组,并且key的值是有效的数组索引。然后将target数组的长度设置为target.length和key中的最大值,这里为什么要这样做呢?是因为我们可能会进行下面这种骚操作:

arr1 = [1,3];
Vue.set(arr1,10,1) // 如果不那样做,这种情况就会出问题

接着向下看,我们发现这里直接调用了target.splice(key, 1, val),在前面我们说过调用arrayMethods提供的push、pop等7个方法可以导致页面重新渲染,刚好splice也是属性arrayMethods提供的7个方法中的一种。

总结一下Vue.set数组实现的原理:其实Vue.set()对于数组的处理其实就是调用了splice方法,是不是发现其实很简单~~

对象的实现原理

我们接着向下看代码:

if (key in target && !(key in Object.prototype)) {
  target[key] = val
  return val
 }

这里先判断如果key本来就是对象中的一个属性,并且key不是Object原型上的属性。说明这个key本来就在对象上面已经定义过了的,直接修改值就可以了,可以自动触发响应。

关于对象的依赖收集和触发原理我们本文也不做详细解释,你可以暂时先这样理解。vue是使用的Object.defineProperty给对象做了一层拦截,当触发get的时候就会进行依赖收集(这里收集的依赖还是像数组那样,理解成渲染函数),当触发set的时候就会触发依赖,导致渲染函数执行页面重新渲染。那么第一次是在哪里触发get的呢?其实是在首次加载页面渲染的时候触发的,这里会进行递归将对象的属性都依赖收集,所以我们修改对象已有属性值得时候会导致页面重新渲染。这也刚好解释了我们使用 vueInstance.$data.obj1.b = 3; 的时候为什么页面不会重新渲染,因为这里的属性b不是对象的已有属性,也就是说属性b没有进行过依赖收集,所以才会导致修改属性b的值页面不会重新渲染。

我们接着向下看代码:

const ob = (target: any).__ob__
 if (target._isVue || (ob && ob.vmCount)) {
  process.env.NODE_ENV !== 'production' && warn(
   'Avoid adding reactive properties to a Vue instance or its root $data ' +
   'at runtime - declare it upfront in the data option.'
  )
  return val
 }
 if (!ob) {
  target[key] = val
  return val
 }

首先定义变量ob的值为 target.__ob__ ,这个 __ob__ 属性到底是什么对象呢?vue给响应式对象都加了一个 __ob__ 属性,如果一个对象有这个 __ob__ 属性,那么就说明这个对象是响应式对象,我们修改对象已有属性的时候就会触发页面渲染。

target._isVue || (ob && ob.vmCount) 的意思是:当前的target对象是vue实例对象或者是根数据对象,那么就会抛出错误警告。

if (!ob) 为真说明当前的target对象不是响应式对象,那么直接赋值返回即可。

接着向下看:

defineReactive(ob.value, key, val)
 ob.dep.notify()
 return val

这里其实才是vue.set()真正处理对象的地方。 defineReactive(ob.value, key, val) 的意思是给新加的属性添加依赖,以后再直接修改这个新的属性的时候就会触发页面渲染。

ob.dep.notify() 这句代码的意思是触发当前的依赖(这里的依赖依然可以理解成渲染函数),所以页面就会进行重新渲染。

总结

从源码层次看vue提供的vue.set()和this.$set()这两个api还是很简单的,由于本文没有详细解释vue依赖收集和触发,所以有的地方说的还是很模糊。

以上所述是小编给大家介绍的vue源码解析Vue.set()和this.$set(),希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
JS下载文件|无刷新下载文件示例代码
Apr 17 Javascript
javascript中兼容主流浏览器的动态生成iframe方法
May 05 Javascript
js语法学习之判断一个对象是否为数组
May 13 Javascript
使用iojs的jsdom库实现同步系统时间
Apr 20 Javascript
使用Ajax与服务器(JSON)通信实例
Nov 04 Javascript
Bootstrap Scrollspy源码学习
Mar 02 Javascript
基于JQuery的购物车添加删除以及结算功能示例
Mar 08 Javascript
微信小程序的分类页面制作
Jun 27 Javascript
Angular限制input框输入金额(是小数的话只保留两位小数点)
Jul 13 Javascript
JavaScript中toLocaleString()和toString()的区别实例分析
Aug 14 Javascript
对angular2中的ngfor和ngif指令嵌套实例讲解
Sep 12 Javascript
小程序转发探索示例
Feb 19 Javascript
vue-router动态设置页面title的实例讲解
Aug 30 #Javascript
解决vue select当前value没有更新到vue对象属性的问题
Aug 30 #Javascript
微信小程序使用wxParse解析html的实现示例
Aug 30 #Javascript
vue中rem的配置的方法示例
Aug 30 #Javascript
Vue2实时监听表单变化的示例讲解
Aug 30 #Javascript
vue-swiper的使用教程
Aug 30 #Javascript
分享vue里swiper的一些坑
Aug 30 #Javascript
You might like
PHP第一季视频教程(李炎恢+php100 不断更新)
2011/05/29 PHP
php探针使用原理和技巧讲解
2019/09/17 PHP
TP5框架页面跳转样式操作示例
2020/04/05 PHP
Javascript模板技术
2007/04/27 Javascript
收集的10个免费的jQuery相册
2011/02/26 Javascript
js/jquery解析json和数组格式的方法详解
2014/01/09 Javascript
jquery实现人性化的有选择性禁用鼠标右键
2014/06/30 Javascript
jQuery代码实现发展历程时间轴特效
2015/07/30 Javascript
jquery中的常见问题及快速解决方法小结
2016/06/14 Javascript
javascript使用 concat 方法对数组进行合并的方法
2016/09/08 Javascript
JS实现页面中所有img对象添加onclick事件及新窗口查看图片的方法
2016/12/27 Javascript
js 输入框 正则表达式(菜鸟必看教程)
2017/02/19 Javascript
jquery实现弹窗功能(窗口居中显示)
2017/02/27 Javascript
javascript回调函数的概念理解与用法分析
2017/05/27 Javascript
详解在 Angular 项目中添加 clean-blog 模板
2017/07/04 Javascript
用js实现每隔一秒刷新时间的实例(含年月日时分秒)
2017/10/25 Javascript
webpack打包非模块化js的方法
2018/10/24 Javascript
微信小程序实现简单评论功能
2018/11/28 Javascript
jQuery中使用validate插件校验表单功能
2019/05/24 jQuery
jquery操作checkbox的常用方法总结【附测试源码下载】
2019/06/10 jQuery
layui上传图片到服务器的非项目目录下的方法
2019/09/26 Javascript
jQuery 动态粒子效果示例代码
2020/07/07 jQuery
[05:05]第三天的dota2
2013/07/29 DOTA
浅谈Matplotlib简介和pyplot的简单使用——文本标注和箭头
2018/01/09 Python
python使用turtle库绘制树
2018/06/25 Python
python tornado微信开发入门代码
2018/08/24 Python
Python读取csv文件分隔符设置方法
2019/01/14 Python
Python列表常见操作详解(获取,增加,删除,修改,排序等)
2019/02/18 Python
PyQt5 在label显示的图片中绘制矩形的方法
2019/06/17 Python
pytorch实现保证每次运行使用的随机数都相同
2020/02/20 Python
在python中利用pycharm自定义代码块教程(三步搞定)
2020/04/15 Python
pymongo insert_many 批量插入的实例
2020/12/05 Python
CSS3实现可爱的小黄人动画
2016/07/11 HTML / CSS
SmartBuyGlasses荷兰:购买太阳镜和眼镜
2020/03/16 全球购物
模特职业生涯规划范文
2014/02/26 职场文书
2019年暑期法院实习报告
2019/12/18 职场文书