vue3 和 vue2 对比与解读

441 阅读5分钟

vue3 亮点

更小、更快、更优雅,更好的 ts 支持

为什么快?

diff优化

vue2中是全量对比,就是对比组件的每一个节点

vue3新增了静态标记,为可能变化的响应式节点添加 flag 属性,在对比的时候只比较标记的节点。

flag 的值对应不同的节点类型,比如取值为1代表动态文本,只对比文本变化,取值为2代表动态calss,只对比class变化等等。

静态提升

vue2 中无论元素是否参与更新,每次都会重新创建

vue3 中对不参与更新的元素会做静态提升,只会被创建一次。

静态提升就是把不参与更新的元素创建vdom的操作提到 render 外面,在 render 中不重复创建。

事件侦听器缓存

默认情况下事件视为动态绑定,每次都会追踪变化。但是因为是同一个函数,缓存复用即可。

创建 vdom 时候通过取消静态标记避免追踪提升性能

为什么优雅?

vue2 中一个功能的逻辑分散在多个区域,项目多次迭代后维护成本增加。

vue3 中使用组合api 可以将相同的逻辑写在一个函数中,而且可以将一个功能提到一个文件中,通过导入的方式使用,直观高效便于维护。

composition 解读

coposition 的实质

组合方式和配置方式可以混用。使用 setup() 暴露的变量会注入到对应的配置项中。

setup 执行时机

在 beforeCreated 和 created 之间。 因为 data 和 methods 未初始化完毕,setup 方法中不能data和methods。 为了避免错误使用,vue将this赋值为 undefine。

setup 可以是异步的么

只能是同步的

API

  1. ref ,toRef,toRefs,reactive

    这些容器的作用都是将原始数据包装成响应式数据。

    区别在于:修改响应式数据是否影响原数据以及是否会自动更新视图

    说明:包装类型是说传的参数是什么数据类型,对应关系中复制关系修改响应式数据不会影响原数据。

    注意点:

reactive 用于数组和对象

如果传了其他对象,比如日期对象,不会自动更新,可以通过重新赋值的方式触发更新。 只有修改经过包装后的对象才会触发视图更新,而修改原对象,值会变化但是不触发更新。 因为 reactive 和 原始值是 引用关系,即存的是内存地址,修改原始值并未修改 reactive 的指向所以不会更新。

ref 用于基本数据类型

本质:ref(xx) => reactive({value: xx})

使用 ref 创建的响应式数据,在模板中不用通过 .value 获取值。 vue 会根据 __v_isRef 取判断,自动添加.value。可以通过 isRef / isReactive 方法来判断响应式对象是什么类型。

  1. shallowRef,shallowReactive,triggerRef

    • shadllowRef / shallowReactive

    ref 和 reactive 都属于递归监听,数据量大时会有性能问题。

    shallowRef 和 shallowReactive 可以实现非递归监听。

    shallowRef 监听的是 value 的变化

    • triggerRef

      根据传入的数据主动更新界面,只适用于 ref ,配合 shallowRef 使用

  2. toRaw

    用于从 ref 或者 reactive 中获得原始数据。

    使用场景:在使用了ref或者reactive跟踪数据变化后,对于一些不需要追踪更新页面的操作,可以通过修改原始数据,防止不必要的更新操作,提升性能。

    想获取 ref 的原始数据,要使用 toRaw(xx.value)

  3. markRaw

    让数据永远不被追踪,包装后的数据再去用响应式容器包装无效

vue2 和 vue3 数据响应式实现

  1. vue2 数据劫持 + 发布订阅

通过数据劫持将data转化为getter和setter并记录相应的依赖。

每个组件实例都对应一个watcher实例,而watcher实例依赖于setter。

当数据变化会触发setter,setter去通知对应的watcher,watcher去更新视图

class Dep {
  constructor() {
      this.subs = []
  }
  depend() {
      Dep.target && this.subs.push(Dep.target)
  }
  notify() {
      this.subs.forEach((sub) => { sub.update() })
  }
}
class Watcher {
  constructor(target, key, cb) {
      Dep.target = this
      this.target = target
      this.key = key
      this.cb = cb // 假设 cb 是渲染函数
      this.val = target[key]
      Dep.target = null
  }
  update() {
      this.val = this.target[this.key]
      this.cb(this.value)
  }
}
class Observer {
  constructor(data) {
      this.data = data
      if (typeof data !== 'object') {
          return data
      }
      if (Array.isArray(data)) {
          const dp = new Dep()
          const methods = ['pop', 'push'] // 等等数组方法
          const proto = Object.create(Array.prototype)
          methods.forEach((method) => {
              const originMethod = proto[method]
              Object.defineProperty(methods, method, {
                  value: function () {
                      const res = originMethod.call(this, ...arguments)
                      // notify
                      dp.notify()
                      return res
                  }
              })
          })
          return
      }
      Object.keys(data).forEach((key) => { defineReactive(data, key, data[key]) })
  }
}
function defineReactive(target, key, val) {
  new Observer(val)
  const dp = new Dep()
  Object.defineProperty(target, key, {
      get() {
          dp.depend()
          console.log('get', val)
          return val
      },
      set(newVal) {
          new Observer(newVal)
          if (val === newVal) { return }
          val = newVal
          console.log('set', newVal)
          dp.notify()
      },
  })
}
class Vue {
  constructor(options) {
      this.data = options.data;
      new Observer(this.data);
      /* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
      new Watcher(this.data, 'name',
          function () {
              console.log('模拟视图渲染')
          });
  }
}
let vm = new Vue({
  data: { name: 'aaa' }
})
vm.data.name = 'ccc'

  1. vue3 proxy重写

    使用 proxy 重写拦截,track 中以 weakMap -> Map -> Set 的数据结构收集依赖,trigger 触发依赖。

const toProxy = new WeakMap() // WeakMap 结构是弱引用,用来解决内存泄露
const toRaw = new WeakMap()
function isOwnProperty(target, key) {
    return target.hasOwnProperty(key)
}
function isObject(obj) {
    return typeof obj === 'object' && obj !== null
}
const activeEffectStacks = []
const targetMap = new WeakMap()
function effect(fn) {
    const effectFn = createReactiveEffect(fn)
    effectFn()
}
function createReactiveEffect(fn) {
    const effect = function () {
        run(effect, fn)
    }
    return effect
}
function run(effect, fn) {
    try {
        activeEffectStacks.push(effect)
        fn()
    } finally {
    	// 防止只存不删
        activeEffectStacks.pop(effect)
    }
}
function track(target, key) {
    let effect = activeEffectStacks[activeEffectStacks.length - 1]
    if (!effect) { return }
    // 构造 weakMap -> Map -> Set 形式的数据结构
    if (!targetMap.get(target)) {
        targetMap.set(target, depsMap = new Map())
    }
    if (!depsMap.get(key)) {
        depsMap.set(target, deps = new Set())
    }
    if (!deps.has(effect)) {
        deps.add(effect)
    }
}
function trigger(target, type, key) {
    // 取 effect 执行
    if (
        targetMap.get(target) &&
        depsMap.get(key)
    ) {
        depsMap.get(key).forEach((effect) => { effect() })
    }
}
function reactive(target) {
    if (!isObject(target)) {
        return target
    }
    let proxy = toProxy.get(target)
    if (proxy) {
        return proxy
    }
    if (toRaw.has(target)) {
        return target
    }
    const handler = {
        get(target, key, receive) {
            const res = Reflect.get(target, key, value, receive)
            // 依赖收集
            track(target, key)
            return isObject(target[key]) ? reactive(target[key]) : res
        },
        set(target, key, value, receive) {
            const oldVal = target[key]
            const res = Reflect.set(target, key, value, receive)
			// 如不区分,拦截数组时会重复触发
            if (!isOwnProperty(target, key)) {
                trigger(target, 'add', key)
                console.log('添加新属性,渲染视图')
            } else if (oldVal !== value) {
                trigger(target, 'set', key)
                console.log('修改原属性,渲染视图')
            }
            return res
        },
        deleteProperty(target, key) {
            const res = Reflect.deleteProperty(target, key)
            console.log('删除,渲染视图')
            return res
        },
    }
    const result = new Proxy(target, handler)
    // 缓存结果,解决重复包装响应式数据的性能问题
    toProxy.set(target, result)
    toRaw.set(result, target)
    return result
}

const data = { age: 18 }
const data2 = [1, 2]
const obj = reactive(data2)
p = reactive(data2)