核心要点
- vue2数据响应式的实现
- vue3数据响应式的实现
- vue2和vue3响应式原理的区别
1、vue2数据响应式
vue 2 是通过 Object.defineProperty 来实现数据 读取和更新时的操作劫持,通过更改默认的 getter/setter 函数,在 get 过程中收集依赖,在 set 过程中派发更新的;
通过下面的简易代码来分析
// 响应式数据处理,构造一个响应式对象
class Observer {
constructor(data) {
this.data = data
this.walk(data)
}
// 遍历对象的每个 已定义 属性,分别执行 defineReactive
walk(data) {
if (!data || typeof data !== 'object') {
return
}
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
// 为对象的每个属性重新设置 getter/setter
defineReactive(obj, key, val) {
// 每个属性都有单独的 dep 依赖管理
const dep = new Dep()
// 通过 defineProperty 进行操作代理定义
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// 值的读取操作,进行依赖收集
get() {
if (Dep.target) {
dep.depend()
}
return val
},
// 值的更新操作,触发依赖更新
set(newVal) {
if (newVal === val) {
return
}
val = newVal
dep.notify()
}
})
}
}
// 观察者的构造函数,接收一个表达式和回调函数
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm
this.getter = parsePath(expOrFn)
this.cb = cb
this.value = this.get()
}
// watcher 实例触发值读取时,将依赖收集的目标对象设置成自身,
// 通过 call 绑定当前 Vue 实例进行一次函数执行,在运行过程中收集函数中用到的数据
// 此时会在所有用到数据的 dep 依赖管理中插入该观察者实例
get() {
Dep.target = this
const value = this.getter.call(this.vm, this.vm)
// 函数执行完毕后将依赖收集目标清空,避免重复收集
Dep.target = null
return value
}
// dep 依赖更新时会调用,执行回调函数
update() {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
// 依赖收集管理者的构造函数
class Dep {
constructor() {
// 保存所有 watcher 观察者依赖数组
this.subs = []
}
// 插入一个观察者到依赖数组中
addSub(sub) {
this.subs.push(sub)
}
// 收集依赖,只有此时的依赖目标(watcher 实例)存在时才收集依赖
depend() {
if (Dep.target) {
this.addSub(Dep.target)
}
}
// 发送更新,遍历依赖数组分别执行每个观察者定义好的 update 方法
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
Dep.target = null
// 表达式解析
function parsePath(path) {
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) {
return
}
obj = obj[segments[i]]
}
return obj
}
}
这里省略了数组部分,但是 数组本身的响应式监听 是通过重写数组方法来实现的,而 每个数组元素 则会再次进行 Observer 处理(需要数组在定义时就已经声明的数组元素)。
因为 Object.definePorperty 只能对 对象的已知属性 进行操作,所有才会导致 没有在 data 中进行声明的对象属性直接赋值时无法触发视图更新,需要通过($set)来处理。
而数组因为是通过重写数组的7个方法【 'push','pop','shift','unshift', 'splice','sort','reverse'】和遍历数组元素进行的响应式处理,也会导致按照数组下标进行赋值或者更改元素时无法触发视图更新
<body>
<div id="app" class="demo-vm-1">
<p>{{arr[0]}}</p>
<p>{{arr[2]}}</p>
<p>{{arr[3].c}}</p>
</div>
</body>
<script>
new Vue({
el: "#app",
data() {
return {
arr: [1, 2, { a: 3 },{ c: 5 }]
}
},
mounted() {
console.log("demo Instance: ", this.$data);
setTimeout(() => {
console.log('update')
this.arr[0] = { o: 1 } //设置完后,发现页面展示的数据不会更新
this.arr[2] = { a: 1 } //设置完后,发现页面展示的数据不会更新
},2000)
},
})
</script>
因为数组元素的前三个元素 在定义时都是简单类型,所以即使在模板中使用了该数据,也无法进行依赖收集和更新响应
2、vue 3 的响应式实现
vue 3 采用了全新的 Proxy 对象来实现整个响应式系统基础,Proxy 是 ES6 新增的一个构造函数,用来创建一个 目标对象的代理对象,拦截对原对象的所有操作;用户可以通过注册相应的拦截方法来实现对象操作时的自定义行为;
但是 只有通过 proxyObj 进行操作的时候才能通过定义的操作拦截方法进行处理,直接使用原对象则无法触发拦截器,这也是 Vue 3 中要求的 reactive 声明的对象修改原对象无法触发视图更新的原因;
并且 Proxy 也只针对 引用类型数据 才能进行代理,所以这也是 Vue 的基础数据都需要通过 ref 进行声明的原因,内部会建立一个新对象保存原有的基础数据值;
// vue3响应式原理
let toProxy = new WeakMap() // 原对象:代理过的对象
let toRaw = new WeakSet() // 代理过的对象:原对象
function isObject(val) {
return typeof val === 'object' && val !== 'null'
}
function reactive(target) {
// 创建响应式对象
return createReactiveObject(target)
}
function createReactiveObject(target) { // 创建代理后的响应式对象
if (!isObject(target)) { // 如果不是对象,直接返回
return target
}
let proxy = toProxy.get(target) // 如果对象已经被代理过了,直接返回
if(proxy) {
return proxy
}
let baseHandler = {
//receiver:被代理后的对象
get(target,key,receiver) {
console.log('获取');
// receiver.get() ==》 new proxy().get 这会报错,也就意味着我们不能直接取到被代理对象上的属性,这时候我们需要用到Reflect,这其实也是一个对象,它只不过也含有一些明显属于对象上的方法,且和proxy上的方法一一对应
let result = Reflect.get(target,key,receiver)
//递归多层代理,相比于vue2的优势是,vue2默认递归,而vue3中,只要不使用就不会递归。
return isObject(result) ? reactive(result) : result
},
set(target,key,value,receiver) {
let hadkey = target.hasOwnProperty(key)
let oldValue = target[key]
if(!hadkey) {
console.log('新增');
} else if (oldValue !== value) {
console.log('修改');
}
let res = Reflect.set(target,key,value,receiver)
return res
},
deleteProperty(target,key) {
console.log('删除');
let res = Reflect.deleteProperty(target,key)
return res
}
}
let observed = new Proxy(target, baseHandler)
toProxy.set(target, observed)
toRaw.add(observed,target)
return observed
}
3、vue2和vue3响应式原理的区别
- vue2使用 Object.defineProperty() 实现,而vue3使用 Proxy() 实现
- vue2 Object.defineProperty 不兼容 IE8,vue3 Proxy 不兼容 IE11
- vue2 Object.defineProperty 是劫持对象属性,vue3 Proxy是代理整个对象
- vue2 Object.defineProperty 不能监听到数组下标变化和对象新增属性,vue3 Proxy 可以
- vue2 Object.defineProperty 会污染原对象,修改时是修改原对象,vue3 Proxy是对原对象进行代理并会返回一个新的代理对象,修改的是代理对象
- vue2 Object.defineProperty 局限性大,只能针对单属性监听,所以在一开始就要全部递归监听,Proxy 对象嵌套属性运行时递归,用到才代理,性能提升很大,首次渲染更快
- vue2 数组响应式的实现是通过 重写数组的原型方法实现,而vue3通过Proxy实现
- vue3 拦截操作更加多样,多达13种拦截方法