0%

vue源码学习-响应式原理


响应式原理是什么?为什么vue能监听到data里面的变化?为什么一定要把变量写在data里?为什么有时候数组监听不到?一起来学习一下吧

响应式核心

我们都知道vue是MVVM模式,数据驱动视图,那么他的响应式是如何实现的呢?

面试的时候也经常会这么问,一般我们都会回答一下根据Object.defineProperty来实现响应式原理,这也就够了,但是具体是怎么实现的呢

Vue在初始化数据的时候,会默认把data里面的值进行遍历,内部会使用Object.defineProperty重新定义所有属性

Object.defineProperty的特点,可以使数据的设置(set)或者获取(get)都增加一个拦截的功能

当页面取到对应属性时,会进行依赖收集(收集当前组件的watcher),当属性发生变化时,就会通知相关的依赖进行更新操作

举个例子,我们在页面上渲染一个数据,我们就会对这个数据进行取值,取值的时候我们就可以把当前的渲染watcher给存起来,当数据变化的时候,就会告诉对应的watcher进行更新,这样就实现了响应式数据原理

Vue实现响应式原理

那Vue的源码是如何实现上面这些步骤的呢(下面的方法都是vue源码定义的)

  1. initData

当Vue初始化数据的时候,会调用一个initdata方法,获取当前data传入的数据

  1. new Observer

创建一个观测类,观测获取到的数据

  1. this.walk(value)

如果数据是一个对象(非数组),就会调用this.walk这个方法,内部会重新遍历这些数据,用defineReactive重新定义(使用Object.defineProperty)

  1. defineReactive(循环对象属性定义响应式变化)
  2. Object.defineProperty(使用Object.defineProperty重新定义数据)

由于我们传入的data必定是个对象,所以一定会观测到数据变化

1
2
3
4
new Vue({
data: {}
// 不能是 data: ''
})

这也是为什么data传入的一定是个对象,不能是字符串;如果对象中嵌套对象,内部会进行递归遍历

但是如果data中的属性是后面添加进行去,Vue就无法对他进行响应式更新,举个例子

1
2
3
4
5
6
7
8
9
10
new Vue({
data:{
a:1
}
})

// `vm.a` 是响应式的

vm.b = 2
// `vm.b` 是非响应式的

因为初始化我们initData的时候,没有获取到他的值,但是我们可以这样操作

1
this.$set(this.someObject,'b',2)

这也是在对象中如何重新定义一个新的属性来被Vue检查到的方法

大致的源码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function observe (obj) { // 我们来用它使对象变成可观察的
// 判断类型
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
function defineReactive (obj, key, value) {
// 递归子属性
observe(value)
Object.defineProperty(obj, key, {
enumerable: true, //可枚举(可以遍历)
configurable: true, //可配置(比如可以删除)
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend() // ** 收集依赖 ** /        
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
val = newVal
childOb = !shallow && observe(newVal) //如果赋值是一个对象,也要递归子属性
dep.notify() /**通知相关依赖进行更新**/
}
})
}
}

vue如何检测数组变化

数组更新的方法有七种,pop/push/reverse/shift/sort/splice/unshift,Vue使用函数劫持的方式,重写了数组的方法,进行了原型链的重写,指向了自己定义的数组原型方法

这样当调用数组api的时候,可以通知依赖更新,如果数组中包含着引用类型,会对数组中的引用类型再次进行监控

  1. initData
  2. new Observer
  3. protoAugment(将数组的原型方法指向重写的原型)
  4. observeArray(深度观察数组中的每一项,如果是对象的话,会进一步观测)

为什么只监听七种方法呢,因为只有这七种方法才可以改变我们的数组,这也是为什么我们直接通过角标修改数组,视图不能及时的响应更新,当这并不是绝对的,举个例子

1
2
3
4
5
// 假设有一个数组
let ary = [ { a: 1 }, 'csing'];

ary[0].a = 2; // 可以监听到
ary[1] = 'chensheng'; // 监听不到

因为observeArray方法会深度观察数组中的每一项,所以如果是对象的话,还是会进行defineReactive定义响应式变化

所以如果需要修改数组中的字符串,需要this.$set(vm.items, indexOfItem, newValue)

大致的源码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const arrayProto = Array.prototype 
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = ['push',  'pop',  'shift',  'unshift',  'splice',  'sort',  'reverse']

methodsToPatch.forEach(function (method) { // 重写原型方法
const original = arrayProto[method] // 调用原数组的方法
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
 if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify() // 当调用数组方法后,手动通知视图更新
return result
})
})
this.observeArray(value) // 进行深度监控

以上就是我对vue响应式原理的一些理解,如果文章由于我学识浅薄,导致您发现有严重谬误的地方,请一定在评论中指出,我会在第一时间修正我的博文,以避免误人子弟。

-------------本文结束感谢您的阅读-------------
没办法,总要恰饭的嘛~~