vue3照搬

基本范例





#动机与目的

#更好的逻辑复用与代码组织

我们都因 Vue 简单易学而爱不释手,它让构建中小型应用程序变得轻而易举。但是随着 Vue 的影响力日益扩大,许多用户也开始使用 Vue 构建更大型的项目。这些项目通常是由多个开发人员组成团队,在很长一段时间内不断迭代和维护的。多年来,我们目睹了其中一些项目遇到了 Vue 当前 API 所带来的编程模型的限制。这些问题可归纳为两类:

  1. 随着功能的增长,复杂组件的代码变得越来越难以阅读和理解。这种情况在开发人员阅读他人编写的代码时尤为常见。根本原因是 Vue 现有的 API 迫使我们通过选项组织代码,但是有的时候通过逻辑关系组织代码更有意义。

  2. 目前缺少一种简洁且低成本的机制来提取和重用多个组件之间的逻辑。 (详见 逻辑抽象、提取与复用)

RFC 中提出的 API 为组件代码的组织提供了更大的灵活性。现在我们不需要总是通过选项来组织代码,而是可以将代码组织为处理特定功能的函数。这些 API 还使得在组件之间甚至组件之外逻辑的提取和重用变得更加简单。我们会在设计细节这一节展示达成的效果。

#更好的类型推导

另一个来自大型项目开发者的常见需求是更好的 TypeScript 支持。Vue 当前的 API 在集成 TypeScript 时遇到了不小的麻烦,其主要原因是 Vue 依靠一个简单的 this 上下文来暴露 property,我们现在使用 this 的方式是比较微妙的。(比如 methods 选项下的函数的 this 是指向组件实例的,而不是这个 methods 对象)。

换句话说,Vue 现有的 API 在设计之初没有照顾到类型推导,这使适配 TypeScript 变得复杂。

当前,大部分使用 TypeScript 的 Vue 开发者都在通过 vue-class-component 这个库将组件撰写为 TypeScript class (借助 decorator)。我们在设计 3.0 时曾有一个已废弃的 RFC,希望提供一个内建的 Class API 来更好的解决类型问题。然而当讨论并迭代其具体设计时,我们注意到,想通过 Class API 来解决类型问题,就必须依赖 decorator——一个在实现细节上存在许多未知数的非常不稳定的 stage 2 提案。基于它是有极大风险的。(关于 Class API 的类型相关问题请移步这里)

相比较过后,本 RFC 中提出的方案更多地利用了天然对类型友好的普通变量与函数。用该提案中的 API 撰写的代码会完美享用类型推导,并且也不用做太多额外的类型标注。

这也同样意味着你写出的 JavaScript 代码几乎就是 TypeScript 的代码。即使是非 TypeScript 开发者也会因此得到更好的 IDE 类型支持而获益。

#设计细节

#API 介绍

为了不引入全新的概念,该提案中的 API 更像是暴露 Vue 的核心功能——比如用独立的函数来创建和监听响应式的状态等。

在这里我们会介绍一些最基本的 API,及其如何取代 2.x 的选项表述组件内逻辑。

请注意,本节主要会介绍这些 API 的基本思路,所以不会展开至其完整的细节。完整的 API 规范请移步 API 参考章节。

#响应式状态与副作用

让我们从一个简单的任务开始:创建一个响应式的状态

import { reactive } from 'vue'

// state 现在是一个响应式的状态
const state = reactive({
  count: 0,
})

reactive 几乎等价于 2.x 中现有的 Vue.observable() API,且为了避免与 RxJS 中的 observable 混淆而做了重命名。这里返回的 state 是一个所有 Vue 用户都应该熟悉的响应式对象。

在 Vue 中,响应式状态的基本用例就是在渲染时使用它。因为有了依赖追踪,视图会在响应式状态发生改变时自动更新。在 DOM 当中渲染内容会被视为一种“副作用”:程序会在外部修改其本身 (也就是这个 DOM) 的状态。我们可以使用 watchEffect API 应用基于响应式状态的副作用,并自动进行重应用。

import { reactive, watchEffect } from 'vue'

const state = reactive({
  count: 0,
})

watchEffect(() => {
  document.body.innerHTML = `count is ${state.count}`
})

watchEffect 应该接收一个应用预期副作用 (这里即设置 innerHTML) 的函数。它会立即执行该函数,并将该执行过程中用到的所有响应式状态的 property 作为依赖进行追踪。

这里的 state.count 会在首次执行后作为依赖被追踪。当 state.count 未来发生变更时,里面这个函数又会被重新执行。

这正是 Vue 响应式系统的精髓所在了!当你在组件中从 data() 返回一个对象,内部实质上通过调用 reactive() 使其变为响应式。而模板会被编译为渲染函数 (可被视为一种更高效的 innerHTML),因而可以使用这些响应式的 property。

watchEffect 和 2.x 中的 watch 选项类似,但是它不需要把被依赖的数据源和副作用回调分开。组合式 API 同样提供了一个 watch 函数,其行为和 2.x 的选项完全一致。

继续我们上面的例子,下面我们将展示如何处理用户输入:

function increment() {
  state.count++
}

document.body.addEventListener('click', increment)

但是在 Vue 的模板系统当中,我们不需要纠结用 innerHTML 还是手动挂载事件监听器。让我们将例子简化为一个假设的 renderTemplate 方法,以专注在响应性这方面:

import { reactive, watchEffect } from 'vue'

const state = reactive({
  count: 0,
})

function increment() {
  state.count++
}

const renderContext = {
  state,
  increment,
}

watchEffect(() => {
  // 假设的方法,并不是真实的 API
  renderTemplate(
    ``,
    renderContext
  )
})

#计算状态 与 Ref

有时候,我们会需要一个依赖于其他状态的状态,在 Vue 中这是通过计算属性来处理的。我们可以使用 computed API 直接创建一个计算值:

import { reactive, computed } from 'vue'

const state = reactive({
  count: 0,
})

const double = computed(() => state.count * 2)

这个 computed 返回了什么? 如果猜一下 computed 内部是如何实现的,我们可能会想出下面这样的方案:

// 简化的伪代码
function computed(getter) {
  let value
  watchEffect(() => {
    value = getter()
  })
  return value
}

但我们知道它不会正常工作:如果 value 是一个例如 number 的基础类型,那么当被返回时,它与这个 computed 内部逻辑之间的关系就丢失了!这是由于 JavaScript 中基础类型是值传递而非引用传递。

值传递 vs 引用传递

在把值作为 property 赋值给某个对象时也会出现同样的问题。一个响应式的值一旦作为 property 被赋值或从一个函数返回,而失去了响应性之后,也就失去了用途。为了确保始终可以读取到最新的计算结果,我们需要将这个值上包裹到一个对象中再返回。

// 简化的伪代码
function computed(getter) {
  const ref = {
    value: null,
  }
  watchEffect(() => {
    ref.value = getter()
  })
  return ref
}

另外我们同样需要劫持对这个对象 .value property 的读/写操作,来实现依赖收集与更新通知 (为了简化我们忽略这里的代码实现)。

现在我们可以通过引用来传递计算值,也不需要担心其响应式特性会丢失了。当然代价就是:为了获取最新的值,我们每次都需要写 .value

const double = computed(() => state.count * 2)

watchEffect(() => {
  console.log(double.value)
}) // -> 0

state.count++ // -> 2

在这里 double 是一个对象,我们管它叫“ref”, 用来作为一个响应性引用保留内部的值。

你可能会担心 Vue 本身已经有 "ref" 的概念了。

但只是为了在模板中获取 DOM 元素或组件实例 (“模板引用”)。可以到这里查看新的 ref 系统如何同时用于逻辑状态和模板引用。

除了计算值的 ref,我们还可以使用 ref API 直接创建一个可变更的普通的 ref:

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

#解开 Ref

我们可以将一个 ref 值暴露给渲染上下文,在渲染过程中,Vue 会直接使用其内部的值,也就是说在模板中你可以把 {{ count.value }} 直接写为 {{ count }}

这是计数器示例的另一个版本, 使用的是 ref 而不是 reactive:

import { ref, watchEffect } from 'vue'

const count = ref(0)

function increment() {
  count.value++
}

const renderContext = {
  count,
  increment,
}

watchEffect(() => {
  renderTemplate(
    ``,
    renderContext
  )
})

除此之外,当一个 ref 值嵌套于响应式对象之中时,访问时会自动解开:

const state = reactive({
  count: 0,
  double: computed(() => state.count * 2),
})

// 无需再使用 `state.double.value`
console.log(state.double)

#组件中的使用方式

到目前为止,我们的代码已经提供了一个可以根据用户输入进行更新的 UI ,但是代码只运行了一次,无法重用。如果我们想重用其逻辑,那么不如重构成一个函数:

import { reactive, computed, watchEffect } from 'vue'

function setup() {
  const state = reactive({
    count: 0,
    double: computed(() => state.count * 2),
  })

  function increment() {
    state.count++
  }

  return {
    state,
    increment,
  }
}

const renderContext = setup()

watchEffect(() => {
  renderTemplate(
    ``,
    renderContext
  )
})

请注意,上面的代码并不依赖于组件实例而存在。实际上,到目前为止介绍的所有 API 都可以在组件上下文之外使用,这使我们能够在更广泛的场景中利用 Vue 的响应性系统。

现在如果我们把调用 setup()、创建侦听器和渲染模板的逻辑组合在一起交给框架,我们就可以仅通过 setup() 函数和模板定义一个组件:





这就是我们熟悉的单文件组件格式,只是逻辑的部分 (

#Svelte



Svelte 的代码看起来更简洁,因为它在编译时做了以下工作:

  • 隐式地将整个

你可能感兴趣的:(vue3照搬)