1 数据劫持是什么
Vue 最独特的特性之一,是其响应式系统。
而实现响应式的过程中,要实现数据劫持才能监听数据的变化
数据劫持就是:拦截属性的读取和修改,简单来说就是数据的任何变化都要能监测到,这样才能根据数据变化做对应操作
2 数据劫持源码逻辑
2.1 initState() 初始化
Vue 在执行完生命周期钩子函数 beforeCreate
之后,会根据配置项是否存在,执行对应的相应的初始化(如Props、methods、data、computed等等)
如果 data 存在
会执行 initData()
,进行 data 数据初始化操作
下面进行 initData()
初始化的分析
这里的源码:
vue-main\src\core\instance\state.ts
1 | export function initState(vm: Component) { |
2.2 initData() 数据初始化
进入 initData()
,主要做了 3 件事:
1 首先就是判断 data 的类型
- 如果 data 是函数,就调用这个函数,返回数据
- 如果是对象,就可以使用这个对象
当然,我们知道 data 还是希望写成函数类型的:
因为数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的
data
,拥有自己的作用域,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据我们组件的date单纯的写成对象形式,这些实例用的是同一个构造函数,由于JavaScript的特性所导致,所有的组件实例共用了一个data,就会造成该组件的数据会被其他组件应影响
2 如果methods、props 和 data 中没有重名的属性,就用 proxy 代理 data 中的属性到 vm 上
即: vm.name -> vm._data.name
3 调用 observe() 进行数据劫持,这里就是数据劫持真正的开始
这部分的源码:
1 | function initData(vm: Component) { |
2.3 observe()
这里主要做一些规范处理。接下来主要列一些什么情况下数据会被标记为响应式,什么情况下不被响应式
- 如果不是数组/对象,则return,不继续执行;
- 如果该数据是
Observer
类的实例,则意味着当前属性已被标记为响应式数据,无需处理 - return new Observer() 将其标记为响应式数据
这部分的源码:
vue-main\src\core\observer\index.ts
1 | export function observe( |
2.4 Observer类
Observer
构造函数会真正的将数据标记为响应式
首先给属性增加一个不可枚举的
__ob__
对象 ,表明已被观测 (为啥要不可枚举?为了不影响实际开发,比如for..in..
循环的时候不遍历到__ob__
属性)1
def(value, '__ob__', this) //给属性增加一个**不可枚举的 `__ob__` 对象**
然后就是判断当前属性是数组还是对象
2.4.1 如果是数组
先对数组的每一项进行监测
调用 observeArray() ,遍历数组,对每一元素调用 observe()
方法将元素变为响应式数据
这里的源码:
vue-main\src\core\observer\index.ts
1 | observeArray(value: any[]) { |
拦截数组方法
有些方法会修改数组本身,这些方法被称为变异方法 (push、pop、shift、unshift、splice、sort、reverse)
当开发者调用这些方法修改
data
中的数组数据的时候,Vue
需要及时得知并作出反应
- 首先,拿到数组的原型链
Array.prototype
,通过Object.create
复制一份
因为需要在数组原型上修改7个方法,但是直接修改会干掉原有的方法,我们需要用
Object.create()
拷贝一份数组原型出来,在复制出来的原型上面做功能扩展
- 然后,会调用原生数组方法(毕竟不能影响数组原本的功能)
- 关键在于代码中间部分,他会调用
__ob__.dep
的notify
方法去通知watcher,告诉对方:我有变化了,你快更新吧; - 如果是能增加数组长度的 3 个方法
push、unshift、splice
,获取到插入的值,然后把新添加的值变成一个响应式对象(再次调用observeArray())。
这里的源码:
vue-main\src\core\observer\array.ts
1 | const arrayProto = Array.prototype |
vue为什么没有提供对数组属性的监听呢?
- 如果通过索引修改数组、或修改数组长度,是无法被拦截到的
既然 Object.defineProperty 有劫持getter,setter能力,那么 vue 为什么没用它来实现对数组属性的监听呢?
尤大在github上这样回答:

如果你知道数组的长度,理论上是可以预先给所有的索引设置 getter/setter 的。但是一来很多场景下你不知道数组的长度,二来,如果是很大的数组,预先加 getter/setter 性能负担较大。
总而言之就是理论上 Vue 是可以这样做,但是出于性能考虑没这样做,而是用了一种数组变异办法来触发视图更新。
2.4.2 如果是对象
vue-main\vue-main\src\core\observer\index.ts
遍历当前对象的每个元素
这里可以理解,为什么只有对象已有属性才能实现数据劫持
1 | const keys = Object.keys(value) // 拿到当前对象value的所有键 |
defineReactive()
方法,会进行 Object.defineProperty()
的逻辑:
首先 new Dep(),该 dep 在后面 get set 时都会使用这个闭包产生的 dep,这个时候就能明白了数据对象中每个数据字段都有属于自己的 dep
常量(这里涉及到依赖收集理念)
1 | const dep = new Dep() |
接下来缓存原有描述属性中可能存在的 get
方法和 set
方法,分别缓存在变量 getter
、setter
中,为什么要缓存呢?主要是接下来会执行Object.defineProperty
重新定义属性的 getter/setter
,这会导致原有的 get
、set
方法被重新覆盖
1 | const getter = property && property.get |
然后,默认会将对象进行深度劫持(如果对象层级比较深,显然是比较大的性能负担,这也是Vue3优化的一个问题)
1 | observe() |
接下来我们看看拦截器的 get
方法都做了些啥:
因为是读取属性,首先要确保不能影响正常逻辑,要调用前面缓存的 getter
,拿到正确的返回结果
然后就是 getter
的主要使命,收集依赖:
若 Dep.target
存在,则执行dep.depend()
。那么 Dep.target
是什么?其实就是 watcher
(依赖收集内容放到后面的文章讲)
vue-main\vue-main\src\core\observer\index.ts
1 | get: function reactiveGetter() { |
接下来我们看看拦截器的set
方法都做了些啥:
set
函数主要做两件事:
如果值改变了,调用缓存的 setter,为属性设置新值;
1
setter.call(obj, newVal)
通知 watcher 更新
1
dep.notify
这便是数据劫持的主要内容
想要理解它,还需要知道依赖收集中的 watcher、dep关系
3 Object.defineProperty API缺点
3.1 不能监听对象/数组新增和删除
在模板中使用对象属性peo
1 | {{ peo }} |
点击事件,为对象新增一个属性 c ,但是界面不会更新数据
1 | this.peo.c = '5555' |
解决方法:
使用 this.$set(object, key, value) 新增
1 | this.$set(this.peo, 'c', 5555); |
而数组前面说过了,只能通过splice、push、unshift方法新增和删除,直接修改数组索引,也是无法监听的
3.2 初始化阶段递归执行 Object.defineProperty 带来的性能负担
为了将对象深度劫持,会递归执行Object.defineProperty,如果对象层级比较深,显然是比较大的性能负担
About this Post
This post is written by Duan WeiCheng, licensed under CC BY-NC 4.0.