响应式原理是什么?为什么vue能监听到data里面的变化?为什么一定要把变量写在data里?为什么有时候数组监听不到?一起来学习一下吧
响应式核心
我们都知道vue是MVVM模式,数据驱动视图,那么他的响应式是如何实现的呢?
面试的时候也经常会这么问,一般我们都会回答一下根据Object.defineProperty
来实现响应式原理,这也就够了,但是具体是怎么实现的呢
Vue在初始化数据的时候,会默认把data里面的值进行遍历,内部会使用Object.defineProperty
重新定义所有属性
而Object.defineProperty
的特点,可以使数据的设置(set)或者获取(get)都增加一个拦截的功能
当页面取到对应属性时,会进行依赖收集(收集当前组件的watcher)
,当属性发生变化时,就会通知相关的依赖进行更新操作
举个例子,我们在页面上渲染一个数据,我们就会对这个数据进行取值,取值的时候我们就可以把当前的渲染watcher给存起来,当数据变化的时候,就会告诉对应的watcher进行更新,这样就实现了响应式数据原理
Vue实现响应式原理
那Vue的源码是如何实现上面这些步骤的呢(下面的方法都是vue源码定义的)
- initData
当Vue初始化数据的时候,会调用一个initdata方法,获取当前data传入的数据
- new Observer
创建一个观测类,观测获取到的数据
- this.walk(value)
如果数据是一个对象(非数组),就会调用this.walk这个方法,内部会重新遍历这些数据,用defineReactive重新定义(使用Object.defineProperty)
- defineReactive(循环对象属性定义响应式变化)
- 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的时候,可以通知依赖更新,如果数组中包含着引用类型,会对数组中的引用类型再次进行监控
- initData
- new Observer
- protoAugment(将数组的原型方法指向重写的原型)
- 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响应式原理的一些理解,如果文章由于我学识浅薄,导致您发现有严重谬误的地方,请一定在评论中指出,我会在第一时间修正我的博文,以避免误人子弟。