100行代码 模拟实现Vue 响应式系统

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>
相关推荐
Setsuna_F_Seiei8 小时前
前端转型 Agent 开发 06 之 Agent Memory 记忆系统(让 Agent 更智能,更懂你)
前端·agent·ai编程
志尊宝9 小时前
Vue3 零基础每日笔记(073):keep-alive 页面缓存实战——列表页返回不丢状态
vue.js·笔记·缓存·#vue #前端 #前端开发·#javascript·#vue.js
zhangzeyuaaa9 小时前
Ruby 多线程、GVL(GIL)与 Mutex 完全指南
开发语言·前端·ruby
默_笙9 小时前
🌊 向量库和 ES 都查不出"关系",我只好给奶茶建了一张人脉网
前端·javascript
优选资讯9 小时前
标签打印软件怎么对接 Excel 数据
前端·excel
一个风轻云淡10 小时前
GCC 和 GDB命令简单解读
java·linux·前端
u01110267511 小时前
图片处理接口如何统一错误信息 前端提示与后端错误码设计
前端·状态模式
福兮说12 小时前
浏览器里把 iPhone 的 HEIC 转成 JPG:Chrome 解不开、转完大了七成、拍摄时间全丢,六个坑实测
前端·javascript·图像处理·chrome·ios·iphone·heic
libokaifa12 小时前
把语音模型塞进手机:sherpa-onnx 在 Android 上的 ASR / TTS
前端
deli00712 小时前
DLA 枝晶生长实测:5000 个粒子集体停摆之后,分形维数收敛到 1.70
前端