vue2响应式原理
在 Vue 2 中,响应式原理主要是通过 Object.defineProperty 方法来实现的。当一个普通对象被传入 Vue 实例作为 data 选项时,Vue 会遍历对象的所有属性,并使用 Object.defineProperty 方法将这些属性转为 getter 和 setter,从而实现对属性的劫持。
具体来说,当访问一个被劫持的属性时,会触发 getter 方法,Vue 会建立一个依赖关系,将观察者 Watcher 对象添加到属性的依赖列表中。当这个属性被修改时,会触发 setter 方法,通知所有相关的 Watcher 对象进行更新,从而实现响应式数据的变更和视图的更新。
此外,Vue 2 中还使用了一种发布订阅模式的设计,通过 Dep 对象来收集依赖和分发更新通知。每个被劫持过的属性都会有一个对应的 Dep 对象,负责管理和维护该属性的依赖关系。
总的来说,Vue 2 的响应式原理通过 Object.defineProperty 方法和发布订阅模式相结合,实现了对数据的双向绑定和响应式更新,使开发者能够更加方便地构建数据驱动的交互式前端应用。
-
当你把一个普通的
JavaScript
对象传入Vue
实例作为data
选项,Vue
将遍历此对象所有的property
,并使用Object.defineProperty
把这些property
全部转为getter/setter
。
这些getter/setter
对用户来说是不可见的,但是在内部它们让Vue
能够追踪依赖,在property
被访问和修改时通知变更。 -
每个组件实例都对应一个
watcher
实例,它会在组件渲染的过程中把“接触”过的数据property
记录为依赖。之后当依赖项的setter
触发时,会通知watcher
,从而使它关联的组件重新渲染。
-
由于
JavaScript
的限制,Vue
不能检测数组和对象的变化。尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。
Vue
无法检测property
的添加或移除。由于Vue
会在初始化实例时对property
执行getter/setter
转化,所以property
必须在data
对象上存在才能让Vue
将它转换为响应式的。例如:var vm = new Vue({ data:{ a:1 } }) // `vm.a` 是响应式的 vm.b = 2 // `vm.b` 是非响应式的
-
对于已经创建的实例,
Vue
不允许动态添加根级别的响应式property
。但是,可以使用Vue.set(object, propertyName, value)
方法向嵌套对象添加响应式property
。例如,对于:Vue.set(vm.someObject, 'b', 2)
-
有时你可能需要为已有对象赋值多个新
property
,比如使用Object.assign() 或 _.extend()。
但是,这样添加到对象上的新property
不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的property
一起创建一个新的对象。// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })` this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
-
Vue
不能检测以下数组的变动:当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
当你修改数组的长度时,例如:vm.items.length = newLength
举个例子:var vm = new Vue({ data: { items: ['a', 'b', 'c' , {k:1}] } }) vm.items[1] = {a:1} // 不是响应性的 可以用vm.items[1].__ob__.dep.notify(); 触发上面的更新 vm.items.length = 2 // 不是响应性的 vm.items[3].k = 2 // 是响应式的
-
为了解决第一类问题,以下两种方式都可以实现和
vm.items[indexOfItem] = newValue
相同的效果,同时也将在响应式系统内触发状态更新:// Vue.set Vue.set(vm.items, indexOfItem, newValue)
// Array.prototype.splice vm.items.splice(indexOfItem, 1, newValue)
-
你也可以使用
vm.$set
实例方法,该方法是全局方法Vue.set
的一个别名:vm.$set(vm.items, indexOfItem, newValue)
-
为了解决第二类问题,你可以使用
splice:
vm.items.splice(newLength)
-
由于
Vue
不允许动态添加根级响应式property
,所以你必须在初始化实例前声明所有根级响应式property
,哪怕只是一个空值:var vm = new Vue({ data: { // 声明 message 为一个空值字符串 message: '' }, template: '<div>{{ message }}</div>' }) // 之后设置 `message` vm.message = 'Hello!'
-
如果你未在
data
选项中声明message
,Vue
将警告你渲染函数正在试图访问不存在的property
。
这样的限制在背后是有其技术原因的,它消除了在依赖项跟踪系统中的一类边界情况,也使Vue
实例能更好地配合类型检查系统工作。但与此同时在代码可维护性方面也有一点重要的考虑:data
对象就像组件状态的结构 (schema)。提前声明所有的响应式property
,可以让组件代码在未来修改或给其他开发人员阅读时更易于理解。
响应式原理简写
class Observer {
// 观测值
constructor(value) {
this.walk(value);
}
walk(data) {
// 对象上的所有属性依次进行观测
let keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
let value = data[key];
defineReactive(data, key, value);
}
}
}
// Object.defineProperty数据劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {
observe(value); // 递归关键
// --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止
// 思考?如果Vue数据嵌套层级过深 >>性能会受影响
Object.defineProperty(data, key, {
get() {
console.log("获取值");
//需要做依赖收集过程 这里代码没写出来
return value;
},
set(newValue) {
if (newValue === value) return;
console.log("设置值");
//需要做派发更新过程 这里代码没写出来
value = newValue;
},
});
}
export function observe(value) {
// 如果传过来的是对象或者数组 进行属性劫持
if (
Object.prototype.toString.call(value) === "[object Object]" ||
Array.isArray(value)
) {
return new Observer(value);
}
}
响应式
-
组件
data
数据一旦变化,立刻触发视图的更新 -
实现数据驱动视图的第一步
-
核心
API
:Object.defineProperty
-
缺点
- 深度监听,需要递归到底,一次计算量大
- 无法监听新增属性、删除属性(使用
Vue.set
、Vue.delete
可以) - 无法监听原生数组,需要重写数组原型
-
defineProperty
// 触发更新视图
function updateView() {
console.log('视图更新')
}
// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function () {
updateView() // 触发视图更新
oldArrayProperty[methodName].call(this, ...arguments)
// Array.prototype.push.call(this, ...arguments)
}
})
// 重新定义属性,监听起来
function defineReactive(target, key, value) {
// 深度监听
observer(value)
// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
// 深度监听
observer(newValue)
// 设置新值
// 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
value = newValue
// 触发更新视图
updateView()
}
}
})
}
// 监听对象属性
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象或数组
return target
}
// 污染全局的 Array 原型
// Array.prototype.push = function () {
// updateView()
// ...
// }
if (Array.isArray(target)) {
target.__proto__ = arrProto
}
// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}
// 准备数据
const data = {
name: 'zhangsan',
age: 20,
info: {
address: 'shenzhen' // 需要深度监听
},
nums: [10, 20, 30]
}
// 监听数据
observer(data)
// 测试
// data.name = 'lisi'
// data.age = 21
// // console.log('age', data.age)
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所有有 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组
proxy
// proxy-demo
// const data = {
// name: 'zhangsan',
// age: 20,
// }
const data = ['a', 'b', 'c']
const proxyData = new Proxy(data, {
get(target, key, receiver) {
// 只处理本身(非原型的)属性
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('get', key) // 监听
}
const result = Reflect.get(target, key, receiver)
return result // 返回结果
},
set(target, key, val, receiver) {
// 重复的数据,不处理
if (val === target[key]) {
return true
}
const result = Reflect.set(target, key, val, receiver)
console.log('set', key, val)
// console.log('result', result) // true
return result // 是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
// console.log('result', result) // true
return result // 是否删除成功
}
})
vue3的响应式原理
Vue 3 的响应式原理主要依赖于 Proxy 对象和 Reflect API。在 Vue 3 中,每个组件实例都通过 reactive
函数转换成了一个响应式对象,而数据变化的侦测和触发则是通过 effect
函数来实现的。
具体来讲,当使用 reactive
函数将一个普通对象转换为响应式对象时,Vue 3 会使用 Proxy 对象来包裹这个对象,并利用 Proxy 的拦截特性来对对象的属性进行劫持。这样一来,当访问或修改响应式对象的属性时,Vue 3 就能够捕捉到这些操作,并触发相应的更新。
另外,当在组件中访问响应式对象的属性时,Vue 3 会通过 effect
函数建立一个响应式依赖关系,这样当被依赖的响应式对象发生变化时,相关的 effect
函数就会重新执行,从而实现了响应式更新的机制。
总的来说,Vue 3 的响应式原理通过 Proxy 对象和 effect
函数的配合,实现了对数据变化的侦测和视图的更新,使开发者能够更加方便地编写响应式的 Vue 组件。
深度监听,性能更好(获取到哪一层才触发响应式get,不是一次性递归)
可监听新增/删除属性
可监听数组变化
// 创建响应式
function reactive(target = {}) {
if (typeof target !== 'object' || target == null) {
// 不是对象或数组,则返回
return target
}
// 代理配置
const proxyConf = {
get(target, key, receiver) {
// 只处理本身(非原型的)属性
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('get', key) // 监听
}
const result = Reflect.get(target, key, receiver)
// 深度监听
// 性能如何提升的?获取到哪一层才触发响应式get,不是一次性递归
return reactive(result)
},
set(target, key, val, receiver) {
// 重复的数据,不处理
if (val === target[key]) {
return true
}
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('已有的 key', key)
} else {
console.log('新增的 key', key)
}
const result = Reflect.set(target, key, val, receiver)
console.log('set', key, val)
// console.log('result', result) // true
return result // 是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
// console.log('result', result) // true
return result // 是否删除成功
}
}
// 生成代理对象
const observed = new Proxy(target, proxyConf)
return observed
}