美文网首页让前端飞Web前端之路
Vue 3 核心原理 -- reactivity 自己实现

Vue 3 核心原理 -- reactivity 自己实现

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

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


    [toc]

    前言

    为了更好地理解 vue3,阅读其源码是一个重要的途径,但是单纯阅读源码可能理解不了作者为什么这样写,因此自己根据 API 来实现一遍然后再与源码作对比,可以更深刻理解 vue3 的实现原理。

    该部分源码复写,可看Vue 3 核心原理 -- reactivity 源码复写


    自己实现一遍

    自己实现前,需要了解一下 vue3 的 API 以及大概的实现原理,可参考以下文章:

    vue 3 一个简单的例子如下

    <template>
      <button @click="increment">
        Count is: {{ state.count }}, double is: {{ state.double }}
      </button>
      <button @click="addOtherCount">
        otherCount is: {{ otherCount }}, double is: {{ otherDouble }}
      </button>
    </template>
    
    <script>
    import { reactive, computed } from 'vue'
    
    export default {
      setup() {
        const state = reactive({
          count: 0,
          double: computed(() => state.count * 2)
        })
        
        function increment() {
          state.count++
        }
        
        const otherCount = ref(0)
        const otherDouble = computed(() => otherCount.value * 2)
        
        function addOhterCount() {
          otherCount.value++
        }
    
        return {
          state,
          increment,
          otherCount,
          addOhterCount
        }
      }
    }
    
    // 后续渲染伪代码
    const renderContext = setup()
    watch(() => {
      renderTemplate('...', renderContext)
    })
    
    </script>
    

    vue 3 使用以上 API 设计的一大原因就是让业务代码可以更加高内聚低耦合,还有其他原因可以参考 API RFC

    根据以上 API ,开始动手写

    ref

    refreactive 两个函数都是用于监听数据修改,实现数据绑定的。 两者的区别在于 ref 是用于监听原始值的。 因为 js 原始值不能引用内存地址,就算修改了也无从知晓,因而可以将其包装成一个对象,这样就可以获取到这个变量的引用,监听修改。 ref 只有一个 value 属性。

    已知使用 ref 监听变量修改,使用 watch 订阅通知。

    
    // 以下按序号阅读,可以复制全部运行。
    
    // 4.2 新建一个保存订阅的 WeakMap<object, Set>,使用 WeakMap 防止内存泄漏
    const subscription = new WeakMap()
    
    // 7.2 当前需要加入订阅列表的回调
    let currentSub
    
    // ---- 例子 ----
    const count = ref(0)
    const double = computed(() => {console.log('computed'); return count.value * 2}) // log computed #这里没做 lazy
    watch(() => console.log(count.value, double.value)) // log 0 0
    // log computed
    count.value++ // log 1 2
    connsole.log(double.value) // log 2 # 没有 log computed 证明缓存了
    
    // -------------
    
    // 1. 先写 ref 函数,已知使用 proxy
    function ref(value) {
      const innerObj = { value }
      return new Proxy(innerObj, {
        get(obj, key, receiver) {
          if (key !== 'value') { return }
          // 9.1 收集依赖,即 将当前订阅放入到每个触发了 getter 的变量的订阅列表中
          track(obj, key)
          return Reflect.get(obj, key, receiver)
        },
        // 2. 暂不知道如何收集依赖,先写 set,修改变量就要通知订阅
        set(obj, key, v, receiver) {
           if (key !== 'value') { return false }
           const res = Reflect.set(obj, key, v, receiver)
           // 3.1 通知该变量订阅的修改
           trigger(obj, key)
           return res
        }
      })
    }
    
    // 9.2 收集依赖
    function track(obj, key) {
      if (!currentSub) { return }
      let subList = subscription.get(obj)
      if (!subList) {
        subList = new Set()
        subscription.set(obj, subList)
      }
      subList.add(currentSub)
    }
    
    // 3.2 通知
    function trigger(obj, key) {
      // 4.1 所有订阅应该在一个列表上才能通知到
      
      // 5. 获取当前监听变量的订阅列表
      const subList = subscription.get(obj) // Set<Function>
      if (!subList) { return }
      // subList.forEach((cb) => cb())
      
      // 13. 先执行 computed 再执行 watch
      Array.from(subList)
        .sort((a, b) => Number(!!b.computed) - Number(!!a.computed))
        .forEach(cb => cb())
    }
    
    // 6. 既然发现有一个订阅列表了,那么 watch 的时候就是将订阅放入对应的列表
    function watch(cb, opt = {}) {
    
      // 12. 标记 computed
      cb.computed = opt.computed
    
      // 7.1 怎么放? 可以使用一个全局变量来标记当前的订阅
      currentSub = cb
      // 8. 执行一下,这样可以触发变量的 getter
      cb()
      // 10. 收集完成,清理
      currentSub = null
      
    }
    
    // 11. 最后实现 computed,其实就是 watch 的 lazy 版,触发订阅时注意要先 computed 再到 watch
    function computed(getter, setter) {
      // 数值要缓存起来,不要每次都算
      let value
      // 这里将订阅放到 computed 所依赖的变量的订阅列表,就是 count
      watch(() => { value = getter() }, { computed: true })
      return new Proxy({}, {
        get(obj, key, receiver) {
          if (key !== 'value') { return }
          return value
        },
        set(obj, key, v, receiver) {
          if (key !== 'value' || !setter) { return false }
          return setter(obj, key, v, receiver)
        }
      })
    }
    
    

    reactive

    有了 ref 的实现思路,实现 reactive 就很简单了。 ref 其实可以算是 reactive 的特化版 -- 只包装 { value } 对象。

    reactive 需要实现对象的遍历监听以及属性增删的监听。其他跟 ref 类似的代码就不写注释了,只写 reactive 特有的

    
    const subMap = new WeakMap()
    let currentCb
    // 保存已经 reactive 的对象
    const alreadyReactive = new WeakMap()
    
    // --- 例子 ---
    const state = reactive({
      count: 0, 
      obj: {a: 0},
      arr: [1,2,3]
    })
    watch(() => {  console.log('count:', state.count) })
    watch(() => {  console.log('obj.a:', state.obj.a) })
    // 这里有问题了,watch 的时候只收集到子对象的,没收集到子对象的属性,那么就监听不到了其属性修改
    watch(() => {  console.log('obj:', state.obj) })
    watch(() => {  console.log('arr:', state.arr) })
    state.count++ // log count: 1
    state.obj.a++ // log obj.a: 1
    // 这里就监听不到了,要查看源码才知道什么做
    state.arr.push(4)
    state.arr.pop()
    state.arr[0] = 11
    // ---------
    
    function reactive(target) {
      const cache = alreadyReactive.get(target)
      if (cache) { return cache }
      const proxy = new Proxy(target, {
        get(obj, key, receiver) {
          track(obj, key)
          const res = Reflect.get(obj, key, receiver)
          // 如果属性是对象,就返回一个 reactive 包装的对象,递归遍历
          // 由于频发触发 reacitve 函数有性能问题,因此可以缓存起来
          // 使用 alreadyReactive 保存已包装过的对象
          return typeof res === 'object' ? reactive(res) : res
        },
        set(obj, key, value, receiver) {
          const res = Reflect.set(obj, key, value, receiver)
          trigger(obj, key)
          return res
        }
      })
      alreadyReactive.set(target, cache)
      return proxy
    }
    
    // 由于对象有多个属性,每个属性都有对应的订阅列表
    // 因此容器 subMap 的数据结构为 WeakMap<object, Map<string, Set>
    function trigger(obj, key) {
      const target = subMap.get(obj)
      if (!target) { return }
      const sub = target.get(key)
      if (!sub) { return }
      sub.forEach(cb => cb())
    }
    
    function track(obj, key) {
      if (!currentCb) { return }
      let target = subMap.get(obj)
      if (!target) {
        target = new Map()
        subMap.set(obj, target)
      }
      let sub = target.get(key)
      if (!sub) {
        sub = new Set()
        target.set(key, sub)
      }
      sub.add(currentCb)
    }
    
    function watch(cb) {
      currentCb = cb
      cb()
      currentCb = null
    }
    
    
    

    以上就是 ref reactive 的核心原理,带着自己实现时的理解与疑惑, 再去阅读源码,更容易理解作者的思路与实现。


    相关文章

      网友评论

        本文标题:Vue 3 核心原理 -- reactivity 自己实现

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