简述 - 备忘录
一些容易被忽略的、vue3简梳理。原理性解释不多,更多原理性解释参考官方文档、百度。
JavaScript 中一些流行的模块系统
一、CommonJS
let b = require("b.js") //导入例子
//使用 nodejs 内置的 global.module.exports 方法导出
module.exports = function() {} //导出例子1
const fn = function(){ //导出例子2
console.log("导出函数")
}
module.exports = {
fn:fn
}
当导入模块产生如下错误时
fn.js?84df:87 Uncaught TypeError: Cannot assign to read only property 'exports' of object '#<Object>'
原因是:The code above is ok. You can mix require and export. You can‘t mix import and module.exports.
翻译过来就是说,代码没毛病,在webpack打包的时候,可以在js文件中混用require和export.但是不能混用import 以及module.exports.
- 导入模块错误参考链接 github.com/vuejs/vue-c…
二、esModule
使用ES模块
- 使用该
type="module"
属性将常规 JavaScript 文件转换为 ES 模块文件
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ES Module - CodeSweetly</title>
</head>
<body>
<h1>ES Module Tutorial</h1>
<h2>Check the console</h2>
<--在入口index.html中可以看到这样一段代码
<script type="module" src="/src/main.ts"></script>-->
<script type="module" src="module-1.js"></script>
<script type="module" src="module-2.js"></script>
</body>
</html>
打开后,如果您检查浏览器的控制台,您将看到一些错误消息。
The browser threw a CORS policy error because ES modules only work through `http://` and `https://` URLs—not locally via a `file://` URL.
In other words, since our HTML file contains two ES modules, we need to load the document via an `http://` scheme.
The two typical ways to load an HTML document via an `http://` scheme are:
- By using a Local Server, or
- Through the use of a Module Bundler
- 导出例子
//默认导出
export default {
a: () => 1
}
export default "Your Club";
//导出
const bestClub = "Your Club";
function multiply(x, y) {
return x * y;
}
const fruits = ["Mango", "Apple", "Orange", "Lemon"];
export { bestClub, multiply, fruits };
export命令要处于模块的顶级作用域的任何位置,不能放在块级作用域内.
以下几种写法都会报错:
1. let s = '';
export s;
2. export '';
3. function s(){
export let l = '';
return bestClub;
}
- 导入例子
//使用`default as`语法导入默认
import { default as newName } from "./module-relative-path.js";
//导入默认导出的另一种方法是忽略大括号 ( `{...}`)、`default`关键字和`as`关键字。
import newName from "./module-relative-path.js";
//导入
import { bestClub } from "./module-1.js";
console.log(bestClub);
- 导入导出
as重命名(是否重命名完全取决于自己)
1.//导出将bestClub重命名为favoriteTeam
const bestClub = "Your Club";
export { bestClub as favoriteTeam };
2.
// module-1.js
const bestClub = "Your Club";
export { bestClub };
// module-2.js
//导入时将bestClub重命名为favoriteTeam
import { bestClub as favoriteTeam } from "./module-1.js";
const myBestClub = favoriteTeam + " " + "is my best club.";
console.log(myBestClub);
- 利用
,分割重命名多个
export { bestClub as favoriteTeam, fruits as crops, multiply as product };
或者
import { bestClub as favoriteTeam, fruits as crops, multiply as product } form "./module.js";
- 为什么要重命名?
重命名有助于防止浏览器因名称冲突而引发错误.例如,考虑这些片段:
// module-2.js
import { bestClub } from "./module-1.js";
const bestClub = bestClub + " " + "is my best club.";
console.log(bestClub);
浏览器抛出错误,因为导入代码的名称与`module-2.js` `bestClub`变量冲突。
- 一次性导入 import * as from "./countries.js
//导入`./countries.js`模块的所有可导出内容并将导入封装在名为`allCountries`.
import * as allCountries from "./countries.js";
笔记
import/export
不能嵌套在任何块级作用域或函数作用域内,必须写在模块顶层(因为 import 会先于其他任何代码执行)--在常规 JavaScript 程序内无效- JavaScript提升
export
语句.因此,您可以在模块中的任何位置定义它们- export default 在同一个模块中只能出现一次,export可以出现多次(export和export default 可同时使用)
- export default const v = 'hello world' 会报错,export default后不能跟let const 等
三、UMD(通用模块)
UMD示例
(function (root, factory) {
if (typeof module === "object" && typeof module.exports === "object") {
console.log("是commonjs模块规范,nodejs环境");
var depModule = require("./umd-module-depended");
module.exports = factory(depModule);
} else if (typeof define === "function" && define.amd) {
console.log("是AMD模块规范,如require.js");
define(["depModule"], factory);
} else if (typeof define === "function" && define.cmd) {
console.log("是CMD模块规范,如sea.js");
define(function (require, exports, module) {
var depModule = require("depModule");
module.exports = factory(depModule);
});
} else {
console.log("没有模块环境,直接挂载在全局对象上");
root.umdModule = factory(root.depModule);
}
})(this, function (depModule) {
console.log("我调用了依赖模块", depModule); // ...省略了一些代码,去代码仓库看吧 return { name: '我自己是一个umd模块' }
});
- UMD示例参考链接 t.zoukankan.com/wenbinjiang…
总结
require() vs import()
- require是运行时调用,所以
require
理论上可以运用在代码的任何地方import
是编译时调用,所以必须放在文件开头 本质:
- require 是赋值过程。module.exports后面的内容是什么,require的结果就是什么,比如对象、数字、字符串、函数等,然后再把require的结果赋值给某个变量,它相当于module.exports的传送门
- import 是解构过程,但是目前所有的引擎都还没有实现import,我们在node中使用babel支持ES6,也仅仅是将ES6转码为ES5再执行,import语法会被转码为require import 虽然是 es6 中的语法,但就目前来说,所有的引擎都还没有实现import。 我们在 node 中使用 babel 支持ES6(在 node 当中,比如 node.js 代码,也不能直接使用 import 来导入,必须使用 babel 支持才能使用 import 语法),实际上也是将 ES6 转码为 ES5 再执行,import 语法实际上会被转码为 require。这也是为什么在模块导出时使 module.exports,在引入模块时使用 import 仍然起效,因为本质上,import 会被转码为 require 去执行。
import x from './x'
和import (./x)
的差异
- import x from './x' export导出之后 才能用import导入。
- import (./x) 直接引入 和script标签是一样的. 只作用在自己的js文件中, 没有挂载在window中,在其它js文件中获取不到。
ECMAScript ___ import.meta
import.meta
是一个给 JavaScript 模块暴露特定上下文的元数据属性的对象,它包含了这个模块的信息。
import.meta
对象是由 ECMAScript 实现的,它带有一个null
的原型对象。这个对象可以扩展,并且它的属性都是可写,可配置和可枚举的。
<script type="module">
console.log(import.meta) // {url: 'http://127.0.0.1:5500/dist/index.html?a=1'}
</script>
- 参考MDN developer.mozilla.org/zh-CN/docs/…
- 应用场景参考文章#巧用 import.meta 实现热更新blog.csdn.net/ligang25851…
vite搭建项目
兼容性注意Vite 需要 Node.js 版本 >= 12.0.0
一、浏览器兼容性处理插件
npm i -D @vitejs/plugin-legacy
[import.meta.url]是一个 ESM 的原生功能
注意:无法在 SSR 中使用
如果你正在以服务端渲染模式使用 Vite 则此模式不支持,因为 `import.meta.url` 在浏览器和 Node.js 中有不同的语义.服务端的产物也无法预先确定客户端主机 URL.
- Vite 的默认浏览器支持基线是Native ESM。此插件为在构建生产时不支持本机 ESM 的旧版浏览器提供支持.
- @vitejs/plugin-legacy
二、生成vite项目或模板创建
npm init vite@latest
yarn create vite
pnpm create vite
vite模板方式创建
npm 6.x
npm init vite@latest my-vue-app --template vue
npm 7+, 需要额外的双横线
npm init vite@latest my-vue-app -- --template vue
yarn
yarn create vite my-vue-app --template vue
pnpm
pnpm create vite my-vue-app -- --template vue
查看 create-vite 以获取每个模板的更多细节:
vanilla
,vanilla-ts
,vue
,vue-ts
,react
,react-ts
,preact
,preact-ts
,lit
,lit-ts
,svelte
,svelte-ts
.
三、vite常用插件
Vite 为 Vue 提供第一优先级支持:
在 TS 中,直接从 `pages-generated` 导入会引起类型错误,需要在 `tsconfig.json` 的 `compilerOptions.types` 数组中加入 `vite-plugin-pages/client` 来加载对应的声明文件.
四、vite中不能使用require
原因:因为vite使用的是浏览器自带的module去解析js的,而require语法是node语法,会产生报错
解决 使用
1import引入
2绝对路径引入
3把需要require引入的图片放到public文件夹下,这样打包前后路径都不会被处理,可以保证路径的正确性
typescript使用
参考链接 zhuanlan.zhihu.com/p/136254808
vue3简梳理
创建一个cli项目模板
vue create h
拉取 2.x 模板 (旧版本)#
Vue CLI >= 3 和旧版使用了相同的
vue
命令,所以 Vue CLI 2 (vue-cli
) 被覆盖了。如果你仍然需要使用旧版本的vue init
功能,你可以全局安装一个桥接工具:
npm install -g @vue/cli-init
# `vue init` 的运行效果将会跟 `vue-cli@2.x` 相同
vue init webpack my-project
- 总结vue create和vue init 的区别
vue create 是vue-cli3.x的初始化方式,目前模板是固定的,模板选项可自由配置,创建出来的是vue-cli3的项目,与cue-cli2项目结构不同,配置方法不同,具体配置方法参考官方文档网页链接。 - vue init 是vue-cli2.x的初始化方式,可以使用github上面的一些模板来初始化项目。
- webpack是官方推荐的标准模板名。
- vue-cli2.x项目向专3.x迁移只需要把static目录复制到public目录下,老项目的src目录覆盖3.x的src目录(如果修改了配置,可以查看文档,用cli3的方法进行属配置)。
@vue/cli-ui 是 vue-cli 内置的一套成熟的 UI。
vue ui
Vue-cli 的 3.x 版本由内到外完全重写,带来了许多很棒的新特性。你可以在你的项目中选用路由、Vuex 和 Typescript 等等特性,并为项目添加“vue-cli 插件”。不过,这么多的选项也意味着它更加复杂,难以上手。因此我们认为,相比局限的命令行界面,一个成熟的 GUI 更能帮助你发掘这些新特性,搜索和安装 vue-cli 插件,解锁更多可能。总的来说,vue-cli 不仅能让你轻松启动新项目,并且在后续的工作中仍会是你的得力助手。
vue2与vue3相比
#vue2迁移介绍
1.vue2迁移3介绍,部分功能不兼容vue2(详情)
2.vue3兼容vue2的兼容插件介绍(兼容详情)
@vue/compat
(即“迁移构建版本”) 是一个 Vue 3 的构建版本,提供了可配置的兼容 Vue 2 的行为。- 对 IE11 的支持:Vue 3 已经官方放弃对 IE11 的支持。如果仍然需要支持 IE11 或更低版本,那你仍需继续使用 Vue 2。
1. 实例化变化
//vue3创建实例
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
//vue2.x创建实例
import Vue from 'vue'
import App from './App'
import router from './router'
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
2. 路由变化
// vue2.x的安装方式
npm install vue-router --save
// vue2.x 创建路由
import Vue from 'vue'
import Router from 'vue-router'
import list from '@/components/list'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'list',
component: list
}
]
})
// vue3的安装方式:
npm install vue-router@next --save
// vue3 创建路由
import {createRouter,createWebHashHistory} from 'vue-router'
import list from '../components/list.vue'
const router = createRouter({
//hash模式:createWebHashHistory,history模式:createWebHistory
history:createWebHashHistory(),
routes:[
{
path: '/',
name: 'list',
component: list
}
]
})
export default router
vue2.x的写法是mode:history 或 hash.
vue3的写法是history:createWebHistory() 或 createWebHashHistory().
3. #片段 template根节点变化 [新增]
Vue 3 现在正式支持了多根节点的组件,也就是片段!
//在 2.x 中,由于不支持多根节点组件,当其被开发者意外地创建时会发出警告。结果是,为了修复这个问题,许多组件被包裹在了一个 `<div>` 中。
<template>
<div>
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
</template>
//在 3.x 中,组件可以包含多个根节点!但是,这要求开发者显式定义 attribute 应该分布在哪里。
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
4.生命周期变化
2.x生命周期
beforeCreate
在实例初始化之后,进行数据侦听和事件/侦听器的配置之前同步调用。
created
在实例创建完成后被立即同步调用。在这一步中,实例已完成对选项的处理,意味着以下内容已被配置完毕:数据侦听、计算属性、方法、事件/侦听器的回调函数。然而,挂载阶段还没开始,且
$el
property 目前尚不可用。beforeMount
在挂载开始之前被调用:相关的
render
函数首次被调用。mounted
实例被挂载后调用,这时
el
被新创建的vm.$el
替换了。如果根实例挂载到了一个文档内的元素上,当mounted
被调用时vm.$el
也在文档内。
注意mounted
不会保证所有的子组件也都被挂载完成。如果你希望等到整个视图都渲染完毕再执行某些操作,可以在mounted
内部使用 vm.$nextTickbeforeUpdate
在数据发生改变后,DOM 被更新之前被调用。这里适合在现有 DOM 将要被更新之前访问它,比如移除手动添加的事件监听器。
updated
在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用。
当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用计算属性或 watcher 取而代之。
注意,updated
不会保证所有的子组件也都被重新渲染完毕。如果你希望等到整个视图都渲染完毕,可以在updated
里使用 vm.$nextTick:activated
被 keep-alive 缓存的组件激活时调用。
deactivated
被 keep-alive 缓存的组件失活时调用。
beforeDestroy
实例销毁之前调用。在这一步,实例仍然完全可用。
destroyed
实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。
errorCaptured 2.5.0+ 新增
在捕获一个来自后代组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回
false
以阻止该错误继续向上传播。
3.x生命周期
beforeCreate
在实例初始化之后、进行数据侦听和事件/侦听器的配置之前同步调用。
created
在实例创建完成后被立即同步调用。在这一步中,实例已完成对选项的处理,意味着以下内容已被配置完毕:数据侦听、计算属性、方法、事件/侦听器的回调函数。然而,挂载阶段还没开始,且
$el
property 目前尚不可用。beforeMount
在挂载开始之前被调用:相关的
render
函数首次被调用。mounted
在实例挂载完成后被调用,这时候传递给
app.mount
的元素已经被新创建的vm.$el
替换了。如果根实例被挂载到了一个文档内的元素上,当mounted
被调用时,vm.$el
也会在文档内。 注意mounted
不会保证所有的子组件也都被挂载完成。如果你希望等待整个视图都渲染完毕,可以在mounted
内部使用 vm.$nextTickbeforeUpdate
在数据发生改变后,DOM 被更新之前被调用。这里适合在现有 DOM 将要被更新之前访问它,比如移除手动添加的事件监听器。
updated
在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用。 当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用计算属性或侦听器取而代之。
注意,updated
不会保证所有的子组件也都被重新渲染完毕。如果你希望等待整个视图都渲染完毕,可以在updated
内部使用 vm.$nextTick:activated
被 keep-alive 缓存的组件激活时调用。
deactivated
被 keep-alive 缓存的组件失活时调用。
beforeUnmount
在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。
unmounted
卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载。
errorCaptured
在捕获一个来自后代组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回
false
以阻止该错误继续向上传播。renderTracked
跟踪虚拟 DOM 重新渲染时调用。钩子接收
debugger event
作为参数。此事件告诉你哪个操作跟踪了组件以及该操作的目标对象和键。renderTriggered
当虚拟 DOM 重新渲染被触发时调用。和
renderTracked
类似,接收debugger event
作为参数。此事件告诉你是什么操作触发了重新渲染,以及该操作的目标对象和键。
小结
在vue3中将beforeDestroy destroyed 变更为 beforeUnmount unmounted新增renderTracked renderTriggered
5.全局(global)配置变化
1.Vue.prototype
替换为 config.globalProperties
// 之前 - Vue 2
Vue.prototype.$http = () => {}
// 之后 - Vue 3
const app = createApp({})
app.config.globalProperties.$http = () => {}
2.config.ignoredElements
替换为 config.isCustomElement
// 之前 - Vue2
Vue.config.ignoredElements = ['my-el', /^ion-/]
// 之后 - Vue3
const app = createApp({})
app.config.compilerOptions.isCustomElement = tag => tag.startsWith('ion-')
3. config.productionTip
移除
// 之前 - Vue2
Vue.config.productionTip=false
//之后 - Vue3
vue3中已被移除
4.Vue.prototype
替换为 config.globalProperties
- 在 Vue2中,
Vue.prototype
通常用于添加所有组件都能访问的 property。 - 在Vue3中与之对应的是
config.globalProperties
。这些 property 将被复制到应用中,作为实例化组件的一部分。
// 之前 - Vue 2
Vue.prototype.$http = () => {}
// 之后 - Vue 3
const app = createApp({})
app.config.globalProperties.$http = () => {}
5.Vue.extend
移除
// 之前 - Vue 2
// 创建构造器
const Profile = Vue.extend({
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
data() {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
})
// 创建一个 Profile 的实例,并将它挂载到一个元素上
new Profile().$mount('#mount-point')
// 之后 - Vue 3
const Profile = {
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
data() {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
}
Vue.createApp(Profile).mount('#mount-point')
6.Provide / Inject
//与在 2.x 根实例中使用 `provide` 选项类似,Vue 3 应用实例也提供了可被应用内任意组件注入的依赖项:
// 在入口中
app.provide('guide', 'Vue 3 Guide')
// 在子组件中
export default {
inject: {
book: {
from: 'guide'
}
},
template: `<div>{{ book }}</div>`
}
在编写插件时使用 `provide` 将尤其有用,可以替代 `globalProperties`
7.在应用之间共享配置
//在应用之间共享配置 (如组件或指令) 的一种方法是创建工厂函数,如下所示:
import { createApp } from 'vue'
import Foo from './Foo.vue'
import Bar from './Bar.vue'
const createMyApp = options => {
const app = createApp(options)
app.directive('focus' /* ... */)
return app
}
createMyApp(Foo).mount('#foo')
createMyApp(Bar).mount('#bar')
现在,`Foo` 和 `Bar` 实例及其后代中都可以使用 `focus` 指令。
6.细节变化
1.v-for 中的 Ref 数组 [非兼容]
在Vue2中 在v-for中使用ref会将数据插入至$refs中去
//示例,详见官网
<div v-for="item in list" :key="item" :ref="item"> {{ item }} </div>
mounted() {
console.log(this.$refs)
}
在vue3中做了优化,改为在v-for中使用ref不会将数据插入至$refs中,而是改为绑定到函数中做处理。
//示例,详见官网
<div v-for="item in list" :key="item" :ref="setItemRef"> {{ item }} </div>
data(){
itemRefs:[]
},
methods: {
setItemRef(el) {
this.itemRefs.push(el);
},
print() {
console.log(this.itemRefs);
}
}
注意:
`itemRefs` 不必是数组:它也可以是一个对象,其 ref 可以通过迭代的 key 被设置。
如有需要,`itemRefs` 也可以是响应式的,且可以被侦听。
2.异步组件 [新增]
//之前 - Vue 2
以前,异步组件是通过将组件定义为返回 Promise 的函数来创建的,例如:
const asyncModal = () => import('./Modal.vue')
或者,对于带有选项的更高阶的组件语法:
const asyncModal = {
component: () => import('./Modal.vue'),
delay: 200,
timeout: 3000,
error: ErrorComponent,
loading: LoadingComponent
}
//之后 - Vue 3
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
})
- 对 2.x 所做的另一个更改是,
component
选项现在被重命名为 loader,以明确组件定义不能直接被提供。 - 此外,与 2.x 不同,loader 函数不再接收
resolve
和reject
参数,且必须始终返回 Promise。
3.attribute 强制行为 [非兼容]
这是一个底层的内部 API 更改,绝大多数开发人员不会受到影响。
- 移除枚举 attribute 的内部概念,并将这些 attribute 视为普通的非布尔 attribute
- 非兼容:如果值为布尔值 `false`,则不再移除 attribute。取而代之的是,它将被设置为 attr="false"。若要移除 attribute,应该使用 `null` 或者 `undefined`。
4.inheritAttrs与$attrs
包含class
&style
[非兼容]
inheritAttrs默认为true
定义一个自定义组件component,给它传入value、type,在props中没有接收它:
<component :value="'value'" type='type'></component>
它会作为这个节点的原生attribute,在浏览器dom结构中显示如下:
<div value="value" type="type"></div>
inheritAttrs为false时可以防止将这个外部传来的值作为原生attribute。在浏览器dom结构中显示如下:
<div></div> //可以发现不会再作为原生属性被使用了
注意
如果未定义的属性比较特殊,可能会对组件产生影响,比如定义了未接收的type属性,input框的类别可能会发生改变。
之前 - Vue 2
在vue2中使用$attrs 不会继承使用组件时设置的class、style。可以参考//之后 - Vue3
之后 - Vue 3
$attrs && inheritAttrs
在vue3中 组件中使用$attrs
<template>
<div class="hello">
//继承个别属性
<h1 :style="$attrs.style">{{ msg }}</h1>
//继承所有属性
<h1 :style="$attrs">{{ msg }}</h1>
</div>
</template>
注意
<hello id="my-id" class="my-class"></hello>
//组件hello
<div class="hello">
//继承所有属性 //:="$attrs" || v-bind="$attrs"
<h1 :="$attrs">{{ msg }}</h1>
</div>
使用#inheritAttrs`为false时组件在浏览器dom中的表现为
<div> //根节点不会有id、class。
<h1 id="my-id" class="my-class"></h1>
</div>
使用#inheritAttrs`为true时组件在浏览器dom中的表现为
<div id="my-id" class="my-class">//根节点会有id、class.
<h1 id="my-id" class="my-class"></h1>
</div>
小结
继承可能并不准确 没想到好的形容词。
vue2中
$attrs
继承调用组件时的所有属性不会包含class style
$attrs.xxx
不支持直接在标签中查找 但是在函数中 生命周期钩子中可以查找
vue3中
$attrs
继承调用组件时的所有属性会包含class style支持直接在标签中
$attrs.xxx
形式查找
inheritAttrs默认值为true 调用组件时的所有属性都会在组件根节点中渲染。 为false时 调用组件时的所有属性都不会在组件根节点中渲染。
5.$children [移除]
2.x 语法
在 2.x 中,开发者可以使用 `this.$children` 访问当前实例的直接子组件:
mounted() {
console.log(this.$children) // [VueComponent]
}
3.x 更新
在 3.x 中,
$children
property 已被移除,且不再支持。如果你需要访问子组件实例,我们建议使用 $refs。
6.自定义指令 [非兼容]
7.#与自定义元素的互操作性 [非兼容]
8.Data 选项 [非兼容]
非兼容:组件选项
data
的声明不再接收纯 JavaScriptobject
,而是接收一个function
。
非兼容:当合并来自 mixin 或 extend 的多个data
返回值时,合并操作现在是浅层次的而非深层次的 (只合并根级属性)。
2.x 语法
在 2.x 中,开发者可以通过 object
或者是 function
定义 data
选项。
//对象声明
data: {
apiKey: 'a1b2c3'
}
//函数声明
data() {
return {
apiKey: 'a1b2c3'
}
}
3.x 更新
在 3.x 中,data
选项已标准化为只接受返回 object
的 function
。
使用上面的示例,代码只可能有一种实现:
<script>
import { createApp } from 'vue'
createApp({
data() {
return {
apiKey: 'a1b2c3'
}
}
}).mount('#app')
</script>
Mixin 合并行为变更
此外,当来自组件的 data()
及其 mixin 或 extends 基类被合并时,合并操作现在将被浅层次地执行:
const Mixin = {
data() {
return {
user: {
name: 'Jack',
id: 1
}
}
}
}
const CompA = {
mixins: [Mixin],
data() {
return {
user: {
id: 2
}
}
}
}
在 Vue 2.x 中,生成的 $data
是:
{
"user": {
"id": 2,
"name": "Jack"
}
}
在 3.0 中,其结果将会是:
{
"user": {
"id": 2
}
}
9.emits
选项 [新增]
Vue 3 现在提供一个
emits
选项,和现有的props
选项类似。这个选项可以用来定义一个组件可以向其父组件触发的事件。
2.x 的行为
在 Vue 2 中,你可以定义一个组件可接收的 prop,但是你无法声明它可以触发哪些事件:
<template>
<div>
<p>{{ text }}</p>
<button v-on:click="$emit('accepted')">OK</button>
</div>
</template>
<script>
export default {
props: ['text']
}
</script>
- 3.x 的行为
和 prop 类似,现在可以通过 emits
选项来定义组件可触发的事件:
<template>
<div>
<p>{{ text }}</p>
<button v-on:click="$emit('accepted')">OK</button>
</div>
</template>
<script>
export default {
props: ['text'],
emits: ['accepted']
}
</script>
该选项也可以接收一个对象,该对象允许开发者定义传入事件参数的验证器,和
props
定义里的验证器类似。
强烈建议使用
emits
记录每个组件所触发的所有事件。
这尤为重要,因为我们移除了
.native
修饰符。任何未在emits
中声明的事件监听器都会被算入组件的$attrs
,并将默认绑定到组件的根节点上。
10.事件 API [非兼容]
$on
,$off
和$once
实例方法已被移除,组件实例不再实现事件触发接口。
11.过滤器 [移除]
在 2.x 中,开发者可以使用过滤器来处理通用文本格式。
<template>
<p>{{ accountBalance | currencyUSD }}</p>
</template>
<script>
export default {
props: {
accountBalance: {
type: Number,
required: true
}
},
filters: {
currencyUSD(value) {
return '$' + value
}
}
}
</script>
在 3.x 中,过滤器已移除,且不再支持。取而代之的是,我们建议用方法调用或计算属性来替换它们。
12.函数式组件 [非兼容]
13. #全局 API Treeshaking [非兼容]
2.x 语法
如果你曾经在 Vue 中手动操作过 DOM,你可能会用过这种方式:
import Vue from 'vue'
Vue.nextTick(() => {
// 一些和 DOM 有关的东西
})
3.x 语法
在 Vue 3 中,全局和内部 API 都经过了重构,并考虑到了 tree-shaking 的支持。因此,对于 ES 模块构建版本来说,全局 API 现在通过具名导出进行访问。例如,我们之前的代码片段现在应该如下所示:
在 Vue 3 中,必须显式导入:
import { nextTick } from 'vue'
nextTick(() => {
// 一些和 DOM 有关的东西
})
//直接调用 `Vue.nextTick()` 将导致臭名昭著的 `undefined is not a function` 错误。
14.内联模板 Attribute [非兼容]
2.x 语法
在 2.x 中,Vue 为子组件提供了 inline-template
attribute,以便将其内部内容作为模板使用,而不是作为分发内容。
<my-component inline-template>
<div>
<p>它们将被编译为组件自己的模板,</p>
<p>而不是父级所包含的内容。</p>
</div>
</my-component>
3.x 语法
将不再支持此功能。
15.key
Attribute [非兼容]
-
新增:对于
v-if
/v-else
/v-else-if
的各分支项key
将不再是必须的,因为现在 Vue 会自动生成唯一的key
。- 非兼容:如果你手动提供
key
,那么每个分支必须使用唯一的key
。你将不再能通过故意使用相同的key
来强制重用分支。
- 非兼容:如果你手动提供
-
非兼容:
<template v-for>
的key
应该设置在<template>
标签上 (而不是设置在它的子节点上)。
非兼容变更体现在如果你手动提供了
key
,那么每个分支都必须使用一个唯一的key
。因此大多数情况下都不需要设置这些key
。
<!-- Vue 2.x -->
<div v-if="condition" key="a">Yes</div>
<div v-else key="a">No</div>
<!-- Vue 3.x (推荐方案:移除 key) -->
<div v-if="condition">Yes</div>
<div v-else>No</div>
<!-- Vue 3.x (替代方案:确保 key 始终是唯一的) -->
<div v-if="condition" key="a">Yes</div>
<div v-else key="b">No</div>
类似地,当使用
<template v-for>
时如果存在使用v-if
的子节点,则key
应改为设置在<template>
标签上。
<!-- Vue 2.x -->
//在 Vue 2.x 中,`<template>` 标签不能拥有 `key`。不过,你可以为其每个子节点分别设置 `key`。
<template v-for="item in list">
<div v-if="item.isVisible" :key="item.id">...</div>
<span v-else :key="item.id">...</span>
</template>
<!-- Vue 3.x -->
//在 Vue 3.x 中,`key` 则应该被设置在 `<template>` 标签上。
<template v-for="item in list" :key="item.id">
<div v-if="item.isVisible">...</div>
<span v-else>...</span>
</template>
16.按键修饰符 [非兼容]
- 非兼容:不再支持使用数字 (即键码) 作为
v-on
修饰符 - 非兼容:不再支持
config.keyCodes
2.x 语法
在 Vue 2 中,keyCodes
可以作为修改 v-on
方法的一种方式。
<!-- 键码版本 -->
<input v-on:keyup.13="submit" />
<!-- 别名版本 -->
<input v-on:keyup.enter="submit" />
此外,也可以通过全局的 config.keyCodes
选项定义自己的别名。
Vue.config.keyCodes = {
f1: 112
}
<!-- 键码版本 -->
<input v-on:keyup.112="showHelpText" />
<!-- 自定义别名版本 -->
<input v-on:keyup.f1="showHelpText" />
3.x 语法
从 KeyboardEvent.keyCode
已被废弃 (键码值链接)开始,Vue 3 继续支持这一点就不再有意义了。因此,现在建议对任何要用作修饰符的键使用 kebab-cased (短横线) 名称。如(PageDown---page-down)。
<!-- Vue 3 在 v-on 上使用按键修饰符 -->
<input v-on:keyup.page-down="nextPage">
<!-- 同时匹配 q 和 Q -->
<input v-on:keypress.q="quit">
因此,这意味着 config.keyCodes
现在也已弃用,不再受支持。
对于某些标点符号键,可以直接把它们包含进去,以 ,
键为例:
<input v-on:keypress.,="commaPress">
语法的限制导致某些特定字符无法被匹配,比如 "
、'
、/
、=
、>
和 .
。对于这些字符,你应该在监听器内使用 event.key
代替。
17.移除$listeners
[非兼容]
2.x 语法
在 Vue 2 中,你可以通过 this.$attrs
访问传递给组件的 attribute,以及通过 this.$listeners
访问传递给组件的事件监听器。结合 inheritAttrs: false
,开发者可以将这些 attribute 和监听器应用到根元素之外的其它元素:
<template>
<label>
<input type="text" v-bind="$attrs" v-on="$listeners" />
</label>
</template>
<script>
export default {
inheritAttrs: false
}
</script>
3.x 语法
在 Vue 3 的虚拟 DOM 中,事件监听器现在只是以 on
为前缀的 attribute,这样它就成为了 $attrs
对象的一部分,因此 $listeners
被移除了。
<template>
<label>
<input type="text" v-bind="$attrs" />
</label>
</template>
<script>
export default {
inheritAttrs: false
}
</script>
如果这个组件接收一个 id
attribute 和一个 v-on:close
监听器,那么 $attrs
对象现在将如下所示:
{
id: 'my-input',
onClose: () => console.log('close 事件被触发')
}
18.被挂载的应用不会替换元素 [非兼容]
2.x 语法
在 Vue 2.x 中,我们为 new Vue()
或 $mount
传入一个 HTML 元素选择器:
new Vue({
el: '#app',
data() {
return {
message: 'Hello Vue!'
}
},
template: `
<div id="rendered">{{ message }}</div>
`
})
// 或
const app = new Vue({
data() {
return {
message: 'Hello Vue!'
}
},
template: `
<div id="rendered">{{ message }}</div>
`
})
app.$mount('#app')
当我们把应用挂载到拥有匹配被传入选择器 (在这个例子中是 id="app"
) 的 div
的页面时:
<body>
<div id="app">
Some app content
</div>
</body>
在渲染结果中,上面提及的 div
将会被应用所渲染的内容替换:
<body>
<div id="rendered">Hello Vue!</div>
</body>
3.x 语法
在 Vue 3.x 中,当我们挂载一个应用时,其渲染内容会替换我们传递给 mount
的元素的 innerHTML
:
const app = Vue.createApp({
data() {
return {
message: 'Hello Vue!'
}
},
template: `
<div id="rendered">{{ message }}</div>
`
})
app.mount('#app')
当这个应用挂载到拥有匹配 id="app"
的 div
的页面时,结果会是:
<body>
<div id="app" data-v-app="">
<div id="rendered">Hello Vue!</div>
</div>
</body>
19.propsData
[移除]
2.x 语法
在 2.x 中,我们可以在创建 Vue 实例的时候传入 prop:
const Comp = Vue.extend({
props: ['username'],
template: '<div>{{ username }}</div>'
})
new Comp({
propsData: {
username: 'Evan'
}
})
3.x 更新
propsData
选项已经被移除。如果你需要在实例创建时向根组件传入 prop,你应该使用 createApp
的第二个参数:
const app = createApp(
{
props: ['username'],
template: '<div>{{ username }}</div>'
},
{ username: 'Evan' }
)
20.在 prop 的默认函数中访问this
[非兼容]
生成 prop 默认值的工厂函数不再能访问 this
。
取而代之的是:
- 组件接收到的原始 prop 将作为参数传递给默认函数;
- inject API 可以在默认函数中使用。
import { inject } from 'vue'
export default {
props: {
theme: {
default (props) {
// `props` 是传递给组件的、
// 在任何类型/默认强制转换之前的原始值,
// 也可以使用 `inject` 来访问注入的 property
return inject('theme', 'default-theme')
}
}
}
}
21.渲染函数 API [非兼容]
此更改不会影响 <template>
用户。
以下是更改的简要总结:
h
现在是全局导入,而不是作为参数传递给渲染函数- 更改渲染函数参数,使其在有状态组件和函数组件的表现更加一致
- VNode 现在有一个扁平的 prop 结构
1.渲染函数参数
在 2.x 中,render
函数会自动接收 h
函数 (它是 createElement
的惯用别名) 作为参数:
// Vue 2 渲染函数示例
export default {
render(h) {
return h('div')
}
}
在 3.x 中,h
函数现在是全局导入的,而不是作为参数自动传递。
// Vue 3 渲染函数示例
import { h } from 'vue'
export default {
render() {
return h('div')
}
}
2.渲染函数签名更改
在 2.x 中,render
函数自动接收参数,如 h
函数。
// Vue 2 渲染函数示例
export default {
render(h) {
return h('div')
}
}
在 3.x 中,由于 render
函数不再接收任何参数,它将主要在 setup()
函数内部使用。这还有一个好处:可以访问在作用域中声明的响应式状态和函数,以及传递给 setup()
的参数。
import { h, reactive } from 'vue'
export default {
setup(props, { slots, attrs, emit }) {
const state = reactive({
count: 0
})
function increment() {
state.count++
}
// 返回渲染函数
return () =>
h(
'div',
{
onClick: increment
},
state.count
)
}
}
有关 setup()
如何工作的详细信息,请参考组合式 API 指南
3.VNode Prop 格式化
在 2.x 中,domProps
包含 VNode prop 中的嵌套列表:
// 2.x
{
staticClass: 'button',
class: { 'is-outlined': isOutlined },
staticStyle: { color: '#34495E' },
style: { backgroundColor: buttonColor },
attrs: { id: 'submit' },
domProps: { innerHTML: '' },
on: { click: submitForm },
key: 'submit-button'
}
在 3.x 中,整个 VNode prop 的结构都是扁平的。使用上面的例子,来看看它现在的样子。
// 3.x 语法
{
class: ['button', { 'is-outlined': isOutlined }],
style: [{ color: '#34495E' }, { backgroundColor: buttonColor }],
id: 'submit',
innerHTML: '',
onClick: submitForm,
key: 'submit-button'
}
4.#注册组件
在 2.x 中,注册一个组件后,把组件名作为字符串传递给渲染函数的第一个参数,它可以正常地工作:
// 2.x
Vue.component('button-counter', {
data() {
return {
count: 0
}
},
template: `
<button @click="count++">
Clicked {{ count }} times.
</button>
`
})
export default {
render(h) {
return h('button-counter')
}
}
在 3.x 中,由于 VNode 是上下文无关的,不能再用字符串 ID 隐式查找已注册组件。取而代之的是,需要使用一个导入的 resolveComponent
方法:
// 3.x
import { h, resolveComponent } from 'vue'
export default {
setup() {
const ButtonCounter = resolveComponent('button-counter')
return () => h(ButtonCounter)
}
}
更多信息请参考渲染函数 API 更改 RFC
22.#插槽统一 [非兼容]
-
this.$slots
现在将插槽作为函数公开 -
非兼容:移除
this.$scopedSlots
-
2.x 语法
当使用渲染函数,即 h
时,2.x 曾经在内容节点上定义 slot
数据 property。
// 2.x 语法
h(LayoutComponent, [
h('div', { slot: 'header' }, this.header),
h('div', { slot: 'content' }, this.content)
])
此外,可以使用以下语法引用作用域插槽:
// 2.x 语法
this.$scopedSlots.header
在 3.x 中,插槽以对象的形式定义为当前节点的子节点:
// 3.x Syntax
h(LayoutComponent, {}, {
header: () => h('div', this.header),
content: () => h('div', this.content)
})
当你需要以编程方式引用作用域插槽时,它们现在被统一到 $slots
选项中了。
// 2.x 语法
this.$scopedSlots.header
// 3.x 语法
this.$slots.header()
23.Suspense [新增][试验性] api可能会发生改动
24.过渡的 class 名更改 [非兼容]
- 过渡类名
v-enter
修改为v-enter-from
、过渡类名v-leave
修改为v-leave-from
。
- 将
.v-enter
字符串实例替换为.v-enter-from
- 将
.v-leave
字符串实例替换为.v-leave-from
- 过渡组件相关的 prop 名称也需要进行字符串实例替换,规则如上所述
25.Transition 作为根节点 [非兼容]
当使用 <transition>
作为根结点的组件从外部被切换时将不再触发过渡效果。
- 2.x 行为
在 Vue 2 中,通过使用 <transition>
作为一个组件的根节点,过渡效果存在从组件外部触发的可能性。
- 3.x
换做向组件传递一个 prop 就可以达到类似的效果:
<template>
<transition>
<div v-if="show" class="modal"><slot/></div>
</transition>
</template>
<script>
export default {
props: ['show']
}
</script>
<!-- 用法 -->
<modal :show="showModal">hello</modal>
26.Transition Group 根元素 [非兼容]
<transition-group>
不再默认渲染根元素,但仍然可以用tag
attribute 创建根元素。
27.移除v-on.native
修饰符 [非兼容]
- 3.x中
v-on
的.native
修饰符已被移除。
28.v-model
[非兼容]
-
非兼容:用于自定义组件时,
v-model
prop 和事件默认名称已更改:- prop:
value
->modelValue
; - 事件:
input
->update:modelValue
;
- prop:
-
非兼容:
v-bind
的.sync
修饰符和组件的model
选项已移除,可在v-model
上加一个参数代替; -
新增:现在可以在同一个组件上使用多个
v-model
绑定; -
新增:现在可以自定义
v-model
修饰符。
29.v-if 与 v-for 的优先级对比 [非兼容]
非兼容:两者作用于同一个元素上时,
v-if
会拥有比v-for
更高的优先级。
2.x 语法
2.x 版本中在一个元素上同时使用 v-if
和 v-for
时,v-for
会优先作用。
3.x 版本中 v-if
总是优先于 v-for
生效。
30.v-bind 合并行为 [非兼容]
不兼容:v-bind 的绑定顺序会影响渲染结果。
2.x 语法
在 2.x 中,如果一个元素同时定义了 v-bind="object"
和一个相同的独立 attribute,那么这个独立 attribute 总是会覆盖 object
中的绑定。
<!-- 模板 -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- 结果 -->
<div id="red"></div>
在 3.x 中,如果一个元素同时定义了 v-bind="object"
和一个相同的独立 attribute,那么绑定的声明顺序将决定它们如何被合并。换句话说,相对于假设开发者总是希望独立 attribute 覆盖 object
中定义的内容,现在开发者能够对自己所希望的合并行为做更好的控制。
<!-- 模板 -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- 结果 -->
<div id="blue"></div>
<!-- 模板 -->
<div v-bind="{ id: 'blue' }" id="red"></div>
<!-- 结果 -->
<div id="red"></div>
31.VNode 生命周期事件 [非兼容]
在 Vue 2 中,我们可以通过事件来监听组件生命周期中的关键阶段。这些事件名都是以
hook:
前缀开头,并跟随相应的生命周期钩子的名字。
在 Vue 3 中,这个前缀已被更改为
vnode-
。额外地,这些事件现在也可用于 HTML 元素,和在组件上的用法一样。
2.x 语法
在 Vue 2 中,这些事件名和相应的生命周期钩子一致,并带有 hook:
前缀:
<template>
<child-component @hook:updated="onUpdated">
</template>
在 Vue 3 中,事件名附带的是 vnode-
前缀:
<template>
<child-component @vnode-updated="onUpdated">
</template>
或者在驼峰命名法的情况下附带前缀 vnode
:
<template>
<child-component @vnodeUpdated="onUpdated">
</template>
32.侦听数组 [非兼容]
非兼容: 当侦听一个数组时,只有当数组被替换时才会触发回调。如果你需要在数组被改变时触发回调,必须指定
deep
选项。
当使用 watch
选项侦听数组时,只有在数组被替换时才会触发回调。换句话说,在数组被改变时侦听回调将不再被触发。要想在数组被改变时触发侦听回调,必须指定 deep
选项。
watch: {
bookList: {
handler(val, oldVal) {
console.log('book list changed')
},
deep: true
},
}
一、在vue3.x项目中出现红色波浪线的几种情况
1、当前引入的组件或者api不存在
2、组件和api存在正常的情况下,还出现红色波浪线有两种情况
(1)eslint校验引起的,可以在VScode中关闭eslint校验
(2)另外一种情况是你的项目必须是独立存在于一个VSCODE中的,不允许有叠加项目,不然编译器就会将其解析成其他的版本,引入的是Vue3.x版本,但是编译器可能会解析成Vue2.x版本,就会出现不识别vue3语法的问题,出现以上的红色波浪线.
二、Composition API(组合式API)
没有组合式api产生的问题:
使用 (data
、computed
、methods
、watch
) 组件选项来组织逻辑通常都很有效。当组件变大时这些碎片化的逻辑使理解和维护复杂组件变得困难。选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。
什么是组合式 API?
可以将组合式API理解为概念词,为了解决碎片化逻辑而产生的一些问题,将同一个逻辑关注点相关代码收集在一起,这就是组合式 API。
1.setup
组件选项
- 新的
setup
选项在组件被创建之前执行,一旦props
被解析完成,它就将被作为组合式 API 的入口。
WARNING
在setup
中你应该避免使用this
,因为它不会找到组件实例。setup
的调用发生在data
property、computed
property 或methods
被解析之前,所以它们无法在setup
中被获取。
setup
选项是一个接收props
和context
的函数,我们将在之后进行讨论。此外,我们将setup
返回的所有内容都暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。
2.带 ref
的响应式变量
- 在 Vue 3.0 中,我们可以通过一个新的
ref
函数使任何响应式变量在任何地方起作用,如下所示:
<template>
//显示ref值
<div ref="root">{{counter}}</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup(){
//const counter = ref(null)
//const counter = ref('123')
//const counter = ref({a:123})
//console.log(counter.value=null)
//console.log(counter.value='226')
//console.log(counter.value={a:226})
// ref作用是定义一个响应式的数据,返回的是Ref对象。
const counter = ref(0)
console.log(counter.value) // 0
counter.value++
console.log(counter.value) // 1
//获取dom元素
const root = ref(null)
onMounted(() => {
console.log(root.value);//<div>...</div>
});
return {
counter,//暴露counter
root
}
}
}
</script>
提示
换句话说,ref
为我们的值创建了一个响应式引用。在整个组合式 API 中会经常使用引用的概念。
JSX 中的用法
export default {
setup() {
const root = ref(null)
//with vue
return () =>
h('div', {
ref: root
})
// with JSX
return () => <div ref={root} />
}
}
3.在 setup
内注册生命周期钩子
-
为了使组合式 API 的功能和选项式 API 一样完整,我们还需要一种在
setup
中注册生命周期钩子的方法。这要归功于 Vue 导出的几个新函数。组合式 API 上的生命周期钩子与选项式 API 的名称相同,但前缀为on
:即mounted
看起来会像onMounted
。 -
这些函数接受一个回调,当钩子被组件调用时,该回调将被执行。
//直接导入 `onX` 函数来注册生命周期钩子
import { onMounted } from 'vue'
setup(){
onMounted(()=>{})
}
原有选项式生命周期与setup生命周期的区别:
|选项式 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` |
4. watch
响应式更改
就像我们在组件中使用 watch
选项并在 user
property 上设置侦听器一样,我们也可以使用从 Vue 导入的 watch
函数执行相同的操作。它接受 3 个参数:
- 一个想要侦听的响应式引用或 getter 函数
- 一个回调
- 可选的配置选项
import { ref, watch } from 'vue'
const counter = ref(0)
watch(counter, (newValue, oldValue) => {
console.log('The new counter value is: ' + counter.value)
})
5.#独立的 computed
属性
- 与
ref
和watch
类似,也可以使用从 Vue 导入的computed
函数在 Vue 组件外部创建计算属性。让我们回到 counter 的例子:
import { ref, computed } from 'vue'
const counter = ref(0)
const twiceTheCounter = computed(() => counter.value * 2)
counter.value++
console.log(counter.value) // 1
console.log(twiceTheCounter.value) // 2
- 这里我们给
computed
函数传递了第一个参数,它是一个类似 getter 的回调函数,输出的是一个只读的响应式引用。为了访问新创建的计算变量的 value,我们需要像ref
一样使用.value
property。
小结
相当于把原有代码逻辑移动到
setup
选项并使它变得更强大。
将代码提取到一个独立的组合式函数中。
setup函数中不能使用this。Vue 为了避免我们错误的使用,直接将 setup函数中的this修改成了 undefined)
6.Setup
- 本节使用单文件组件代码示例的语法
WARNING
但是,因为props
是响应式的,你不能使用 ES6 解构,它会消除 prop 的响应性。
如果需要解构 prop,可以在setup
函数中使用toRefs
函数来完成此操作:
Props
// MyBook.vue
import { toRefs } from 'vue'
setup(props) {
const { title } = toRefs(props)
console.log(title.value)
}
如果 title
是可选的 prop,则传入的 props
中可能没有 title
。在这种情况下,toRefs
将不会为 title
创建一个 ref 。你需要使用 toRef
替代它:
// MyBook.vue
import { toRef } from 'vue'
setup(props) {
const title = toRef(props, 'title')
console.log(title.value)
}
Context
- 传递给
setup
函数的第二个参数是context
。context
是一个普通 JavaScript 对象,暴露了其它可能在setup
中有用的值:
// MyBook.vue
export default {
setup(props, context) {
// Attribute (非响应式对象,等同于 $attrs)
console.log(context.attrs)
// 插槽 (非响应式对象,等同于 $slots)
console.log(context.slots)
// 触发事件 (方法,等同于 $emit)
console.log(context.emit)
// 暴露公共 property (函数)
console.log(context.expose)
}
}
context
是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对context
使用 ES6 解构。
// MyBook.vue
export default {
setup(props, { attrs, slots, emit, expose }) {
...
}
}
attrs
和slots
是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以attrs.x
或slots.x
的方式引用 property。请注意,与props
不同,attrs
和slots
的 property 是非响应式的。如果你打算根据attrs
或slots
的更改应用副作用,那么应该在onBeforeUpdate
生命周期钩子中执行此操作。
访问组件的 property
执行
setup
时,你只能访问以下 property:
props
attrs
slots
emit
换句话说,你将无法访问以下组件选项:
data
computed
methods
refs
(模板 ref)
使用渲染函数
-
setup
还可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态。将阻止我们返回任何其它的东西。从内部来说这不应该成为一个问题,但当我们想要将这个组件的方法通过模板 ref 暴露给父组件时就不一样了。 -
我们可以通过调用
expose
来解决这个问题,给它传递一个对象,其中定义的 property 将可以被外部组件实例访问:
import { h, ref } from 'vue'
export default {
setup(props, { expose }) {
const count = ref(0)
const increment = () => ++count.value
expose({
increment
})
return () => h('div', count.value)
}
}
这个 increment
方法现在将可以通过父组件的模板 ref 访问。
7.#getCurrentInstance
getCurrentInstance
支持访问内部组件实例。
WARNING
getCurrentInstance
只暴露给高阶使用场景,典型的比如在库中。强烈反对在应用的代码中使用getCurrentInstance
。请不要把它当作在组合式 API 中获取this
的替代方案来使用。
import { getCurrentInstance } from 'vue'
const MyComponent = {
setup() {
const internalInstance = getCurrentInstance()
internalInstance.appContext.config.globalProperties // 访问 globalProperties
}
}
getCurrentInstance
只能在 setup 或生命周期钩子中调用。
如需在 setup 或生命周期钩子外使用,请先在
setup
中调用getCurrentInstance()
获取该实例然后再使用。
const MyComponent = {
setup() {
const internalInstance = getCurrentInstance() // 有效
const id = useComponentId() // 有效
const handleClick = () => {
getCurrentInstance() // 无效
useComponentId() // 无效
internalInstance // 有效
}
onMounted(() => {
getCurrentInstance() // 有效
})
return () =>
h(
'button',
{
onClick: handleClick
},
`uid: ${id}`
)
}
}
// 在组合式函数中调用也可以正常执行
function useComponentId() {
return getCurrentInstance().uid
}
三、单文件组件<script setup> [语法糖]
单文件组件SFC是组合式api的语法糖和组合式api最终实现的是一样的效果。但比组合式api实现方式看起来更友好。
<script setup>
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。相比于普通的 <script>
语法,它具有更多优势:
- 更少的样板内容,更简洁的代码。
- 能够使用纯 TypeScript 声明 props 和抛出事件。
- 更好的运行时性能 (其模板会被编译成与其同一作用域的渲染函数,没有任何的中间代理)。
- 更好的 IDE 类型推断性能 (减少语言服务器从代码中抽离类型的工作)。
1.import 导入的内容也会以同样的方式暴露。意味着可以在模板表达式中直接使用导入的 helper 函数,并不需要通过 methods
选项来暴露它:
<script setup>
import { capitalize } from './helpers'
</script>
<template>
<div>{{ capitalize('hello') }}</div>
</template>
2.响应式状态需要明确使用响应式 APIs 来创建。和从 setup()
函数中返回值一样,ref 值在模板中使用的时候会自动解包:
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
3.使用组件
<script setup>
范围里的值也能被直接作为自定义组件的标签名使用:
<script setup>
//不再需要注册组件,直接在模板中就可以使用。
import MyComponent from './MyComponent.vue'
</script>
<template>
<MyComponent />
</template>
4.动态组件
由于组件被引用为变量而不是作为字符串键来注册的,在
<script setup>
中要使用动态组件的时候,就应该使用动态的:is
来绑定:
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>
<template>
<component :is="Foo" />
<component :is="someCondition ? Foo : Bar" />
</template>
5.递归组件
一个单文件组件可以通过它的文件名被其自己所引用。例如:名为
FooBar.vue
的组件可以在其模板中用<FooBar/>
引用它自己。
请注意这种方式相比于 import 导入的组件优先级更低。如果有命名的 import 导入和组件的推断名冲突了,可以使用 import 别名导入:
import { FooBar as FooBarChild } from './components'
6.命名空间组件
可以使用带点的组件标记,例如
<Foo.Bar>
来引用嵌套在对象属性中的组件。这在需要从单个文件中导入多个组件的时候非常有用:
<script setup>
import * as Form from './form-components'
</script>
<template>
<Form.Input>
<Form.Label>label</Form.Label>
</Form.Input>
</template>
7.使用自定义指令
全局注册的自定义指令将以符合预期的方式工作,且本地注册的指令可以直接在模板中使用,就像上文所提及的组件一样。
但这里有一个需要注意的限制:必须以
vNameOfDirective
的形式来命名本地自定义指令,以使得它们可以直接在模板中使用。
<script setup>
const vMyDirective = {
beforeMount: (el) => {
// 在元素上做些操作
}
}
</script>
<template>
<h1 v-my-directive>This is a Heading</h1>
</template>
<script setup>
// 导入的指令同样能够工作,并且能够通过重命名来使其符合命名规范
import { myDirective as vMyDirective } from './MyDirective.js'
</script>
8.defineProps
和 defineEmits
- 在
<script setup>
中必须使用defineProps
和defineEmits
API 来声明props
和emits
,它们具备完整的类型推断并且在<script setup>
中是直接可用的:
<script setup>
const props = defineProps({
foo: String
})
const emit = defineEmits(['change', 'delete'])
// setup code
</script>
defineProps
和defineEmits
都是只在<script setup>
中才能使用的编译器宏。他们不需要导入且会随着<script setup>
处理过程一同被编译掉。defineProps
接收与props
选项相同的值,defineEmits
也接收emits
选项相同的值。defineProps
和defineEmits
在选项传入后,会提供恰当的类型推断。- 传入到
defineProps
和defineEmits
的选项会从 setup 中提升到模块的范围。因此,传入的选项不能引用在 setup 范围中声明的局部变量。这样做会引起编译错误。但是,它可以引用导入的绑定,因为它们也在模块范围内。
如果使用了 TypeScript,使用纯类型声明来声明 prop 和 emits 也是可以的。
9.defineExpose
使用
<script setup>
的组件是默认关闭的,也即通过模板 ref 或者$parent
链获取到的组件的公开实例,不会暴露任何在<script setup>
中声明的绑定。
为了在
<script setup>
组件中明确要暴露出去的属性,使用defineExpose
编译器宏:
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
defineExpose({
a,
b
})
</script>
当父组件通过模板 ref 的方式获取到当前组件的实例,获取到的实例会像这样
{ a: number, b: number }
(ref 会和在普通实例中一样被自动解包)
10.useSlots
和 useAttrs
在
<script setup>
使用slots
和attrs
的情况应该是很罕见的,因为可以在模板中通过$slots
和$attrs
来访问它们。在你的确需要使用它们的罕见场景中,可以分别用useSlots
和useAttrs
两个辅助函数:
<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
</script>
useSlots
和useAttrs
是真实的运行时函数,它会返回与setupContext.slots
和setupContext.attrs
等价的值,同样也能在普通的组合式 API 中使用。
11.与普通的 <script>
一起使用
<script setup>
可以和普通的<script>
一起使用。普通的<script>
在有这些需要的情况下或许会被使用到:
- 无法在
<script setup>
声明的选项,例如inheritAttrs
或通过插件启用的自定义的选项。 - 声明命名导出。
- 运行副作用或者创建只需要执行一次的对象。
<script>
// 普通 <script>, 在模块范围下执行(只执行一次)
runSideEffectOnce()
// 声明额外的选项
export default {
inheritAttrs: false,
customOptions: {}
}
</script>
<script setup>
// 在 setup() 作用域中执行 (对每个实例皆如此)
</script>
WARNING
该场景下不支持使用render
函数。请使用一个普通的<script>
结合setup
选项来代替。
12.顶层 await
<script setup>
中可以使用顶层await
。结果代码会被编译成async setup()
:
<script setup>
const post = await fetch(`/api/post/1`).then(r => r.json())
</script>
另外,await 的表达式会自动编译成在
await
之后保留当前组件实例上下文的格式。
注意
async setup()
必须与Suspense
组合使用,Suspense
目前还是处于实验阶段的特性。我们打算在将来的某个发布版本中开发完成并提供文档 - 如果你现在感兴趣,可以参照 tests 看它是如何工作的。
13.仅限 TypeScript 的功能
1.仅限类型的 props/emit 声明
props 和 emits 都可以使用传递字面量类型的纯类型语法做为参数给 defineProps
和 defineEmits
来声明:
const props = defineProps<{
foo: string
bar?: number
}>()
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
-
defineProps
或defineEmits
只能是要么使用运行时声明,要么使用类型声明。同时使用两种声明方式会导致编译报错。 -
使用类型声明的时候,静态分析会自动生成等效的运行时声明,以消除双重声明的需要并仍然确保正确的运行时行为。
- 在开发环境下,编译器会试着从类型来推断对应的运行时验证。例如这里从
foo: string
类型中推断出foo: String
。如果类型是对导入类型的引用,这里的推断结果会是foo: null
(与any
类型相等),因为编译器没有外部文件的信息。 - 在生产模式下,编译器会生成数组格式的声明来减少打包体积 (这里的 props 会被编译成
['foo', 'bar']
)。 - 生成的代码仍然是有着类型的 TypeScript 代码,它会在后续的流程中被其它工具处理。
- 在开发环境下,编译器会试着从类型来推断对应的运行时验证。例如这里从
-
截至目前,类型声明参数必须是以下内容之一,以确保正确的静态分析:
- 类型字面量
- 在同一文件中的接口或类型字面量的引用
现在还不支持复杂的类型和从其它文件进行类型导入。理论上来说,将来是可能实现类型导入的。
2.使用类型声明时的默认 props 值
仅限类型的 defineProps
声明的不足之处在于,它没有可以给 props 提供默认值的方式。为了解决这个问题,提供了 withDefaults
编译器宏:
interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
labels: () => ['one', 'two']
})
- 上面代码会被编译为等价的运行时 props 的
default
选项。此外,withDefaults
辅助函数提供了对默认值的类型检查,并确保返回的props
的类型删除了已声明默认值的属性的可选标志。
14.限制:没有 Src 导入
由于模块执行语义的差异,
<script setup>
中的代码依赖单文件组件的上下文。当将其移动到外部的.js
或者.ts
文件中的时候,对于开发者和工具来说都会感到混乱。因而<script setup>
不能和src
attribute 一起使用。
单文件组件样式特性
- 当
<style>
标签带有scoped
attribute 的时候,它的 CSS 只会应用到当前组件的元素上。
插槽选择器
- 默认情况下,作用域样式不会影响到
<slot/>
渲染出来的内容,因为它们被认为是父组件所持有并传递进来的。使用:slotted
伪类以确切地将插槽内容作为选择器的目标:
<style scoped>
:slotted(div) {
color: red;
}
</style>
全局选择器
- 如果想让其中一个样式规则应用到全局,比起另外创建一个
<style>
,可以使用:global
伪类来实现 (看下面的代码):
<style scoped>
:global(.red) {
color: red;
}
</style>
<style module>
<style module>
标签会被编译为 CSS Modules 并且将生成的 CSS 类作为$style
对象的键暴露给组件:
<template>
<p :class="$style.red">
This should be red
</p>
</template>
<style module>
.red {
color: red;
}
</style>
-
hash 计算以避免冲突,实现了和 scope CSS 一样将 CSS 仅作用于当前组件的效果。
-
参考 CSS Modules 规范以查看更多详情,例如 global exceptions 和 composition
自定义注入名称
你可以通过给 module
attribute 一个值来自定义注入的类对象的 property 键:
<template>
<p :class="classes.red">red</p>
</template>
<style module="classes">
.red {
color: red;
}
</style>
与组合式 API 一同使用
- 注入的类可以通过
useCssModule
API 在setup()
和<script setup>
中使用。对于使用了自定义注入名称的<style module>
模块,useCssModule
接收一个对应的module
attribute 值作为第一个参数。
// 默认, 返回 <style module> 中的类
useCssModule()
// 命名, 返回 <style module="classes"> 中的类
useCssModule('classes')
- 单文件组件的
<style>
标签可以通过v-bind
这一 CSS 函数将 CSS 的值关联到动态的组件状态上:
<template>
<div class="text">hello</div>
</template>
<script>
export default {
data() {
return {
color: 'red'
}
}
}
</script>
<style>
.text {
color: v-bind(color);
}
</style>
- 这个语法同样也适用于
<script setup>
,且支持 JavaScript 表达式 (需要用引号包裹起来)
<script setup>
const theme = {
color: 'red'
}
</script>
<template>
<p>hello</p>
</template>
<style scoped>
p {
color: v-bind('theme.color');
}
</style>
- 实际的值会被编译成 hash 的 CSS 自定义 property,CSS 本身仍然是静态的。自定义 property 会通过内联样式的方式应用到组件的根元素上,并且在源值变更的时候响应式更新。
四、pinia(精简版vuex)
pinia@next 最新一代的轻量级替代vuex的状态管理插件.
1.与vuex相比 没有mutation 只有state,getters,actions(支持同步异步)。
2.可以直接修改state数据或者通过getters,actions修改。
3.没有modules,每一个文件都是一个单独的store。
3.语法上比vuex更容易理解和使用,灵活。
五、vue-class-component介绍
- vue-class-component 是 vue 的官方库,作用是用类的方式编写组件。这种编写方式可以让 .vue 文件的 js 域结构更扁平,并使 vue 组件可以使用继承、混入等高级特性。
- vue-property-decorator 是一个非官方库,是 vue-class-component 的很好的补充。它可以让vue的某些属性和方法,通过修饰器的写法让它也写到vue组件实例的类里面。比如
@Prop
@Watch
@Emit。
- vue2.x 对 TS 的支持不友好,所以 vue2.x 跟 TS 的整合,通常需要基于 vue-class-component 来用基于 class(类) 的组件书写方式。
- vue3.x对 TS 有很好的支持。所以在vue3中 vue-class-component是可选项。
六、Babel Plugin JSX
babel-plugin-jsx只兼容3.x:
Babel 7+
Vue 3+
- 在babe.config.js中加入:
{
"plugins": ["@vue/babel-plugin-jsx"]
- 在 TypeSript 中使用
tsconfig.json
:
{
"compilerOptions": {
"jsx": "preserve"
}
}
通过 Babel 插件,用于在 Vue 中使用 JSX 语法,它可以让我们回到更接近于模板的语法上。
import AnchoredHeading from './AnchoredHeading.vue'
const app = createApp({
render() {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
}
})
app.mount('#demo')
文末
还有很多知识点没有涉及到。
参考链接
JavaScript 中一些流行的模块系统[codesweetly文档]、 es module详解、 CommonJS和ES6中的导入导出使用总结、 esModule - 知乎、 js模块规范大盘点
vue2文档官方、 vue3中文文档官网、 vue3英文文档官网、 vue-class-component文档、 TypeScript中文文档、 vue 2.x迁移3.x官网文档、 单文件SFC在线演练场、 Codepen上的浏览器内试验田、 CodeSandbox上的浏览器内沙盒、 红色波浪线