Vue3 快速入手
新特性:
- Performance:性能比 Vue2.x 快 1.2~2倍;
- Tree shaking support:按需编译,体积比 Vue2.x 更小;
- Composition API:组合API(类似 React Hooks);
- Better typeScript support:更好的 TS 支持;
- Custom Render API:暴露了自定义渲染 API;
- Fragment、Teleport(Protal)、Suspense:更先进的组件
Vue 3.0 是如何变快的:Performance
diff算法 的优化:
- Vue2.x中的vdom是进行全量的对比;
- Vue3.x新增了静态标记(PatchFlag):在与上次虚拟节点进行对比的时候,只对比带有 patch flag 的节点,并且可以通过 flag 的信息得知当前节点要对比的具体内容;
- 编译模板时,动态节点会被标记,并且将标记分为不同的类型;在 diff算法 时,可以区分静态节点,以及不同类型的动态节点
释义:在创建 vdom 的时候,会根据 DOM 中的内容会不会发生变化(即: DOM 中的内容是否为动态),添加静态标记!
案例:
<div>
<p>AHu</p>
<p>AHu</p>
<p>AHu</p>
<p>{{msg}}</p>
</div>
转变为:
export function render(_ctx, _cache, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("p", null, "AHu"),
_createVNode("p", null, "AHu"),
_createVNode("p", null, "AHu"),
_createVNode("p", null, _toDisplayString(_ctx.msg) + "}", 1 /* TEXT */)
]))
}
hoistStatic 静态提升:
定义:
- 将 静态节点 的定义,提升到父作用域,缓存起来;(空间换时间)
- 多个相邻的 静态节点,会被合并;(编译优化)
- 典型的拿空间换时间的优化策略
- Vue2.x中无论元素是否参与更新,每次都会重建,然后再渲染;
- Vue3.x中对于不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停地复用
关键字:静态提升
案例:
<div>
<p>AHu</p>
<p>AHu</p>
<p>AHu</p>
<p>{{msg}}</p>
</div>
静态提升之前:
export function render(_ctx, _cache, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("p", null, "AHu"),
_createVNode("p", null, "AHu"),
_createVNode("p", null, "AHu"),
_createVNode("p", null, _toDisplayString(_ctx.msg) + "}", 1)
]))
}
静态提升之后:
const _hoisted_1 = _createVNode("p", null, "AHu", -1)
const _hoisted_2 = _createVNode("p", null, "AHu", -1)
const _hoisted_3 = _createVNode("p", null, "AHu", -1)
export function render(_ctx, _cache, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_hoisted_1, // 直接复用
_hoisted_2, // 直接复用
_hoisted_3, // 直接复用
_createVNode("p", null, _toDisplayString(_ctx.msg) + "}", 1)
]))
}
cacheHandlers 事件侦听器缓存:
- 默认情况下,onClick 会被视为 动态绑定,因此每次都会去追踪它的变化!如果是同一个函数,就没有必要追踪变化,直接缓存起来复用即可
案例:
<div>
<button @click="onClick">按钮</button>
</div>
事件侦听之前:
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("button", { onClick: _ctx.onClick }, "按钮", 8 /* PROPS */, ["onClick"])
]))
}
事件侦听之后:
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("button", {
onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.onClick && _ctx.onClick(...args))) // 有 _cache[0] 就用 _cache[0],没有就 重新定义一个
}, "按钮")
]))
}
注意:转换之后的代码中,静态标记 patch flag 已消失,则不需要再追踪对比
SSR渲染:
- 静态节点直接输出,绕过了 vdom:
当有大量静态的内容时候,这些内容会被当作纯字符串推进一个 buffer 里面,即使存在动态的绑定,会通过模板插值嵌入进去。这样一来,会比通过 vdom 来渲染的快上许多;
当静态内容大到一定量级的时候,会用_createStaticVNode 方法在客户端去生成一个 static node,这些 静态node,会被直接 innerHtml,就不需要创建对象,然后根据对象渲染
案例:
<div>
<span>AHu</span>
<span>AHu</span>
<span>AHu</span>
<span :id="1">{{msg}}</span>
<button @click="btnHandler">按钮</button>
</div>
import { mergeProps as _mergeProps } from "vue"
import { ssrRenderAttr as _ssrRenderAttr, ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "vue/server-renderer"
export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
const _cssVars = { style: { color: _ctx.color }}
_push(`<div${
_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))
}><span>AHu</span><span>AHu</span><span>AHu</span><span${
_ssrRenderAttr("id", 1)
}>${
_ssrInterpolate(_ctx.msg)
}</span><button>按钮</button></div>`)
}
Tree shaking support:
- 编译时,根据不同的情况,引入不同的 API
Composition API:
关于 Vite 和 ES Module:
Vite:
Vite 是 Vue 作者开发的一款意图取代 webpack 的工具;
原理:利用 ES6 的 import 会发送请求去加载文件的特性,拦截这些请求,做一些预编译,省去 webpack 冗长的打包时间
安装 Vite:
npm install -g create-vite-app
利用 Vite 创建 Vue3 项目“
create-vite-app projectName
安装依赖,运行项目:
cd projectName
npm install
npm run dev
为什么 Vite 启动快:
- 开发环境使用 ES6 Module,无需打包 —— 非常快;
- 生产环境使用 rollup,并不会快很多
ES Module 在浏览器中的应用:
<script type="module">
// ...
</script>
基本使用:
<script type="module">
import { add, multi } from './src/math.js'
console.log('add res', add(10, 20))
console.log('multi res', multi(10, 20))
</script>
外链形式使用:
<script type="module" src="./src/index.js"></script>
远程引用:
<script type="module">
import { createStore } from 'https://unpkg.com/...'
console.log('createStore', createStore)
</script>
动态引入:
<body>
<p>动态引入</p>
<button id="btn1">load_1</button>
<button id="btn2">load_2</button>
</body>
<script type="module">
document.getElementById('btn1').addEventListener('click', async () => {
const add = await import('./src/add.js')
console.log('add', add) // 注意:方法在 default 中
const res = add.default(1, 2)
console.log('add res', res)
})
document.getElementById('btn1').addEventListener('click', async () => {
const { add, multi } = await import('./src/math.js')
console.log(add, multi)
console.log('add res', add(10, 20))
console.log('multi res', multi(10, 20))
})
</script>
Composition API:
在 Vue2.x 中,如果需要新增功能:
1、需要在 data 属性中 新增数据;
2、需要在 methods / watch / computed 中 新增业务逻辑
弊端:数据 和 业务逻辑 分散,导致代码不便于管理和维护
在 Vue3.x 中,提供了 setup() (Composition API 的入口函数):
<template>
<div>
<p>{{count}}</p>
<button @click="myFn">按钮</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'App',
// setup() 是 Composition API 的入口函数
setuo() {
// let count = 0
// 定义了一个名称叫做 count 的变量,这个变量的初始值为 0
// 这个变量发生改变之后,Vue 会自动更新
let count = ref(0) // 是一个对象
// 在 Composition API 中,如果想定义方法,不用定义到 methods 中,直接定义即可
function myFn() {
// console.log(count.value)
count.value += 1
}
// 注意:在 Composition API 中定义的变量/方法,要想在外界使用,必须通过 return {xxx, xxx} 暴露出去
return { count, myFn }
}
}
</script>
注意:
- ref函数 只能监听 简单类型 的变化,不能监听 复杂类型(数组/对象) 的变化
- 复杂类型 的变化需要通过 reactive() 进行监听
案例:
<template>
<div>
<ul>
<li v-for="(stu, index) in state.stus" :key="stu.id" @click="remStu(index)">{{stu.name}} - {{stu.age}}</li>
</ul>
</div>
</template>
<script>
import { reactive } from 'vue'
export default {
name: 'App',
setup() {
let state = reactive({
stus: [
{ id: 1, name: 'zs', age: 10 },
{ id: 2, name: 'ls', age: 20 },
{ id: 3, name: 'ww', age: 30 }
]
})
function remStu(index) {
// 注意:此处不再是 this,而是 state
state.stus = state.stus.filter((stu, idx) => idx !== index)
}
return { state, remStu }
}
}
</script>
利用 Composition API 进行优化:
将 各个功能的 数据 和 业务逻辑 独立开来
<template>
<div>
<form>
<input type="text" v-model="state2.stu.id">
<input type="text" v-model="state2.stu.name">
<input type="text" v-model="state2.stu.age">
<input type="submit" @click="addStu">
</form>
<ul>
<li v-for="(stu, index) in state.stus" :key="stu.id" @click="remStu(index)">{{stu.name}} - {{stu.age}}</li>
</ul>
</div>
</template>
<script>
import { reactive } from 'vue'
export default {
name: 'App',
setup() {
// 移除学生的数据
let { state, remStu } = useRemoveStudent()
// 添加学生的数据
// 注意:外界传值
let { state2, addStu } = useAddStudent(state)
return { state, remStu, state2, addStu }
}
}
// 移除学生的业务逻辑
function useRemoveStudent() {
let state = reactive({
stus: [
{ id: 1, name: 'zs', age: 10 },
{ id: 2, name: 'ls', age: 20 },
{ id: 3, name: 'ww', age: 30 }
]
})
function remStu(index) {
// 注意:此处不再是 this,而是 state
state.stus = state.stus.filter((stu, idx) => idx !== index)
}
return { state, remStu }
}
// 添加学生的业务逻辑
function useAddStudent(state) {
let state2 = reactive({
stu: {
id: '',
name: '',
age: ''
}
})
function addStu(e) {
e.preventDefault();
const stu = Object.assign({}, state2.stu)
state.stus.push(stu)
state2.stu.id = ''
state2.stu.name = ''
state2.stu.age = ''
}
return { state2, addStu }
}
</script>
还可以将 业务逻辑 独立成为一个单独的 JS文件:
例如:
// Step1: 注意 依赖
import { reactive } from 'vue'
// 添加学生的业务逻辑
function useAddStudent(state) {
let state2 = reactive({
stu: {
id: '',
name: '',
age: ''
}
})
function addStu(e) {
e.preventDefault();
const stu = Object.assign({}, state2.stu)
state.stus.push(stu)
state2.stu.id = ''
state2.stu.name = ''
state2.stu.age = ''
}
return { state2, addStu }
}
// Step2: 重要 暴露出去
export default useAddStudent
// Step3: 然后在 数据 的文件中,引入该文件!!!
Composition API 和 Option API 可以混合使用:
案例:
<template>
<div>
<p>{{name}}</p>
<button @click="myFn1">按钮</button>
<p>{{age}}</p>
<button @click="myFn2">按钮</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'App',
data: function() {
return {
name: 'AHu'
}
},
methods: {
myFn1() {
alert('abc')
}
},
setup() {
let age = ref(24)
function myFn2() {
alert('welcome')
}
return { age, myFn2 }
}
}
</script>
Composition API (组合 API / 注入 API)的本质:
Vue 在运行时,将 Composition API 当中暴露出去的数据 注入 到 Option API 中
setup() 的执行时机 和 注意点:
setup() 的执行时机:
- 在生命周期中的 beforeCreate 和 Created 之间:
beforeCreate:组件刚被创建,data 和 methods 还未初始化完成
setup
Created:组件刚被创建,data 和 methods 初始化完成
- 生命周期中,setup 代替了 beforeCreate 和 Created
setup() 的注意点:
- 由于在执行 setup() 时,还没有执行 Created 生命周期方法,因此在 setup函数中,无法使用 data 和 methods;
- 由于不能在 setup() 中使用 data 和 methods,因此 Vue 为了避免错误地使用,它直接将 setup() 中的 this 修改成了 undefined;
- setup() 只能是 同步 的,不能是 异步 的;
- setup 和 其他的 Composition API 中没有 this
在 Options API 中,可以照常使用 this
在 Composition API 中,可通过 getCurrentInstance() 获取当前实例
<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) // undefined
console.log('instance', instance) // x 1
})
const instance = getCurrentInstance()
console.log('instance', instance)
// 注意 setup 在 生命周期 中的位置
console.log('x', instance.data.x) // undefined
}
}
</script>
setup() 的本质:
将传入的数据包装成一个 Proxy 对象
reactive 的 定义 和 注意点:
reactive 的定义:
reactive 是 Vue3.x 中提供的实现响应式数据的方法:
在 Vue2.x 中,响应式数据是通过 Object.defineProperty 来实现的;而在 Vue3.x 中,响应式数据是通过 ES6 的 proxy 来实现的
reactive 的注意点:
1、reactive 的参数必须是 对象(json/arr);
2、如果给 reactive 传递了其他对象,在默认情况下,界面不会自动更新;如果想更新,可以通过重新赋值的方式
案例:
<template>
<div>
<p>{{state.time}}</p>
<button @click="myFn">按钮</button>
</div>
</template>
<script>
import { reactive } from 'vue'
export default{
name: 'App',
setup() {
let state = reactive({
time: new Date()
})
function myFn() {
// 直接修改以前的,界面不会更新
// state.time.setDate(state.time.getDate() + 1)
// 重新赋值
const newTime = new Date(state.time.getTime())
newTime.setDate(state.time.getDate() + 1)
state.time = newTime
console.log(state.time)
}
return { state, myFn }
}
}
</script>
ref 的 定义 和 本质 和 注意点:
ref 的定义:
ref 和 reactive 一样,也是用来实现响应式数据的方法:
但是 reactive 必须传递一个 对象,因此在企业开发中只想让某个变量实现响应式的时候会非常麻烦;
因此,Vue3.x 提供了 ref 方法,实现对 简单值 的 监听
ref 的本质:
ref 的本质其实还是 reactive:
当我们给 ref() 传值之后,ref() 底层逻辑会自动将 ref() 转换成 reactive(),即:ref(24) -> reactive({value: 24})
因此,在为 ref() 绑定的标签赋值时,需要赋值在 value 属性上:
<template>
<div>
<p>{{age}}</p>
<button @click="myFn">按钮</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'App',
setup() {
let age = ref(24)
function myFn() {
age.value = 18
console.log(age)
}
return { age, myFn }
}
}
</script>
ref 的注意点:
如果是通过 ref() 创建的数据,那么在 template 中使用的时候不需要再通过 .value 来获取!因为 Vue 会自动给我们添加 .value
ref 和 reactive 的区别:
如果在 template 里使用的是 ref 类型的数据,那么 Vue 会自动帮我们添加 .value;如果使用的是 reactive 类型的数据,Vue 不会自动添加 .value
Vue在解析数据之前,会自动判断这个数据是否为 ref 类型:
通过当前数据的 __v_ref 来判断:
如果有这个私有的属性,并且取值为 true,那么就代表是一个 ref 类型的数据
自主判断:
关键 API:isRef 和 isReactive
<template>
<div>
<p>{{age}}</p>
<button @click="myFn">按钮</button>
</div>
</template>
<script>
import { isRef, isReactive } from 'vue'
// import { ref } from 'vue'
import { reactive } from 'vue'
export default {
name: 'App',
setup() {
// let age = ref(24)
let age = reactive({value; 24})
function myFn() {
console.log(isRef(age)) // false
console.log(isReactive(age)) // true
}
return { age, myFn }
}
}
</script>
递归监听 与 非递归监听:
递归监听:
递归监听 的定义:
默认情况下,无论是通过 ref 还是 reactive 都是递归监听
<template>
<div>
<p>{{state.value.a}}</p>
<p>{{state.value.gf.b}}</p>
<p>{{state.value.gf.f.c}}</p>
<p>{{state.value.gf.f.s.d}}</p>
<button @click="myFn">按钮</button>
</div>
</template>
<script>
import { ref } from 'vue'
let state = ref({
a: 'a',
gf: {
b: 'b',
f: {
c: 'c',
s: {
d: 'd'
}
}
}
})
function myFn() {
state.value.a = '1'
state.value.gf.b = '2'
state.value.gf.f.c = '3'
state.value.gf.f.s.d = '4'
}
return { state, myFn }
</script>
递归监听 存在的问题:
如果数据量比较大,会非常消耗性能:
将数据进行递归,取出每一层级的值,然后将其包装成为 Proxy 对象
非递归监听:shallowRef 和 shallowReactive
定义:
只能监听 第一层级 的数据,无法深度监听更深层级的数据:
只要 第一层级 的数据发生了改变,就会更新 UI(第一层级 没变化,则不会更新 UI)
<template>
<div>
<p>{{state.a}}</p>
<p>{{state.gf.b}}</p>
<p>{{state.gf.f.c}}</p>
<p>{{state.gf.f.s.d}}</p>
<button @click="myFn">按钮</button>
</div>
</template>
<script>
import { shallowReactive } from 'vue'
let state = shallowReactive({
a: 'a',
gf: {
b: 'b',
f: {
c: 'c',
s: {
d: 'd'
}
}
}
})
function myFn() {
state.a = '1'
state.gf.b = '2'
state.gf.f.c = '3'
state.gf.f.s.d = '4'
console.log(state) // Proxy 对象
console.log(state.gf) // 普通对象
console.log(state.gf.f) // 普通对象
console.log(state.gf.f.s) // 普通对象
}
return { state, myFn }
</script>
<template>
<div>
<p>{{state.value.a}}</p>
<p>{{state.value.gf.b}}</p>
<p>{{state.value.gf.f.c}}</p>
<p>{{state.value.gf.f.s.d}}</p>
<button @click="myFn">按钮</button>
</div>
</template>
<script>
import { shallowRef } from 'vue'
let state = shallowRef({
a: 'a',
gf: {
b: 'b',
f: {
c: 'c',
s: {
d: 'd'
}
}
}
})
function myFn() {
state.value.a = '1'
state.value.gf.b = '2'
state.value.gf.f.c = '3'
state.value.gf.f.s.d = '4'
console.log(state) // 被包装的对象
console.log(state.value) // 普通对象
console.log(state.value.gf) // 普通对象
console.log(state.value.gf.f) // 普通对象
console.log(state.value.gf.f.s) // 普通对象
}
return { state, myFn }
</script>
非递归监听的 注意点:
如果是通过 shallowRef 创建数据,那么 Vue 监听的是 .value 的变化,并不是第一层的变化
<template>
<div>
<p>{{state.value.a}}</p>
<p>{{state.value.gf.b}}</p>
<p>{{state.value.gf.f.c}}</p>
<p>{{state.value.gf.f.s.d}}</p>
<button @click="myFn">按钮</button>
</div>
</template>
<script>
import { shallowRef } from 'vue'
let state = shallowRef({
a: 'a',
gf: {
b: 'b',
f: {
c: 'c',
s: {
d: 'd'
}
}
}
})
function myFn() {
// state.value.a = '1'
// state.value.gf.b = '2'
// state.value.gf.f.c = '3'
// state.value.gf.f.s.d = '4'
state.value = {
a: '1',
gf: {
b: '2',
f: {
c: '3',
s: {
d: '4'
}
}
}
}
console.log(state) // 被包装的对象
console.log(state.value) // 普通对象
console.log(state.value.gf) // 普通对象
console.log(state.value.gf.f) // 普通对象
console.log(state.value.gf.f.s) // 普通对象
}
return { state, myFn }
</script>
如果只想修改某一层的数据:使用 triggerRef
triggerRef:
根据传入的数据,主动更新界面
<template>
<div>
<p>{{state.value.a}}</p>
<p>{{state.value.gf.b}}</p>
<p>{{state.value.gf.f.c}}</p>
<p>{{state.value.gf.f.s.d}}</p>
<button @click="myFn">按钮</button>
</div>
</template>
<script>
import { shallowRef, triggerRef } from 'vue'
let state = shallowRef({
a: 'a',
gf: {
b: 'b',
f: {
c: 'c',
s: {
d: 'd'
}
}
}
})
function myFn() {
state.value.gf.f.s.d = '4'
triggerRef(state)
console.log(state) // 被包装的对象
console.log(state.value) // 普通对象
console.log(state.value.gf) // 普通对象
console.log(state.value.gf.f) // 普通对象
console.log(state.value.gf.f.s) // 普通对象
}
return { state, myFn }
</script>
注意点:
Vue3.x 只提供了 triggerRef 的方法,并没有提供 triggerReactive 的方法!因此,如果是 reactive 类型的数据,无法主动触发界面更新
应用场景:
1、一般情况下,使用 ref 和 reactive 即可;
2、当需要监听的数据量比较大的时候,才能使用 shallowRef 和 shallowReactive
shallowRef 的本质:
shallowRef 的底层逻辑是 shallowReactive:
即:shallowRef(10) -> shallowReactive({value: 10})
因此,如果是通过 shallowRef 创建的数据,它监听的是 .value 的变化!因为底层本质上 value 才是第一层
let state2 = shallowRef({
a: 'a',
gf: {
b: 'b',
f: {
c: 'c',
s: {
d: 'd'
}
}
}
})
等同于:
let state2 = shallowReactive({
value: {
a: 'a',
gf: {
b: 'b',
f: {
c: 'c',
s: {
d: 'd'
}
}
}
}
})
toRaw:
前置知识:数据的引用关系
<template>
<div>
<p>{{state}}</p>
<button @click="myFn">按钮</button>
</div>
</template>
<script>
import { reactive } from 'vue'
export default {
name: 'App',
setup() {
let obj = {name: 'AHu', age: 24}
let state = reactive(obj)
console.log(obj === state) // false
function myFn() {
obj.name = 'zs' // 数据会发生变化,但页面不更新
console.log(obj)
console.log(state)
// state.name = 'zs' // 数据会发生变化,同时页面更新
// console.log(state)
}
return { state, myFn }
}
}
</script>
state 和 obj 的关系:
【引用关系】state 的本质是一个 Proxy 对象,在这个 Proxy 对象中引用了 obj
state 和 obj 关系的注意点:
- 如果直接修改 obj,那么无法触发界面更新;
- 只有通过包装之后的对象来修改,才会触发界面更新
toRaw 方法:
定义:
从 ref 或 reactive 中得到 原始数据
<template>
<div>
<p>{{state}}</p>
<button @click="myFn">按钮</button>
</div>
</template>
<script>
import { reactive, toRaw } from 'vue'
export default {
name: 'App',
setup() {
let obj = {name: 'AHu', age: 24}
let state = reactive(obj)
let obj2 = toRaw(state)
console.log(obj2 === obj) // true
console.log(obj === state) // false
function myFn() {
obj.name = 'zs' // 数据会发生变化,但页面不更新
console.log(obj)
console.log(state)
// state.name = 'zs' // 数据会发生变化,同时页面更新
// console.log(state)
}
return { state, myFn }
}
}
</script>
作用:
做一些不想被监听的事情(提升性能):
- ref 和 reactive 的数据类型特点,每次修改都会被监听,并更新UI界面!但这样非常消耗性能!
- toRaw 方法可以拿到它的原始数据,对原始数据进行修改,这样 就不会被监听,也不会更新UI界面,进而达到优化性能的目的
<template>
<div>
<p>{{state}}</p>
<button @click="myFn">按钮</button>
</div>
</template>
<script>
import { reactive, toRaw } from 'vue'
export default {
name: 'App',
setup() {
let obj = {name: 'AHu', age: 24}
let state = reactive(obj)
let obj2 = toRaw(state)
console.log(obj2 === obj) // true
console.log(obj === state) // false
function myFn() {
obj2.name = 'zs' // 数据会发生变化,但页面不更新
console.log(obj2)
console.log(state)
// state.name = 'zs' // 数据会发生变化,同时页面更新
// console.log(state)
}
return { state, myFn }
}
}
</script>
注意点:
如果想通过 toRaw 拿到 ref 类型的原始数据(创建时传入的那个数据),就必须明确地告诉 toRaw 方法,要获取的是 .value 的值!
- 因为经过 Vue 处理之后,.value 中保存的才是当初创建时传入的那个原始数据
let obj = toRaw(state.value)
markRaw 方法:
定义:
使该数据永远无法被监听
<template>
<div>
<p>{{state}}</p>
<button @click="myFn">按钮</button>
</div>
</template>
<script>
import { reactive, markRaw } from 'vue'
export default {
name: 'App',
setup() {
let obj = {name: 'AHu', age: 24}
obj = markRaw(obj)
let state = reactive(obj)
function myFn() {
state.name = 'zs'
}
return { state, myFn }
}
}
</script>
toRef 方法:
前置知识:ref
<template>
<div>
<p>{{state}}</p>
<button @click="myFn">按钮</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'App',
setup() {
let obj = {name: 'AHu'}
let state = ref(obj.name)
function myFn() {
state.value = 'zs'
console.log(obj) // name: 'AHu'
console.log(state) // name: 'zs'
}
return { state, myFn }
}
}
</script>
结论:
- 如果利用 ref 将某一个对象中的 属性 变成响应式的数据,修改响应式的数据是不会影响到原始数据的;
- 通过 ref 修改的数据,当数据发生改变,会影响以前的数据
- ref 实际上就是 复制:ref(obj.name) -> ref('AHu') -> reactive({value: 'AHu'})
- ref(obj.name)
注意:区分 reactive
toRef:
<template>
<div>
<p>{{ageRef}} - {{state.name}} {{state.age}}</p>
</div>
</template>
<script>
import { toRef, reactive } from 'vue'
export default {
name: 'App',
setup() {
// toRef 如果用于普通对象(非响应式对象),产出的结果不具备响应式
// let state = {
// age: 18,
// name: 'AHu'
// }
let state = reactive({
name: 'AHu',
age: 24
})
let ageRef = toRef(state, 'age') // 针对 响应式数据 的 属性
setTimeout(() => {
state.age = 18
}, 1500)
setTimeout(() => {
ageRef.value = 20
}, 3000)
return { state, ageRef }
}
}
</script>
结论:
- 如果利用 toref 将某一个对象中的属性变成响应式的数据,修改响应式的数据是会影响到原始数据的;
- 但是如果响应式的数据是通过 toRef 创建的,那么修改了数据并不会触发 UI 界面的更新;
- toRef 实际上就是 引用;
- toRef(obj, 'name')
注意:
- toRef 针对的是一个响应式对象(reactiv封装)的prop;
- 普通对象 用 reactive 实现响应式,如果该响应式 单独某个数据 需要响应式,用 toRef
ref 和 toRef 的区别:
- ref -> 复制:修改响应式数据不会影响以前的数据;
toRef -> 引用:修改响应式数据会影响以前的数据
- ref -> 数据发生改变,界面就会自动更新;
toRef -> 数据发生改变,界面不会自动更新【因此,需要先用 reactive】
- ref -> ref(obj.name)
toRef -> toRef(obj, 'name')
toRef 的应用场景:
如果想让 响应式数据 和 以前的数据 关联起来,并且更新响应式数据之后不想更新UI,那么就可以使用 toRef
toRefs 方法:
定义:
- 将响应式对象(reactive封装)转换为 普通对象;
- 对象的每个 prop 都对应 ref;
- 两者保持引用关系
toRef 和 toRefs 的区别:
- toRef 是针对 某一个单一属性;
- toRefs 是针对 所有(整个)的属性
<template>
<div>
<p>{{name}} - {{age}}</p>
</div>
</template>
<script>
import { toRefs, reactive} from 'vue'
export default {
name: 'App',
setup() {
let state = reactive({
age: 24,
name: 'AHu'
})
let stateAsRefs = toRefs(state) // 将响应式对象,变成普通对象
// const {age: ageRef, name: nameRef} = stateAsRefs //每个属性,都是 ref 对象
// return {
// ageRef,
// nameRef
// }
setTimeout(() => {
state.age = 18
}, 1500)
return stateAsRefs
}
}
</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、toRef 和 toRefs 的最佳使用方式:
- 用 reactive 做对象的响应式,用 ref 做值类型的响应式【用 reactive 定义】;
- setup 中返回 toRefs(state),或者 toRef(state, 'xxx')【用 toRef 或 toRefs 包装后返回】;
- ref 的变量命名都用 xxxRef;
- 合成函数返回响应式对象时,使用 toRefs【方便解构】
为什么需要 ref:
- 返回 值类型,会丢失响应式;如在 setup、computed、合成函数,都有可能返回 值类型;Vue 如不定义 ref,用户将自造 ref,反而混乱
为什么需要.value:
- ref 是一个对象(不丢失响应式),.value 存储值;
- 通过 .value 属性的 get 和 set 实现响应式
- 用于 模板、reactive 时,不需要 .value,其他情况都需要
// 错误
function computed(getter) {
let value = 0
setTimeout(() => {
value = getter()
}, 1500)
}
/*
相当于:
let a = 100
let b = a
a = 200
console.log(b) // 100
*/
// 正确
function computed(getter) {
const ref = {
value: null
}
setTimeout(() => {
ref.value = getter()
}, 1500)
return ref
}
/*
相当于:
let obj1 = {x: 100}
let obj2 = obj1
obj1.x = 200
console.log(obj2.x) // 200
*/
为什么需要 toRef 和 toRefs:
- 初衷:在不丢失响应式的情况下,将对象数据进行 解构;
- 前提:针对的是 响应式对象(reactive封装的),而非普通对象;
- 注意:不是 创造 响应式,而是 延续 响应式
customRef 方法:
定义:
- 允许自定义一个 ref
- 返回一个 ref 对象,可以显式地控制依赖追踪和触发响应
<template>
<div>
<p>{{age}}</p>
<button @click="myFn">按钮</button>
</div>
</template>
<script>
import { ref, customRef } from 'vue'
function myRef(value) {
return customRef((track, trigger) => {
return {
get() {
track() // 告诉 Vue 这个数据需要追踪变化
console.log('get', value)
return value
},
set(newValue) {
console.log('set', newValue)
value = newValue
trigger() // 告诉 Vue 触发界面更新
}
}
})
}
export default {
name: 'App',
setup() {
// let age = ref(18) // reactive({value: 18})
let age = myRef(18)
function myFn() {
age.value += 1
}
return { age, myFn }
}
}
</script>
为什么需要自定义一个 ref:
应用场景:
当发送 网络请求 时,是异步操作,但是 setup() 只能是 同步函数,无法使用 async/await 语法处理 backhell!因此,需要在 自定义的ref 中,将 网络请求 的处理放在 myRef() 中进行
<template>
<div>
<ul>
<li v-for="item in state" :key="item.id">{{item.name}}</li>
</ul>
</div>
</template>
<script>
import { ref, customRef } from 'vue'
function myRef(value) {
return customRef((track, trigger) => {
fetch(value).then((res) => {
return res.json()
}).then((data) => {
value = data
trigger() // 获取成功则更新UI界面
}).catch((err) => {
console.log(err)
})
return {
get() {
track()
// 注意:不能在 get 中发送网络请求
// 渲染界面 -> 调用 get -> 发送网路请求 -> 保存数据 -> 更新界面 -> 调用 get
/*
fetch(value).then((res) => {
return res.json()
}).then((data) => {
value = data
trigger()
}).catch((err) => {
console.log(err)
})
*/
return value
},
set(newValue) {
value = newValue
trigger()
}
}
})
}
export default {
name: 'App',
// setup() 只能是一个 同步的函数,不能是一个异步函数
// 因此 无法采用 async/await 解决 backhell
setup() {
/*
let state = ref([])
fetch('../public/data.json').then((res) => {
return res.json()
}).then((data) => {
console.log(data)
state.value = data
}).catch((err) => {
console.log(err)
})
return { state }
*/
let state = myRef('../public.data.json')
return { state }
}
}
</script>
ref 获取元素:
- 在 Vue2.x 中,可以通过给元素添加 ref='xxx' 属性,然后在代码中通过 this.$refs.xxx 的方式来获取元素
注意点:
- setup() 的作用时机:
beforeCreate
setup
Created
- 但是在上述生命周期中,$el 还拿不到
- 因此,需要 Mounted 生命周期中 获取元素
<template>
<div ref="box">
我是 div
</div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
name: 'App',
setup() {
let box = ref(null)
onMounted(() => {
console.log('onMounted', box.value) // 第二个出现,正常值
})
console.log(box.value) // null
return { box }
}
}
</script>
readonly、isreadonly、shallowreadonly:
定义:
- readonly:用于创建一个 只读 的数据,并且是 递归只读;【数据不会发生改变,页面不会发生更新】
- shallowReadonly:用于创建一个 第一层数据 只读;【数据会发生改变,页面不会发生更新】
- isreadonly:用于 判断是否为 readonly
<template>
<div>
<p>{{state.name}}</p>
<p>{{state.attr.age}}</p>
<p>{{state.attr.height}}</p>
<button @click="myFn">按钮</button>
</div>
</template>
<script>
import { readonly, isReadonly, shallowReadonly } from 'vue'
export default {
name: 'App',
setup() {
let state = readonly({name: 'AHu', attr:{age: 24, height: 1.88}})
function myFn() {
state.name = 'zs'
state.attr.age = 18
state.attr.height = 1.66
console.log(state) // 此时,数据没改动、页面没改动
}
return { state, myFn }
}
}
</script>
注意点:
const 和 readonly 的区别:
- const:赋值保护,不能给 变量 重新赋值;
- readonly:属性保护,不能给 属性 重新赋值
const:
const value = 123
value = 456
console.log(value) // error
const value = {name: 'AHu', age: 24}
value.name = 'zs'
value.age = 18
console.log(value) // {name: 'zs', age: 18}
Vue3.x 中 响应式数据 的本质:
- 在 Vue2.x 中是通过 Object.defineProperty 来实现响应式数据;
- 在 Vue3.x 中是通过 Proxy 来实现响应式数据
Proxy 的基本使用:(对象)
let data = {
name: 'AHu',
age: 24
} // 对象形式
// let data = ['a', 'b', 'c'] // 数组形式
let proxyData = new Proxy(data, {
get(target, key, receiver) {
let result = Reflect.get(target, key, receiver)
console.log('get', key)
return result // 返回结果
}
set(target, key, val, receiver) {
let result = Reflect.set(target, key, val, receiver)
console.log('set', key, val)
console.log('result', result) // true / false
return result // 是否设置成功
}
deleteProperty(target, key) {
let result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
console.log('result', result) // true / false
return result // 是否删除成功
}
})
注意点:
- 由于每次 set 不一定仅仅是设置一个数据,因此,Vue 规定,需要在 set 中 return true 来判断是否已经设置完成
案例:(数组)
- 例如:push 一个数组,实际上需要改变的两个部分:(即:同一数据处理两次)
1、arr.push 追加元素进去;
2、修改 arr.length
// let data = {
// name: 'AHu',
// age: 24
// } // 对象形式
let data = ['a', 'b', 'c'] // 数组形式
let proxyData = new Proxy(data, {
get(target, key, receiver) {
// 只处理本身(非原型的)属性,即:不包含数组的方法,仅仅是数据本身
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('get', key) // 仅仅起到监听的作用
}
let result = Reflect.get(target, key, receiver)
return result // 返回结果
}
set(target, key, val, receiver) {
// 重复的数据不处理
const oldVal = target[key]
if (val === oldVal) {
return true
}
let result = Reflect.set(target, key, val, receiver)
console.log('set', key, val) // set 3 d; 4
console.log('result', result) // true / false
return result // 是否设置成功
}
deleteProperty(target, key) {
let result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
console.log('result', result) // true / false
return result // 是否删除成功
}
})
// test
proxyData.push('d')
Reflect 的作用:
- 和 Proxy 的能力 一一对象(包括 API 和 参数);
- 规范化、标准化、函数式
JS中:'a' in obj // true
delete.obj.b // true
Reflect中:Reflect.has(obj, 'a') // true
Reflect.deleteProperty(obj, 'b') // true
- 替代掉 Object 上的工具函数(使得 Object 更加纯粹为 数据结构)
Object: Object.getOwnPropertyNames(obj)
Reflect: Reflect.ownKeys(obj)
Proxy 实现响应式:
- 深度监听(包括 性能优化);
- 可监听 新增/删除 属性;
- 可监听 数组变化
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) // 仅仅起到监听的作用
}
let result = Reflect.get(target, key, receiver)
// 深度监听
// 性能的提升:在 get 中进行递归,什么时候用什么时候递归,而不是一次性递归到底
// 本质:获取到哪一层,那一层才会立即触发响应式,深层次不会立即触发响应式
return reactive(result) // 返回结果
}
set(target, key, val, receiver) {
// 重复的数据不处理
const oldVal = target[key]
if (val === oldVal) {
return true
}
// 判断是否为新增属性
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('已有的 key', key)
} else {
console.log('新增的 key', key)
}
let result = Reflect.set(target, key, val, receiver)
console.log('set', key, val) // set 3 d; 4
console.log('result', result) // true / false
return result // 是否设置成功
}
deleteProperty(target, key) {
let result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
console.log('result', result) // true / false
return result // 是否删除成功
}
}
// 生成代理对象
const observed = new Proxy(target, proxyConf)
return observed
}
// 测试数据
let data = {
name: 'AHu',
age: 24,
info: {
city: 'chengdu'
}
}
const proxyData = reactive(data)
总结:
- Proxy 能规避 Object.defineProperty 的问题;
- Proxy 无法兼容所有浏览器,无法 polyfill;
v-model 参数的用法:
前置知识:Vue2.x 中的 .sync 修饰符:
- 本质上是 update.myPropName 的语法糖,能够更加简单地对 prop 进行“双向绑定”
案例:
子组件中:
this.$emit('update:title', newTitle)
父组件中:
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>
为了方便起见,使用 .sync 修饰符:
<text-document v-bind:title.sync="doc.title"></text-document>
在 Vue3 中使用 v-model 替换了 .sync:
案例:
<ChildComponent :title.sync="pageTitle" />
<!-- 替换为 -->
<ChildComponent v-model:title="pageTitle" />
注意点:
对于所有不带参数的 v-model,需要分别将 prop 和 event 命名更改为 modelValue 和 update:modelValue
<ChildComponent v-model="pageTitle" />
// ChildComponent.vue
export default {
props: {
modelValue: String // 以前是`value:String`
},
emits: ['update:modelValue'],
methods: {
changePageTitle(title) {
this.$emit('update:modelValue', title) // 以前是 `this.$emit('input', title)`
}
}
}
watch 和 watchEffect 的区别:
- 两者都可以监听 data 属性的变化;
- watch:需要明确监听哪个属性;【默认初始化不执行】
- watchEffect:会根据其中的属性,自动监听其变化【初始化时,执行一次】(因为,收集需要监听的数据)
watch:
<template>
<p>watch vs watchEffect</p>
<p>{{numberRef}}</p>
<p>{{name}} - {{age}}</p>
</template>
<script>
import { reactive, ref, toRefs, watch, watchEffect } from 'vue'
export default {
name: 'Watch',
setup() {
let numberRef = ref(100)
let state = reactive({
name: 'AHu',
age: 24
})
watch(numberRef, (newNumber, oldNumber) => {
// 注意:无需再写 .value
console.log('ref watch', newNumber, oldNumber)
}, {
immediate: true // 初始化之前就监听【可选】
})
setTimeout(() => {
numberRef.value = 200
}, 1500)
watch (
// 第一个参数,确定要监听哪个属性(函数形式)
() => state.age,
// 第二个参数,回调函数
(newAge, oldAge) => {
console.log('state watch', newState, oldState)
},
// 第三个参数,配置项【可选】
{
immediate: true // 初始化之前就监听
deep: true // 深度监听
}
)
setTimeout(() => {
state.age = 18
}, 1500)
setTimeout(() => {
state.name = 'zs'
}, 3000)
return { numberRef, toRefs(state) }
}
}
</script>
watchEffect:
<template>
<p>watch vs watchEffect</p>
<p>{{numberRef}}</p>
<p>{{name}} - {{age}}</p>
</template>
<script>
import { reactive, ref, toRefs, watch, watchEffect } from 'vue'
export default {
name: 'Watch',
setup() {
let numberRef = ref(100)
let state = reactive({
name: 'AHu',
age: 24
})
watchEffect(() => {
// 初始化时,一定会执行一次(收集要监听的数据)
console.log('hello watchEffect')
})
watchEffect(() => {
console.log('state.name', state.name)
})
watchEffect(() => {
console.log('state.name', state.name)
})
watchEffect(() => {
console.log('state.name', state.name)
console.log('state.name', state.name)
})
setTimeout(() => {
state.age = 18
}, 1500)
setTimeout(() => {
state.name = 'zs'
}, 3000)
return { numberRef, toRefs(state) }
}
}
</script>