1
头图

Words: 7887, Reading time: 40 minutes, click on read the original text

It has been almost eight years since the first version was released on December 8, 2013. Do you know the meaning of each version's name?

version numbernameParaphrasetime
V0.9AnimatrixThe Matrix Animation2014.2.25
V0.10Blade RunnerBlade Runner2014.3.23
V0.11Cowboy BebopStar Cowboy2014.11.7
V0.12Dragon BallDragon Ball2015.6.12
V1.0EvangelionEvangelion2015.10.27
V2.0Ghost in the ShellGhost in the Shell2016.9.30
V2.1Hunter X HunterFull-time hunter2016.11.22
V2.2Initial DInitial D2017.2.26
V2.3JoJo's Bizarre AdventureJoJo's Bizarre Adventure2017.4.2
V2.4Kill la KillKill the girl2017.7.13
V2.5Level ESupernatural E touch2017.10.13
V2.6MacrossMacross2019.2.4
V3.0One PieceOne Piece2020.9.18
V3.1PlutoThe strongest robot on the ground2021.6.8
V3.2Quintessential QuintupletsQuintuplets2021.8.10

It turns out that the name of each version is named after a manga, so how many of these anime have you watched?

Then we will focus on Vue3.0.

origin

The emergence of a new tool must be to solve the problems of existing tools. We often hear that Vue is not suitable for developing large and complex projects. One fundamental reason is that Vue's existing API forces us to organize code through options, but sometimes it makes more sense to organize code through logical relationships. Another reason is the lack of a simple and low-cost mechanism to extract and reuse the logic between multiple components.

Then let's take a look at the 2.0 problem and how Vue3 solves it.

Defects of Option Organization Code

The options type organization code, the same function is scattered in each option, which causes the data, methods, computed and other options to jump horizontally during development.

option

Vue3 launched CompositionApi, the purpose is to solve this problem, it combines the logic scattered in each option, let's compare it below:

Mixin problem

For complex functions, we might think of using Mixin to extract separate files. But Mixin has some usage problems, such as naming conflicts and unclear attribute sources.

Vue3 proposes the Hooks method, which can extract each function to hooks. A hook is an independent function, so the above problems will no longer occur.

TypeScript support is not sound

Now large projects will be equipped with TypeScript as standard. Vue's current API has encountered a lot of trouble when integrating TypeScript. The main reason is that Vue relies on a simple this context to expose property. this is more subtle. (Such methods functions in the option this is to a component instance, instead of the methods object).

In other words, Vue's existing API did not take care of type deduction at the beginning of its design, which complicates the adaptation of TypeScript.

Currently, most Vue developers who use TypeScript are writing components as TypeScript classes (with the help of decorator) vue-class-component It must rely on decorator-a very unstable stage 2 proposal with many unknowns in its implementation details. Based on it is extremely risky.

The scheme proposed in Vue3 makes more use of common variables and functions that are naturally type-friendly, enjoys type deduction perfectly, and does not need to make too many additional type annotations.

This also means that the JavaScript code you write is almost TypeScript code. Even non-TypeScript developers will benefit from better IDE type support.

Better responsiveness and performance

As we all know, Vue2's response getter and setter an existing property of the object Object.defineProperty , so it can only monitor the change of this property value, but not the addition and deletion of the object property. In the implementation of Vue 2, when the data becomes responsive during the component initialization phase, when the sub-attribute is still an object, it will recursively execute Object.defineProperty define the responsive type of the sub-object, and there will be some performance problems. And there is a common problem that is to modify the array through the index and directly add properties to the object, and it will not trigger the responsive update mechanism.

In Vue3, Proxy is used to achieve responsiveness. In fact, it is not that the performance of Proxy itself is better than Object.defineProperty , in fact, it is the opposite. So why choose Proxy?

Because Proxy is essentially hijacking an object, so it can not only monitor the change of an attribute value of the object, but also monitor the addition and deletion of object attributes. Moreover, when implementing the responsive style, a delayed processing method is adopted. When a deeply nested object is used, the responsive style of the attribute will be processed only when its attribute is accessed, and there will be a certain improvement in performance.

Support global API Treeshaking

Vue3 reconstructs the global and local APIs, both use ESModule's named export access, support tree-shaking, and only package the functions that are used. Users only pay for the functions that are actually used. At the same time, the reduction in package size also means performance. promote.

// vue2
import Vue from 'vue'

Vue.nextTick(() => {
  // 一些和DOM有关的东西
})
// vue3
import { nextTick } from 'vue'

nextTick(() => {
  // 一些和DOM有关的东西
})

The above are the main changes in Vue, so let's take a look at the new features.

New features and changes

Next, we mainly look at some non-compatible major changes:

Global API

  • Support multiple application root instances to prevent global configuration pollution

    // 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({
  /* ... */
})

Global API details of some other global API changes.

  • Global API refactored to be Treeshaking

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

Template and instruction related

  • Better to use v-model

Replace the original v-model and v-bind.sync modifiers, and support the use of multiple v-model for two-way binding through the parameter form.

<!-- 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> changes
<!-- 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 merge order change
<!-- 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>
  • Remove v-on.native modifier

    In previous versions, to add a native DOM listener to the root element of a child component, you can use the .native modifier.

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

In vue3, assembly not is defined as all events listener component triggered, Vue now will add them as a native event listener to the root element subassembly (unless you have set the option subassembly inheritAttrs: false )

<!-- vue3 -->
<my-component
  v-on:close="handleComponentEvent"
  v-on:click="handleNativeClickEvent"
/>
<script>
  export default {
    emits: ['close']
  }
</script>
  • Support fragments (multiple root nodes)
<!-- vue2 -->
<template>
  <div>
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
  </div>
</template>

In vue2, components must be contained within an element, and multiple root nodes are not supported. This sometimes brings troubles to us in writing styles, so multiple root nodes are supported in vue3.

<!-- vue3 -->
<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>
  • Added Teleport portal

The core of vue application development is component writing, which encapsulates UI and related behaviors into components to build UI. But sometimes part of the component template logically belongs to the component, and from a technical point of view, it is best to move this part of the template to a location other than the Vue app in the DOM.

For example, the most common modal window, we hope that the logic of the modal window exists in the component, but on the UI, it is best to mount the element to the DOM root node (such as body) to facilitate our css positioning.

<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
    }
  }
})

In the above example, we can see a problem - the frame mode is deeply nested div rendered, whereas the modal box position:absolute to the parent positioned opposite div as a reference, the final results will be affected by the parent The impact of level positioning, this may not be the result we expect.

Teleport provides a clean method that allows us to control which parent node in the DOM renders HTML without having to resort to global state or split it into two components.

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
    }
  }
})

Component

  • Functional component

In vue2, we may use functional components because of the performance and the needs of multiple root nodes. When in vue3, the performance of ordinary components is optimized, which is almost the same as the performance of functional components, and it also supports multiple root nodes, so functional The usage scenarios of components are not very necessary, so some adjustments have been made to functional components:

<!-- 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>

Deleted In vue3 functional option and functional attribute , the above two methods can not be used in vue3.

In vue3, a functional component is an ordinary function, which receives two parameters: props and context .

// vue3
import { h } from 'vue'

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

DynamicHeading.props = ['level']

export default DynamicHeading
  • Create asynchronous components

It used to be possible to define asynchronous components by returning a Promise function:

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

In vue3, a new api (defineAsyncComponent) is added to display and define asynchronous components.

// 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
})
  • Add emits option to define and verify the custom event sent
<!-- vue2 -->
<template>
  <div>
    <p>{{ text }}</p>
    <button v-on:click="$emit('accepted')">OK</button>
  </div>
</template>
<script>
  export default {
    props: ['text']
  }
</script>

The emits option is added to vue3 to display the custom events of the defined component. emits will be counted in the $attrs the component and bound to the root node of the component.

<!-- 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 can also support the verification of custom events, just change it to an object form.

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

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

It is strongly recommended to use emits record all events triggered by each component, and the recorded events will have code prompts.

Rendering function

  • Unified Slot API

    Previously, there were two different this.$scopedSlots (061317aadd0dd7 and this.$slots ) to obtain slots in the component. Now, this.$slots .

  • Integrate $listeners , class, style to $attrs

In vue2, we can access attribute and the event listener in the following way:

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

In the virtual DOM of Vue 3, the event listener is now just an attribute prefixed on , which becomes part of the $attrs $listeners is removed.

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

There are some special treatments class and style attributes in the virtual DOM implementation of Vue 2. Thus, they not contained in $attrs in, Vue3 simplifies this part of the process, with $attrs comprising all attribute, including class and style .

Custom element

  • is prop can only be used in the <component> element

is attribute cannot be used in ordinary components and elements, but can only be used in the component built-in components.

other

  • Life cycle changes

    • destroyed life cycle option was renamed to unmounted
    • beforeDestroy life cycle option was renamed to beforeUnmount
  • Custom instruction life cycle adjustment, unified with component life cycle

    • created-new! Called before the element's attribute or event listener is applied.
    • bind → beforeMount
    • inserted → mounted
    • beforeUpdate : New! This is called before the element itself is updated, much like a component lifecycle hook.
    • update → remove! There are too many similarities to update, so this is redundant, please use updated instead.
    • componentUpdated → updated
    • beforeUnmount : New! Similar to the component lifecycle hook, it will be called before the element is unloaded.
    • unbind -> unmounted
  • Mixin merge behavior changes

data() and its mixin or extends base class from the component shallow level will now be merged.

  • Transitional class name change

The transition class name v-enter modified to v-enter-from , and the transition class name v-leave modified to v-leave-from .

  • VNode life cycle event changes

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

Obsolete API

  • keyCode as v-on modifier and the config.keyCodes configuration.
<!-- 键码版本(废弃) -->
<input v-on:keyup.13="submit" /> 

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

<script>
Vue.config.keyCodes = { // 废弃
  f1: 112
}
</script>
  • $on , $off and $once instance methods have been removed, component instances no longer implement the event trigger interface

In vue2, we can implement component communication through 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 被挂载,控制台中将显示一条消息
    }
  }
}

In vue3, this method is no longer valid because the $on , $off and $once methods are completely removed. If necessary, you can use some external libraries that implement the event trigger interface, or use Provide, and just go to Vuex for complicated ones.

  • Filters are no longer supported
<!-- 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>

In vue3, you can use methods or calculated properties instead:

<!-- 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>
  • Delete $children property

    $children property has been removed and is no longer supported. If you need to access the sub-component instance, we recommend using $refs .

  • Global functions set and delete and instance methods $set and $delete . They are no longer needed for agent-based change detection.

Of course, the above are just appetizers, and the next ones are our most noteworthy new features.

Combined Api

In order to solve the problems of logic reuse and code organization we mentioned earlier, vue3 introduced a new code writing method. This most important feature of vue3 is also the main trend of writing vue in the future.

The following is a view showing the warehouse list of a certain user, with search and filtering functions. The pseudo code is as follows:

// 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
  }
}

It can be seen that the code is organized by option, and the functional logic points are fragmented and scattered in each component option. Especially when you encounter some components with more content, you need to jump repeatedly in each option. Reading and writing code will be the same. A very painful thing, greatly reducing the maintainability of the component.

In fact, when developing and reading the component code, we pay more attention to the functional points, rather than to the options that are used. This is the problem that the combined api solves.

// 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
    }
  }
}

The combined API separates the logical concerns of the components, is more organized, and the code is more readable and maintainable. Moreover, the reusable logic can be separated into hooks, which has better reusability.

Due to the particularity of combined APIs, new APIs need to be used, so let's take a look at these APIs next.

setup

setup is the entrance modular API, all the content needs to be included in them, it only created in the component before execution once , this is not so in this case points to the current component instance.

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

parameter

  • {Data} props : The received props data is responsive.
  • {SetupContext} context : An object that contains the context information needed by the component, including attrs , slots , emit .

return value

  • If it returns an object, then the property of the object and passed to setup of props parameter property will have access to the template.
<!-- 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>
  • If a rendering function is returned, the function can directly use the reactive state declared in the same scope.
// 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])
  }
}

Lifecycle hook

In order to make the function of the combined API as complete as the optional API, we also need a setup to register the lifecycle hook in 061317aadd1e9e. The lifecycle hook on the combined API has the same name as the optional API, but the prefix is on : that is, mounted will look like onMounted .

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

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

setup replaces beforeCreate and created , the comparison is as follows:

Option APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
activatedonActivated
deactivatedonDeactivated

Responsive

In vue3 using the Proxy instead Object.defineProperty , so that in response Vue. 3 avoids some problems present in earlier versions of the Vue.

When we data function of a component, Vue will wrap the object in a Proxy get and set .

Give me 🌰:

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. track when a value is read : the Proxy get handler track function records the current property and side effects.
  2. when a certain value changes. : Calls the set processing function on the proxy.
  3. re-run the code to read the original value : trigger function to find which side effects depend on the property and execute them.

The proxied objects are invisible to the user, but internally, they enable Vue to perform dependency tracking and change notifications when the value of the property is accessed or modified.

So how does the component make the rendering respond to data changes?

The component template will be compiled into a render function, which is used to create VNodes , describing how the component should be rendered. This render function is wrapped in a side effect, allowing Vue to track the property that is "touched" at runtime. When the property changes, the corresponding side effect will be executed, thereby executing render re-rendering. Of course, the rendering will not be completely re-rendered. Here are some optimization methods. There are many online materials, so I won't go into it here.

Next we look at a few commonly used reactive APIs.

ref

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

Accepts an internal value and returns a responsive and variable ref object. The ref object has a single property .value that points to an internal 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

Because in JavaScript, Number or String are passed by value instead of by reference, there is an encapsulated object around any value, so that we can safely pass it throughout the application without worrying about being somewhere Lose its responsiveness.

pass-by-reference-vs-pass-by-value-animation

Note: If ref is nested in a reactive object (such as reactive, readonly) or used in a template, will be automatically unpacked.

reactive

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

Return a responsive copy of the object, that is, a proxy object that is deeply recursively transformed.

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

ref and reactive :

  • General basic data types use ref, objects use reactive
  • If the object is assigned as a ref value, the reactive method makes the object highly responsive.

readonly

Accept an object (responsive or pure object) or ref and return the read-only proxy of the original object. The read-only proxy is deep: any nested property that is accessed is also read-only.

const original = reactive({ count: 0 })

const copy = readonly(original)

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

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

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

unref

If the parameter is a ref , the internal value is returned, otherwise the parameter itself is returned. This is the syntactic sugar function val = isRef(val) ? val.value : val

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

toRef

ref for a certain property on the source responsive object, and it will maintain a responsive connection to the source 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

Convert the responsive object into an ordinary object, where each property of the result object is ref that points to the corresponding property of the original object.

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

  // 操作 state 的逻辑

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

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

    return {
      foo,
      bar
    }
  }
}

To identify whether the data has been processed using the above APIs, you can use these APIs: isRef isProxy isReactive .

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
}
  • Accept a getter function and return an immutable response type ref object according to the return value of the getter.
const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误
  • Accept an get and set to create a writable ref object.
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

Execute a function passed in immediately, track its dependencies responsively, and re-run the function when its dependencies change.

const count = ref(0)

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

setTimeout(() => {
  count.value++
  // -> logs 1
}, 100)
  • Stop listening

When watchEffect in the component's setup() function or the life cycle hook , the listener will be linked to the life cycle of the component and will automatically stop when the component is uninstalled. Of course, you can also call the return value explicitly to stop listening:

const stop = watchEffect(() => {
  /* ... */
})
// later
stop()
  • Clear side effects

Sometimes the side-effect function executes some asynchronous side-effects, and these responses need to be cleared when they fail. So the function that listens to the incoming side effects can receive a onInvalidate function as an input parameter to register the callback when the cleanup fails. This invalidation callback will be triggered when the following situations occur:

  1. When side effects are about to be re-executed
  2. Listener is stopped (if setup() using a hook function in the life cycle or watchEffect , when the assembly is unloaded)
watchEffect(onInvalidate => {
  const token = performAsyncOperation(id.value)
  onInvalidate(() => {
    // id has changed or watcher is stopped.
    // invalidate previously pending async operation
    token.cancel()
  })
})

In addition, you can use the flush option or watchPostEffect and watchSyncEffect to adjust the refresh timing.

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 needs to listen to a specific data source and execute side effects in a separate callback function. By default, it is also lazy—that is, the callback is only called when the listening source changes.

  • Compared with watchEffect , watch allows us to:

    • Execute side effects lazily;
    • More specifically specify the state in which the listener should be triggered to re-run;
    • Access the previous and current values of the listened state.
// 侦听一个 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]) => {
  /* ... */
})

Of course, I just introduced the commonly used APIs. For more information, please see Responsive API .

Disadvantages

Of course, the combined API is not a silver bullet, at least not for now, there are still some problems.

  • Ref's mental burden

Reading and writing ref must bring .value, the syntax is redundant, there is no definite solution to solve this problem. However, Youda gave the refSuger2 proposal, depending on how the follow-up community recognition is.

<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>
  • ugly and lengthy return statement

setup() becomes lengthy, like repetitive work, and there are still questions about the code jumping up and down.

In vue3.2, SetupScript syntax sugar is provided, so there is no such problem.

  • Need more self-restraint

Although the combined API provides more flexibility in code organization, it also requires more self-restraint by developers to "get it right." Some people worry that the API will allow inexperienced people to write noodle code. In other words, although the combined API raises the upper limit of code quality, it also lowers the lower limit.

We need to think more about how to organize the code reasonably. It is recommended to decompose the program into functions and modules to organize it according to logical concerns.

SetupScript

<script setup> is compile-time syntactic sugar for using combined API in single file component (SFC). Compared with the ordinary <script> syntax, it has more advantages:

  • Less boilerplate content, more concise code.
  • Ability to declare props and emit events using pure Typescript.
  • Better runtime performance (the template will be compiled into a rendering function in the same scope as it, without any intermediate proxy).
  • Better IDE type inference performance (reduce the work of the language server to extract types from the code).
<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>

The above <script setup> will be compiled into setup() function, the difference is <script setup> will be executed every time the component instance is created. And all the top level of (including variables, function declarations, and content introduced by import) will be exposed to the template and can be used directly, even the components do not need to be manually registered.

Before continuing the following content, let's look at a word compiler macro , they do not need to be imported, and will be compiled and processed when <script setup> <script setup> provides the following compiler macros:

- defineProps
- defineEmits
- defineExpose
- withDefaults

Next, take a look at the unique API of <script setup>

  • defineProps declares Props and receives the same value as the props
const props = defineProps({
  foo: {
    type:String,
    default:''
  }
})

If you use TypeScript, you can also use pure type declarations to declare 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 declares emerges and receives the same value as the emits
// 普通
const emit = defineEmits(['change', 'delete'])
// TS类型声明
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
  • defineExpose declares the exposed binding

Use <script setup> component is off by default , i.e. template or ref $parent disclosed acquired Examples of chain assembly, not exposed in any <script setup> binding declaration. Developers need to clearly declare the exposed attributes.

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

const a = 1
const b = ref(2)

defineExpose({
  a,
  b
})
</script>
  • useSlots and useAttrs correspond setupContext.slots and setupContext.attrs , can also be used in a conventional combined API.
<script setup>
import { useSlots, useAttrs } from 'vue'

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

There are still some things that <script setup> can't do, and they need to be used together with <script>

<script>
// 普通 <script>, 在模块范围下执行(只执行一次)
runSideEffectOnce()

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

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

For more information, please see SetupScript .

other

  • Style new features

    • Selector
    /* 深度选择器 */
    .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>

You can also customize the name of the injection:

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

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

useCssModule in the combined API:

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

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

Use state-driven dynamic CSS:

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

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

<style scoped>
p {
  color: v-bind('theme.color');
}
</style>
  • Follow RFCS , look back on history and gain insight into the future

BWrong
363 声望304 粉丝

愿你走出半生,归来仍是少年