Vue3响应式原理
1. Vue3响应式系统与Vue2响应式系统的区别
底层实现 Vue 2 使用了 Object.defineProperty 来追踪属性的变化,并触发相应的更新。而 Vue 3 改用了 JavaScript 的 Proxy API 来实现响应式系统。Proxy API 提供了更强大和灵活的功能,使得 Vue 3 的响应式系统更高效和可扩展
性能优化 Vue 3 在响应式系统方面进行了一些优化,提高了性能。Vue 3 使用了基于 Proxy 的跟踪机制,可以更精确地追踪属性的变化,避免了 Vue 2 中的一些性能瓶颈。此外,Vue 3 还引入了静态标记(Static Marking)和 树摇(Tree Shaking)等优化技术,减少了不必要的更新和渲染操作
嵌套响应式 Vue 2 的响应式系统在嵌套对象或数组的情况下存在一些限制,需要通过 Vue.set 或 this.$set 进行手动触发更新。而 Vue 3 的响应式系统可以自动追踪嵌套对象和数组的变化,无需手动触发更新
Composition API Vue 3 引入了 Composition API,这是一个基于函数的 API 风格,可以更好地组织和重用组件逻辑。Composition API 提供了更灵活的响应式能力,使得开发者可以更自由地定义响应式数据和副作用
TypeScript 支持 Vue 3 对 TypeScript 的支持更加完善,提供了更准确的类型推断和类型检查。Vue 3 的响应式系统与 TypeScript 的类型系统更好地集成,使得在使用 TypeScript 开发时更加方便和可靠
2. 实现Vue响应式
Vue3中 reactivity 和 effect 基本用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"></div> <script type="module"> import { reactive, effect} from '../../../node_modules/@vue/reactivity/dist/reactivity.esm-browser.js' const state = reactive({name:'gzy',age:18})
effect(()=>{ app.innerHTML = state.name + state.age }) setTimeout(()=> { state.name = 'ggg' },1000)
</script> </body> </html>
|
可以得知Vue3的响应式 离不开 reactive 和 effect 的互相配合,下面来一步步实现Vue3的响应式
reactivity源码实现
使用 ES6 的 Proxy API 来做一层代理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import { isObject } from "@vue/shared";
export function reactive(target){ if(!isObject(target)) return target
const proxy = new Proxy(target,{ get(target,key,receiver){ return Reflect.get(target,key ,receiver) }, set(target,key,value,receiver){
return Reflect.set(target,key,value,receiver) } }) return proxy }
|
这里会有一个问题 为什么取值和设置值的时候不直接通过 return target[key] 而是要 Reflect.get(target,key ,receiver)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| let person = { name: 'gzy', get aliasName(){ return `**${this.name}**` } }
let proxyPerson = new Proxy(person, { get(target, key , receiver){ return target[key] } })
proxyPerson.aliasName
|
proxyPerson.aliasName 来取值的时候,会走 proxyPerson 的 get 方法 , 其中 target 为 person对象,key为 person.aliasName 方法,这时候的 this是person ,返回的是 person.name
为了name 的依赖收集,需要再获取aliasName的时候 ,也希望name属性也会触发get , 所以需要调用 Reflect.get(target,key ,receiver)
而 receiver 指的是代理对象,所以会从代理对象上取name属性 ,这样就达到了我们的需求
为了代码的简介和复用, 新建 handlers.ts 文件并且把 proxy的对象参数提取 成为 mutableHandlers 方法
src/handler.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| export const mutableHandlers = { get(target,key,receiver){ return Reflect.get(target,key ,receiver) }, set(target,key,value,receiver){ return Reflect.set(target,key,value,receiver) } }
|
src/reactivity.ts
1 2 3 4 5 6 7 8 9 10 11 12
| import { isObject } from "@vue/shared"; import { mutableHandlers } from "./handlers";
export function reactive(target){ if(!isObject(target)) return target
const proxy = new Proxy(target,mutableHandlers) return proxy }
|
代理对象缓存
1 2 3 4 5 6
| import { reactive } from './reactivity.js' const obj = {name:'gzy',age:18} const state1 = reactive(obj) const state2 = reactive(obj)
console.log(state1 === state2)
|
当对同一个对象创建两次代理对象,应该只需要代理一次就行,使用WeakMap做一下缓存表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import { isObject } from "@vue/shared"; import { mutableHandlers } from "./handlers";
const reactiveMap = new WeakMap();
export function reactive(target){ if(!isObject(target)) return target
let existingProxy = reactiveMap.get(target)
if(existingProxy) return existingProxy
const proxy = new Proxy(target,mutableHandlers) reactiveMap.set(target,proxy)
return proxy }
|
处理对象循环代理
1 2 3 4 5 6 7
| import { reactive } from './reactivity.js' const obj = {name:'gzy',age:18} const state1 = reactive(obj) const state2 = reactive(state1)
console.log(state1 === state2)
|
代理过的对象继续代理 (已经被代理过的对象不能再代理了)
- 在vue3.0的时候 会创建一个反向映射表 { 代理的结果 -> 原内容 }
- 目前不用创建反向映射表, 用的方式是,如果这个对象被代理过了说明已经被proxy拦截过了(添加属性)
src/reactivity.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import { isObject } from "@vue/shared"; import { mutableHandlers } from "./handlers";
export const enum ReactiveFlags { IS_REACTIVE = '_v_isReactive' }
const reactiveMap = new WeakMap();
export function reactive(target){ if(!isObject(target)) return target
let existingProxy = reactiveMap.get(target)
if(existingProxy) return existingProxy
if(target[ReactiveFlags.IS_REACTIVE]){ return target }
const proxy = new Proxy(target,mutableHandlers) reactiveMap.set(target,proxy)
return proxy }
|
src/handler.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import { ReactiveFlags } from "./reactivity"
export const mutableHandlers = { get(target,key,receiver){
if(key === ReactiveFlags.IS_REACTIVE){ return true } return Reflect.get(target,key ,receiver) }, set(target,key,value,receiver){
return Reflect.set(target,key,value,receiver) } }
|
effect源码实现
effect的基础用法
1 2 3 4 5 6 7 8
| import { reactive, effect} from './reactivity.js'
const state = reactive({name:'gzy',age:18})
effect(()=>{ app.innerHTML = state.name + state.age + state.name })
|
effect.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| export let activeEffect = undefined
export class ReactiveEffect { constructor(private fn){ } run(){ activeEffect = this return this.fn() } }
export function effect(fn){ const _effect = new ReactiveEffect(fn) _effect.run() }
|
- effect默认会执行一次
- 执行的时候会先把当前的实例挂载到全局 activeEffect属性
- 然后调用effect的函数执行,执行期间会调用响应式对象的get取值,这是在get里就能拿到 activeEffect属性值
多次调用effect
1 2 3 4 5 6 7
| effect(()=>{ app.innerHTML = state.name }) state.age effect(()=>{ app.innerHTML = state.age })
|
需要每次执行完effect的方法后,把activeEffect赋值为null,这样不在effect中进行取值的话就不会收集依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| export let activeEffect = undefined
export class ReactiveEffect { constructor(private fn){ } run(){ try{ activeEffect = this return this.fn() }finally{ activeEffect = null } } }
export function effect(fn){ const _effect = new ReactiveEffect(fn) _effect.run() }
|
effect嵌套
1 2 3 4 5 6 7
| effect(()=>{ app.innerHTML = state.name effect(()=>{ app.innerHTML = state.age }) app.innerHTML = state.address })
|
- effect1执行,activeEffect = effect1, 这时候去取 name 收集 effect1
- effect2执行 会把 activeEffect = effect2 ,这时 age 收集 effect2 ,执行完毕后会把全局的 activeEffect = null
- 下一次去 state 取 address 就找activeEffect无法收集
原来的是 stack 方法解决,3.2版本后采用的树解决
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| export let activeEffect = undefined
export class ReactiveEffect { constructor(private fn){ } parent = undefined run(){ try{ this.parent = activeEffect activeEffect = this return this.fn() } finally { activeEffect = this.parent this.parent = undefined } } }
export function effect(fn){ const _effect = new ReactiveEffect(fn) _effect.run() }
|
- effect1执行 parent 为 undefined, activeEffect = effect1, 这时 name 收集 effect1
- effect2执行 因为全局的 activeEffect = effect1,所以先执行 parent = effect1 , 再把全局的 activeEffect = effect2, 这时 age 收集 effect2 , 最后会把 全局的 activeEffect 设置为 effect1
- 下一次去 state 取 address 收集 effect1
依赖收集(属性和effect关联起来)
src/handler.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import { activeEffect, track, trigger } from "./effect" import { ReactiveFlags } from "./reactivity"
export const mutableHandlers = { get(target,key,receiver){ if(key === ReactiveFlags.IS_REACTIVE){ return true }
const res = Reflect.get(target,key ,receiver) track(target,key) return res }, set(target,key,value,receiver){
let oldValue = target[key]
const r = Reflect.set(target,key,value,receiver) if(oldValue !== value){ trigger(target,key,value,oldValue) }
return r } }
|
构造依赖关系映射表
src/effect.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
const targetMap = new WeakMap() export function track(target,key){ if(activeEffect){ let depsMap = targetMap.get(target)
if(!depsMap){ targetMap.set(target,(depsMap = new Map)) }
let dep = depsMap.get(key)
if(!dep){ depsMap.set(key, (dep = new Set())) }
let shouldTrack = !dep.has(activeEffect) if(shouldTrack){ dep.add(activeEffect) activeEffect.deps.push(dep) } } }
|
在浏览器打开应该是这样的

属性改变时执行对应的effect
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"></div> <script type="module"> import { reactive, effect} from './reactivity.js' const state = reactive({name:'gzy',age:18}) effect(()=>{ state.name = Math.random() app.innerHTML = state.name + state.age + state.name })
setTimeout(()=>{ state.name = 'ggggg' },2000) </script> </body> </html>
|
src/effect.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| export function trigger(target,key,newVal,oldVal){ const depsMap = targetMap.get(target) if(!depsMap) { return }
const dep = depsMap.get(key);
dep && dep.forEach(effect => { if(effect !== activeEffect) effect.run() }) }
|
cleanupEffect
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"></div> <script type="module"> import { reactive, effect} from './reactivity.js'
const state = reactive({name:'gzy',age:18,flag:true})
effect(()=>{ app.innerHTML = state.flag ? state.name : state.age })
setTimeout(()=>{ state.flag = false setTimeout(()=>{ state.name = 'xxx' },1000) },2000) </script> </body> </html>
|
在 ReactiveEffect 类上新增 deps = [],并且保存 new Set(effect)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
let dep = depsMap.get(key)
if(!dep){ depsMap.set(key, (dep = new Set())) }
let shouldTrack = !dep.has(activeEffect) if(shouldTrack){ dep.add(activeEffect)
activeEffect.deps.push(dep) }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| export class ReactiveEffect { constructor(private fn){ } parent = undefined deps = [] run(){ try{ this.parent = activeEffect activeEffect = this
cleanupEffect(this)
return this.fn() } finally { activeEffect = this.parent this.parent = undefined } } stop(){ } }
|
1 2 3 4 5 6 7 8 9
| function cleanupEffect(effect){ const { deps } = effect for(let i = 0 ; i < deps.length;i++){ deps[i].delete(effect) } effect.deps.length = 0 }
|
为了防止删除Set中的effect和往Set种新增effect造成死循环,对trigger方法进行修改
trigger
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| export function trigger(target,key,newVal,oldVal){ const depsMap = targetMap.get(target) if(!depsMap) { return }
const dep = depsMap.get(key);
const effects = [...dep] effects && effects.forEach(effect => { if(effect !== activeEffect) effect.run() })
|