前端目前两个当家花旦框架 VUE React,它们能够流行开来,响应式原理做出了巨大贡献。毕竟,它通过数据的变更就能够更新相应的视图,极大的将我们从繁琐的DOM操作中解放出来。
所以掌握它们的响应式原理,对掌握前端框架的精髓就很重要了。
Vue2.x的响应式
Vue2中通过 Object.defineProperty 实现数据劫持
,使得数据实现响应式更新。Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
Object.defineProperty(obj, prop, descriptor)
- obj:要定义属性的对象。
- prop:要定义或修改的属性的名称或 Symbol 。
- descriptor:要定义或修改的属性描述符。
返回值:被传递给函数的对象。
响应实现
对象类型
通过Object.defineProperty()
对属性的读取、修改进行拦截
(数据劫持)。
数组类型
通过重写更新数组
的一系列方法来实现拦截
(对数组的变更方法进行了包裹)。
1 | let person = { // 模拟Vue2实现响应式 |
存在问题
- 递归遍历数据对象属性,消耗大
- 新增/删除属性,数据无响应;需要额外方法实现(Vue.set/Vue.delete、this.set/set/get/$delete)
- 数组修改需要额外方法实现(Vue.set),或者通过重写的push/pop/shift/unshift/splice/sort/reverse方法实现
Vue3的响应式
Proxy和Reflect实现响应式原理
- 通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
- 通过Reflect(反射): 对源对象的属性进行操作。
Proxy
Vue3 的响应式原理依赖了 Proxy 这个核心 API,通过 Proxy 可以劫持对象的某些操作。
1 | const obj = { a: 1 }; |
如上例子,用 Proxy 代理了 Obj 对象的属性访问、属性赋值、in 操作符、delete 的操作,并进行 console.log 输出。
Reflect
Reflect 是与 Proxy 搭配使用的一个 API,当劫持了某些操作时,如果需要再把这些操作反射回去,那么就需要 Reflect 这个 API。
由于拦截了对象的操作,所以这些操作该有的功能都丧失了,例如,访问属性 p.a 应该得到 a 属性的值,但此时却不会有任何结果,如果还想拥有拦截之前的功能,那就需要用 Reflect 反射回去。
1 | const obj = { a: 1 }; |
举个例子
以下全文都会通过这个例子来讲述 Vue3 响应式的原理。
1 | <div id="app"></div> |
用 reactive 创建了一个响应式对象 state,并调用了 effect 方法,该方法接受一个副作用函数,effect 的执行会立即调用副作用函数,并将 state.counter
赋值给 #app.innerHTML
;两秒后,state.counter += 1
,此时,effect 的副作用函数会重新执行,页面也会变成 2.
内部的执行过程大概如下图所示:
- 调用
reactive()
返回一个 Proxy 代理对象,并劫持对象的 get 与 set 操作 - 调用
effect()
方法时,会访问属性state.counter
,此时会触发 proxy 的 get 操作。 - get 方法会调用
track()
进行依赖收集;建立一个对象(state)、属性(counter)、effect 副作用函数的依赖关系; - set 方法会调用
trigger()
进行依赖更新;通过对象(state)与属性(coutner)找到对应的 effect 副作用函数,然后重新执行。
reactive
reactive
会返回如下一个 Proxy 对象
1 | const reactive = (target) => { |
effect
1 | let activeEffect; |
首先定义全局的 activeEffect
,它永远指向当前正在执行的 effect 副作用函数。effect 为 fn 创建一个内部的副作用函数,然后立即执行,此时会触发对象的 get 操作,调用 track()
方法。
1 | effect(() => { |
track
track 会建立一个 对象(state) => 属性(counter) => effect 的一个依赖关系
1 | const targetMap = new WeakMap(); |
执行完成成后得到一个如下的数据结构:
1 | [ // map 集合 |
注意:当调用 effect 时,会将当前的副作用函数赋值给全局的 activeEffect
,所以此时可以正确关联其依赖。
trigger
当给 state.counter
赋值的时候就会触发代理对象的 set 操作,从而调用 trigger 方法
1 | setTimeout(() => { |
1 | function trigger(target, key) { |