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>
相关推荐
腻害兔7 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
不好听6137 小时前
从一行 JSX 到屏幕像素:前端开发者必须懂的浏览器渲染管线
前端
恒拓高科WorkPlus7 小时前
企业级内网即时通讯建设模型:BeeWorks内网IM四层架构
前端
前端小李子8 小时前
前端环境变量裸奔?我用 EnvShield 给它穿了件防弹衣
前端
youtootech8 小时前
HarmonyOS《柚兔学伴》项目实战25-我的页面、Web 嵌入与项目总结
前端·华为·harmonyos
小林ixn9 小时前
从零到一理解 React 父子组件通信:手写一个 Todo 应用带你彻底搞懂单向数据流
前端·javascript·react.js
醇氧10 小时前
CountDownLatch / CyclicBarrier / Semaphore 面试高频问答清单
前端·面试·职场和发展
qetfw11 小时前
MXU:Tauri 2 + React 的 MaaFramework 跨平台 GUI 源码
前端·python·react.js·前端框架·开源项目·效率工具
甲维斯11 小时前
我要开始吹牛逼了!Kimi K3 “宇宙无敌”!
前端·人工智能