vue3光速学习
开始前的准备-安装
- CDN
<script src="https://unpkg.com/vue@next"></script>
- npm
# 最新稳定版
$ npm install vue@next
- 命令行工具(CLI) 升级vue-cli v4.5
yarn global add @vue/cli
# OR
npm install -g @vue/cli
#然后
npm i -g @vue/cli@next
- Vite 使用 Vite 可以快速构建 Vue 项目
#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
开始
基础API
<body>
<div id="app"></div>
<script>
console.log('Vue', Vue)
const {
createApp,
reactive, // 创建响应式数据对象
ref, // 创建一个响应式的数据对象
toRefs, // 将响应式数据对象转换为单一响应式对象
isRef, // 判断某值是否是引用类型
computed, // 创建计算属性
watch, // 创建watch监听
watchEffect,
// 生命周期钩子
onBeforeMount,
onMounted,
onUpdated,
onUnmounted,
} = Vue
const MyComponent = {
template: `
<div>
<div>count is {{ state.count }} </div>
<div>plusOne is {{ state.plusOne }}</div>
<div>Date is {{ time }}</div>
<div>count is {{ count }} </div>
<button @click="increment">count++</button>
</div>
`,
setup(props, context) {
console.log('setup....',)
console.log('props',props)
console.log('context',context)
// reactive state
const state = reactive({
count: 0,
plusOne: computed(() => state.count + 1)
})
// 定义创建响应式数据
const time = ref(new Date())
// 设置定时器为了测试数据响应
setInterval(() => time.value = new Date(), 1000)
// 判断某值是否是响应式类型
console.log('time is ref:', isRef(time))
console.log('time', time)
console.log('time.value', time.value)
// method
const increment = () => {
console.log('increment....')
state.count++
}
// 定义监听
watch(() => state.count * 2, val => {
console.log(`count * 2 is ${val}`)
})
// 副作用函数
watchEffect(() => {
console.log('数值被修改了..',state.count)
})
// lifecycle
onBeforeMount(() => {
console.log('onBeforeMount....')
})
onMounted(() => {
console.log(`onMounted ...!`)
})
onUpdated(() => {
console.log(`onUpdated ...!`)
})
onUnmounted(() => {
console.log(`onUnmounted ...!`)
})
// expose bindings on render context
return {
state,
... toRefs(state),
time,
increment
}
}
}
createApp(MyComponent).mount('#app')
</script>
</body>
setup 使用composition API的入口
setup函数会在beforeCreate之后created之前执行
reactive
reactive() 函数接受一个普通对象返回一个响应式数据对象
ref与isRef
- ref是将给定的值(确切的说是基本数据类型)创建一个响应式的数据对象
- isRef 其实就是判断一下是不是ref生成的响应式数据对象
toRefs
toRefs 可以将reactive 创建出的对象展开为基础类型
watch定义监听器
watch(() => state.count * 2, val => {
console.log(`count * 2 is ${val}`)
})
watchEffect 副作用函数
响应式对象修改会触发这个函数
watchEffect(() => {
console.log('数值被修改了..',state.count)
})
computed 计算属性
const state = reactive({
count: 0,
plusOne: computed(() => state.count + 1)
})
新特性
- Composition API
- Teleport
- Fragments
- Emits Component Option
createRendererAPI用于创建自定义渲染器
Composition API
与原来的 OptionsAPI 相比,Composition API 可以让我们将相同功能的代码组织在一起,而不需要散落到 OptionsAPI 的各个角落,为vue应用提供更好的逻辑复用和代码组织。
<body>
<div id="app"></div>
<script>
const {
createApp,
reactive,
computed,
watchEffect,
ref,
toRefs,
onMounted,
onUnmounted,
watch
} = Vue
console.log('Vue',Vue)
const MyComponent = {
template: `
<button @click="click">
{{ state.message }}
</button>
<p> counter: {{counter}} </p>
<p> doubleCounter: {{doubleCounter}} </p>
<p ref="desc"></p>
`,
setup() {
const state = reactive({
message:'Hello Vue 3!!',
})
watchEffect(() => {
console.log('state change ', state.message)
})
function click() {
state.message = state.message.split('').reverse().join('')
}
let timer
const counterState = reactive({
counter:1,
doubleCounter: computed(() => counterState.counter * 2)
})
onMounted(()=>{
timer = setInterval(() => {
counterState.counter++
}, 1000);
})
onUnmounted(() => {
clearInterval(timer)
})
const desc = ref(null)
watch(()=>counterState.counter, (val,oldVal)=>{
desc.value.textContent = `counter change from ${oldVal} to ${val}`
})
return {
state,
click,
desc,
...toRefs(counterState)
}
}
}
createApp(MyComponent).mount('#app')
</script>
</body>
Teleport
传送门组件提供一种简洁的方式可以指定它里面内容的父元素。
<template>
<button @click="modalOpen = true">
弹出一个全屏模态窗口
</button>
<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
这是一个模态窗口!
我的父元素是"body"!
<button @click="modalOpen = false">Close</button>
</div>
</div>
</teleport>
</template>
Fragments
vue3中组件可以拥有多个根
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
Emits Component Option
vue3中组件发送的自定义事件需要定义在emits选项中:
好处:
- 原生事件会触发两次,比如
click - 更好的指示组件工作方式
- 对象形式事件校验
<template>
<div @click="$emit('my-click')">
<h3>自定义事件</h3>
</div>
</template>
<script>
export default {
emits: ['my-click']
}
</script>
自定义渲染器 custom renderer
这个 API 可以用来自定义渲染逻辑
案例:把数据渲染到canvas上
- 创建CanvasApp.vue。创建一个组件描述要渲染的数据,因为我们只是想把它携带的数据绘制到canvas上,不需要单独声明该组件。
<template>
<piechart
@click="handleClick"
:data="state.data"
:x="200"
:y="200"
:r="200">
</piechart>
</template>
<script>
import { reactive, ref } from "vue";
export default {
setup() {
const state = reactive({
data: [
{ name: "大专", count: 200, color: "brown" },
{ name: "本科", count: 300, color: "yellow" },
{ name: "硕士", count: 100, color: "pink" },
{ name: "博士", count: 50, color: "skyblue" }
]
});
function handleClick() {
state.data.push({ name: "其他", count: 30, color: "orange" });
}
return {
state,
handleClick
};
}
};
</script>
index.html 里面添加一个 div#demo
- 创建自定义渲染器,main.js
import { createApp, createRenderer } from 'vue'
import CanvasApp from './CanvasApp.vue'
const nodeOps = {
insert: (child, parent, anchor) => {
// 我们重写了insert逻辑,因为在我们canvasApp中不存在实际dom插入操作
// 这里面只需要将元素之间的父子关系保存一下即可
child.parent = parent;
if (!parent.childs) {
parent.childs = [child]
} else {
parent.childs.push(child);
}
// 只有canvas有nodeType,这里就是开始绘制内容到canvas
if (parent.nodeType == 1) {
draw(child);
// 如果子元素上附加了事件,我们给canvas添加监听器
if (child.onClick) {
ctx.canvas.addEventListener('click', () => {
child.onClick();
setTimeout(() => {
draw(child)
}, 0);
})
}
}
},
remove: child => {},
createElement: (tag, isSVG, is) => {
// 创建元素时由于没有需要创建的dom元素,只需返回当前元素数据对象
return {tag}
},
createText: text => {},
createComment: text => {},
setText: (node, text) => {},
setElementText: (el, text) => {},
parentNode: node => {},
nextSibling: node => {},
querySelector: selector => {},
setScopeId(el, id) {},
cloneNode(el) {},
insertStaticContent(content, parent, anchor, isSVG) {},
patchProp(el, key, prevValue, nextValue) {
el[key] = nextValue;
},
};
// 创建一个渲染器
let renderer = createRenderer(nodeOps);
// 保存画布和其上下文
let ctx;
let canvas;
// 扩展mount,首先创建一个画布元素
function createCanvasApp(App) {
const app = renderer.createApp(App);
const mount = app.mount
app.mount = function (selector) {
canvas = document.createElement('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.querySelector(selector).appendChild(canvas);
ctx = canvas.getContext('2d');
mount(canvas);
}
return app
}
createCanvasApp(CanvasApp).mount('#demo')
- 编写绘制逻辑
const draw = (el,noClear) => {
if (!noClear) {
ctx.clearRect(0, 0, canvas.width, canvas.height)
}
if (el.tag == 'piechart') {
let { data, r, x, y } = el;
let total = data.reduce((memo, current) => memo + current.count, 0);
let start = 0,
end = 0;
data.forEach(item => {
end += item.count / total * 360;
drawPieChart(start, end, item.color, x, y, r);
drawPieChartText(item.name, (start + end) / 2, x, y, r);
start = end;
});
}
el.childs && el.childs.forEach(child => draw(child,true));
}
const d2a = (n) => {
return n * Math.PI / 180;
}
const drawPieChart = (start, end, color, cx, cy, r) => {
let x = cx + Math.cos(d2a(start)) * r;
let y = cy + Math.sin(d2a(start)) * r;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(x, y);
ctx.arc(cx, cy, r, d2a(start), d2a(end), false);
ctx.fillStyle = color;
ctx.fill();
ctx.stroke();
ctx.closePath();
}
const drawPieChartText = (val, position, cx, cy, r) => {
ctx.beginPath();
let x = cx + Math.cos(d2a(position)) * r/1.25 - 20;
let y = cy + Math.sin(d2a(position)) * r/1.25;
ctx.fillStyle = '#000';
ctx.font = '20px 微软雅黑';
ctx.fillText(val,x,y);
ctx.closePath();
}
非兼容的变更
- Global API
- 模板指令
- 组件
- 渲染函数
- 自定义元素
- 其他改变
- 移除API
Global API
Global API 改为应用程序实例调用
vue2.X许多全局API和配置可以全局改变Vue的行为,比如:
//全局组件
Vue.component('button-counter', {
data: () => ({
count: 0
}),
template: '<button @click="count++">Clicked {{ count }} times.</button>'
})
//类似的全局指令声明方式
Vue.directive('focus', {
inserted: el => el.focus()
})
导致的一些问题:
- vue2 ,没有“app”概念,w我们定义的应用只是通过new Vue()创建的根Vue实例。从同一个 Vue 构造函数创建的每个根实例共享相同的全局配置。测试期间,全局配置很容易意外污染其他测试用例,导致测试变得困难
- 全局配置也导致没有办法在单页面创建不同全局配置的多个app实例 vue3为了避免这些问题,使用createApp返回一个应用实例,由它暴露一系列全局API
import { createApp } from 'vue'
const app = createApp({})
.component('comp', { render: () => h('div', 'i am comp') })
.mount('#app')
vue2全局API及其相应的实例API列表:
| 2.x 全局 API | 3.x 实例 API (app) |
|---|---|
| Vue.config | app.config |
| Vue.config.productionTip | 移除 (见下方) |
| Vue.config.ignoredElements | app.config.compilerOptions.isCustomElement (见下方) |
| Vue.component | app.component |
| Vue.directive | app.directive |
| Vue.mixin | app.mixin |
| Vue.use | app.use (见下方) |
| Vue.prototype | app.config.globalProperties (见下方) |
| Vue.extend | 移除 (见下方) |
全局和内部 API 重构为可 tree-shakable(摇树优化)
vue2中一些类似Vue.nextTick(),作为静态函数直接挂在构造函数上,如果我们没在应用中使用过它,就会形成'死代码'。这类global-api造成的dead code无法使用webpack的tree-shaking排除掉,都会被包含在最终的打包产物中。
vue3中做出相应变化,考虑到tree-shaking的支持,通过具名导出进行访问这些global-api
import { nextTick } from 'vue'
nextTick(()=>{
// something something DOM-related
})
受影响的API:
Vue.nextTickVue.observable(用Vue.reactive替换)Vue.versionVue.compile(仅完整构建版本)Vue.set(仅兼容构建版本)Vue.delete(仅兼容构建版本)
模板指令
model选项和v-bind的sync 修饰符被移除,统一为v-model参数形式
vue2中.sync和v-model功能有重叠,容易混淆,vue3做了统一
- prop:
value->modelValue; - 事件:
input->update:modelValue v-bind的.sync修饰符和组件的model选项已移除,可在v-model上加一个参数代替
<div @click="$emit('update:modelValue',modelValue + 1)">
update:modelValue:{{modelValue}}
</div>
<div @click="$emit('update:counter',counter + 1)">
update:counter:{{counter}}
</div>
props:{
modelValue:{
type:Number,
default:0
},
counter:{
type:Number,
default:0
}
}
<VmodelTest v-model = "count"></VmodelTest>
<VmodelTest v-model:counter = "count"></VmodelTest>
<template v-for> 和非 v-for 节点上的 key 用法已更改
- vue2中
template标签不能拥有key,不过可以为其每个子节点分别设置key
<template v-for="item in list">
<div :key="'heading-' + item.id">...</div>
<span :key="'content-' + item.id">...</span>
</template>
vue3中,key则应该设置在template标签上。
<template v-for="item in list" :key="item.id">
<div>...</div>
<span>...</span>
</template>
当使用 <template v-for> 时如果存在使用 v-if 的子节点,则 key 应改为设置在 <template> 标签上。
<!-- Vue 2.x -->
<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 -->
<template v-for="item in list" :key="item.id">
<div v-if="item.isVisible">...</div>
<span v-else>...</span>
</template>
组件
只能使用普通函数创建函数式组件
- 性能提升在vue3中可忽略不计,所以vue3中推荐使用状态组件
- 函数式组件只能由接收
props和context(即:slots、attrs、emit) 的普通函数创建 - SFC中
<template>不能添加functional特性声明函数式组件 { functional: true }选项已从通过函数创建的组件中移除
import { h } from 'vue'
const Heading = (props, context) => {
return h(`h${props.level}`, context.attrs, context.slots)
}
Heading.props = ['level']
export default Heading
异步组件使用变化
异步组件要求使用defineAsyncComponent方法创建
定义一个异步组件:
import { defineAsyncComponent } from "vue"
//不带选项的异步组件
const asyncComp = defineAsyncComponent(()=>import('./other.vue'))
//带选项的异步组件
//loader选项是以前的component
// 如error loading 组件
const asyncModalWithOptions = defineAsyncComponent({
loader: () => import('./Modal.vue'),
delay: 200,
timeout: 3000,
errorComponent: ErrorComponent,
loadingComponent: LoadingComponent
})
渲染函数
渲染函数 API 更改
$scopedSlots 移除,所有插槽都通过 $slots 作为函数暴露
- 不再传入 h 函数,需要我们手动导入
import { h } from "vue";
render() {
// 获取插槽
// 2.x this.$scopedSlots.content()
// this.$slots.default()
// console.log(this.$slots.content())
const emit = this.$emit
return h("div", [
h(
"div",{
onClick:this.onClick
},
`i am renderTest, ${this.counter}`,
this.$slots.default(),
this.$slots.content()
)
]);
}
<RenderTest v-model:counter="count">
<template v-slot:default>
<p>默认插槽</p>
</template>
<template v-slot:content>
<p>content 具名插槽</p>
</template>
</RenderTest>
自定义元素
自定义组件白名单
vue3中在模板编译期间检测自定义元素,如果添加一些vue之外的自定义元素,需要在编译器选项中设置 isCustomElement选项
- 使用了
vue-loader,则应通过vue-loader的compilerOptions选项传递
// webpack 中的配置
rules: [
{
test: /.vue$/,
use: 'vue-loader',
options: {
compilerOptions: {
isCustomElement: tag => tag === 'plastic-button'
}
}
}
// ...
]
-
vite 在vite.config.js中配置
vueCompilerOptions -
如果使用动态模板编译,通过
app.config.compilerOptions.isCustomElement传递:
const app = Vue.createApp({})
app.config.compilerOptions.isCustomElement = tag => tag === 'plastic-button'
is 属性仅限于用在component标签
vue3中设置动态组件时,is属性仅能用于component标签上
<component is='comp'></component>
在 DOM 模板中使用时,模板受原生 HTML 解析规则的约束。一些 HTML 元素,例如
<ul>、<ol>、<table>和<select>对它们内部可以出现的元素有限制,以及一些像<li>、<tr>、和<option>只能出现在特定的其他元素中。
2.X 在原生标签上使用 is attribute 来绕过这些限制:
<table>
<tr is="blog-post-row"></tr>
</table>
3.x 随着 is 的行为发生变化,现在将元素解析为 Vue 组件需要添加一个 vue: 前缀:(仅限in-dom模板)
<table>
<tr is="vue:blog-post-row"></tr>
</table>
其他
组件data选项总是声明为函数
2.x中可通过object 或者是 function 定义 data 选项,在 3.x 中,data 选项只接受返回 object 的 function。
//2.x
const app = new Vue({
data: {
apiKey: 'a1b2c3'
}
})
//3.x
createApp({
data() {
return {
apiKey: 'a1b2c3'
}
}
}).mount('#app')
自定义指令Api和组件保持一致
- 指令的钩子函数已经被重命名,以更好地与组件的生命周期保持一致。 2.x
**bind** - 指令绑定到元素后调用。只调用一次。
**inserted** - 元素插入父 DOM 后调用。
**update** - 当元素更新,但子元素尚未更新时,将调用此钩子。
**componentUpdated** - 一旦组件和子级被更新,就会调用这个钩子。
**unbind** - 一旦指令被移除,就会调用这个钩子。也只调用一次。
<p v-highlight="'yellow'">以亮黄色高亮显示此文本</p>
Vue.directive('highlight', {
bind(el, binding, vnode) {
el.style.background = binding.value
}
})
3.x
- created - 新增!在元素的 attribute 或事件监听器被应用之前调用。
- bind → beforeMount
- inserted → mounted
- beforeUpdate:新增!在元素本身被更新之前调用,与组件的生命周期钩子十分相似。
- update → 移除!该钩子与
updated有太多相似之处,因此它是多余的。请改用updated。 - componentUpdated → updated
- beforeUnmount:新增!与组件的生命周期钩子类似,它将在元素被卸载之前调用。
- unbind -> unmounted
const app = Vue.createApp({})
app.directive('highlight', {
created(el, binding, vnode, prevVnode) {}, // 新增
beforeMount(el, binding, vnode) {
el.style.background = binding.value
},
mounted() {},
beforeUpdate() {}, // 新增
updated() {},
beforeUnmount() {}, // 新增
unmounted() {}
})
transition(过渡)类名变更
v-enter修改为v-enter-fromv-leave修改为v-leave-from
watch 变化
以.分割的表达式不再被watch和watch支持,可以使用计算函数作为watch支持,可以使用计算函数作为watch参数实现
this.$watch(() => this.foo.bar, (v1, v2) => {
console.log(this.foo.bar)
})
已挂载的应用不会取代它所挂载的元素
在 Vue 2.x 中,当挂载一个具有 template 的应用时,被渲染的内容会替换我们要挂载的目标元素。在 Vue 3.x 中,被渲染的应用会作为子元素插入,从而替换目标元素的 innerHTML
//vue2.x
<body>
<div id="rendered">Hello Vue!</div>
</body>
//vue3.x
<body>
<div id="app" data-v-app="">
<div id="rendered">Hello Vue!</div>
</div>
</body>
被移除的API
keyCode 作为 v-on 修饰符被移除
不再支持使用数字 (即键码) 作为 v-on 修饰符
<!-- 键码版本 -->
<input v-on:keyup.13="submit" />
<!-- 别名版本 -->
<input v-on:keyup.enter="submit" />
on,$off 和 $once 实例方法移除
on,$off 和 $once 可以使用其他三方库实现:
示例
// npm i mitt -S
// 创建emitter
const emitter = mitt()
// 发送事件
emitter.emit('foo', 'foooooooo')
// 监听事件
emitter.on('foo', msg => console.log(msg))
filters 过滤器移除
在 3.x 中,过滤器已移除,且不再支持。建议用方法调用或计算属性来替换它们
一个案例通关vue3 核心特性
组件化实战
将上面todo案例组件化:
1. EditTodo组件:新增、编辑待办项(todo)
- 非属性特性的展开
v-bind = '$attrs' - 因为要复用 , 使用全局注册
- 自定义组件v-model的实现
- 传参改变默认modelValue
v-model:todo-title="newTodo"
- 传参改变默认modelValue
<input
type="text"
:value="todoTitle"
@input = "onInputChange"
v-bind="$attrs"
/>
import EditTodo from './components/todos/EditTodo.vue'
createApp(App).component('EditTodo',EditTodo)
<EditTodo
v-model:todo-title="newTodo"
@keyup.enter="addTodo"
autofocus
placeholder="新增今日待办"
autocomplete="off"
></EditTodo>
2. TodoItem组件:todo列表
- 输入
- todo
- editedTodo
v-model:edited-todo="editedTodo"(对传入的todo选择和编辑,要和外面交互)
- 输出
- editTodo:更新editedTodo的事件
- removeTodo:todo删除事件
- 内部状态 beforeEditCache(缓存编辑前的title)
- 逻辑、交互
- editTodo
- editTodo为父组件状态,
emit("update:edited-todo", todo);
- editTodo为父组件状态,
- removeTodo
- 派发事件,父级去做删除
emit("remove-todo", todo); - setup(props, { emit }) {}
- 派发事件,父级去做删除
- doneEdit
- cancelTodo
- editTodo
- emits: ["remove-todo", "update:edited-todo"]
- v-todo-focus 局部指令
directives: {
"todo-focus": (el, { value }) => {
if (value) {
el.focus();
}
},
}
成功抽离
<ul>
<TodoItem v-for="todo in filterdTodos"
:key="todo.id"
:todo = "todo"
v-model:edited-todo="editedTodo"
@remove-todo="removeTodo"
></TodoItem>
</ul>
3. filter
- 输入
- 过滤条件 items
- 选中项 modelValue
- 输出
'update:modelValue'
4. 进一步抽离
- 与todos相关逻辑
- 缓存和获取
- todos、addTodo、removeTodo、watchEffect
- filter相关