关于vue3:Vue3基础与入门

3次阅读

共计 22356 个字符,预计需要花费 56 分钟才能阅读完成。

字数:7887,浏览工夫:40 分钟,点击浏览原文

从 2013 年 12 月 8 日公布第一个版本至今已,Vue 已走过了快八个年头,你理解每个版本名字的意义吗?

版本号 名字 释义 工夫
V0.9 Animatrix 黑客帝国动画版 2014.2.25
V0.10 Blade Runner 银翼杀手 2014.3.23
V0.11 Cowboy Bebop 星际牛仔 2014.11.7
V0.12 Dragon Ball 龙珠 2015.6.12
V1.0 Evangelion 新世纪福音战士 2015.10.27
V2.0 Ghost in the Shell 攻壳机动队 2016.9.30
V2.1 Hunter X Hunter 全职猎人 2016.11.22
V2.2 Initial D 头文字 D 2017.2.26
V2.3 JoJo’s Bizarre Adventure JoJo 的微妙冒险 2017.4.2
V2.4 Kill la Kill 斩服少女 2017.7.13
V2.5 Level E 灵异 E 接触 2017.10.13
V2.6 Macross 超时空要塞 2019.2.4
V3.0 One Piece 海贼王 2020.9.18
V3.1 Pluto 地上最强机器人 2021.6.8
V3.2 Quintessential Quintuplets 五等分的花嫁 2021.8.10

原来每个版本的名字都是以漫画命名,那么这些动漫,你看过几部呢?

那么接下来咱们就重点聊聊 Vue3.0。

缘起

一个新工具的呈现,肯定是为了解决已有工具存在的问题。咱们经常据说 Vue 不适宜开发大型简单的我的项目,一个根本原因是 Vue 现有的 API 迫使咱们通过选项组织代码,然而有的时候通过逻辑关系组织代码更有意义。另一个起因是目前短少一种简洁且低成本的机制来提取和重用多个组件之间的逻辑。

那么接下来咱们就来看看 2.0 的问题,以及 Vue3 是如何来解决的。

Option 式组织代码的缺点

options 式组织代码,同一个性能扩散在各个 option 中,导致在开发时须要在 data、methods、computed 等 option 横跳。

Vue3 推出了 CompositionApi,目标就是为了解决这个问题,它将扩散在各个 option 中的逻辑组合到一起,上面咱们比照看下:

Mixin 的问题

对于简单的性能,咱们可能会想到应用 Mixin 来抽离到独自的文件。然而 Mixin 会有一些应用上的问题,比方命名抵触、属性起源不明确。

Vue3 提出了 Hooks 的形式,能够将每个性能提取到 hooks,一个 hooks 即是一个独立的函数,所以不会再有上述问题。

TypeScript 反对的不健全

当初大型项目都会标配 TypeScript,Vue 以后的 API 在集成 TypeScript 时遇到了不小的麻烦,其次要起因是 Vue 依附一个简略的 this 上下文来裸露 property,咱们当初应用 this 的形式是比拟奥妙的。(比方 methods 选项下的函数的 this 是指向组件实例的,而不是这个 methods 对象)。

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

以后,大部分应用 TypeScript 的 Vue 开发者都在通过 vue-class-component 这个库将组件撰写为 TypeScript class (借助 decorator)。它必须依赖 decorator——一个在实现细节上存在许多未知数的十分不稳固的 stage 2 提案。基于它是有极大危险的。

Vue3 中提出的计划更多地利用了人造对类型敌对的一般变量与函数,完满享受类型推导,并且也不必做太多额定的类型标注。

这也同样意味着你写出的 JavaScript 代码简直就是 TypeScript 的代码。即便是非 TypeScript 开发者也会因而失去更好的 IDE 类型反对而获益。

更好的响应式和性能

家喻户晓,Vue2 的响应式是通过Object.defineProperty 是给对象的某个已存在的属性增加对应的 gettersetter,所以它只能监听这个属性值的变动,而不能去监听对象属性的新增和删除。在 Vue 2 的实现中,在组件初始化阶段把数据变成响应式时,遇到子属性依然是对象的状况,会递归执行 Object.defineProperty 定义子对象的响应式,会有一些性能问题。而且还有一个常见的问题就是通过索引批改数组、为对象间接新增属性,并不会触发响应式更新机制。

而在 Vue3 中则应用了 Proxy 来实现响应式,其实并不是 Proxy 的自身的性能优于Object.defineProperty,其实恰恰相反。那么为什么还要抉择 Proxy 呢?

因为 Proxy 实质上是对某个对象的劫持,这样它不仅仅能够监听对象某个属性值的变动,还能够监听对象属性的新增和删除。而且在实现响应式时,采纳了延时解决的形式,当嵌套较深的对象时,只有在其属性被拜访的时候才会解决属性的响应式,在性能上会有肯定的晋升。

反对全局 API Treeshaking

Vue3 重构了全局和部分的 api,均采纳 ESModule 的命名导出拜访,反对 tree-shaking,只打包应用到的性能,用户只为理论应用的性能买单,同时包体积的缩小,也意味着性能的晋升。

// vue2
import Vue from 'vue'

Vue.nextTick(() => {// 一些和 DOM 无关的货色})
// vue3
import {nextTick} from 'vue'

nextTick(() => {// 一些和 DOM 无关的货色})

以上就是 Vue 次要的变动,那么接下来咱们就来看看有哪些新个性。

新个性与变更

接下来咱们次要看一下一些非兼容的重大变更:

全局 API

  • 反对多个利用根实例,避免全局配置净化

    // vue2
    // 这会影响两个根实例
    Vue.mixin({/* ... */})
    const app1 = new Vue({el: '#app-1'})
    const app2 = new Vue({el: '#app-2'})
    // vue3
    import {createApp} from 'vue'
    
    const app = createApp({})
    app.mixin({/* ... */})

    一些其余全局 Api 的变更详情请查阅全局 API。

  • 全局 API 重构为可 Treeshaking

    import {nextTick} from 'vue'
    
    nextTick(() => {// 一些和 DOM 无关的货色})
    // **** 受影响的 API
    // Vue.nextTick
    // Vue.observable (用 Vue.reactive 替换)
    // Vue.version
    // Vue.compile (仅残缺构建版本)
    // Vue.set (仅兼容构建版本)
    // Vue.delete (仅兼容构建版本)

模板和指令相干

  • 更好用的v-model

代替原有的 v-modelv-bind.sync修饰符,通过参数模式,反对应用多个 v-model 进行双向绑定。

<!-- vue2 -->
<ChildComponent v-model="pageTitle" :title.sync="title"/>
<!-- 残缺 -->
<ChildComponent :value="pageTitle" @input="(title)=> (pageTitle=title)" :title="title" @update:title="(title)=> (title=title)"/>
<!-- vue3 -->
<ChildComponent v-model="pageTitle" v-modle:title="title"/>
<!-- 残缺 -->
<ChildComponent :model-value="pageTitle" @update:modelValue="(title)=> (pageTitle=title)" :title="title" @update:title="(title)=> (title=title)"/>
  • <template v-for>的变动
<!-- vue2 -->
<template v-for="item in list">
  <div :key="'heading-' + item.id">...</div>
  <span :key="'content-' + item.id">...</span>
</template>
<!-- vue 3 -->
<template v-for="item in list" :key="item.id">
  <div>...</div>
  <span>...</span>
</template>
  • v-bind 合并程序变动
<!-- vue2 -->
<div id="red" v-bind="{id:'blue'}"></div>
<!-- result -->
<div id="red"></div>
<!-- vue3 -->
<div id="red" v-bind="{id:'blue'}"></div>
<!-- result -->
<div id="blue"></div>
  • 移除 v-on.native 修饰符

    在以前的版本中,要将原生 DOM 监听器增加到子组件的根元素中,能够应用 .native 修饰符。

<!-- vue2 -->
<my-component
  v-on:close="handleComponentEvent"
  v-on:click.native="handleNativeClickEvent"
/>

在 vue3 中,组件中 被定义为组件触发的所有事件监听器,Vue 当初将把它们作为原生事件监听器增加到子组件的根元素中 (除非在子组件的选项中设置了 inheritAttrs: false)

<!-- vue3 -->
<my-component
  v-on:close="handleComponentEvent"
  v-on:click="handleNativeClickEvent"
/>
<script>
  export default {emits: ['close']
  }
</script>
  • 反对片段(多根节点)
<!-- vue2 -->
<template>
  <div>
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
  </div>
</template>

在 vue2 中,组件必须蕴含在某个元素外部,不反对多个根节点,这有时会给咱们写款式带来懊恼,所以 vue3 中反对了多根节点。

<!-- vue3 -->
<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>
  • 新增 Teleport 传送门

vue 利用开发的外围就是组件编写,将 UI 和相干行为封装到组件中来构建 UI。然而有时组件模板的一部分逻辑上属于该组件,而从技术角度来看,最好将模板的这一部分挪动到 DOM 中 Vue app 之外的其余地位。

比方最常见的模态窗,咱们心愿模态窗的逻辑存在于组件中,但在 UI 上,元素最好又挂载到 DOM 根节点(如 body)上,不便咱们进行 css 来定位。

<body>
  <div style="position: relative;">
    <h3>Tooltips with Vue 3 Teleport</h3>
    <div>
      <modal-button></modal-button>
    </div>
  </div>
</body>
const app = Vue.createApp({});

app.component('modal-button', {
  template: `
    <button @click="modalOpen = true">
        Open full screen modal!
    </button>

    <div v-if="modalOpen" class="modal">
      <div>
        I'm a modal! 
        <button @click="modalOpen = false">
          Close
        </button>
      </div>
    </div>
  `,
  data() {
    return {modalOpen: false}
  }
})

在下面的例子中,咱们能够看到一个问题——模态框是在深度嵌套的 div 中渲染的,而模态框的 position:absolute 以父级绝对定位的 div 作为援用,最终的成果会受父级定位的影响,这可能并不是咱们冀望的后果。

Teleport 提供了一种洁净的办法,容许咱们管制在 DOM 中哪个父节点下渲染了 HTML,而不用求助于全局状态或将其拆分为两个组件。

app.component('modal-button', {
  template: `
    <button @click="modalOpen = true">
        Open full screen modal! (With teleport!)
    </button>

    <teleport to="body">
      <div v-if="modalOpen" class="modal">
        <div>
          I'm a teleported modal! 
          (My parent is "body")
          <button @click="modalOpen = false">
            Close
          </button>
        </div>
      </div>
    </teleport>
  `,
  data() {
    return {modalOpen: false}
  }
})

组件

  • 函数式组件

在 vue2 中咱们可能因为性能和多根节点的需要而应用函数式组件,当在 vue3 中,针对一般组件性能做了优化,曾经和函数组件性能相差无几,而且也反对多根节点,因而函数式组件的应用场景不是很有必要了,所以针对函数式组件进行了一些调整:

<!-- Vue 2 函数式组件示例 -->
<script>
export default {
  functional: true,
  props: ['level'],
  render(h, { props, data, children}) {return h(`h${props.level}`, data, children)
  }
}
</script>

<!-- Vue 2 函数式组件示例应用 <template> -->
<template functional>
  <component
    :is="`h${props.level}`"
    v-bind="attrs"
    v-on="listeners"
  />
</template>
<script>
export default {props: ['level']
}
</script>

在 vue3 中删除了 functional optionfunctional attribute, 下面两种形式不能在 vue3 中应用了。

在 vue3 中函数式组件即是一个一般函数,接管两个参数:propscontext

// vue3
import {h} from 'vue'

const DynamicHeading = (props, context) => {return h(`h${props.level}`, context.attrs, context.slots)
}

DynamicHeading.props = ['level']

export default DynamicHeading
  • 创立异步组件

以前能够通过返回一个 Promise 的函数来定义异步组件:

// vue2
const asyncModal = () => import('./Modal.vue');
// 或者带上配置
const asyncModal = {component: () => import('./Modal.vue'),
  delay: 200,
  timeout: 3000,
  error: ErrorComponent,
  loading: LoadingComponent
}

在 vue3 中,新增呢一个 api(defineAsyncComponent)来显示定义异步组件。

// vue3
import {defineAsyncComponent} from 'vue'
import ErrorComponent from './components/ErrorComponent.vue'
import LoadingComponent from './components/LoadingComponent.vue'

// 不带选项的异步组件
const asyncModal = defineAsyncComponent(() => import('./Modal.vue'))

// 带选项的异步组件
const asyncModalWithOptions = defineAsyncComponent({loader: () => import('./Modal.vue'),
  delay: 200,
  timeout: 3000,
  errorComponent: ErrorComponent,
  loadingComponent: LoadingComponent
})
  • 新增 emits 选项,定义和验证收回的自定义事件
<!-- vue2 -->
<template>
  <div>
    <p>{{text}}</p>
    <button v-on:click="$emit('accepted')">OK</button>
  </div>
</template>
<script>
  export default {props: ['text']
  }
</script>

vue3 中减少了 emits 选项来显示定义组件的自定义事件,未声明 emits 的事件监听器都会被算入组件的 $attrs 并绑定在组件的根节点上。

<!-- vue3 -->
<template>
  <div>
    <p>{{text}}</p>
    <button v-on:click="$emit('accepted')">OK</button>
  </div>
</template>
<script>
  export default {props: ['text'],
    emits: ['accepted']
  }
</script>

emits 还能够反对对自定义事件进行验证,只须要改为对象模式即可。

emits: {
    // 没有验证函数
    click: null,

    // 带有验证函数
    submit: payload => {if (payload.email && payload.password) {return true} else {console.warn(`Invalid submit event payload!`)
        return false
      }
    }
  }

强烈建议应用 emits 记录每个组件所触发的所有事件,而且记录的事件会有代码提醒。

渲染函数

  • 对立插槽 API

    以前,在组件中获取插槽是两个不同的 api(this.$scopedSlotsthis.$slots),当初对立应用this.$slots

  • 整合$listeners、class、style 到$attrs

在 vue2 时,咱们能够通过如下形式拜访attribute 和事件监听器:

<!-- vue3 -->
<template>
  <label>
    <input type="text" v-bind="$attrs" v-on="$listeners" />
  </label>
</template>
<script>
  export default {inheritAttrs: false}
</script>

在 Vue 3 的虚构 DOM 中,事件监听器当初只是以 on 为前缀的 attribute,这样就成了 $attrs 对象的一部分,因而 $listeners 被移除了。

<!-- vue3 -->
<template>
  <label>
    <input type="text" v-bind="$attrs" />
  </label>
</template>
<script>
export default {inheritAttrs: false}
</script>

在 Vue 2 的虚构 DOM 实现中对 classstyle attribute 有一些非凡解决。因而,它们 蕴含在 $attrs 中,Vue3 中简化了这部分的解决,用 $attrs 蕴含 所有的 attribute,包含classstyle

自定义元素

  • 只能在 <component> 元素中应用isprop

在 vue3 中不能在一般组件和元素应用 is 属性,仅能在 component 内置组件中应用。

其余

  • 生命周期变更

    • destroyed 生命周期选项被重命名为 unmounted
    • beforeDestroy 生命周期选项被重命名为 beforeUnmount
  • 自定义指令生命周期调整,和组件生命周期对立

    • created – 新的!在元素的 attribute 或事件侦听器利用之前调用。
    • bind → beforeMount
    • inserted → mounted
    • beforeUpdate:新的!这是在元素自身更新之前调用的,很像组件生命周期钩子。
    • update → 移除!有太多的相似之处要更新,所以这是多余的,请改用 updated
    • componentUpdated → updated
    • beforeUnmount:新的!与组件生命周期钩子相似,它将在卸载元素之前调用。
    • unbind -> unmounted
  • Mixin 合并行为变更

当来自组件的 data() 及其 mixin 或 extends 基类被合并时,当初将 浅层次 执行合并。

  • 过渡的 class 名更改

过渡类名 v-enter 批改为 v-enter-from、过渡类名 v-leave 批改为 v-leave-from

  • VNode 生命周期事件变更

    <!-- vue2 -->
    <template>
      <child-component @hook:updated="onUpdated">
    </template>
    <!-- vue3 -->
    <template>
      <child-component @vnode-updated="onUpdated">
    </template>

废除的 API

  • keyCode作为 v-on 的修饰符及 config.keyCodes 配置。
<!-- 键码版本(废除)-->
<input v-on:keyup.13="submit" /> 

<!-- 别名版本 -->
<input v-on:keyup.enter="submit" />

<script>
Vue.config.keyCodes = { // 废除
  f1: 112
}
</script>
  • $on$off$once 实例办法已被移除,组件实例不再实现事件触发接口

在 vue2 中咱们能够通过 EventBus 实现组件通信:

// eventBus.js
const eventBus = new Vue()
export default eventBus
// ChildComponent.vue
import eventBus from './eventBus'
export default {mounted() {
    // 增加 eventBus 监听器
    eventBus.$on('custom-event', () => {console.log('Custom event triggered!')
    })
  },
  beforeDestroy() {
    // 移除 eventBus 监听器
    eventBus.$off('custom-event')
  }
}
// ParentComponent.vue
import eventBus from './eventBus'
export default {
  methods: {callGlobalCustomEvent() {eventBus.$emit('custom-event') // 当 ChildComponent 被挂载,控制台中将显示一条音讯
    }
  }
}

在 vue3 中,此形式不再无效,因为齐全移除了 $on$off$once 办法。如果须要,能够应用一些实现了事件触发器接口的内部库,或者应用 Provide,简单的间接上 Vuex 就对了。

  • 不再反对过滤器
<!-- vue2 -->
<template>
  <h1>Bank Account Balance</h1>
  <p>{{accountBalance | currencyUSD}}</p>
</template>

<script>
  export default {
    props: {
      accountBalance: {
        type: Number,
        required: true
      }
    },
    filters: {currencyUSD(value) {return '$' + value}
    }
  }
</script>

在 vue3 中能够应用办法或者计算属性代替:

<!-- vue3 -->
<template>
  <h1>Bank Account Balance</h1>
  <p>{{accountInUSD}}</p>
</template>

<script>
  export default {
    props: {
      accountBalance: {
        type: Number,
        required: true
      }
    },
    computed: {accountInUSD() {return '$' + this.accountBalance}
    }
  }
</script>
  • 删除$childrenproperty

    $children property 已移除,不再反对。如果你须要拜访子组件实例,咱们倡议应用 $refs。

  • 全局函数 setdelete 以及实例办法 $set$delete。基于代理的变化检测不再须要它们了。

当然,下面的只是开胃菜,接下来的才是咱们最值得关注的新个性。

组合式 Api

为了解决咱们后面说的逻辑复用和代码组织的问题,vue3 推出了新的代码编写形式,这个 vue3 最重要的个性,也是将来编写 vue 的次要趋势。

上面是一个显示某个用户的仓库列表的视图,同时带有搜寻和筛选性能,伪代码如下:

// src/components/UserRepositories.vue

export default {components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList},
  props: {
    user: { 
      type: String,
      required: true
    }
  },
  data () {
    return {repositories: [], // 1
      filters: {...}, // 3
      searchQuery: '' // 2
    }
  },
  computed: {filteredRepositories () {...}, // 3
    repositoriesMatchingSearchQuery () { ...}, // 2
  },
  watch: {user: 'getUserRepositories' // 1},
  methods: {getUserRepositories () {// 应用 `this.user` 获取用户仓库}, // 1
    updateFilters () { ...}, // 3
  },
  mounted () {this.getUserRepositories() // 1
  }
}

能够看到,按 option 组织代码,性能逻辑点是碎片化的扩散在各个组件选项中,特地是遇到一些内容较多的组件,须要在各个选项中重复跳转,浏览和书写代码将是一件十分苦楚的事件,大大降低了组件的可维护性。

其实,在开发和浏览组件代码的时候,咱们更多是关注的性能点,而不是去关注用了那些 options,这正是组合式 api 解决的问题。

// src/composables/useUserRepositories.js

import {fetchUserRepositories} from '@/api/repositories'
import {ref, onMounted, watch} from 'vue'

export default function useUserRepositories(user) {const repositories = ref([])
  const getUserRepositories = async () => {repositories.value = await fetchUserRepositories(user.value)
  }

  onMounted(getUserRepositories)
  watch(user, getUserRepositories)

  return {
    repositories,
    getUserRepositories
  }
}
// src/composables/useRepositoryNameSearch.js

import {ref, computed} from 'vue'

export default function useRepositoryNameSearch(repositories) {const searchQuery = ref('')
  const repositoriesMatchingSearchQuery = computed(() => {
    return repositories.value.filter(repository => {return repository.name.includes(searchQuery.value)
    })
  })

  return {
    searchQuery,
    repositoriesMatchingSearchQuery
  }
}
// src/components/UserRepositories.vue
import {toRefs} from 'vue'
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import useRepositoryFilters from '@/composables/useRepositoryFilters'

export default {components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList},
  props: {
    user: {
      type: String,
      required: true
    }
  },
  setup(props) {const { user} = toRefs(props)

    const {repositories, getUserRepositories} = useUserRepositories(user)

    const {
      searchQuery,
      repositoriesMatchingSearchQuery
    } = useRepositoryNameSearch(repositories)

    const {
      filters,
      updateFilters,
      filteredRepositories
    } = useRepositoryFilters(repositoriesMatchingSearchQuery)

    return {
      // 因为咱们并不关怀未经过滤的仓库
      // 咱们能够在 `repositories` 名称下裸露过滤后的后果
      repositories: filteredRepositories,
      getUserRepositories,
      searchQuery,
      filters,
      updateFilters
    }
  }
}

组合式 API 拆散了组件的逻辑关注点,更具组织性,代码的可读性和可维护性也更好。而且能够将可复用的逻辑抽离成 Hooks,具备更好的可复用性。

因为组合式 API 的特殊性,须要应用新的 API,接下来咱们就来看看这些 API。

setup

setup 是组合式 API 的入口,所有的内容都须要蕴含在其中,它仅在组件创立 之前 执行 一次,所以此时 this 并不是指向以后组件实例。

setup(props,context){const { attrs, slots, emit} = context;
    // ...
}

参数

  • {Data} props:接管到的 props 数据,是响应式的。
  • {SetupContext} context:一个对象,蕴含组件须要的上下文信息,蕴含attrsslotsemit

返回值

  • 如果返回一个对象,那么该对象的 property 以及传递给 setupprops 参数中的 property 就都能够在模板中拜访到。
<!-- MyBook.vue -->
<template>
  <div>{{collectionName}}: {{readersNumber}} {{book.title}}</div>
</template>

<script>
  import {ref, reactive} from 'vue'

  export default {
    props: {collectionName: String},
    setup(props) {const readersNumber = ref(0)
      const book = reactive({title: 'Vue 3 Guide'})

      // 裸露给 template
      return {
        readersNumber,
        book
      }
    }
  }
</script>
  • 如果返回一个渲染函数,该函数能够间接应用在同一作用域中申明的响应式状态。
// MyBook.vue

import {h, ref, reactive} from 'vue'

export default {setup() {const readersNumber = ref(0)
    const book = reactive({title: 'Vue 3 Guide'})
    // 请留神这里咱们须要显式调用 ref 的 value
    return () => h('div', [readersNumber.value, book.title])
  }
}

生命周期钩子

为了使组合式 API 的性能和选项式 API 一样残缺,咱们还须要一种在 setup 中注册生命周期钩子的办法。组合式 API 上的生命周期钩子与选项式 API 的名称雷同,但前缀为 on:即 mounted 看起来会像 onMounted

import {onMounted, onUpdated, onUnmounted} from 'vue'

const MyComponent = {setup() {onMounted(() => {console.log('mounted!')
    })
    onUpdated(() => {console.log('updated!')
    })
    onUnmounted(() => {console.log('unmounted!')
    })
  }
}

setup 代替了 beforeCreatecreated,比照如下:

选项式 API Hook inside setup
beforeCreate Not needed*
created Not needed*
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered
activated onActivated
deactivated onDeactivated

响应式

在 vue3 中,应用了 Proxy 代替了Object.defineProperty,使 Vue 3 防止了 Vue 晚期版本中存在的一些响应性问题。

当咱们从一个组件的 data 函数中返回一个一般的 JavaScript 对象时,Vue 会将该对象包裹在一个带有 getset 处理程序的 Proxy 中。

举个🌰:

const dinner = {meal: 'tacos'}

const handler = {get(target, property, receiver) { // 捕获器
    track(target, property)  // 跟踪 property 读取,收集依赖
    return Reflect.get(...arguments) // Reflect 将 this 绑定到 Proxy
  },
  set(target, property, value, receiver) {trigger(target, property) // 执行副作用依赖项
    return Reflect.set(...arguments)
  }
}

const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)

// tacos
  1. 当一个值被读取时进行追踪:proxy 的 get 处理函数中 track 函数记录了该 property 和以后副作用。
  2. 当某个值扭转时进行检测:在 proxy 上调用 set 处理函数。
  3. 从新运行代码来读取原始值trigger 函数查找哪些副作用依赖于该 property 并执行它们。

该被代理的对象对于用户来说是不可见的,然而在外部,它们使 Vue 可能在 property 的值被拜访或批改的状况下进行依赖跟踪和变更告诉。

那么组件是如何让渲染响应数据变动的呢?

组件的模板会被编译成一个 render 函数,它用来创立 VNodes,形容该组件应该如何被渲染。这个 render 函数被包裹在一个副作用中,容许 Vue 在运行时跟踪被“触达”的 property,当 property 变动的时候,就会执行对应的副作用,从而执行 render 从新渲染。当然在渲染并不会整个从新渲染,这里有一些优化伎俩,网上材料很多,这里不开展讲。

接下来咱们看看几个罕用的响应式 API。

ref

interface Ref<T> {value: T}
function ref<T>(value: T): Ref<T>

承受一个外部值并返回一个响应式且可变的 ref 对象。ref 对象具备指向外部值的单个 property .value

import {ref} from 'vue'

const counter = ref<number>(0)

console.log(counter) // {value: 0}
console.log(counter.value) // 0

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

因为在 JavaScript 中,NumberString 等根本类型是通过值而非援用传递的,在任何值四周都有一个封装对象,这样咱们就能够在整个利用中平安地传递它,而不用放心在某个中央失去它的响应性。

留神:ref 嵌套在响应式对象(如 reactive、readonly)或者用在模板中,将主动解包

reactive

function reactive<T extends object>(target: T): UnwrapNestedRefs<T>

返回对象的响应式正本,即一个深层递归转换的 proxy 对象。

import {reactive} from 'vue'
interface IState{count:number}
// state 当初是一个响应式的状态
const state = reactive<IState>({count: 0,})

ref 和 reactive

  • 个别根底数据类型应用 ref,对象应用 reactive
  • 如果将对象调配为 ref 值,则通过 reactive 办法使该对象具备高度的响应式。

readonly

承受一个对象 (响应式或纯对象) 或 ref 并返回原始对象的只读代理。只读代理是深层的:任何被拜访的嵌套 property 也是只读的。

const original = reactive({count: 0})

const copy = readonly(original)

watchEffect(() => {
  // 用于响应性追踪
  console.log(copy.count)
})

// 变更 original 会触发依赖于正本的侦听器
original.count++

// 变更正本将失败并导致正告
copy.count++ // 正告!

unref

如果参数是一个 ref,则返回外部值,否则返回参数自身。这是 val = isRef(val) ? val.value : val 的语法糖函数。

function useFoo(x: number | Ref<number>) {const unwrapped = unref(x) // unwrapped 当初肯定是数字类型
}

toRef

能够用来为源响应式对象上的某个 property 新创建一个 ref, 它会放弃对其源 property 的响应式连贯。

const state = reactive({
  foo: 1,
  bar: 2
})

const fooRef = toRef(state, 'foo')

fooRef.value++
console.log(state.foo) // 2

state.foo++
console.log(fooRef.value) // 3

toRefs

将响应式对象转换为一般对象,其中后果对象的每个 property 都是指向原始对象相应 property 的 ref

function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2
  })

  // 操作 state 的逻辑

  // 返回时转换为 ref
  return toRefs(state)
}

export default {setup() {
    // 能够在不失去响应性的状况下解构
    const {foo, bar} = useFeatureX()

    return {
      foo,
      bar
    }
  }
}

要辨认数据是否应用上述 api 解决过,能够应用这些 api:isRefisProxyisReactiveisReadonly`。

computed

// 只读的
function computed<T>(getter: () => T,
  debuggerOptions?: DebuggerOptions
): Readonly<Ref<Readonly<T>>>

// 可写的
function computed<T>(
  options: {get: () => T
    set: (value: T) => void
  },
  debuggerOptions?: DebuggerOptions
): Ref<T>
interface DebuggerOptions {onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
}
interface DebuggerEvent {
  effect: ReactiveEffect
  target: any
  type: OperationTypes
  key: string | symbol | undefined
}
  • 承受一个 getter 函数,并依据 getter 的返回值返回一个不可变的响应式 ref 对象。
const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 谬误
  • 承受一个具备 getset 函数的对象,用来创立可写的 ref 对象。
const count = ref(1)
const plusOne = computed({get: () => count.value + 1,
  set: val => {count.value = val - 1}
})

plusOne.value = 1
console.log(count.value) // 0

watchEffect

function watchEffect(effect: (onInvalidate: InvalidateCbRegistrator) => void,
  options?: WatchEffectOptions
): StopHandle

interface WatchEffectOptions {
  flush?: 'pre' | 'post' | 'sync' // 默认:'pre'
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
}

interface DebuggerEvent {
  effect: ReactiveEffect
  target: any
  type: OperationTypes
  key: string | symbol | undefined
}

type InvalidateCbRegistrator = (invalidate: () => void) => void

type StopHandle = () => void

立刻执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时从新运行该函数。

const count = ref(0)

watchEffect(() => console.log(count.value))
// -> logs 0

setTimeout(() => {
  count.value++
  // -> logs 1
}, 100)
  • 进行侦听

watchEffect 在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时主动进行。当然也能够显式调用返回值以进行侦听:

const stop = watchEffect(() => {/* ... */})
// later
stop()
  • 革除副作用

有时副作用函数会执行一些异步的副作用,这些响应须要在其生效时革除。所以侦听副作用传入的函数能够接管一个 onInvalidate 函数作入参,用来注册清理生效时的回调。当以下状况产生时,这个生效回调会被触发:

  1. 副作用行将从新执行时
  2. 侦听器被进行 (如果在 setup() 或生命周期钩子函数中应用了 watchEffect,则在组件卸载时)
watchEffect(onInvalidate => {const token = performAsyncOperation(id.value)
  onInvalidate(() => {
    // id has changed or watcher is stopped.
    // invalidate previously pending async operation
    token.cancel()})
})

另外能够应用 flush option 或者 watchPostEffectwatchSyncEffect 来调整其刷新机会。

watch

// 侦听繁多源
function watch<T>(
  source: WatcherSource<T>,
  callback: (
    value: T,
    oldValue: T,
    onInvalidate: InvalidateCbRegistrator
  ) => void,
  options?: WatchOptions
): StopHandle

// 侦听多个源
function watch<T extends WatcherSource<unknown>[]>(
  sources: T
  callback: (
    values: MapSources<T>,
    oldValues: MapSources<T>,
    onInvalidate: InvalidateCbRegistrator
  ) => void,
  options? : WatchOptions
): StopHandle

type WatcherSource<T> = Ref<T> | (() => T)

type MapSources<T> = {[K in keyof T]: T[K] extends WatcherSource<infer V> ? V : never
}

// 参见 `watchEffect` 共享选项的类型申明
interface WatchOptions extends WatchEffectOptions {
  immediate?: boolean // 默认:false
  deep?: boolean
}

watch 须要侦听特定的数据源,并在独自的回调函数中执行副作用。默认状况下,它也是惰性的——即回调仅在侦听源发生变化时被调用。

  • 与 watchEffect 相比,watch 容许咱们:

    • 惰性地执行副作用;
    • 更具体地说明应触发侦听器从新运行的状态;
    • 拜访被侦听状态的先前值和以后值。
// 侦听一个 getter
const state = reactive({count: 0})
watch(() => state.count,
  (count, prevCount) => {/* ... */}
)

// 间接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {/* ... */})
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {/* ... */})

当然,后面只是介绍了罕用的 API,更多的请查看响应式 API。

弊病

当然组合式 API 也并不是银弹,至多目前来说不是,还是存在一些问题。

  • Ref 的心智累赘

读写 ref 必须带上.value,语法冗余,目前还没有确定的计划来解决此问题。不过尤大给出了 refSuger2 提案,就看后续社区认可度怎么样了。

<script setup>
  // declaring a variable that compiles to a ref
  let count = $ref(1)

  console.log(count) // 1

  function inc() {
    // the variable can be used like a plain value
    count++
  }
</script>

<template>
  <button @click="inc">{{count}}</button>
</template>
  • 难看简短的返回语句

setup() 的返回语句变得简短,像是重复劳动,而且还是存在代码高低横跳问。

在 vue3.2 提供了 SetupScript 语法糖,就没有这个问题了。

  • 须要更多的自我克服

尽管组合式 API 在代码组织方面提供了更多的灵活性,但它也须要开发人员更多地自我克服来“正确地实现它”。也有些人放心 API 会让没有教训的人编写出面条代码。换句话说,尽管组合式 API 进步了代码品质的下限,但它也升高了上限。

咱们须要更多的思考如何正当的组织代码,举荐依据逻辑关注点将程序分解成函数和模块来组织它。

SetupScript

<script setup> 是在单文件组件 (SFC) 中应用组合式 API 的编译时语法糖。相比于一般的 <script> 语法,它具备更多劣势:

  • 更少的样板内容,更简洁的代码。
  • 可能应用纯 Typescript 申明 props 和收回事件。
  • 更好的运行时性能 (其模板会被编译成与其同一作用域的渲染函数,没有任何的两头代理)。
  • 更好的 IDE 类型推断性能 (缩小语言服务器从代码中抽离类型的工作)。
<script setup>
// 导入  
import {capitalize} from './helpers'
// 组件
import MyComponent from './MyComponent.vue'
// 变量
const msg = 'Hello!'

// 函数
function log() {console.log(msg)
}
  
</script>

<template>
  <div @click="log">{{msg}}</div>
  <div>{{capitalize('hello') }}</div>
  <MyComponent />  
</template>

下面 <script setup> 中的代码会被编译成组件 setup() 函数的内容,不同的是 <script setup> 中的代码会在每次组件实例被创立的时候执行。而且所有 顶层 的绑定 (包含变量,函数申明,以及 import 引入的内容) 都会裸露给模板,能够间接应用,甚至连组件都无需手动注册。

在持续上面的内容前,咱们来看一个词 编译器宏 ,他们不须要导入,且会在解决 <script setup> 的时候被编译解决掉。<script setup> 提供了如下几个编译器宏:

- defineProps
- defineEmits
- defineExpose
- withDefaults

接下来看一下 <script setup> 独有的 API:

  • defineProps申明 Props,接管 props 选项雷同的值
const props = defineProps({
  foo: {
    type:String,
    default:''
  }
})

如果应用了 TypeScript,也能够应用纯类型申明来申明 Props。

// 一般
const props = defineProps<{
  foo: string
  bar?: number
}>()


// 默认值
interface Props {
  msg?: string
  labels?: string[]}

const props = withDefaults(defineProps<Props>(), {
  msg: 'hello',
  labels: () => ['one', 'two']
})
  • defineEmits申明 emits,接管 emits 选项雷同的值
// 一般
const emit = defineEmits(['change', 'delete'])
// TS 类型申明
const emit = defineEmits<{(e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
  • defineExpose申明裸露的绑定

应用 <script setup> 的组件是 默认敞开 的,也即通过模板 ref 或者 $parent 链获取到的组件的公开实例,不会裸露任何在 <script setup> 中申明的绑定。须要开发者明确申明裸露的属性。

<script setup>
import {ref} from 'vue'

const a = 1
const b = ref(2)

defineExpose({
  a,
  b
})
</script>
  • useSlotsuseAttrs别离对应setupContext.slotssetupContext.attrs,也能够在一般的组合式 API 中应用。
<script setup>
import {useSlots, useAttrs} from 'vue'

const slots = useSlots()
const attrs = useAttrs()
</script>

目前还有一些事件是 <script setup>不能做到的,就须要和一般的 <script> 一起应用:

<script>
// 一般 <script>, 在模块范畴下执行(只执行一次)
runSideEffectOnce()

// 申明额定的选项
export default {
  inheritAttrs: false,
  customOptions: {}}
</script>

<script setup>
// 在 setup() 作用域中执行 (对每个实例皆如此)
</script>

更多内容请查看 SetupScript。

其余

  • Style 新个性

    • 选择器
    /* 深度选择器 */
    .a :deep(.b) {/* ... */}
    
    /* 插槽选择器 */
    :slotted(div) {color: red;}
    
    /* 全局选择器 */
    :global(.red) {color: red;}
    • <style module>
<template>
  <p :class="$style.red">
    This should be red
  </p>
</template>

<style module>
.red {color: red;}
</style>

也能够自定义注入的名称:

<template>
  <p :class="classes.red">red</p>
</template>

<style module="classes">
.red {color: red;}
</style>

咱们能够在组合 API 中通过 useCssModule 来应用:

// 默认, 返回 <style module> 中的类
useCssModule()

// 命名, 返回 <style module="classes"> 中的类
useCssModule('classes')

应用状态驱动的动静 CSS:

<script setup>
const theme = {color: 'red'}
</script>

<template>
  <p>hello</p>
</template>

<style scoped>
p {color: v-bind('theme.color');
}
</style>
  • 关注 RFCS,回溯历史,洞悉将来
正文完
 0