美文网首页让前端飞Web前端之路
Vue 3 核心原理 -- reactivity 源码复写

Vue 3 核心原理 -- reactivity 源码复写

作者: philoZhe | 来源:发表于2019-12-03 13:05 被阅读0次

    标签(空格分隔): vue 前端


    前言

    首先自己实现了一遍 reactive 的两个api, 对依赖变化的监测有了一定的了解, 现在再看看源码是怎么写。 为了更好理解, 自己按着源码重新写一遍。

    vue3 源码

    重写源码

    以下代码可直接复制到一个 html 文件上运行。
    或者直接在 codepen 上查看

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>vue3 reactive demo</title>
    </head>
    <body>
      <div id="app">
      </div>
      <div id="app2"></div>
    </body>
    
    <!-- core -->
    <script>
    function isObject(obj) {
      return typeof obj === 'object'
    }
    function isArray(obj) {
      return obj instanceof Array
    }
    function isRef(raw) {
      return raw ? !!raw._isRef : false
    }
      
    /** 监听原始值 */
    function ref(primitive) {
      // 是否已经包装过
      if (isRef(primitive)) { return primitive }
      // object 类型使用 reactive 包装,其他不用变
      const convertObj = (raw) => isObject(raw) ? reactive(raw) : raw
      primitive = convertObj(primitive)
      // 将原始值包装起来
      const wrapper = {
        _isRef: true,
        get value() {
          // 收集依赖
          track(wrapper, 'get', 'value')
          return primitive
        },
        set value(v) {
          primitive = convertObj(v)
          trigger(wrapper, 'set', 'value')
        }
      }
      return wrapper
    }
    
    // 原始对象 与 代理对象 互相映射,缓存
    const rawToReactive = new WeakMap()
    const reactiveToRaw = new WeakMap()
    const isReative = (obj) => reactiveToRaw.has(obj)
    
    /** 对象类型 响应式包装 */
    function reactive(obj) {
      if (!isObject(obj)) { return obj }
    
      const cache = rawToReactive.get(obj)
      // 原始对象已经包装过
      if (cache) { return cache }
      // 已经是代理对象
      if (isReative(obj)) { return obj }
      const proxy = new Proxy(obj, {
        get(obj, key, receiver) {
          const res = Reflect.get(obj, key, receiver)
          // 收集依赖
          track(obj, 'get', key)
          // 递归包装子对象
          return isObject(res) ? reactive(res) : res
        },
        set(obj, key, newVal, receiver) {
          const hasKey = Reflect.has(obj, key)
          const oldVal = obj[key]
          const isChanged = oldVal !== newVal
    
          const res = Reflect.set(obj, key, newVal, receiver)
          // 如果当前对象其实在原型链上被设值,就不通知订阅
          if (obj === rawToReactive.get(receiver)) { return res}
          if (!hasKey) {
            // add
            trigger(obj, 'add', key, newVal)
          } else if (isChanged) {
            // set
            trigger(obj, 'set', key, newVal)
          }
          
          
          return res
        },
        has(obj, key) {
          track(obj, 'has', key)
          return Reflect.has(obj, key)
        },
        ownKeys(obj) {
          track(obj, 'iterate')
          return Reflect.ownKeys(obj)
        }
      })
    
      rawToReactive.set(obj, proxy)
      reactiveToRaw.set(proxy, obj)
      // 这里其实不是必须,track 的时候会新建
      // if (!targetMap.get(obj)) { targetMap.set(obj, new Map())}
      return proxy
    }
    
    /** 储存订阅 WeakMap<object, Map<key, Set<Function>>> */
    const targetMap = new WeakMap()
    
    /** 数组各 index(012345...) 的订阅统一到该键上 */
    const ITERATE_KEY = Symbol('iterate')
    
    /** 收集依赖 */
    function track(obj, type, key = '') {
      console.log('track key', key)
      // 没有当前需要收集的订阅事件,就不需要收集
      if (effectStack.length === 0) { return }
      // ownKeys 时收集
      if (type === 'iterate') {
        key = ITERATE_KEY
      }
      // 存取订阅到对应位置
      let target = targetMap.get(obj)
      if (!target) {
        targetMap.set(obj, target = new Map())
      }
      let deps = target.get(key)
      if (!deps) {
        console.log('key', key)
        target.set(key, deps= new Set())
      }
      const currentEffect = effectStack[effectStack.length - 1]
      // 如果已经订阅,就不收集
      if (deps.has(currentEffect)) { return }
      deps.add(currentEffect)
      currentEffect.deps.push(deps)
    }
    
    // setter 时通知订阅
    function trigger(obj, type, key, newValue) {
      const target = targetMap.get(obj)
      if (!target) { return }
    
      // 区分两种订阅,先通知 computed
      const computedRunners = new Set()
      const effects = new Set()
    
      const addRunners = (key) => {
        const depList = target.get(key)
        if (!depList) { return }
        depList.forEach(effect => {
          if (effect.options.computed) {
            computedRunners.add(effect)
          } else {
            effects.add(effect)
          }
        })
      }
      console.log('trigger key', key)
      // 普通修改先加 key
      if (key != void 0) { addRunners(key) }
      // 属性添加 删除 操作还有通知对应的 key
      // 数组属性 0 1 2 3统一为一个 IterateKey
      if (type === 'add' || type === 'delete') {
        const iterationKey = isArray(obj) ? 'length' : ITERATE_KEY
        addRunners(iterationKey)
      }
    
      const run = (effect) => {
        // 自定义执行方式,主要是 computed 需要自定义
        if (effect.options.scheduler !== void 0) {
          effect.options.scheduler(effect)
        } else {
          effect()
        }
      }
      // 先执行 computed
      computedRunners.forEach(run)
      effects.forEach(run)
    }
    
    /** 存储当前需要收集依赖的订阅,暂时性 */
    const effectStack = []
    const isEffect = (e) => !!e._isEffect
    function effect(fn, opt = {}) {
      if (isEffect(fn)) {
        fn = fn.raw
      }
      // 包装 订阅函数
      // 这样每次执行回调函数都会收集依赖
      const effectWrapper = function reactiveEffect(...args) {
        if (!effectStack.includes(effectWrapper)) {
          // 去掉所有订阅列表中当前 effect
          effectWrapper.deps.forEach(dep => {
            dep.delete(effectWrapper)
          })
          effectWrapper.deps.length = 0
          try {
            // 当前要订阅的 effect, 重新收集
            effectStack.push(effectWrapper)
            // 开始收集
            return fn(...args)
          } finally {
            // 收集完清理
            effectStack.pop()
          }
        }
      }
      effectWrapper._isEffect = true
      effectWrapper.raw = fn
      effectWrapper.deps = []
      effectWrapper.options = opt
      if (!opt.lazy) {
        effectWrapper()
      }
      return effectWrapper
    }
    
    // computed 是一个有 effect 的 ref
    function computed(fnOrObj) {
      let getter, setter
      if (typeof fnOrObj === 'object') {
        getter = fnOrObj.gettter
        setter = fnOrObj.setter
      } else {
        getter = fnOrObj
      }
      let value
      // 脏值检查,第一次要设为true, 
      // 这样第一次get的时候 才会跑一下 runner 收集到订阅的事件
      let dirty = true
      const runner = effect(getter, {
        computed: true,
        lazy: true,
        // 自定义订阅的执行方式,这里意思是依赖发送通知时,不执行,但标记为脏值。
        // 延迟到 getter 时才执行
        scheduler:() => { dirty = true }
      })
      return {
        _isRef: true,
        // 暴露 effect 用于停止监听
        effect: runner,
        get value() {
          if (dirty) {
            dirty = false
            value = runner()
          }
          // 将依赖 computed 的订阅函数 记录到对应列表
          if (effectStack.length !== 0) {
            const currentEffect = effectStack[effectStack.length - 1]
            runner.deps.forEach(dep => {
              if (!dep.has(currentEffect)) {
                dep.add(currentEffect)
                currentEffect.deps.push(dep)
              }
            })
          }
          return value
        },
        set value(newVal) {
          if (setter) setter(newVal)
        }
      }
      
    }
    
    </script>
    
    <!-- example -->
    <script> 
    
    function setup() {
      const count = ref(0)
      const double = computed(() => count.value * 2)
      const state = reactive({
        text: 'hello',
        obj: {
          a: 1,
          b: 2,
        },
        arr: [1, 2, 3]
      })
      return {
        count,
        double,
        addCount: () => { count.value ++ },
        state,
        changeState: () => {
          console.log(targetMap)
          state.text = Math.random().toFixed(2)
          state.obj.a ++
          state.obj.b --
          state.arr.push(Math.random().toFixed(2) * 100)
        }
      }
    }
    
    
    function main() {
      // 渲染上下文
      const ctx = setup()
    
      // 模板渲染,事件绑定 
      const app = document.querySelector('#app')
      const app2 = document.querySelector('#app2')
      window.changeState = ctx.changeState
      window.addCount = ctx.addCount
      effect(() => {
        app.innerHTML = `
          <p><button onclick="changeState()">change</button></p>
          <p>state: ${JSON.stringify(ctx.state)}</p>
          <p><button onclick="addCount()">count: ${ctx.count.value}</button></p>
          <p>double: ${ctx.double.value}</p>
        `
      })
      effect(() => {
        console.log("=== app2 changed ===")
        app2.innerHTML = `arr: ${JSON.stringify(ctx.state.arr)}`
      })
    }
    
    window.onload = main
    </script>
    </html>
    
    

    相关文章

      网友评论

        本文标题:Vue 3 核心原理 -- reactivity 源码复写

        本文链接:https://www.haomeiwen.com/subject/dqscgctx.html