继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Vue3 开发文档

2020-07-28 20:36:1910245浏览

Sunday

6实战 · 14手记 · 9推荐
TA的实战

本博客会长期更新(在 Vue 的中文官方文档没有发布前)。

博客的边界:本博客只会带来 Vue 3 新增内容的整理文档(经过验证的)


重点内容:

  • Vite
  • Composition API
  • 新的响应式机制
  • 计算属性和侦听器
  • teleport (传送)
  • Fragment(碎片)
  • 自定义事件($emit 验证)
  • 组件的 v-model 升级

利用 vite 创建下一代的 Vue 项目

两个重要的点:

  1. Vue CLI v4.5 之上的版本
  2. Vite

Vue CLI

想要构建 Vue 3.x 项目,那么需要使用 Vue CLI v4.5 之上的版本

yarn global add @vue/cli@next
# or
npm install -g @vue/cli@next

如果想要在现有的项目中(Vue 2.x),升级构建工具,则需要执行以下代码

vue upgrade --next

Vite

Vite 是一个想要用来代替 webpackWeb 开发构建工具,也是官方推荐使用的构建工具,使用 vite 将脱离 webpack

// ----------npm----------
npm init vite-app <project-name>
cd <project-name>
npm install
npm run dev
// ----------yarn----------
yarn create vite-app <project-name>
cd <project-name>
yarn
yarn dev

Composition API

简介

为什么会有 Composition API

创建Vue组件使我们能够将接口的可重复部分及其功能提取到可重用的代码段中。就维护性和灵活性而言,仅此一项就可以使我们的应用程序发展得相当远。

但是,经过大量的开发经验证明,仅凭这一项可能还不够,特别是当你的应用程序变得非常大时。

想象一下数百个组件的项目,在处理如此大的应用程序时,共享和重用代码变得尤为重要。

摘要自:https://v3.vuejs.org/guide/composition-api-introduction.html#why-composition-api

但是因为 Vue 2.x 中,以 组件 为最小颗粒的原因(PS:mixin 虽然可以达到复用的效果,但是它依然缺少独立性。比如:命名冲突、隐式依赖的问题),导致组件中的逻辑无法被进行 共享重用

Composition API 概念

Composition API组合式 API) 是一个概念,而非具体的实现。

在组件中,实际使用 Composition API 的地方,是 setup 函数。在它内部,我们可以描述一个组件所需要的 所有逻辑

setup

简介

setup 函数是Composition API 的入口函数,也是它的核心函数。

但是要注意:setup 函数执行时,尚未创建组件实例,所以 this 中没有任何内容。这意味着,除了props,你将无法访问通过 this 来访问组件中声明的任何属性、本地状态、计算属性、方法。

参数

setup 函数有两个参数:

  1. {Data} props
    1. props 是响应式的
    2. 由于props是反应性的,你不能使用ES6解构,详见 对象的响应式
  2. {SetupContext} context
    1. context 是普通的 js 对象,所以他不是响应式的
    2. context 下有三个属性:
      1. attrs:包含父组件属性绑定、未识别出的为组件属性或自定义事件的事件
      2. slots:插槽
      3. emit:通知(发出的事件)

下面是官方的示例代码:

// Data 为  key:value 结构的对象
interface Data {
  [key: string]: unknown
}
// context 为上下文,包含:
// attrs:包含父组件属性绑定、未识别出的为组件属性或自定义事件的事件
// slots:插槽
// emit:通知
interface SetupContext {
  attrs: Data
  slots: Slots
  emit: (event: string, ...args: unknown[]) => void
}

function setup(props: Data, context: SetupContext): Data

可访问的组件属性

setup被执行时,该组件实例尚未建立。如此一来,您将只能访问以下属性:

  • props
  • attrs
  • slots
  • emit

换句话说,您将无权访问以下组件选项:

  • data
  • computed
  • methods

模板使用 setup 中的数据

如果setup返回对象,则可以在组件的模板中访问对象的属性(注意代码中的备注):

<!-- MyBook.vue -->
<template>
  <div>{{ readersNumber }} {{ book.title }}</div>
</template>

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

  export default {
    setup() {
      // 通过 ref 或 reactive 创建响应式数据,现在你可以先不需要理解什么是 ref 和 reactive。
      // 你只需要知道此时 readersNumber 和 book 为响应式的
      const readersNumber = ref(0)
      const book = reactive({ title: 'Vue 3 Guide' })
	 // return 的对象,可以直接在 模板中直接访问(不需要 .value)
      return {
        readersNumber,
        book
      }
    }
  }
</script>

渲染功能的实现

setup 还可以返回一个渲染函数,该函数可以直接使用在同一作用域中(setup 函数中)声明的反应状态 :

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

export default {
  setup() {
    const readersNumber = ref(0)
    const book = reactive({ title: 'Vue 3 Guide' })
    // Please note that we need to explicitly expose ref value here
    return () => h('div', [readersNumber.value, book.title])
  }
}

this 指向问题

setup 函数中,thisundefined 而不是 组件的引用。因为 setup 函数执行时,尚未创建组件实例。所以 thissetup 中无法正常使用。

setup 函数和其他的 Options API 一起使用时,可能会出现无法预知的错误,所以一定要谨慎。

生命周期钩子

setup 中,可以通过 onX 的方式注册 生命周期钩子

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

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

Options API(Vue 2.x) 生命周期选项和 Composition API(Vue 3.x) 之间的映射

  • beforeCreate ->使用 setup()
  • created ->使用 setup()
  • beforeMount -> onBeforeMount
  • mounted -> onMounted
  • beforeUpdate -> onBeforeUpdate
  • updated -> onUpdated
  • beforeUnmount -> onBeforeUnmount
  • unmounted -> onUnmounted
  • errorCaptured -> onErrorCaptured
  • renderTracked -> onRenderTracked
  • renderTriggered -> onRenderTriggered

提供 provide / 注入 inject

首先必须要明确:提供 provide / 注入 inject 的功能都只能在 setup 函数中进行使用。

如果你希望在 父组件 中提供一个值,然后在 子组件 中可以使用。那么你就应该考虑 提供 provide / 注入 inject 的功能。

// 父组件
setup() {
    // 为子组件提供一个值,并且这个值是响应式的(ref)
    // 则代表:子组件可以修改该值(book.value),并且会 响应式的 作用于父组件的变量中
    let book = ref('父组件的book');
    // 为子组件提供一个值,并且这个值不是响应式的
    // 则代表:子组件获取到了 非响应式的 数据。
    // 此时:子组件可以通过 ref(inject('book', '默认值')) 的方式,把该数据变为 响应式的 ,但是要注意,此时的响应式仅在 子组件中生效
    let book = '父组件的book';
    provide('book', book);
    return {
        book,
    };
},
    
// 子组件
setup () {
    // 利用 inject 函数,获取父组件提供的值
    let book = inject('book', '默认值');
    // 利用 inject 函数,获取父组件提供的值,同时把它变为响应式的。但是要注意,此时的响应式仅在 子组件中生效
    let book = ref(inject('book', '默认值'))
}

新的响应式

代理对象

当我们把 JavaScript 对象作为 data 选项传递给 Vue 实例的时候,Vue 会遍历对象的所有属性,并且会把它们转化成带有 gettersetter 函数的 Proxies(代理对象)

同时为了兼顾低版本的浏览器,对于较久的版本,Vue 依然使用 Object.defineProperty 来进行支持。

两者之间在使用中并没有区别,只不过 代理对象 会提供更好的性能。

响应式数据的声明 - reactive 函数

想要在 setup函数中 创建响应式数据,可以使用 reactive方法(注意:需要主动导入 reactive

<template>
  <div>
  	<h1>Vue 3</h1>
  	<button @click="state.count++">count is: {{ state.count }}</button>  
  </div>
</template>

<script>
import { reactive } from 'vue';
export default {
  name: 'HelloWorld',
  setup() {
     // 创建的响应式数据对象
    const state = reactive({
      count: 0,
    });
    return {
      // 直接放回创建的响应式对象
      state,
    };
  },
};
</script>

注意:Vue 3 中并没有取消 data 声明,也就是说,我们依然可以使用 data 函数来创建响应式数据。以下两种写法在响应式效果 中等效

// setup() {
  //   const state = reactive({
  //     count: 0,
  //   });
  //   return {
  //     state,
  //   };
  // },
  // 效果等效
  data() {
    return {
      state: {
        count: 0,
      },
    };
  },

将一个非响应式数据转化为响应式数据 - ref 函数

Vue 3 对 ref 函数进行了改变,使 ref 具备了两种能力:

  1. 使用 ref 获取 元素 或 组件实例
  2. 可以把一个非响应式的数据改变为响应式的

我们在这里探讨的就是 ref 函数的第二个能力:

<template>
  <h1>Vue 3</h1>
  <button @click="count++">count is: {{ count }}</button>
</template>

<script>
import { ref } from 'vue';
export default {
  setup() {
    // ----- 利用 setup 函数返回 ------
    let count = 0;
     return {
      count,
    };
    // ----- 利用 reactive 函数 ------
    const state = reactive({
      count: 0,
    });
    return {
      // count 值的改变,将不会影响视图的变化,原因请查看 《对象的响应式》  
      count: state.count,
    };
    // ----- 利用 ref 函数 ------
    let count = ref(0);
    return {
      count,
    };
  }
};
</script>

以上三种方式,貌似可以达到同样的效果。

那么 ref 的作用是什么呢?再来回顾一下:ref 函数可以将一个非响应式数据转化为响应式数据。即:当数据发生了变化的时候,视图也理应发生变化。我们来看下面的代码:

setup() {
    // ----- 利用 setup 函数返回 ------
    let count = 0;
    setTimeout(() => {
    // 视图并没有发生变化
      count = 1;
    }, 1000);
    return {
      count,
    };
    // ----- 利用 reactive 函数 ------
    et state = reactive({
      count: 0,
    });
    setTimeout(() => {
    // 视图并没有发生变化
       state.count = 1;
    }, 1000);
    return {
      count: state.count,
    };
    // ----- 利用 ref 函数 ------
    let count = ref(0);
    setTimeout(() => {
      // 访问 count 的值,需要使用 value 属性
      // 注意:在模板中,我们不需要手动写入 value
      // 视图发生了变化
      count.value = 1;
    }, 1000);
    return {
      count,
    };
  },

在上面的代码中,我们通过 setTimeout 延迟 1 秒钟之后改变数据源 count 的值,从结果中,我们可以发现,只有被 ref 函数声明的 count 才会直接影响视图的变化。

对象的响应式

通常情况下,我们希望利用 解构赋值 的方式来获取到响应式数据:

<template>
  <h1>Vue 3</h1>
  <button @click="count++">count is: {{ count }}</button>
</template>

<script>
import { reactive } from 'vue';
export default {
  setup() {
    // ----- 利用 reactive 函数 ------
    let state = reactive({
      count: 0,
    });
    let { count } = state;
    setInterval(() => {
      // 视图并没有发生变化
      count++;
    }, 1000);
    return {
      count,
    };
  },
};
</script>

但是很遗憾,这种方式将会让 count 失去响应性

如果我们想要解决这个问题,那么需要借助一个函数 toRefs。同样的,我们需要先 导入 这个函数。

// 不要忘记,我们需要导入 toRefs
import { toRefs } from 'vue';

let state = reactive({
    count: 0,
});
// 借助 toRefs 函数,让数据保持响应性
let { count } = toRefs(state);
setInterval(() => {
    // 同样需要是使用 value 来访问到数据源
    count.value++;
}, 1000);
return {
    count,
};

只读的响应式数据

有时我们希望 数据为响应式 的同时,该数据不被其他的组件所修改 (即:我们想要其他组件 享受 响应数据(refreactive )的变化,同时也想要防止其他的组件修改响应式数据),那么我们需要给其他组件 只读的响应式数据 - readonly

<!-- state.count 发生改变,readonlyState.count 会同步跟随变化 -->
<button @click="state.count++">count is: {{ state.count }}</button> 
<!-- 如果想要直接修改 readonlyState.count 的值,那么会得到一个警告:Set operation on key "count" failed: target is readonly -->
<button @click="readonlyState.count++">count is: {{ readonlyState.count }}</button>

<script>
import { reactive, readonly } from 'vue';
export default {
setup() {
      // 响应式数据对象 state
      const state = reactive({
        count: 0
      })
      // 只读数据 readonlyState
      // state 称为 readonlyState 源数据
      const readonlyState = readonly(state)
      return {
        state,
        readonlyState
      }
    }
</script>

这样我们就获得了一个只读的响应式数据 readonlyState 和它的源数据 state 。如果我们想要修改 readonlyState 的值,那么唯一的方式是 修改state

对于 readonly 来说,源数据的变化会响应式的作用于 readonly

计算属性与侦听器

计算属性

基本用法

Vue 计划使用 computed 函数 来定义 计算属性 (这并不代表 Options API 被完全丢弃),看下面的示例:

<template>
  <input type="text" v-model="str" />
  <h2>{{ comStr }}</h2>
</template>

<script>
import { ref, computed } from 'vue';
export default {
  setup() {
    const str = ref('Hello');
    // Vue 3 中推荐写法,完全等效于 computed 属性中函数定义
    const comStr = computed(() => str.value.split('').reverse().join(''));
    return {
      str,
      comStr,
    };
  },
  /*  Vue 2.x 的写法并没有被废弃,只是不再被推荐使用。
  以下写法完全等效于 Vue 3 中的 computed 函数 */
  computed: {
    comStr() {
      return this.str.split('').reverse().join('');
    },
  },
};
</script>

set 和 get

computed 函数 包含了 getset 方法:

const count = ref(1)
// 此代码等于使用了 getter 函数
const plusOne = computed(() => count.value++)
// 当我们获取 plusOne 的值时,等于调用了 get
console.log(plusOne.value) // 2
// 但是因为没有实现 setter 函数,所以无法设置新值
// err:setter is not a function
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

此时对 plusOneset 操作会回调到 传入对象的 set 方法中。

监听函数:watchEffect

如果你想要监听响应式数据的变化,那么可以使用 watchEffect 函数

watchEffect 函数 的运行时机有两个:

  1. 调用函数时,立刻运行
  2. 依赖项的数据发生变化时,会立刻运行
const count = ref(0);
// 立刻打印:0
watchEffect((v) => {
    // 依赖数据发生变化时,依次打印 1.2.3.4....
    console.log(count.value);
});

setInterval(() => {
    count.value++;
}, 1000);
return {
    count,
};

如果你想要停止 watcher 的话,那么可以返回方法进行停止

const stop = watchEffect(() => {
  /* ... */
})

// 调用返回的停止函数
stop()

watch

watch API完全等同于组件 watch 属性。

watch需要查看特定的数据源,并在单独的回调函数中应用副作用。

默认情况下,它也是惰性的。即仅在监视的源已更改时才调用回调。

参数:

  • {string | Function} source
  • {Function | Object} callback
  • {Object} [options]
    • {boolean} deep
    • {boolean} immediate

监听单一的数据源变化

watcher 的数据源可以有两种:

  1. 返回值的 getter 函数
  2. ref
// 监听 返回值的 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]) => {
  /* ... */
})

options 参数对象

options 参数对象主要有两个属性:

  1. deep:监听对象内部嵌套值的改变。注意:你不需要制定 deep 既可以监听到 Array 的变化
  2. immediate:立刻触发回调

teleport (传送)

在 Vue 的技术体系中,我们一直以来都是根元素中做事情(<div id="app" ></div>),这个逻辑是没有问题的。

但是在 Vue 3 中提出了一个新的概念 teleport

它是一个组件,使用它可以让我们的渲染区 脱离 根元素。这也是 传送 所代表的意思:将视图传送出渲染区(同时视图将保持响应式的渲染)

<body>
  <!-- 渲染区 -->
  <div id="app">
  </div>
  <!-- 
    转移区。
    注意:目标元素必须在组件挂在前就已经存在。
    即目标不能由组件本身渲染,并且理想情况下应位于整个Vue组件树之外。 
  -->
  <div id="endofbody"></div>
</body>

<script>
  const app = Vue.createApp({
    template: `
    <h1>根组件</h1>
    <parent-component />
  `
  })

  app.component('parent-component', {
    template: `
    <h2>父组件</h2>
    <teleport to="#endofbody">
      <child-component :name="name" />
    </teleport>
  `,
    data() {
      return {
        name: '张三'
      }
    },
    created: function () {
      setTimeout(() => {
        this.name = '李四'
      }, 1000)
    }
  })

  app.component('child-component', {
    props: ['name'],
    template: `
    <h4>被转移的子组件,它将出现在 app 之外</h4>
    <h4>同时享受响应式数据改变带来的视图变化,不信?你刷新页面,等一秒钟看一下</h4>
    <div>Hello, {{ name }}</div>
  `
  })
  app.mount('#app')
</script>

并且,你需要知道,传送是可以支持多个的。即:可以存在多个 teleport 组件指向同一个 目标区域

<teleport to="#modals">
  <div>A</div>
</teleport>
<teleport to="#modals">
  <div>B</div>
</teleport>

<!-- 结果-->
<div id="modals">
  <div>A</div>
  <div>B</div>
</div>

Fragment

组件不再强制单个根元素,这个特性 Vue 3 称为 Fragment(碎片)

以下代码可以正常运行(虽然会有 ESLint 警告)

<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>

自定义事件

Vue 3 建议对组件中所有发出的事件 emit 进行记录 emits 。如果你声明了 emits 选项,并且发送的事件没有被 emits 记录,则会收到一个警告:

app.component('custom-form', {
    template: `
      <form>
		<!-- 此处会收到警告:
    [Vue warn]: Component emitted event "submit" but it is neither declared in the emits option nor as an "onSubmit" prop 
-->
        <input type="button" value="submi" @click="$emit('submit')"/>
      </form>
    `,
    emits: {
    }
  })

记录的形式分为两种:

  1. 数组:如果使用数组进行记录,那么可以只可以进行声明,而不可以进行验证(参考 prop 机制)
  2. 对象:对象语法可以对 发送的事件 进行验证

以下为对象验证:

  app.component('custom-form', {
    template: `
      <form>
        <input type="button" value="submi" @click="$emit('submit', {email: 'aa@163.com', password: 'aaa'})"/>
      </form>
    `,
    emits: {
      // 无验证
      click: null,

      // 为 submit 事件添加验证规则
      // 如果不符合验证规则,你会收到一个警告
      submit: ({ email, password }) => {
        if (email && password) {
          console.log('验证通过');
          return true
        } else {
          console.warn('这是一个无效的提交事件!')
          return false
        }
      }
    }
  })

组件中的 v-model

Vue2.2.0 的版本中新增了 自定义组件的 v-model2.3.0 中新增了 .sync 修饰符。 在功能上 sync 更像是 自定义组件的 v-model 的增强。

Vue 3 中,自定义组件的 v-model 进行了进一步的增强,使其更像是 v-bind:foo.sync 的另一种写法:

// 子组件声明
app.component('my-component', {
  props: {
    foo: String
  },
  template: `
    <input 
      type="text"
      :value="foo"
      @input="$emit('update:foo', $event.target.value)">
  `
})

// 父组件调用
<my-component v-model:foo="bar"></my-component>

同时,自定义组件的 v-model 支持了 多重绑定

// 子组件声明
app.component('user-name', {
  props: {
    firstName: String,
    lastName: String
  },
  template: `
    <input 
      type="text"
      :value="firstName"
      @input="$emit('update:firstName', $event.target.value)">

    <input
      type="text"
      :value="lastName"
      @input="$emit('update:lastName', $event.target.value)">
  `
})

// 父组件调用
<user-name
  v-model:first-name="firstName"
  v-model:last-name="lastName"
></user-name>
打开App,阅读手记
6人推荐
发表评论
随时随地看视频慕课网APP