从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 相关文章推荐
jquery 防止表单重复提交代码
Jan 21 Javascript
Jquery Ajax请求代码(2)
Jan 07 Javascript
JS(JQuery)操作Array的相关方法介绍
Feb 11 Javascript
JavaScript使用slice函数获取数组部分元素的方法
Apr 06 Javascript
JavaScript基于ajax编辑信息用法实例
Jul 15 Javascript
vue子组件使用自定义事件向父组件传递数据
May 27 Javascript
JS实现电商放大镜效果
Aug 24 Javascript
详解webpack3编译兼容IE8的正确姿势
Dec 21 Javascript
vue + typescript + 极验登录验证的实现方法
Jun 27 Javascript
Vue实现商品详情页的评价列表功能
Sep 04 Javascript
简单了解JavaScript arguement原理及作用
May 28 Javascript
前端学习——JavaScript原生实现购物车案例
Mar 31 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去掉字符串的最后一个字符附substr()的用法
2011/03/23 PHP
php 如何获取数组第一个值
2013/08/06 PHP
PHP base64编码后解码乱码的解决办法
2014/06/19 PHP
PHP判断FORM表单或URL参数来的数据是否为整数的方法
2016/03/25 PHP
向fckeditor编辑器插入指定代码的方法
2007/05/25 Javascript
CSS+Table图文混排中实现文本自适应图片宽度(超简单+跨所有浏览器)
2009/02/14 Javascript
Draggable Elements 元素拖拽功能实现代码
2011/03/30 Javascript
javascript 学习笔记(一)DOM基本操作
2011/04/08 Javascript
Jvascript学习实践案例(开发常用)
2012/06/25 Javascript
THREE.JS入门教程(5)你应当知道的十件事
2013/01/24 Javascript
jquery 表格的增行删行实现思路
2013/03/21 Javascript
javascript history对象(历史记录)使用方法(实现浏览器前进后退)
2014/01/07 Javascript
node.js中的fs.symlink方法使用说明
2014/12/15 Javascript
javascript实现仿腾讯游戏选择
2015/05/14 Javascript
简要了解jQuery移动web开发的响应式布局设计
2015/12/04 Javascript
详解jQuery UI库中文本输入自动补全功能的用法
2016/04/23 Javascript
AngularJS入门教程之Cookies读写操作示例
2016/11/02 Javascript
在 webpack 中使用 ECharts的实例详解
2018/02/05 Javascript
redux处理异步action解决方案
2020/03/22 Javascript
Python 字典(Dictionary)操作详解
2014/03/11 Python
Python中类的继承代码实例
2014/10/28 Python
python通过函数属性实现全局变量的方法
2015/05/16 Python
浅述python中深浅拷贝原理
2018/09/18 Python
Python线性拟合实现函数与用法示例
2018/12/13 Python
python中的colorlog库使用详解
2019/07/05 Python
浅谈python之自动化运维(Paramiko)
2020/01/31 Python
Python DataFrame使用drop_duplicates()函数去重(保留重复值,取重复值)
2020/07/20 Python
StubHub德国:购买和出售门票
2017/09/06 全球购物
AJAX检测用户名是否存在的方法
2021/03/24 Javascript
护士自我介绍信
2014/01/13 职场文书
竞选学习委员演讲稿
2014/09/01 职场文书
2014年实习期工作总结
2014/11/27 职场文书
2014年大班保育员工作总结
2014/12/02 职场文书
应届生简历自我评价
2015/03/11 职场文书
2015年中秋节活动总结
2015/03/23 职场文书
简述python四种分词工具,盘点哪个更好用?
2021/04/13 Python