vue3数据响应式革新及手写实现

307 阅读1分钟

全新数据响应式初体验

<div id="app">
  <h3>{{state.title}}</h3>
  <p>counter: {{state2.counter}}</p>
</div>

<script src="https://unpkg.com/vue@next"></script>
<script>

  const { createApp, reactive, onMounted } = Vue

  // 复用性
  function useTitle() {
    const state = reactive({
      title: 'hello vue3!!!!!'
    })

    onMounted(() => {
      state.title = 'vue3, hello!!!'
    })

    setTimeout(() => {
      state.title = 'title 2s'
    }, 2000);

    return state
  }

  function useCounter() {
    const state2 = reactive({
      counter: 0
    })

    onMounted(() => {
      state2.counter = 1
    })
    return state2
  }

  const app = createApp({
    setup() {
      // title
      const state = useTitle()

      // counter
      const state2 = useCounter()

      // 上下文给render使用
      return {
        state,
        state2
      }
    },
  })
  app.mount('#app')
</script>

数据响应式革新

  1. 初始化需要递归,速度慢
  2. 依赖关系占用资源较多
  3. 数组支持需要特殊实现
  4. 动态增加,删除属性需要额外API
  5. 不支持Map,Set等

手写数据响应式实现

  1. 基于Proxy的数据响应式
  • reactive()
    
  1. 依赖收集
  • 添加副作用effect()
  • 依赖收集track()
  • 触发副作用trigger()

<div id="app">
  <h3>{{title}}</h3>
</div>

<script>
  // 接收obj,代理它,使他成为响应式的
  // 替换Object.defineProperty()
  function reactive(obj) {
    // vue3中基于Proxy
    return new Proxy(obj, {
      get(target, key) {
        console.log('get', key);

        // 依赖收集
        track(target, key)

        return target[key]
      },
      set(target, key, val) {
        console.log('set', key);
        target[key] = val
        // update()
        // app.update()
        // 触发依赖
        trigger(target, key)
      },
    })
  }

  // 添加副作用函数 
  const effectStack = []
  function effect(fn) {
    // 如果fn中用到了响应式数据,当它们放生变化,fn会再次执行
    // 称为副作用函数
    const eff = function () {
      try {
        effectStack.push(eff)
        fn()
      } finally {
        effectStack.pop()
      }
    }

    // 执行一次,触发依赖收集
    eff()

    return eff

  }

  // 依赖收集函数track
  // {target: {key: [eff]}}
  const targetMap = {}
  function track(target, key) {
    // 获取副作用函数
    const effect = effectStack[effectStack.length - 1]
    if (effect) {
      let map = targetMap[target]
      if (!map) {
        map = targetMap[target] = {}
      }

      let deps = map[key]
      if (!deps) {
        deps = map[key] = []
      }

      // 将副作用函数放入deps
      if (deps.indexOf(effect) === -1) {
        deps.push(effect)
      }
    }
  }

  function trigger(target, key) {
    const map = targetMap[target]
    if (map) {
      const deps = map[key]
      if(deps) {
        deps.forEach(dep => dep())
      }
    }
  }

  // createApp返回应用程序实例是什么样子
  const Vue = {
    createApp(options) {
      // 暴露给浏览器平台
      const renderer = Vue.createRenderer({
        querySelector(selector) {
          return document.querySelector(selector)
        },
        insert(child, parent, anchor) {
          parent.insertBefore(child, anchor || null)
        }
      })
      return renderer.createApp(options)
    },
    createRenderer({ querySelector, insert }) {
      // 返回自定义渲染器
      return {
        createApp(options) {
          // 返回的就是app实例
          return {
            mount(selector) {
              // mount的目标是什么?
              const parent = querySelector(selector)

              // 需要将组件配置解析为dom
              // 通过render函数实现
              if (!options.render) {
                options.render = this.compile(parent.innerHTML)
              }

              // 兼容options api
              if (options.setup) {
                // setupState已经是一个代理对象
                this.setupState = options.setup()
              } else {
                this.data = options.data()
              }

              // Proxy
              // 确定render中数据从哪获取
              this.proxy = new Proxy(this, {
                get(target, key) {
                  if (key in target.setupState) {
                    return target.setupState[key]
                  } else {
                    return target.data[key]
                  }
                },
                set(target, key, val) {
                  if (key in target.setupState) {
                    target.setupState[key] = val
                  } else {
                    target.data[key] = val
                  }
                }
              })

              // app实例上加了一个更新函数
              this.update = effect(() => {
                const el = options.render.call(this.proxy)

                // 追加到宿主元素上去
                parent.innerHTML = ''
                // parent.appendChild(el)
                insert(el, parent)
              })

              // 初始化
              this.update()
            },
            compile(template) {
              // 没有用到template
              return function render() {
                const h3 = document.createElement('h3')
                h3.textContent = this.title
                return h3
              }
            }
          }
        }
      }
    }
  }
</script>
<script>
  const { createApp } = Vue
  const app = createApp({
    setup() {
      // 调函数等等
      // 规避this
      const state = reactive({
        title: 'hello vue3!!!!!!'
      })
      setTimeout(() => {
        state.title = 'vue3, hello!!!!!!'
      }, 2000);
      return state
    },
  })
  app.mount('#app')
</script>