vue 3.0 浅析

604 阅读9分钟

阅读vue 3.0 pre-alpra源码可以看到和之前的vue2.x可以说完全不是一个代码编写风格,不仅仅是用ts全面重写,还有借鉴react hooks思路演变而来的全新的逻辑复用方案:composition-api,下面一一阐述相关变化以及用法上的不同之处。原文地址

Composition API 设计细节

1. setup()函数

作者引入了一个新的组件选项,替代了原先的两个生命周期(beforeCreate, created),初始化过程见下:

setup

setup 本身的调用时机,从目前的源码来看,介于 beforeCreate 和 created 这两个生命周期之间,很显然此时vue实例还未创建,我们无法在这里使用 this 指向当前组件实例。所以作者

setup 是在组件实例被创建时, 初始化了 props 之后调用,处于 created 前。同时setup接受两个参数:

props:和vue2.x一样,指的是外部下发的数据 context:上下文对象,打印出来里面有 attrs,emit,listeners,parent,refs,root,slots, 这些都是vue2.x里能通过this访问的全局属性,在3.0里采用了函数组合式编程的思想,可以完全代替之前this调用的方式。 setup()和之前的data()一样可以return一个对象(render context),这个对象上的属性将会被暴露给模版的渲染上下文。

与vue2.x不同的是如果我们要在setup内部创建被管理的的值,必须用ref或者reactive函数,使我们创建的值成为响应式,这两个函数的本质都是使用proxy(不熟悉的同学可以去学习es6 proxy)。

至于为什么会有两个函数来实现,后面再单独说明。

例子如下:(注:本文代码片段均是在vue2.x+@vue/composition-api下的语法,正式vue3.0发布可能会有细微调整)

<template>
  <div class="hooks">
    <button @click="count++">{{ count }}</button>
  </div>
</template>

<script lang="ts">
import { ref, Ref, createComponent, onMounted, onBeforeMount, computed, watch } from '@vue/composition-api'
export default createComponent({
  setup (props) {
    const count: Ref<number> = ref(0)
	onMounted(()=>{
	})
	onBeforeMount(() => {
     });
    return {
      count
    }
  }
})
</script>

2. ref 和 reactive

从官方的RFC文档了解到,ref常用于基本类型,reactive用于引用类型。why?

让我们从源码来看

export function ref(raw?: unknown) {
  if (isRef(raw)) {
    return raw
  }
  // 转化数据
  raw = convert(raw)
  const r = {
    _isRef: true,
    get value() {
	  // track的代码在effect中,猜测此处就是监听函数收集依赖的方法。
      track(r, OperationTypes.GET, 'value')
      return raw
    },
    set value(newVal) {
	  // 将设置的值,转化为响应式数据,赋值给raw
      raw = convert(newVal)
	  // trigger,猜测此处就是触发监听函数执行的方法
      trigger(
        r,
        OperationTypes.SET,
        'value',
        __DEV__ ? { newValue: newVal } : void 0
      )
    }
  }
  return r
}

我们看到,这里也定义了get/set,却没有任何Proxy相关的操作。但ref的入参是对象时,用到了reactive做转化。但是对于基本数据类型,函数传递或者对象解构时,会丢失原始数据的引用,换言之,我们没法让基本数据类型,或者解构后的变量(如果它的值也是基本数据类型的话),成为响应式的数据。例如:

// 我们是永远没办法让`a`或`x`这样的基本数据成为响应式的数据的,Proxy也无法劫持基本数据。
const a = 1;
const { x: 1 } = { x: 1 }

但是有时候,我们确实就是想一个数字、一个字符串是响应式的,或者就是想利用解构的写法。那么这个时候ref函数就派上用场了,通过创建一个对象,也即是源码中的Ref数据,然后将原始数据保存在Ref的属性value当中,再将它的引用返回给使用者。既然是我们自己创造出来的对象,也就没必要使用Proxy再做代理了,直接劫持这个value的get/set即可,这就是ref函数与Ref类型的由来。

同时ref还提供了isRef和toRefs两个函数,前者用于我们判断是否为Ref类型,后者能将reactive对象的每个属性都转化为Ref对象,这点再我们解构reactive的时候特别方便。

再说回reactive,话不多说,看码:

// 函数类型声明,接受一个对象,返回不会深度嵌套的Ref数据
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
// 函数实现
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  // 如果传递的是一个只读响应式数据,则直接返回,这里其实可以直接用isReadonly
  if (readonlyToRaw.has(target)) {
    return target
  }
  // target is explicitly marked as readonly by user
  // 如果是被用户标记的只读数据,那通过readonly函数去封装
  if (readonlyValues.has(target)) {
    return readonly(target)
  }

  // 到这一步的target,可以保证为非只读数据

  // 通过该方法,创建响应式对象数据
  return createReactiveObject(
    target, // 原始数据
    rawToReactive, // 原始数据 -> 响应式数据映射
    reactiveToRaw, // 响应式数据 -> 原始数据映射
    mutableHandlers, // 可变数据的代理劫持方法
    mutableCollectionHandlers // 可变集合数据的代理劫持方法
  )
}

// 函数声明+实现,接受一个对象,返回一个只读的响应式数据。
export function readonly<T extends object>(
  target: T
): Readonly<UnwrapNestedRefs<T>> {
  // value is a mutable observable, retrieve its original and return
  // a readonly version.
  // 如果本身是响应式数据,获取其原始数据,并将target入参赋值为原始数据
  if (reactiveToRaw.has(target)) {
    target = reactiveToRaw.get(target)
  }
  // 创建响应式数据
  return createReactiveObject(
    target,
    rawToReadonly,
    readonlyToRaw,
    readonlyHandlers,
    readonlyCollectionHandlers
  )
}

两个方法代码其实很简单,主要逻辑都封装到了createReactiveObject,两个方法的主要作用是:

透传给createReactiveObject相应地的代理数据与响应式数据的双向映射 map。 reactive会做readonly的相关校验,反之readonly方法也是。 继续看createReactiveObject:

function createReactiveObject(
  target: any,
  toProxy: WeakMap<any, any>,
  toRaw: WeakMap<any, any>,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  // 不是一个对象,直接返回原始数据,在开发环境下会打警告
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // 通过原始数据 -> 响应数据的映射,获取响应数据
  let observed = toProxy.get(target)
  // target already has corresponding Proxy
  // 如果原始数据已经是响应式数据,则直接返回此响应数据
  if (observed !== void 0) {
    return observed
  }
  // target is already a Proxy
  // 如果原始数据本身就是个响应数据了,直接返回自身
  if (toRaw.has(target)) {
    return target
  }
  // only a whitelist of value types can be observed.
  // 如果是不可观察的对象,则直接返回原对象
  if (!canObserve(target)) {
    return target
  }
  // 集合数据与(对象/数组) 两种数据的代理处理方式不同。
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlerstargetMap
  // 声明一个代理对象,也即是响应式数据
  observed = new Proxy(target, handlers)
  // 设置好原始数据与响应式数据的双向映射
  toProxy.set(target, observed)
  toRaw.set(observed, target)

  // 在这里用到了targetMap,但是它的value值存放什么暂时不知道
  if (!targetMap.has(target)) {
    targetMap.set(target, new Map())
  }
  return observed
}

上文我们说过reactive用于引用类型,在createReactiveObject函数第一行就体现了,我们传入基本类型会在开发环境告警。上面有一些辅助函数,这里就不一一列出,大家自行去阅读源码。

reactive文件其实非常通俗易懂,看完以后,我们心中只有 2 个问题:

baseHandlers,collectionHandlers的具体实现以及为什么要区分? targetMap到底是啥? 当然我们知道handlers肯定是做依赖收集跟响应触发的。和上面ref函数里的track和trigger类似。大家可以自行学习这两个方法

在baseHandlers中

对于原始对象数据,会通过 Proxy 劫持,返回新的响应式数据(代理数据)。 对于代理数据的任何读写操作,都会通过Refelct反射到原始对象上。 在这个过程中,对于读操作,会执行收集依赖的逻辑。对于写操作,会触发监听函数的逻辑 关于collectionHandlers,是针对由于Set|Map等集合数据的底层设计问题,Proxy无法直接劫持set或直接反射行为而做的单独处理,这块大家可以自行研究。

至此,我们基本掌握了ref和reactive的基本内部实现以及用法。

3. watch、computed

既然3.0提倡函数式组合编程,可想而至,这两个我们常用的api也是函数,与2.x不同的是这两个函数得在setup里执行,同样意味着我们无法在里面使用this。先看一下官方使用示例:

watch

// eg.1
// watch Ref数据
const count = ref(0)
watch(() => console.log(count.value))

// eg.2
// watch reactive state 以及 获取它之前的状态值
// watching a getter
const state = reactive({ count: 0 })
watch(() => state.count, (count, prevCount) => { /* ... */ })

// eg.3
// watch多个Ref数据, 注意:以数组的方式,callback支持两个参数,第一个数组是当前value,第二个是pre-value
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})

//eg.4
// watch reactive 下多个state
watch([()=>state.fooRef, ()=>state.barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})

//eg.5
const stop = watch(() => { /* ... */ })
// later
stop()

需要注意的是,watch会返回一个StopHandle函数,让我们在需要的时候关闭监听函数。同时watch还支持第三个参数options,官方解释是说,为了向下兼容vue2的watch,提供了lazy属性。大家都知道vue2里的watch在初始化后不会立即执行,而是一个惰性执行,只在第一次监听value改变的时候才执行,但是很多时候我们需要它立即执行,这一点在vue中就会需要我们自己来用额外的state来实现。在vue3中默认就是实例化的时候就会执行,如果需要惰性执行,我们可以在option里加上lazy: true。

computed

computed 和 vue2没有什么变化,我们看一下声明类型:


// read-only
// 参数为function的时候会返回一个Readonly只读类型,无法在外部赋值
function computed<T>(getter: () => T): Readonly<Ref<Readonly<T>>>

// writable
function computed<T>(options: {
  get: () => T,
  set: (value: T) => void
}): Ref<T>

4. 生命周期

废弃了beforeCreate和created,同setup代替,其他声明周期在setup函数中执行,注意一点的是,不在像2.x中那样覆写声明周期函数,而是在setup里面调用生命周期函数执行,传参为回调函数,在回调里执行我们需要的操作。

新增了两个调试钩子函数:onRenderTrackedonRenderTriggered,作用是能让我们在触发onTrackonTrigger的时候debug。

点击查看更多原文地址


官方资料