在理解Vue响应式原理之前,可以想想这几个问题,数据改变后,Vue是如何监听、修改、渲染页面的。
在此之前,我们先来说说Vue的双向数据绑定原理吧,或许对你有点启发
- 实现一个监听器
Observer
:对数据对象进行遍历,包括子属性对象的属性,利用Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发setter,那么就监听到了数据变化。 - 实现一个解析器
Compile
:解析Vue模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。 - 实现一个订阅者
Watcher
:Watcher 订阅者是 Observer 和 Compile之间通信的桥梁,主要的任务是订阅Observer中的属性值变化的消息,当收到属性值变化的消息时,触发解析器Compile中对应的更新函数 - 实现一个订阅器
Dep
:订阅器采用发布-订阅设计模式,用来收集订阅者Wahcher,对监听器Observer和订阅者Watcher进行统一管理
Vue
的响应式 就是当数据变化后,可以监听到变化,并且将数据修改成新的值,重新渲染页面
一个简单的例子:
<div id="app">
{{ msg }}
</div>
const vm = new Vue({
el: '#app',
data: {
msg: '风姿绰约、花枝招展'
}
});
vm.msg = '手如柔荑、肤如凝脂'; // 见证奇迹的时刻,页面变化啦
-
问:为什么data会直接出现在vm实例对象中咧?
-
答:当创建vue实例时,vue会将data中的成员代理给vue实例,目的是为了实现响应式,监控数据变化,执行某个
监听函数
那么如何实现响应式呢?
Vue响应式原理 的核心主要是使用Object.defineProperty()
的get()
和set()
方法监听数据的变化,不需要手动操作dom元素
Object.defineProperty()
是javascript的标准内置对象,它的作用就是直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
思路:主要是监听数组和对象的的变化,如果是对象,就用defineProperty进行监听,
如果是数组就用数组的变异方法,并改写原型上的方法
监听对象
// 监听对象
function defineReactive(data, key, value) {
observe(value)
Object.defineProperty(data, key, {
get(value) {
console.log('读')
},
set(newVal) {
console.log('写')
// 如果修改的值和原来的值相同,就不修改,可以提升性能
if (value == newVal) {
return
}
value = newVal
render()
}
})
}
// 观察者
function observe(data) {
// 观察数组
if (Array.isArray(data)) {
data.__proto__ = arrayMethods;
}
// 观察对象
if (typeof data === 'object') {
for (let key in data) {
defineReactive(data, key, data[key])
}
}
}
function render() {
console.log('页面渲染了')
}
observe(data)
监听数组
监听数组,用数组的变异方法,并改写原型上的方法,保存原来的原型
数组的变异方法:'push', 'pop', 'shift', 'unshift', 'sort', 'splice', 'reverse'
Array.prototype不是指单个数组,而是指Array()对象本身。prototype函数允许您向Array()对象添加新属性和方法
const arrayProto = Array.prototype;
// 克隆一个新的原型
const arrayMethods = Object.create(arrayProto)
['push', 'pop', 'shift', 'unshift', 'sort', 'splice', 'reverse'].forEach(method => {
// 改变原型上的this指向,将this指向克隆的原型
arrayMethods[method] = function () {
arrayProto[method].call(this, ...arguments)
render()
}
})
Vue的实例方法$set
和$delete
的实现原理也是利用Object.defineProperty()
实现的
function $set(data, key, value) {
// 观察数组
if (Array.isArray(data)) {
data.splice(key, 1, value) //修改
// data.splice(key, 0, value) // 新增
}
// 观察对象
defineReactive(data, key, value)
render()
return value
}
function $delete(data, key, value) {
if (Array.isArray(data)) {
data.splice(key, 1)
}
// 观察对象
delete data[key]
render()
}
利用Object.defineProperty()实现响应式的缺点
- 天生就需要进行递归
- 监听不到数组不存在的索引的改变
- 监听不到数组长度的改变
- 监听不到对象的增删
不用Object.defineProperty()
监听数组的原因,数组中的数据量一般都比较大,Object.defineProperty()
天生就需要递归进行遍历,会耗费性能
Vue3.0使用proxy
代替Object.defineProperty()
实现响应式,就不会存在这些问题了,之后会更新使用proxy
的实现方法,并介绍其优缺点哈~