Vue3 新功能
createApp
// vue2 初始化实例
const app = new Vue({ /* 选项 */ });
// vue3 初始化实例
const app = Vue.createApp({ /* 选项 */ });
// vue2
Vue.use(/* ... */);
Vue.mixin(/* ... */);
Vue.component(/* ... */);
Vue.directive(/* ... */);
// vue3
app.use(/* ... */);
app.mixin(/* ... */);
app.component(/* ... */);
app.directive(/* ... */);
emits 属性
两种形式声明触发的事件:
- 使用字符串数组的简易形式。
- 使用对象的完整形式。该对象的每个属性键是事件的名称,值是
null
或一个验证函数。
<!-- 父组件 -->
<template>
<!-- 引用子组件 -->
<HelloWorld :msg="msg" @onSayHello="sayHello" />
</template>
<!-- 子组件 -->
<script>
export default {
name: 'Helloworld',
props: {
msg: String
},
// 声明父组件传递的事件
emits: ['onSayHello'],
setup(props, { emit }) {
// 引用
emit('onSayHello', 'bbb');
}
}
</script>
// 对象语法
export default {
emits: {
// 没有验证函数
click: null,
// 具有验证函数
submit: (payload) => {
if (payload.email && payload.password) {
return true
} else {
console.warn(`Invalid submit event payload!`)
return false
}
}
}
}
多事件处理
<!-- 在 methods 里定义 one、two 两个函数 -->
<button @click="one($event), two($event)">Submit</button>
Fragment
vue2
模板里面只能是单一节点;vue3
里面可以是多个节点
<!-- vue2 组件模板 -->
<template>
<div class="blog-post">
<h3>{{ title }}</h3>
<div v-html="content"></div>
</div>
</template>
<!-- vue3 组件模板 -->
<template>
<h3>{{ title }}</h3>
<div v-html="content"></div>
</template>
移除 .sync 改为 v-model 参数
<!-- vue2 -->
<MyComponent v-bind:title.sync="title" />
<!-- 是以下的简写 -->
<MyComponent
v-bind:title="title"
v-on:update:title="title = $event"
/>
<!-- vue3 -->
<MyComponent v-model:title="title" />
<!-- 是以下的简写 -->
<MyComponent :title="title" @update:title="title = $event" />
异步组件的引用方式
// vue2
new Vue({
// ...
components: {
'my-component': () => import('./my-async-component.vue')
}
});
// vue3
import { createApp, defineAsyncComponent } from 'vue';
createApp({
// ...
components: {
AsyncComponent: defineAsyncComponent(() => import('./my-async-component.vue'))
}
});
移除 filter
<!-- 以下 filter 在 vue3 中不可用了 -->
<!-- 在双花括号中 -->
{{ message | capitalize }}
<!-- 在 v-bind 中 -->
<div v-bind:id="rawId | fomatId"></div>
Teleport
把弹窗放到组件外面去。
<!-- 在 data 中设置 modalOpen: false -->
<button @click="modalOpen = true">
Open full screen modal!(With teleport!)
</button>
<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
teleport 弹窗(父元素是 body)
<button @click="modalOpen = false">Close</button>
</div>
</div>
</teleport>
Suspense
<Suspense>
<template>
<!-- Test1 是一个异步组件 -->
<Test1 />
</template>
<!-- #fallback 就是一个具名插槽
即 Suspense 组件内部,有两个 slot
其中一个具名为 fallback -->
<template #fallback>
loading...
</template>
</Suspense>
composition API
composition API
带来了什么?
- 更好的代码组织
- 更好的逻辑复用
- 更好的类型推导
- 更好的代码组织、更好的逻辑复用
Option API
代码逻辑是分散的;组合式API
代码逻辑是组合到一起的。
出处:https://coding.imooc.com/lesson/419.html
- 更好的类型推导
Options API
不利于推导类型和属性。
{
data() {
return {
a: 10
};
}
methods: {
fn1() {
const a = this.a;
}
},
mounted() {
this.fn1(); // 获取 a
// 不利于推导类型和属性
}
}
Composition API
函数在哪定义,传入什么,返回什么都是很清晰的。
出处:https://coding.imooc.com/lesson/419.html
出处:https://coding.imooc.com/lesson/419.html
reactive
通过它来创建引用类型的响应式数据。
ref、toRef、toRefs
ref
- 生成值类型的响应式数据
- 可用于模板和
reactive
- 通过
.value
修改值 - 可以获取
dom
节点
<template>
<p>ref demo {{ageRef}} {{state.name}}</p>
</template>
<script>
import { ref, reactive } from 'vue';
export default {
name: 'Ref',
setup() {
const ageRef = ref(20); // 值类型 响应式
const nameRef = ref('章三');
const state = reactive({
name: nameRef
});
setTimeout(() => {
console.log('ageRef', ageRef.value);
ageRef.value = 25; // .value 修改值
nameRef.value = '章三A';
}, 1500);
return {
ageRef,
state
};
}
}
</script>
<template>
<p ref="elemRef">我是一行文字</p>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
name: 'RefTemplate',
setup() {
const elemRef = ref(null)
onMounted(() => {
console.log('ref template', elemRef.value.innerHTML, elemRef.value)
})
return {
elemRef
}
}
}
</script>
toRef
- 针对一个响应式对象(
reactive
封装)的prop
- 常见一个
ref
,具有响应式 - 两者保持引用关系(一个改了,另一个也同步被改)
<template>
<p>toRef demo - {{ageRef}} - {{state.name}} {{state.age}}</p>
</template>
<script>
import { toRef, reactive } from 'vue';
export default {
name: 'ToRef',
setup() {
const state = reactive({
age: 20,
name: '章三'
});
// // toRef 如果用于普通对象(非响应式对象),产出的结果不具备响应式
// const state = {
// age: 20,
// name: '章三'
// };
const ageRef = toRef(state, 'age');
setTimeout(() => {
state.age = 25;
}, 1500);
setTimeout(() => {
ageRef.value = 30; // .value 修改值
}, 3000);
return {
state,
ageRef
};
}
}
</script>
普通对象要实现响应式,用 reactive
;对象的某个属性要实现响应式,用 toRef
。
toRefs
- 将响应式对象(
reactive
封装)转换成普通对象 - 对象的每个
prop
都是对应的ref
- 两者保持引用关系
- 合成函数返回响应式对象
<template>
<p>toRefs demo {{age}} {{name}}</p>
</template>
<script>
import { toRefs, reactive } from 'vue';
export default {
name: 'ToRefs',
setup() {
const state = reactive({
age: 20,
name: '章三'
})
const stateAsRefs = toRefs(state) // 将响应式对象,变成普通对象
// const { age: ageRef, name: nameRef } = stateAsRefs; // 每个属性,都是 ref 对象
// return {
// ageRef,
// nameRef
// };
setTimeout(() => {
state.age = 25;
}, 1500);
return stateAsRefs;
// 正常写是 return { state }; 使用时是 {{state.name}} {{state.age}}
// 如果想直接使用 {{name}} {{age}} 就需要解构 state
// 直接解构 state 会失去响应式
// return {
// ...state
// };
}
}
</script>
// 合成函数返回响应式对象
function useFeatureX() {
const state = reactive({
x: 1,
y: 2
});
// 逻辑运行状态,省略 n 行
// 返回时转换为 ref
return toRefs(state);
}
export default {
setup() {
// 可以在不失去响应式的情况下破坏结构
const { x, y } = useFeatureX();
return {
x,
y
};
}
}
最佳使用方式
- 用
reactive
做对象的响应式,用ref
做值类型响应式 setup
中返回toRefs(state)
,或者toRef(state, 'xxx')
ref
的变量命名都用xxxRef
- 合成函数返回响应式对象时,用
toRefs
为什么需要使用 ref ?
- 返回值类型,会丢失响应式
- 如在
setup
、computed
、合成函数,都有可能返回值类型 Vue
如果不定义ref
,用户将自造ref
,反而混乱
为何需要 .value ?
ref
是一个对象,才能不丢失响应式,value
用来存储值- 通过
.value
属性的get
和set
实现响应式 - 用于模板、
reacttive
时,不需要 .value
,其他情况都需要
ref
也是靠 reactive
实现的。类似于 ref = reactive({ value: '张三' }) ;
。
为什么需要 toRef 和 toRefs ?
初衷:在不丢失响应式的情况下,把对象数据分解/扩散。
前提:针对的是 reactive
封装的响应式对象,并非普通对象。
reactive、ref
是创造响应式;toRef
和 toRefs
是延续响应式。
readonly
创建一个只读的数据。
setup
setup 中如何获取组件实例
- 在
setup
和其他Composition API
中没有this
- 可通过
getCurrentInstance
获取当前实例 - 若使用
Options API
可照常使用this
<template>
<p>get instance</p>
</template>
<script>
import { onMounted, getCurrentInstance } from 'vue';
export default {
name: 'GetInstance',
data() {
return {
x: 1,
y: 2
};
},
setup() {
console.log('this1', this); // undefined
onMounted(() => {
console.log('this in onMounted', this);
console.log('x', instance.data.x); // 1 应该放在 onMounted 这里获取
});
const instance = getCurrentInstance();
console.log('instance', instance);
console.log('x', instance.data.x); // undefined 因为 setup 等于 beforeCreate 和 created 此时还没有初始化完成 所以是 undefined
},
mounted() {
console.log('this2', this); // Proxy
console.log('y', this.y); // 2
}
}
</script>
watch、watchEffect
watch
和 watchEffect
的区别:
- 两者都可监听
data
属性变化 watch
需要明确监听哪个属性watchEffect
会根据其中的属性,自动监听其变化
<template>
<p>watch vs watchEffect</p>
<p>{{numberRef}}</p>
<p>{{name}} {{age}}</p>
</template>
<script>
// watch 监听
import { reactive, ref, toRefs, watch } from 'vue';
export default {
name: 'Watch',
setup() {
const numberRef = ref(100);
const state = reactive({
name: '章三',
age: 20
});
watch(
numberRef,
(newNumber, oldNumber) => {
console.log('ref watch', newNumber, oldNumber);
},
{
immediate: true; // 初始化之前就监听,可选
}
);
setTimeout(() => {
numberRef.value = 200;
}, 1500);
watch(
// 第一个参数,确定要监听哪个属性
() => state.age,
// 第二个参数,回调函数
(newAge, oldAge) => {
console.log('state watch', newAge, oldAge);
},
// 第三个参数,配置项
{
immediate: true, // 初始化之前就监听,可选
// deep: true // 深度监听
}
);
setTimeout(() => {
state.age = 25;
}, 1500);
setTimeout(() => {
state.name = '章三A';
}, 3000);
return {
numberRef,
...toRefs(state);
};
}
}
</script>
<template>
<p>watch vs watchEffect</p>
<p>{{numberRef}}</p>
<p>{{name}} {{age}}</p>
</template>
<script>
// watchEffect 监听
import { reactive, ref, toRefs, watchEffect } from 'vue';
export default {
name: 'Watch',
setup() {
const numberRef = ref(100);
const state = reactive({
name: '章三',
age: 20
});
watchEffect(() => {
// 初始化时,一定会执行一次(收集要监听的数据)
console.log('hello watchEffect');
});
watchEffect(() => {
// 写了哪个就监听哪个
console.log('state.name', state.name);
});
watchEffect(() => {
console.log('state.age', state.age);
});
// watchEffect(() => {
// console.log('state.age', state.age);
// console.log('state.name', state.name);
// });
setTimeout(() => {
state.age = 25;
}, 1500);
setTimeout(() => {
state.name = '章三A';
}, 3000);
return {
numberRef,
...toRefs(state);
};
}
}
</script>
Composition API 如何实现逻辑复用
- 抽离逻辑代码到一个函数
- 函数命名约定为
useXxxx
格式(React Hooks
也是) - 在
setup
中引用useXxxx
函数
// useMousePosition.js
import { reactive, ref, onMounted, onUnmounted } from 'vue';
function useMousePosition() {
const x = ref(0);
const y = ref(0);
function update(e) {
x.value = e.pageX;
y.value = e.pageY;
}
onMounted(() => {
console.log('useMousePosition mounted');
window.addEventListener('mousemove', update);
});
onUnmounted(() => {
console.log('useMousePosition unMounted');
window.removeEventListener('mousemove', update);
});
return {
x,
y
};
}
// function useMousePosition2() {
// const state = reactive({
// x: 0,
// y: 0
// });
// function update(e) {
// state.x = e.pageX;
// state.y = e.pageY;
// }
// onMounted(() => {
// console.log('useMousePosition mounted');
// window.addEventListener('mousemove', update);
// });
// onUnmounted(() => {
// console.log('useMousePosition unMounted');
// window.removeEventListener('mousemove', update);
// });
// return state;
// }
export default useMousePosition;
// export default useMousePosition2;
<template>
<p>mouse position {{x}} {{y}}</p>
</template>
<script>
import useMousePosition from './useMousePosition';
// import useMousePosition2 from './useMousePosition';
export default {
name: 'MousePosition',
setup() {
const { x, y } = useMousePosition();
return {
x,
y
};
// const state = useMousePosition2();
// return {
// state
// };
}
}
</script>
Composition API 和 React Hooks 的对比
Composition API
的setup
只会被调用一次,而React Hooks
函数会被多次调用Composition API
无需useMemo
、useCallback
,因为setup
只调用一次Composition API
无需顾虑调用顺序,而React Hooks
需要保证hooks
的顺序一致Composition API
的 reacttive + ref
比React Hooks
的useState
要难理解
生命周期
vue3
给了两种生命周期的方式,可以用 Options API
,也可以用 Composition API
。
Options API 生命周期:
beforeDestroy
改为beforeUnmount
destroyed
改为unmounted
- 其他沿用
vue2
的生命周期 - 在
vue3
中可以使用vue2
的生命周期
<template>
<p>生命周期 {{msg}}</p>
</template>
<script>
export default {
name: 'LifeCycles',
props: {
msg: String
},
beforeCreate() {
console.log('beforeCreate')
},
created() {
console.log('created')
},
beforeMount() {
console.log('beforeMount')
},
mounted() {
console.log('mounted')
},
beforeUpdate() {
console.log('beforeUpdate')
},
updated() {
console.log('updated')
},
// beforeDestroy 改名
beforeUnmount() {
console.log('beforeUnmount')
},
// destroyed 改名
unmounted() {
console.log('unmounted')
}
}
</script>
Composition API 生命周期:
setup
:等于beforeCreate
和created
onBeforeMount
onMounted
onBeforeUpdate
onUpdated
onBeforeUnmount
onUnmounted
<template>
<p>生命周期 {{msg}}</p>
</template>
<script>
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'
export default {
name: 'LifeCycles',
props: {
msg: String
},
// 等于 beforeCreate 和 created
setup() {
console.log('setup')
onBeforeMount(() => {
console.log('onBeforeMount')
})
onMounted(() => {
console.log('onMounted')
})
onBeforeUpdate(() => {
console.log('onBeforeUpdate')
})
onUpdated(() => {
console.log('onUpdated')
})
onBeforeUnmount(() => {
console.log('onBeforeUnmount')
})
onUnmounted(() => {
console.log('onUnmounted')
})
},
}
</script>
原理
Proxy 实现响应式
背景
Object.defineProperty
的缺点:
- 深度监听,需要递归到底,一次性计算量很大
- 无法监听新增属性、删除属性(所以需要
Vue.set
和Vue.delete
这两个API
去 做这个事情) - 无法原生监听数组,需要特殊处理
Proxy 基本使用
Reflect
是和 Proxy
配合使用的。
proxy
可以看做是一个拦截器,会拦截对某个属性的操作;- 而
Reflect
是提供操作对象的api
。
Reflect
的作用:
- 和
Proxy
能力一一对应 - 规范化、标准化、函数式
const data = {
name: 'zhangsan',
age: 20
};
const proxyData = new Proxy(data, {
get(target, key, recevier) {
// get 方法和直接读取对象的属性实现的效果是一样的,
// 但直接读取对象的属性是一种魔法,不符合函数式编程的理念,
// 而 get 方法是一种基于底层的 API,更符合理念。
const result = Reflect.get(target, key, recevier);
console.log('get', key);
return result; // 返回结果
},
set(target, key, val, recevier) {
const result = Reflect.set(target, key, val, recevier);
console.log('set', key, val);
return result; // 是否设置成功 true / false
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key);
console.log('delete property', key);
return result; // 是否删除成功 true / false
}
});
// 测试监听数组
const data = ['a', 'b', 'c'];
const proxyData = new Proxy(data, {
get(target, key, receiver) {
// 只处理本身(非原型的)属性
const ownKeys = Reflect.ownKeys(target); // 获取不是原型的属性
if (ownKeys.includes(key)) {
console.log('get', key); // 监听
}
const result = Reflect.get(target, key, receiver);
return result; // 返回结果
},
set(target, key, val, receiver) {
// 重复的数据,不处理
if (val === target[key]) {
return true;
}
const result = Reflect.set(target, key, val, receiver);
console.log('set', key, val);
// console.log('result', result); // true
return result; // 是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key);
console.log('delete property', key);
// console.log('result', result) // true
return result; // 是否删除成功
}
});
vue3 如何实现响应式
使用 Proxy
实现响应式:能规避 Object.defineProperty
的问题。
- 深度监听,性能更好
- 可监听 新增/删除 属性
- 可监听数组变化
缺点:
Proxy
无法兼容所有浏览器,无法 polyfill
。
// 创建响应式
function reactive(target = {}) {
if (typeof target !== 'object' || target == null) {
// 不是对象或数组,则返回
return target;
}
// 代理配置
const proxyConf = {
get(target, key, receiver) {
// 只处理本身(非原型的)属性
const ownKeys = Reflect.ownKeys(target);
if (ownKeys.includes(key)) {
console.log('get', key); // 监听
}
const result = Reflect.get(target, key, receiver);
// 深度监听
return reactive(result);
},
set(target, key, val, receiver) {
// 重复的数据,不处理
if (val === target[key]) {
return true;
}
const ownKeys = Reflect.ownKeys(target);
if (ownKeys.includes(key)) {
console.log('已有的 key', key);
} else {
console.log('新增的 key', key);
}
const result = Reflect.set(target, key, val, receiver);
console.log('set', key, val);
// console.log('result', result); // true
return result; // 是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key);
console.log('delete property', key);
// console.log('result', result); // true
return result; // 是否删除成功
}
};
// 生成代理对象
const observed = new Proxy(target, proxyConf);
return observed;
}
// 测试数据
const data = {
name: 'zhangsan',
age: 20,
info: {
city: 'beijing',
a: {
b: {
c: {
d: {
e: 100
}
}
}
}
}
};
const proxyData = reactive(data);
Proxy
深度监听,性能如何提升的?
之前 Object.defineProperty
是一开始就进行递归了,没有任何条件;而 Proxy
是在 get
的时候才进行递归,获取到哪一层哪一层才触发响应式,没有获取到那一层的时候不会触发响应式。
编译优化
PatchFlag 静态标记
- 编译模板时,动态节点做标记
- 标记 分为不同的类型,如
TEXT
、PROPS
diff
算法时,可以区分静态节点,以及不同类型的动态节点
可以区分出动态和静态节点,静态节点就不用比较了,因为静态节点是不会变的,这样就可以优化 diff
算法。(从输入来源做的优化,并不是针对 diff
算法这个流程做的优化)
出处:https://coding.imooc.com/lesson/419.html
hoistStatic 静态提升
- 将静态节点的定义,提升到父作用域,缓存起来
- 多个相邻的静态节点,会被合并起来
- 典型的拿空间换时间的优化策略
以上三个静态节点定义到父作用域,以后这个函数怎么执行,都不会重新定义这个三个变量。(空间换时间)
如果相邻的静态节点很多,到一定程度后,就会合并起来做一个静态的集合,只定义一个变量。(不是空间换时间,类似于合并,提前帮我们做的操作)
cacheHandler 缓存事件
- 缓存事件
有缓存就去拿缓存,没有缓存就把这个事件定义并缓存。
SSR 优化
开启 ssr
后直接渲染字符串,没有经过 vdom
的一个转换。
Tree-shaking 优化
- 编译时,根据不同的情况,引入不同的
API
根据模板的指令,选择需要 import
的 api
,需要才引入
编译时尽可能的减少体积,从而进行优化
Vite
Vite 是什么?
Vite
是一个轻量级的、速度极快的构建工具。
浏览器的原生 ES Modules
能力允许在不将代码打包到一起的情况下运行 JavaScript
应用。Vite
的核心理念,就是借助浏览器原生 ES Modules
能力,当浏览器发出请求时,为浏览器按需提供 ES Module
文件,浏览器获取 ES Module
文件会直接执行。
Vite 为什么启动快?
- 开发环境使用
ES6 Module
,无需打包(非常快) - 生产环境使用
roolup
,并不会快很多
开发环境下,由于浏览器原生 ES6 Modules
的支持,当浏览器发出请求时,Vite
可以在不将源码打包为一个 Bundle
文件的情况下,将源码文件转化为 ES Modules
文件之后返回给浏览器。这样 Vite
的应用启动和热更新 HMR
时的速度都不会随着应用规模的增加而变慢。
ES6 module
基本使用
<!-- test1.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ES Module demo</title>
</head>
<body>
<p>基本演示</p>
<script type="module">
import add from './src/add.js';
const res = add(1, 2);
console.log('add res', res);
</script>
</body>
</html>
// print.js
export default function (a, b) {
console.log(a, b);
}
// add.js
import print from './print.js';
export default function add(a, b) {
print('print', 'add');
return a + b;
}
Vite
直接使用 ES6 Module
这种模块化的形式去执行;
Webpack
要把 ES6
打包成 ES5
。
在浏览器中的应用
// print.js
export default function (a, b) {
console.log(a, b)
}
// add.js
import print from './print.js'
export default function add(a, b) {
print('print', 'add')
return a + b
}
// math.js
export function add(a, b) {
return a + b
}
export function multi(a, b) {
return a * b
}
// index.js
import { add, multi } from './math.js'
console.log('add res', add(10, 20))
console.log('multi res', multi(10, 20))
外链
<!-- test2.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ES Module demo</title>
</head>
<body>
<p>外链</p>
<script type="module" src="./src/index.js"></script>
</body>
</html>
远程引用
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ES Module demo</title>
</head>
<body>
<p>远程引用</p>
<script type="module">
import { createStore } from 'https://unpkg.com/redux@latest/es/redux.mjs'
console.log('createStore', createStore)
</script>
</body>
</html>
动态引用
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ES Module demo</title>
</head>
<body>
<p>动态引入</p>
<button id="btn1">load1</button>
<button id="btn2">load2</button>
<script type="module">
document.getElementById('btn1').addEventListener('click', async () => {
const add = await import('./src/add.js')
const res = add.default(1, 2)
console.log('add res', res)
})
document.getElementById('btn2').addEventListener('click', async () => {
const { add, multi } = await import('./src/math.js')
console.log('add res', add(10, 20))
console.log('multi res', multi(10, 20))
})
</script>
</body>
</html>
Vite 和 Webpack 的区别
Webpack
:
- 支持的模块规范:
ES Modules
,CommonJS
和AMD Modules
Dev Server
:通过webpack-dev-server
托管打包好的模块- 生产环境构建:
webpack
Vite
:
- 支持的模块规范:
ES Modules
Dev Server
:原生ES Modules
,浏览器托管执行- 生产环境构建:
Rollup
Vue3 和 JSX
JSX
最早是 React
提出的概念。
JSX 基本使用
- 使用
.jsx
格式的文件和defineComponent
// child.jsx
import { defineComponent } from 'vue'
export default defineComponent({
props: ['a'],
setup(props) {
const render = () => {
return <p>Child {props.a}</p>
}
return render
}
})
<!-- Demo.vue -->
<template>
<p @click="changeFlag">Demo {{flagRef}}</p>
<child a="abc" v-if="flagRef"></child>
<ul>
<li v-for="item in state.list" :key="item">{{item}}</li>
</ul>
</template>
<script>
import { ref, reactive } from 'vue'
import Child from './Child'
export default {
name: 'Demo',
components: { Child },
setup() {
const flagRef = ref(true)
function changeFlag() {
flagRef.value = !flagRef.value
}
const state = reactive({
list: ['a', 'b', 'c']
})
return {
flagRef,
changeFlag,
state
}
}
}
</script>
<!-- Demo1.vue -->
<script>
import { ref } from 'vue'
import Child from './Child'
export default {
components: { Child },
setup() {
const countRef = ref(200)
const render = () => {
return <p>demo1 {countRef.value}</p> // jsx
}
return render
}
}
</script>
// Demo1.jsx
import { defineComponent, ref, reactive } from 'vue'
import Child from './Child'
export default defineComponent(() => {
const flagRef = ref(true)
function changeFlag() {
flagRef.value = !flagRef.value
}
const state = reactive({
list: ['a1', 'b1', 'c1']
})
const render = () => {
return <>
<p onClick={changeFlag}>demo1 {flagRef.value.toString()}</p>
{flagRef.value && <Child a={flagRef.value}></Child>}
<ul>
{state.list.map(item => <li>{item}</li>)}
</ul>
</>
}
return render
})
// 1. setup 函数
// 2. 组件的配置
JSX 和 template 的区别
-
语法上有很大区别:
JSX
本质上就是js
代码,可以使用js
的任何能力template
只能嵌入简单的js
表达式,其他需要指令,如v-if
JSX
已经成为ES
规范,template
还是Vue
自家规范
-
本质是相同的:
- 都会被编译为
js
代码(render
函数)
- 都会被编译为
-
具体示例:插值,自定义组件,属性和事件,条件和循环
JSX 和 Slot
- slot 是 Vue 发明的概念,为了完善 template 的能力
- slot 对初学者难理解,特别是作用域插槽
- 但使用 JSX 将很容易理解,因为 JSX 本质就是 js
vue3 template 实现 tabs
<!-- Demo.vue -->
<template>
<tabs default-active-key="1" @change="onTabsChange">
<tab-panel key="1" title="title1">
<div>tab panel content 1</div>
</tab-panel>
<tab-panel key="2" title="title2">
<div>tab panel content 2</div>
</tab-panel>
<tab-panel key="3" title="title3">
<div>tab panel content 3</div>
</tab-panel>
</tabs>
</template>
<script>
import Tabs from './Tabs'
import TabPanel from './TabPanel'
export default {
components: { Tabs, TabPanel },
methods: {
onTabsChange(key) {
console.log('tab changed', key)
}
},
}
</script>
<!-- TabPanel.vue -->
<template>
<slot></slot>
</template>
<script>
export default {
name: 'TabPanel',
props: ['key', 'title'],
}
</script>
<!-- Tabs.vue -->
<template>
<div>
<!-- tabs 头,按钮 -->
<button
v-for="titleInfo in titles"
:key="titleInfo.key"
:style="{ color: titleInfo.key === actKey ? 'blue' : '#333' }"
@click="changeActKey(titleInfo.key)"
>
{{titleInfo.title}}
</button>
</div>
<slot></slot>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'Tabs',
props: ['defaultActiveKey'],
emits: ['change'],
setup(props, context) {
const children = context.slots.default()
const titles = children.map(panel => {
const { key, title } = panel.props || {}
return {
key,
title
}
})
// 当前 actKey
const actKey = ref(props.defaultActiveKey)
function changeActKey(key) {
actKey.value = key
context.emit('change', key)
}
return {
titles,
actKey,
changeActKey
}
}
}
</script>
jsx 实现 tabs
<!-- Demo.vue -->
<template>
<tabs default-active-key="1" @change="onTabsChange">
<tab-panel key="1" title="title1">
<div>tab panel content 1</div>
</tab-panel>
<tab-panel key="2" title="title2">
<div>tab panel content 2</div>
</tab-panel>
<tab-panel key="3" title="title3">
<div>tab panel content 3</div>
</tab-panel>
</tabs>
</template>
<script>
import Tabs from './Tabs.jsx'
import TabPanel from './TabPanel'
export default {
components: { Tabs, TabPanel },
methods: {
onTabsChange(key) {
console.log('tab changed', key)
}
},
}
</script>
<!-- TabPanel.vue -->
<template>
<slot></slot>
</template>
<script>
export default {
name: 'TabPanel',
props: ['key', 'title'],
}
</script>
// Tabs.jsx
import { defineComponent, ref } from 'vue'
export default defineComponent({
name: 'Tabs',
props: ['defaultActiveKey'],
emits: ['change'],
setup(props, context) {
const children = context.slots.default()
const titles = children.map(panel => {
const { key, title } = panel.props || {}
return {
key,
title
}
})
// 当前 actKey
const actKey = ref(props.defaultActiveKey)
function changeActKey(key) {
actKey.value = key
context.emit('change', key)
}
// jsx
const render = () => <>
<div>
{/* 渲染 buttons */}
{titles.map(titleInfo => {
const { key, title } = titleInfo
return <button
key={key}
style={{ color: actKey.value === key ? 'blue' : '#333' }}
onClick={() => changeActKey(key)}
>{title}</button>
})}
</div>
<div>
{children.filter(panel => {
const { key } = panel.props || {}
if (actKey.value === key) return true // 匹配上 key ,则显示
return false // 否则,隐藏
})}
</div>
</>
return render
}
})
vue3 template 实现 作用域插槽
<!-- Child.vue -->
<template>
<p>child</p>
<slot :msg="msg"></slot>
</template>
<script>
export default {
data() {
return {
msg: '作用域插槽 Child'
}
}
}
</script>
<!-- Demo.vue -->
<template>
<child>
<!-- <p>scoped slot</p> -->
<template v-slot:default="slotProps">
<p>msg: {{slotProps.msg}} 123123</p>
</template>
</child>
</template>
<script>
import Child from './Child'
export default {
components: { Child }
}
</script>
jsx 实现 作用域插槽
// Child.jsx
import { defineComponent, ref } from 'vue'
export default defineComponent({
props: ['render'],
setup(props) {
const msgRef = ref('作用域插槽 Child - JSX')
return () => {
return <p>{props.render(msgRef.value)}</p>
}
}
})
// Demo.jsx
import { defineComponent } from 'vue'
import Child from './Child'
export default defineComponent(() => {
function render(msg) {
return <p>msg: {msg} 123123</p>
}
return () => {
return <>
<p>Demo - JSX</p>
<Child render={render}></Child>
</>
}
})
Vue3 script setup (3.2 发布)
Vue3
引入了Composition API
Composition API
最重要的是setup
函数<script>
只有一个setup
函数,显得有点多此一举,所以使用script setup
进行简化
基本使用
- 顶级变量、自定义组件,可直接用于模板
- 可正常使用
ref
、reactive
、computed
的能力 - 和其他
<script>
同时使用
<!-- Demo.vue -->
<script>
function add(a, b) { return a + b }
</script>
<script setup>
import { ref, reactive, toRefs, onMounted } from 'vue'
const countRef = ref(100)
function addCount() {
countRef.value++
}
const state = reactive({
name: '双越'
})
const { name } = toRefs(state)
console.log( add(10, 20) )
</script>
<template>
<p @click="addCount">{{countRef}}</p>
<p>{{name}}</p>
</template>
属性和事件
defineProps
:定义属性defineEmits
:定义事件
<script setup>
<!-- Child2.vue -->
import { defineProps, defineEmits } from 'vue'
// 定义属性
const props = defineProps({
name: String,
age: Number
})
// 定义事件
const emit = defineEmits(['change', 'delete'])
function deleteHandler() {
emit('delete', 'aaa')
}
</script>
<template>
<p>Child2 - name: {{props.name}}, age: {{props.age}}</p>
<button @click="$emit('change', 'bbb')">change</button>
<button @click="deleteHandler">delete</button>
</template>
<!-- Demo.vue -->
<script>
function add(a, b) { return a + b }
</script>
<script setup>
import { ref, reactive, toRefs, onMounted } from 'vue'
import Child2 from './Child2'
const countRef = ref(100)
function addCount() {
countRef.value++
}
const state = reactive({
name: '章三'
})
const { name } = toRefs(state)
console.log( add(10, 20) )
function onChange(info) {
console.log('on change', info)
}
function onDelete(info) {
console.log('on delete', info)
}
</script>
<template>
<p @click="addCount">{{countRef}}</p>
<p>{{name}}</p>
<child-2 :name="name" :age="countRef" @change="onChange" @delete="onDelete"></child-2>
<hr>
</template>
defineExpose
- 暴露数据给父组件
<!-- Child3.vue -->
<script setup>
import { ref, defineExpose } from 'vue'
const a = ref(101)
const b = 201
defineExpose({
a,
b
})
</script>
<template>
<p>Child3</p>
</template>
<!-- Demo.vue -->
<script setup>
import { ref, reactive, toRefs, onMounted } from 'vue'
import Child3 from './Child3'
const child3Ref = ref(null)
onMounted(() => {
// 拿到 Child3 组件的一些数据
console.log(child3Ref.value)
console.log(child3Ref.value.a)
console.log(child3Ref.value.b)
})
</script>
<template>
<child-3 ref="child3Ref"></child-3>
</template>
FQA
-
Vue3
比Vue2
有什么优势?- 性能更好
- 体积更小
- 更好的
ts
支持 - 更好的代码组织
- 更好的逻辑抽离
- 更多新功能
-
描述
vue3
的生命周期。Options API
生命周期:beforeDestroy
改为beforeUnmount
destroyed
改为unmounted
- 其他沿用
vue2
的生命周期
Composition API
生命周期:setup
:等于beforeCreate
和created
onBeforeMount
onMounted
onBeforeUpdate
onUpdated
onBeforeUnmount
onUnmounted
-
如何看待
Composition API
和Options API
?composition API
带来了什么?- 更好的代码组织
- 更好的逻辑复用
- 更好的类型推导
- 如何选择?
- 不建议共用,会引起混乱
- 小型项目、业务逻辑简单,用
Options API
- 中大型项目、逻辑复杂,用
Composition API
- 别误解
Composition API
Composition API
属于高阶技巧,不是基础必会Composition API
是为解决复杂业务逻辑而设计Composition API
就像Hooks
在React
中的地位
-
如何理解
ref
、toRef
和toRefs
?ref
- 生成值类型的响应式数据
- 可用于模板和
reactive
- 通过
.value
修改值 - 可获取
dom
节点
toRef
- 针对一个
reactive
封装的响应式对象的 属性 - 常见一个
ref
,具有响应式 - 两者保持引用关系
- 针对一个
toRefs
- 将
reactive
封装的响应式对象转换成普通对象 - 对象的每个属性都是对应的
ref
- 两者保持引用关系
- 合成函数返回响应式对象
- 将
-
关于
ref
为什么是一个对象。
因为Object.defineProperty
、proxy
都只能监听复杂数据类型,只不过vue2
中是放在data
中进行监听,而vue3
不存在这样一个数据收集的地方,所以只能单个去收集数据。本质上vue2
、vue3
都是需要外面“包裹”一层对象作为被监听数据的“承载容器”来进行defineProperty
、proxy
监听。 -
为什么需要
ref
?
不管是Object.definePropety
还是proxy
,都是用来监听复杂数据类型的,object
或array
,不能对简单数据类型string,number,boolean
这些的变化进行直接监听。vue2
中是把监听内容放在data
的返回对象中,在初始化的时候使用Object.definePropety
对该对象进行监听,所以在对data
的返回对象新添加数据的时候,需要使用特殊方法,而不能直接this.xxx
这样添加,因为vue2
这时候不会对该对象进行重新监听,数据会不具有响应式特性。
而vue3
中没有vue2
这种一个特定地方(即vue2
中的data
函数返回的那个对象)去对所有数据进行统一监听处理,所以我们需要告诉vue3
,哪些是我们需要使用响应式属性的数据。本质上ref
和reactive
是在收集这些数据。关于ref
,因为proxy
无法直接监听简单数据类型,所以需要包裹成一个对象的形式来进行监听,proxy
就是去监听ref
定义的这个对象,以此来监听ref.value
这个值的变化。 -
vue3
升级了哪些重要功能?- 由原来的
new Vue()
变成createApp
创建实例 emits
属性:在子组件中使用emits
属性声明父组件传递的事件- 多事件处理:
v-on
可以传递多个事件 Fragment
:vue2
模板里面只能是单一节点;vue3
里面可以是多个节点- 移除
.sync
改为v-model
参数 - 改变了异步组件的引用方式:使用
defineAsyncComponent
- 移除了
filter
- 新增
Teleport
可以把弹窗放到组件外面去 - 新增
Suspense
:具有深层异步依赖的组件,使用Suspense
,在初始渲染时,Suspense
将在内存中渲染其默认的插槽内容 - 新增了
Composition API
- 生命周期:提供两种生命周期方式,
Options API
和Composition API
;Options API
将vue2
的beboreDestroy
和destroyed更名为beforeUnmount
和unmounted
,其余沿用vue2
的生命周期;Composition API
生命周期包括onBeforeMount、onMounted、onBeforeUpdate、onUpdated、onBeforeUnmout、onUnmounted
,setup
等于beforeCreate
和created
。
- 由原来的
-
Composition API
如何实现代码逻辑复用?- 抽离逻辑代码到一个函数
- 函数命名约定为
useXxxx
格式(React Hooks
也是) - 在
setup
中引用useXxxx
函数
-
Vue3
如何实现响应式?
使用Proxy
实现响应式:- 深度监听,性能更好:在
get
中进行递归,获取到哪一层才进行递归。 - 可监听 新增/删除 属性:有
deleteProperty
属性监听删除;可在set
中判断出是已有属性还是新增属性。 - 可监听数组变化
缺点:
Proxy
无法兼容所有浏览器,无法polyfill
。
- 深度监听,性能更好:在
-
watch
和watchEffect
的区别是什么?- 两者都可监听
data
属性变化 watch
需要明确监听哪个属性watchEffect
会根据其中的属性,自动监听其变化
- 两者都可监听
-
setup
中如何获取组件实例?- 在
setup
和其他Composition API
中没有this
- 可通过
getCurrentInstance
获取当前实例
- 在
-
Vue3
为何比Vue2
快?-
Proxy
实现响应式:
能规避Object.defineProperty
的问题。- 深度监听,性能更好
- 可监听 新增/删除 属性
- 可监听数组变化
-
PatchFlag
静态标记:- 编译模板时,动态节点做标记
- 标记 分为不同的类型,如
TEXT
、PROPS
diff
算法时,可以区分静态节点,以及不同类型的动态节点
-
hoistStatic
静态提升:
将静态节点提升到父作用域,只定义一次。 -
cacheHandler
缓存事件:
有缓存就去取缓存,没有缓存就定义函数并缓存,不用每次都去定义函数。 -
SSR
优化:
开启ssr
后直接渲染字符串,不用经过vdom
的一个转换。 -
Tree-shaking
优化:
编译时,根据不同的情况,引入不同的API
,尽可能的减少使体积
-
-
Vite
是什么?
Vite
是一个轻量级的、速度极快的构建工具。
Vite
是旨在提升开发者体验的下一代JavaScript
构建工具,核心借助了浏览器的原生ES Modules
和像esbuild
这样的将代码编译成native code
的打包工具。极大的降低了应用的启动和热更新时间。 -
Composition API
和React Hooks
的对比。Composition API
的setup
只会被调用一次,而React Hooks
函数会被多次调用Composition API
无需useMemo
、useCallback
,因为setup
只调用一次Composition API
无需顾虑调用顺序,而React Hooks
需要保证hooks
的顺序一致Composition API
的 reacttive + ref
比React Hooks
的useState
要难理解