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>
相关推荐
书中枫叶15 小时前
做了个「句拾」小程序,最难的不是业务,是字体
前端·javascript·vue.js
FFF_6345602315 小时前
简单的画板小工具,下载即用
前端·javascript·css
嘟嘟071716 小时前
Next.js 里 SSR、CSR 和水合到底差在哪?从一段待办代码说起
前端·后端·next.js
南雨北斗16 小时前
回调地狱、jQuery jqXHR .then 链式、Promise + async/await写法对比
前端
liuxiaocheng16 小时前
快速上手:5 分钟跑通第一个 AI SDK 例子
前端·人工智能·后端
霹雳桃16 小时前
移动端 H5 折叠屏适配实战:为什么 max-width 没用,min(vw, px) 才是正解
前端·前端框架
Goodbye16 小时前
useContext 上下文与自定义 Hooks:跨层级数据共享的优雅方案
前端
新时代牛马16 小时前
Vue.js 响应式原理详解
前端·javascript·vue.js
jaysee-sjc16 小时前
【JavaWeb】Tlias智能学习辅助系统|后端Web实战(登录认证)
java·开发语言·前端·学习·mybatis