实现简版 Vuejs3.0 响应式

243 阅读1分钟

Object.defineProperty 版本

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title> Object.defineProperty 方式</title>
</head>
<body>
  <div id="app"></div>
  <script>

    const target = { count: 0, count1: 10 }
    const targetStack = []

    function isObject(value) {
      return typeof value === 'object' && value !== null 
    }
    
    // Dep 用来收集 watcher 实例
    class Dep {
      constructor() {
        this.subs = []
      }
      depend(watcher) {
        this.subs.push(watcher)
      }
      notify() {
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
      }
    }

    Dep.target = null
    
    // 响应式数据初始化
    function observe (data) {
      if (!isObject(data)) {
        return data
      }
      walk(data)
    }

    function walk (data) {
      Object.keys(data).forEach(key => {
        defineReactive(data, key)
      })
    }

    function defineReactive(target, key) {
      let value = target[key]
      const dep = new Dep()

      Object.defineProperty(target, key, {
        get() {
          if (Dep.target) {
            dep.depend(Dep.target)
          }
          return value
        },
        set(newVal) {
          value = newVal
          dep.notify()
        }
      })
    }

    function pushTarget(watcher) {
       targetStack.push(watcher)  
       Dep.target = watcher
    }

    function popTarget() {
      targetStack.pop()   
      Dep.target = targetStack[targetStack.length - 1]
    }

    // Watcher 
    class Watcher {
      constructor(fn) {
        this.fn = fn
        this.get()
      }
      get () {
        pushTarget(this)  
        let value
        try {
          value = this.fn()
        } finally {   
          popTarget()
        }
        return value
      } 
      update () {
        this.get()
      }
    }

    observe(target)
    
    new Watcher(() => {
      app.innerHTML = `展示${target.count}`
    })
    
    // 2秒后 修改数据
    setTimeout(() => {
      target.count = 10
    }, 2000)

  </script>
</body>
</html>

Proxy 简单版本

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>简版响应式</title>
</head>
<body>
  <div id="app"></div>
  
  <script>
    const reactiveMap = new WeakMap()
    const targetMap = new WeakMap()
    const effectStack = []

    const target = { count: 0, count1: 10 }

    function isObject(value) {
      return typeof value === 'object' && value !== null 
    }
    
    function reactive(target) {
      if (!isObject(target)) {
        console.warn(`value cannot be made reactive: ${String(target)}`);
        return 
      }

      const existProxy = reactiveMap.get(target)
      if (existProxy) {
        return existProxy
      }

      const proxy = new Proxy(target, {
        get: function get(target, key, receiver) { 
          // proxy.key 知晓数据被访问
          const ret = Reflect.get(target, key, receiver)
          track(target, key)
          return ret
        },
        set: function set(target, key, value, receiver) { 
          // proxy.key = newVal 知晓数据被更改
          const result = Reflect.set(target, key, value, receiver)
          trigger(target, key)
          return result
        },
      })

      reactiveMap.set(target, proxy)

      return proxy
    }

    // track: 依赖收集(简版)
    function track(target, key) {
      if (!activeEffect) return;
      let depsMap = targetMap.get(target);
      if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()));
      }
      let dep = depsMap.get(key);
      if (!dep) {
        depsMap.set(key, (dep = new Set()));
      }
      if (!dep.has(activeEffect)) {
        dep.add(activeEffect);
      }
    }

    // trigger: 派发更新(简版)
    function trigger(target, key) {
      let depsMap = targetMap.get(target);
      if (!depsMap) return
      let dep = depsMap.get(key)
      if (dep) {
        dep.forEach(effect => {
          effect()
        })
      }
    }
    
    // 使用高阶函数来模拟 vue3.0 中的 activeEffect
    function wrapper(fn) {
      const wrappered = function () {
        activeEffect = fn
        fn()
      }
      return wrappered
    }

    
    // 测试
    const state = reactive(target)
    
    function printCount() {
      app.innerHTML = `展示${state.count}`
    }

    const wrappered = wrapper(printCount)
    wrappered()

    // 2秒后 修改数据
    setTimeout(() => {
      state.count = 10
    }, 2000)
    
  </script>
</body>
</html>

Proxy 衍进版本

使用 ReactiveEffect 来替代 高阶函数 的实现方式,并且新增了 stack 栈的方式来改进。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>响应式</title>
</head>
<body>
  <div id="app"></div>
  <script>
    let activeEffect

    const reactiveMap = new WeakMap()
    const targetMap = new WeakMap()
    const effectStack = []

    const target = { count: 0, count1: 10 }

    function isObject(value) {
      return typeof value === 'object' && value !== null 
    }
    
    function reactive(target) {
      if (!isObject(target)) {
        console.warn(`value cannot be made reactive: ${String(target)}`);
        return 
      }

      const existProxy = reactiveMap.get(target)
      if (existProxy) {
        return existProxy
      }

      const proxy = new Proxy(target, {
        get: function get(target, key, receiver) { 
          // proxy.key 知晓数据被访问
          const ret = Reflect.get(target, key, receiver)
          track(target, key)
          return ret
        },
        set: function set(target, key, value, receiver) { 
          // proxy.key = newVal 知晓数据被更改
          const result = Reflect.set(target, key, value, receiver)
          trigger(target, key)
          return result
        },
      })

      reactiveMap.set(target, proxy)

      return proxy
    }

    // track: 依赖收集(简版)
    function track(target, key) {
      if (!activeEffect) return;
      let depsMap = targetMap.get(target);
      if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()));
      }
      let dep = depsMap.get(key);
      if (!dep) {
        depsMap.set(key, (dep = new Set()));
      }
      if (!dep.has(activeEffect)) {
        dep.add(activeEffect);
      }
    }

    // trigger: 派发更新(简版)
    function trigger(target, key) {
      let depsMap = targetMap.get(target);
      if (!depsMap) return
      let dep = depsMap.get(key)
      if (dep) {
        dep.forEach(effect => {
          effect.run()
        })
      }
    }
    
    
    class ReactiveEffect {
      constructor(fn) {
        this.fn = fn
      }
      run () {
        try {
          effectStack.push(this)
          activeEffect = this;
          return this.fn();
        }  finally {
          effectStack.pop()
          activeEffect = effectStack[effectStack.length-1]
        }
      }
    }
    function effect(fn) {
      const _effect = new ReactiveEffect(fn)
      _effect.run()
    }

    
    // 测试
    const state = reactive(target)
    
    effect(() => {
      app.innerHTML = `展示${state.count}`
    })

    // 2秒后 修改数据
    setTimeout(() => {
      state.count = 10
    }, 2000)
  </script>
  
</body>
</html>