100行代码 模拟实现 Vue 响应式系统
Vue 的响应式系统背后原理并不复杂 ------ 用 Proxy 代理对象 + 订阅-发布模式,就能实现 依赖收集 和 触发更新。下面我用 100 行代码,手写一个简化版。
订阅发布
首先创建一个 subscriptions 表(Map),用来收集 响应式数据 对应的 effect 函数(订阅 过程),以及数据变化时找到这些 effect 并执行(发布 触发更新过程)。
typescript
type Effect = (() => void)
const subscriptions = new Map<Object, Set<Effect>>()
function subscribe(target: any, effect: Effect){
const effects = subscriptions.get(target) || new Set<Effect>()
effects.add(effect)
subscriptions.set(target, effects)
}
function publish(target: any){
const effects = subscriptions.get(target)
if(!effects || effects.size === 0) return
effects.forEach(effect => effect())
}
响应式数据创建
第二步,实现 ref 函数,它使用 Proxy 返回一个代理对象,通过拦截 .value 的 get、set 实现 依赖收集 和 触发更新。
注意:ref 依赖外部的 currentEffect 来建立关联,所以需要先声明它。subscribe 和 publish 上一步已经定义好了,这里直接用。
typescript
let currentEffect: Effect | null = null
function ref(value: any){
const refObj = {
value: value,
__isRef: true
}
return new Proxy(refObj,{
get(target, key){
if(key !== 'value') return
if(currentEffect !== null) subscribe(target, currentEffect) // 订阅
return target['value']
},
set(target, key, value){
if(key !== 'value') return true
target['value'] = value
publish(target) // 发布
return true
}
})
}
App 类实现
好了现在我们来模拟一个 app,实现 挂载 方法,执行首轮 渲染 和 依赖收集。我们会把 渲染 + diff 更新 DOM 的过程打包成一个 effect 函数,先把 currentEffect 指向它,再执行它。这样当 渲染 过程中读取到 响应式数据 时,就能自动完成 依赖收集。后续 响应式数据 变化时,就会找到这个 effect 并重新执行。
typescript
type Render = (() => string)
type Component = (() => Render)
class App {
private rootComponent: null | Component = null
public createApp(component: Component){
this.rootComponent = component
return this
}
public mount(selector: string){
const container = null // document.querySelector(selector)
let render = this.rootComponent()
let oldVnode: string | null = null
let effect = function(){
const newVnode = render()
// patch(oldVnode, newVnode, container) diff 更新过程
}
currentEffect = effect
effect()
currentEffect = null
}
}
组件实现
现在,实现这个 组件,并将 点击事件 暴露到全局,以便后续模拟 响应式数据 变化触发 组件 更新过程。
typescript
let click: (()=>void) | null = null // 点击事件
/** 模拟该组件实现
* <template>
* <button @click="()=>count.value++">{{count}}</button>
* </template>
* <script setup>
* const count = ref(1)
* </script>
*/
function APP(){
const count = ref(1)
function render(){
return `<Button>${count.value}</Button>`
}
function add(){
count.value++
}
click = add // 点击事件注册到浏览器
return render
}
主线程
整个流程,创建 app,挂载根 组件;再模拟 点击事件 执行。
typescript
const app = new App()
app.createApp(APP)
app.mount('#app')
// 模拟点击事件执行
if(click !== null) click()
全部代码
typescript
type Render = (()=>string)
type Component = () => Render
let currentEffect: (()=>void) | null = null
const subscriptions = new Map<Object, Set<() => void>>()
let click: (()=>void) | null = null
let realDOM = `<div id="app"></div>`
function ref(value: any){
const refObj = {
value: value,
__isRef: true
} as { value: any, __isRef: Readonly<true> }
console.log("响应式对象创建");
return new Proxy(refObj,{
get(target, key){
console.log("响应式数据被读取 收集依赖");
if(key !== 'value') return
if(currentEffect !== null) subscribe(target, currentEffect)
return target['value']
},
set(target, key, value){
if(key === 'value'){
target['value'] = value
console.log("响应式数据被修改 触发更新");
publish(target)
}
return true
}
})
}
function subscribe(target: Object, effect: () => void){
const effects = subscriptions.get(target) ?? new Set()
effects.add(effect)
subscriptions.set(target, effects)
}
function publish(target: Object){
const effects = subscriptions.get(target)
if(!effects || effects.size === 0) return
effects.forEach(effect => effect())
}
function APP(){
console.log('=== setup 函数开始执行 ===');
const count = ref(1)
console.log('=== setup 函数执行完毕 ===');
function render(){
console.log('执行渲染函数');
return `<button>${count.value}</button>`
}
// 模拟点击事件
function add(){
console.log('执行点击事件 模拟响应式数据变化');
count.value++
}
click = add
return render
}
class App {
private rootComponent: null | Component = null
public createApp(component: Component){
this.rootComponent = component
return this
}
public mount(selector: string){
console.log("=== 根组件挂载 ===");
const container = null // document.querySelector(selector)
if(this.rootComponent === null) return
const render = this.rootComponent()
let oldVnode: string | null = null
let effect = function(){
console.log("=== 执行副作用函数 ===");
const newVnode = render()
patch(oldVnode,newVnode,container)
console.log("=== 副作用函数执行完毕 ===");
console.log("当前DOM显示效果", realDOM);
}
currentEffect = effect
effect()
currentEffect = null
console.log("=== 根组件挂载完毕 ===");
}
}
function patch(oldVnode: string | null, newVnode: string | null, container: Element | null){
// diff 比较 合并更新。。。
realDOM = `<div id="app">${newVnode}</div>`
oldVnode = newVnode
}
function main(){
const app = new App()
app.createApp(APP)
app.mount('#app')
console.log('====================');
if(click !== null) click()
}
main()
代码执行输出
powershell
=== 根组件挂载 ===
=== setup 函数开始执行 ===
响应式对象创建
=== setup 函数执行完毕 ===
=== 执行副作用函数 ===
执行渲染函数
响应式数据被读取 收集依赖
=== 副作用函数执行完毕 ===
当前DOM显示效果 <div id="app"><button>1</button></div>
=== 根组件挂载完毕 ===
====================
执行点击事件 模拟响应式数据变化
响应式数据被读取 收集依赖
响应式数据被修改 触发更新
=== 执行副作用函数 ===
执行渲染函数
响应式数据被读取 收集依赖
=== 副作用函数执行完毕 ===
当前DOM显示效果 <div id="app"><button>2</button></div>