从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 相关文章推荐
获取Javscript执行函数名称的方法
Dec 22 Javascript
JSON 编辑器实现代码
Dec 06 Javascript
javascript cookies 设置、读取、删除实例代码
Apr 12 Javascript
javascript检查表单数据是否改变的方法
Jul 30 Javascript
js拖动div 当鼠标移动时整个div也相应的移动
Nov 21 Javascript
关于JavaScript命名空间的一些心得
Jun 07 Javascript
Javascript代码实现仿实例化类
Apr 03 Javascript
jquery背景跟随鼠标滑动导航
Nov 20 Javascript
详解vue-cli 构建Vue项目遇到的坑
Aug 30 Javascript
使用命令行工具npm新创建一个vue项目的方法
Dec 27 Javascript
jQuery实现模糊查询的方法分析
May 10 jQuery
javascript实现视频弹幕效果(两个版本)
Nov 28 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
实用函数5
2007/11/08 PHP
php+ajax注册实时验证功能
2016/07/20 PHP
php实现图片以base64显示的方法
2016/10/13 PHP
使用PHP+MySql+Ajax+jQuery实现省市区三级联动功能示例
2017/09/15 PHP
php根据地址获取百度地图经纬度的实例方法
2019/09/03 PHP
PHP如何解决微信文章图片防盗链
2020/12/09 PHP
A标签中通过href和onclick传递的this对象实现思路
2013/04/19 Javascript
Jquery 模板数据绑定插件的使用方法详解
2013/07/08 Javascript
二叉树的非递归后序遍历算法实例详解
2014/02/07 Javascript
JQuery的Ajax请求实现局部刷新的简单实例
2014/02/11 Javascript
js通过更改按钮的显示样式实现按钮的滑动效果
2014/04/23 Javascript
firefox浏览器用jquery.uploadify插件上传时报HTTP 302错误
2015/03/01 Javascript
js实现遍历含有input的table实例
2015/12/07 Javascript
AngularJS 基础ng-class-even指令用法
2016/08/01 Javascript
Vue.js实现无限加载与分页功能开发
2016/11/03 Javascript
JS实现类似51job上的地区选择效果示例
2016/11/17 Javascript
JavaScript屏蔽Backspace键的实现代码
2017/11/02 Javascript
JavaScript中利用Array filter() 方法压缩稀疏数组
2018/02/24 Javascript
vue 导航菜单刷新状态不消失,显示对应的路由界面操作
2020/08/06 Javascript
vue中提示$index is not defined错误的解决方式
2020/09/02 Javascript
浅析Python数据处理
2018/05/02 Python
Tensorflow 合并通道及加载子模型的方法
2018/07/26 Python
django框架使用orm实现批量更新数据的方法
2019/06/21 Python
Python 判断时间是否在时间区间内的实例
2020/05/16 Python
以色列的身体护理及家居香薰品牌:Sabon NYC
2018/02/23 全球购物
Ray-Ban雷朋太阳眼镜英国官网:Ray-Ban UK
2019/11/23 全球购物
Habitat家居英国官方网站:沙发、家具、照明、厨房和户外
2019/12/12 全球购物
如何用Java判断一个文件或目录是否存在
2012/11/19 面试题
招聘专员岗位职责
2014/03/07 职场文书
师范类求职信
2014/06/21 职场文书
共产党员批评与自我批评
2014/10/15 职场文书
2014年人事工作总结范文
2014/11/19 职场文书
2014年标准化工作总结
2014/12/17 职场文书
2016年公司新年寄语
2015/08/17 职场文书
2016清明节森林防火广播稿
2015/12/17 职场文书
《孙子兵法》:欲成大事者,需读懂这些致胜策略
2019/08/23 职场文书