核心要点
- vue2数据响应式的实现
- vue3数据响应式的实现
- vue2和vue3响应式原理的区别
1、vue2数据响应式
vue 2 是通过 Object.defineProperty 来实现数据 读取和更新时的操作劫持,通过更改默认的 getter/setter 函数,在 get 过程中收集依赖,在 set 过程中派发更新的;
通过下面的简易代码来分析
// 响应式数据处理,构造一个响应式对象
class Observer {constructor(data) {this.data = datathis.walk(data)}// 遍历对象的每个 已定义 属性,分别执行 defineReactivewalk(data) {if (!data || typeof data !== 'object') {return}Object.keys(data).forEach(key => {this.defineReactive(data, key, data[key])})}// 为对象的每个属性重新设置 getter/setterdefineReactive(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 = newValdep.notify()}})}
}// 观察者的构造函数,接收一个表达式和回调函数
class Watcher {constructor(vm, expOrFn, cb) {this.vm = vmthis.getter = parsePath(expOrFn)this.cb = cbthis.value = this.get()}// watcher 实例触发值读取时,将依赖收集的目标对象设置成自身,// 通过 call 绑定当前 Vue 实例进行一次函数执行,在运行过程中收集函数中用到的数据// 此时会在所有用到数据的 dep 依赖管理中插入该观察者实例get() {Dep.target = thisconst value = this.getter.call(this.vm, this.vm)// 函数执行完毕后将依赖收集目标清空,避免重复收集Dep.target = nullreturn value}// dep 依赖更新时会调用,执行回调函数update() {const oldValue = this.valuethis.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种拦截方法
4、参考博客
- Vue2、Vue3响应式原理的区别
- Vue2与Vue3响应式原理与依赖收集详解
- Vue3 和 Vue2 的响应式原理区别