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 返回一个代理对象,通过拦截 .valuegetset 实现 依赖收集触发更新

注意:ref 依赖外部的 currentEffect 来建立关联,所以需要先声明它。subscribepublish 上一步已经定义好了,这里直接用。

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>
相关推荐
Heo1 小时前
Vite进阶用法详解
前端·javascript·面试
狂炫冰美式2 小时前
人均配了AI, 为什么公司还是没变快? 🤔 本质还是分布式系统问题
前端·后端·架构
乘风gg3 小时前
多 Agent 不是万能的!搞懂这 5 个原则,少走 1 年弯路!
前端·agent·ai编程
猩猩程序员3 小时前
Vercel 推出 Agent 框架 Eve:让 AI Agent 像写 Web 应用一样简单
前端
爱读源码的大都督4 小时前
Claude Code源码分析(三):为什么系统提示词中需要有tools呢?
前端·人工智能·后端
爱勇宝4 小时前
Claude Code 被曝暗藏“隐形检测”代码:封代理不是最可怕的,可怕的是你根本不知道它在干什么
前端·后端·程序员
小牛不牛的程序员4 小时前
我用 Claude Code 半天撸完了一个完整网站,AI 编程到底提升了多少效率?
前端
东风破_4 小时前
JavaScript 面试常考的字符串算法:从反转字符串到回文判断
前端·javascript
ITOM运维行者4 小时前
从零搭建企业级服务器监控体系:踩坑实录与架构设计
前端·后端