1. 监听数组变化
- 其实 Vue 监听数组变化的原理非常简单, 就是将数组的主要方法包裹了一遍
- 只要用户调用以下方法, 就会通知
Watcher
自动更新视图:push()
pop()
shift()
unshift()
splice()
sort()
reverse()
演示
工程源码:
src/core/observer/array.js
// 获取数组的原型 Array.prototype, 为了方便拿到数组原有的方法
const arrayProto = Array.prototype
// 创建一个空对象 arrayMethods, 并将 arrayMethods 的原型指向 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 就是 defineProperty() 可以参见 util/lang.js
def(arrayMethods, method, function mutator (...args) {
// 调用原来的函数
const result = original.apply(this, args)
// 该数组是响应式的时候, 上面会有一个 __ob__ 属性
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)
// 重点在这里: 每个响应式数组上都会有一个 __ob__ 利用我们保留的 __ob__ 属性获取 notify 方法来通知 Watcher 更新视图
ob.dep.notify()
return result
})
})
**Object.create()**方法用于创建一个对象,使现有的对象作为新创建对象的原型(prototype)
总结
- Vue 之所以能监听到数组的变化, 是因为把数组中常用的方法重新包装了一层
- 所谓的包装就是又写了个对象, 这个对象里拥有 Array 原型中的方法, 内部也是调用的 Array 原型中的方法, 额外多调用了一下
notify()
通知Watcher
更新视图, 最后将原型也指向Array
的原型, 形成了一种继承关系 - 拓展: 这种对方法的包装, 我们也称为方法的重写
- Vue 通过对方法的重写实现了数组修改而通知视图变化, 程序员们无需学习任何新的知识, 中间的过程是完全无感的, 这种设计理念也被称为非侵入式的响应式原理, 与之对应的就是侵入式的响应式设计理念, 如: 小程序 / react
- 小程序:
setData()
- react:
setState()
- 小程序:
2.$set 的原理
数组修改数据为什么不会被监测? 对象属性为什么可以?
网友对尤雨溪提出的 issues:
https://github.com/vuejs/vue/issues/8562
- Vue 实现响应式原理是依赖
Object.defineProperty()
方法 - 数组可以看做是索引和值的键值对, 同样可以被监测到
// 对象
const obj = {
age: 18 // 键为 age 值为 18
}
// 数组
const arr = [18] // 等同于键为 0, 值为 18
监测数组
-
通过结果可以观察出, 如果数据量提升到十万甚至百万, 对数组进行修改时, 性能损耗会非常大
-
Vue 官方通过权衡后, 觉得这样做性价比不高, 所以没有对数组进行监测
-
尤雨溪原话
性能代价和获得的用户体验收益不成正比。
const arr = []
defineReactive(arr, 0, 'a')
defineReactive(arr, 1, 'b')
defineReactive(arr, 2, 'c')
defineReactive(arr, 3, 'd')
defineReactive(arr, 4, 'e')
console.log(arr) // 访问数组的所有方法, 所有属性的 get 都会执行一次
arr.push('f') // 末尾追加, 所以只需要把数组中所有元素遍历一遍, 所有属性 get 都会执行一次
arr.unshift('-a') // 开头插入, 会导致数组元素顺序发生变化, 所有属性的 get 和 set 都会执行一次
演示
工程源码:
src\core\observer\index.js
复习 $set 使用方法
- $set() 有三个参数
- 参数1: 要修改的对象 / 数组
- 参数2: 要修改的键(索引)是什么
- 参数3: 要设置的值是什么
export function set (target, key, val) {
// 判断是否为数组
if (Array.isArray(target)) {
// 这里的操作是怕传入一个越界的索引, 往里面添加数据
// 传入的索引和当前长度取最大值
target.length = Math.max(target.length, key)
// 删除原来位置的元素, 然后在当前位置添加一个数据, 就完成了替换更新
// 而由于 splice() 是数组重写的方法, 所以会重新更新页面
target.splice(key, 1, val)
return val
}
// 如果是对象就走后面的代码, 如果是数组就不看后面的了
if (key in target && !(key in Object.prototype)) {
// 如果你修改的对象, 属性在原来就存在, 说明已经被劫持了
// 直接修改对象属性即可, 会自动更新视图
target[key] = val
return val
}
const ob = target.__ob__
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
总结
- 所谓的
$set
方法, 内部做了判断, 分为以下三种情况:- 如果传入的是数组, 修改数组中的元素, 就是用的
splice()
- 如果传入的是对象,
- 属性已存在于对象中, 就直接改值, 说明数据已被劫持
- 属性不存在于对象中, 说明这是新的属性, 所以使用
defineReactive()
进行数据劫持
- 如果传入的是数组, 修改数组中的元素, 就是用的