vue3 新功能
vue3 升级的重要功能
Composition API
10.composition API 对比 option API
13.为什么需要ref?为何需要 .value?为何需要 toRef toRefs?
原理
Vite
vue3 是通过 createApp 函数来创建vue的应用实例的
vue2 是通过 new Vue() 来创建vue实例的
// vue2.x import Vue from 'vue' const app = new Vue({ // 选项 }) //vue3 import {createApp} from 'vue' // 从一个单文件组件中导入根组件 import App from './App.vue' const app = createApp(App) app.mount('#app')
1. emits 属性是vue3中新增的组件触发的自定义事件,代码示例:
父组件:
<HelloWorld @sayHello="sayHello">
子组件
export default{ name: 'HelloWorld', emits:['sayHello'], // 声明自定定义的事件 setup(props,{ emit }){ emit('sayHello','aaaa') //触发事件 } }
2、emits 不能在组件的 <script setup> 部分中使用,可以用defineEmits()返回一个相同的函数供我们使用。
父组件:
<HelloWorld @sayHello="sayHello">
子组件. 需要用defineEmits(['xxx','xxx']) 返回的函数来触发事件
<script setup> const emit = defineEmits(['sayHello']) // 多个触发事件,可以都放到数组中。返回的是一个函数,这个函数和$emits函数作用相同 // 触发事件 emit('sayHello','aaa') </script>
触发自定义事件的方法总结:
emits 属性:用于 vue3 中 Options API 中
defineEmits: 只能用在 在Composition API 的setup 函数中
$emit 方法:组件的模板表达式中可以直接使用$emit(),代码示例:
<!-- MyComponent --> <button @click="$emit('someEvent')">Click Me</button>
- vu2.x 组件模板必须是单一根节点
<template> <div> <h2>{{title}}</h2> <div v-html="content"></div> </div> </template>
- vue3 组件模板可以有多个根节点
<template> <h2>{{title}}</h2> <div v-html="content"></div> </template>
在自定义组件上对一个 prop 进行双向绑定的实现,vue3中改为 v-model:参数
<!--vue2.x -->
<MyComponent :title.sync="title" />
<!--等价于-->
<MyComponent :title="title" @update:title="title=$event"/>
<!--vue3-->
<MyComponent v-model:title="title" v-model:age="age"/>
<!--等价于-->
<MyComponent :title="title" @update:title="title = $event" @update:age="age = $event"/>
组件中v-model在Vue3.4开始推荐使用defineModel()宏:
- defineModel() 返回的值是一个ref
- 它的.value 和父组件的 v-model 的值同步
- 当子组件变更了,会触发父组件绑定的值一起更新
- 多个v-model 绑定可以在defineModel('name')指定参数
父组件:
<script setup> import Child from './Child.vue' import {ref} from 'vue' const titleRef = ref('hello') const ageRef= ref(20) </script> <template> <h1>{{titleRef}} {{titleRef}}</h1> <Child v-model:title="titleRef" v-model:age="ageRef" /> </template>
子组件:
<script setup> const title = defineModel('title') const age = defineModel('age') </script> <template> <input v-model="title" type="text"> <input type="number" v-model="age"/> </template>
defineModel() 底层机制:
- 一个名为modelValue 的prop(v-model不加参数时,默认的参数是modelValue,定义了参数使用定义的名称),本地ref的值与其同步
- 一个名为update:modelValue(v-model不加参数时的事件,自定了参数适应定义的名称) 的事件,当本地ref的值发生变更式触发
<!--父组件--> <script setup> import { ref } from 'vue' import UserName from './UserName.vue' const first = ref('John') const last = ref('Doe') </script> <template> <h1>{{ first }} {{ last }}</h1> <UserName v-model="first" v-model:last-name="last" /> </template> <!--子组件--> <script setup> defineProps({ modelValue: String, //v-model不加参数时,会增加默认参数modelValue lastName: String. // v-model:last-name 加参数就用自定义的参数名 }) defineEmits(['update:modelValue', 'update:lastName']) </script> <template> <input type="text" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /> <input type="text" :value="lastName" @input="$emit('update:lastName', $event.target.value)" /> </template>
- vu2.x 通过 () => import('')
export default { //... components: { 'my-component': () => import('./component/my-async-component') } }
- vue3中通过defineAsyncComponent(()=>import(''))
<script setup> import {defineAsyncCompnent } from 'vue' const MyComponent = defineAsyncComponent(() => import('./component/my-async-component')) </script>
vue3 中 将filter移除,使用计算属性代替
- <Teleport> 是一个内置组件,将组建中某个模板传送到该组件的DOM结构外层的位置
- 场景:全屏的模态窗
- 代码:
<Teleport to="body"> <div class="modal"> <p>内容</p> <button @click="open=false">关闭</button> </div> </Teleport>
这样,我们就把模板插入到 body标签下了
- <Suspense> 内置组件,用来处理异步组件的加载,提供了一个优雅的加载状态处理机制。
- <Suspense>组件有两个插槽: #default 和 #fallback
- 例子:
<template> <Suspense> <template #default> <AsyncComponent /> <!-- 是一个异步组件 --> </template> <template #fallback> <div>Loading...</div> </template> </Suspense> </template>
- vue3 选项式API(options API)形式的生命周期钩子
- beforeCreate
- created
- beforeMount
- mounted
- beforeUpdate
- updated
- beforeUnmount
- unmounted
- vue3 Composition AP(组合API) 形式的生命周期钩子
- setup() ====> setup 相当于之前的 beforeCreate 和 created。setup执行的时间比beforeCreate 还要早
- onBeforeMount
- onMounted
- onBeforeUpdate
- onUpdated
- onBeforeUnmount
- onUnmounted
- 在组合式API中,创建响应式数据用 ref 或者 reactive
- ref: 基本类型、对象类型的响应式数据都可以创建。ref 创建对象类型的响应式数据,底层的实现是:将非原始值通过reactive()转换为响应式代理。
- reactive:只能创建对象类型的响应式数据。上面提到的ref创建的对象类型时,底层也是调用了这个方法实现的
- ref 创建的变量必须使用.value(可以使用volar 插件自动添加.value);
<template> <div>{ageRef}}</div> <div>{{state.name}}- {{state.age}}</div> <button @click="change">修改数据</button> </template> <script setup> import {ref} from 'vue' const ageRef = ref(20) const state = ref({ name:'zhangsan', age: 18 }) function change(){ ageRef.value = 25 state.value = {name:'lisi',age: 18} //可以直接赋值新对象,reactive则不可以 } </script>
reactive创建的响应式数据的缺陷:
-
- 重新分配一个新对象,会失去响应式(可以使用Object.assign代替)
<script setup> const obj = reactive({ name:'zhangsan', age: 14 }) function change(){
// 此时 obj 失去了响应式,视图不会更新 // obj = reactive({name:'lisi',age:18}) //直接赋值新的对象会失去响应式 // 用Object.assign()来替换 Object.assign(obj,{name:'lisi',age: 18})
}
</script> -
对解构操作不友好:解构出来的变量会失去响应
const state = reactive({count: 0}) // 解构时,count已经与 state.count 断开链接 let {count} = state count++ // 此时不会影响原始 state
- 重新分配一个新对象,会失去响应式(可以使用Object.assign代替)
使用规则:
-
- 若是基本类型,必须用ref
- 若是对象类型,层级不深,ref和 reactive 都行
- 若是一个对象类型,层级比较深,推荐使用reactive
十一、composition API 和 options API对比
- toRef
- 将reactive 定义的响应式对象的一个属性,创建成一个对应的ref
- 创建的fef,具有响应式
- 两者保持引用关系
- 示例:
<tamplte> <div>{{ageRef}} ---{{state.age}}</div> </template> <script setup> import {reactive, toRef} from 'vue' const state = reactive({ name:'zhangsan', age: 14 }) const ageRef = toRef(state,'age') setTimeout(()=>{ ageRef.value = 18 //此时state.age 也会更新 },1000) </script>
- toRefs
- 将reactive 创建的响应式对象,转换成普通对象
- 对象的每个prop 都是对应的ref,具有响应式
- 两者保持引用关系
- 示例:
<template> <div>名字:{{ name }} - 年龄: {{age}}</div> </template> <script setup> import {reactive,toRefs} from 'vue' const state = reactive({ name: 'zhangsan', age: 14 }) // toRefs() 返回的是一个普通对象,每个属性都是ref
// 可以解构,不会失去响应式
const {age,name} = toRefs(state) setTimeout(()=>{ age.value = 20 name.value = 'lisi' state.age = 22 },1000) </script>
- 示例:
- 组合式函数中返回响应式对象时,toRefs 相当有用
function useFeatureX(){ const state = reactive({ foo: 1, bar: 2 }) // 在返回时都转为 ref return roRefs(state) } // 可以结构2而不失去响应性 const { foo, bar } = useFeatureX
十三、为什么需要ref?为何需要 .value?为何需要 toRef toRefs?
1. 定义响应式数据可以用reactive,值类型的我们可以在包一层对象即可,那么为什么vue要再设计出一个ref呢?
-
-
- 返回值类型会丢失响应式
-
<template> <div>{{age}}</div><!--一直是20--> <div>{{books}}</div><!--1s后books变了--> </template> <script setup> import {reactive} from 'vue' const state = reactive({age:20,name:'zhangsan',books:['css','html','javascript']) let age = state.age // state.age 是值类型,所以age 失去了响应式 let books = state.books //state.books 是对象类型,books 仍具有响应式 setTimeout(()=>{ state.age = 21 state.books.push('vue') } ,1000) <script>
-
-
- 而在开发中 computed有可能返回值类型,我们开发中计算属性用的非常多,如果返回的值类型不具有响应式,那么开发起来很痛苦啊
-
<template> <div>{{age}}</div><!---1s后变成21-> </template> <script setup> import {reactive, computed } from 'vue' const state = reactive({age:20,name:'lisi'}) const age = computed(()=>{ return state.age }) console.log(age) // ref类型的响应式数据 setTimeout(()=>{ state.age = 21 },1000) </script>
从上面代码可以看出,vue底层在实现computed属性时,将返回的值类型变成了ref。
-
-
- 所以不只是定义数据的时候需要ref,vue中其他地方也需要ref。
-
2.ref定义的对象,为什么需要.value ?
-
-
- ref是一个对象,value 存储值
- 通过.value 属性的get和 set实现响应式
- 用于模板、reactive时,不需要.value,其他情况都需要
-
3.为和需要toRef和toRefs
-
-
- 初衷:不丢失响应式的情况下,把对象数据分解
- 前提: 针对的是响应式数据对象(reactive封装的),非普通对象
- 注意:toRef 和 toRefs是延续响应式,不是创造响应式,创造响应式是ref 和 reactive
-
vue的组合式API中,将通过组合式函数 来实现逻辑复用
组合式函数:利用Vue的组合式API来封装和复用有状态逻辑的函数
具体实现:
-
-
- 抽离逻辑代码放到一个函数中
- 函数命名为useXxxxx格式
- 在setup 中饮用useXxxx 函数的形式提取到外部文件中
-
示例: 鼠标踪器功能
src/composable/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue' // 组合式函数命名以 “use” 开头 export function useMouse(){ const x = ref(0) const y = ref(0) function update(event:Event){ //使用 as 关键字进行类型断言 const mouseEvent = event as MouseEvent x.value = mouseEvent.pageX; y.value = mouseEvent.pageY } onMounted(()=>{ window.addEventListener('mousemove',update) }) onUnmounted(()=>{ window.removeEventListener('mouseove',update) }) return { x, y } }
组件中引用组合函数:
<template> <div>鼠标位置: {{x}},{{y}}</div> </template> <script setup lang="ts"> import {useMouse} from '../composable/useMouse.ts' const {x,y} = useMouse() <script>
- setup 和 其他Composition API 中没有this
- 可通过getCurrentInstance() 获取当前实力
- 若使用Options API 可照常使用this
<script setup> import {getCurrentInstance, onMounted} from 'vue' onMounted(()=>{ console.log(this) //undefined console.log(getCurrentInstance()) //获取组件实例 }) </script>
十六、vue3 script setup
- <script setup> 中顶级变量、自定义组建,可以直接用于模板 ,不需要有默认导出和return
- 使用该功能的vue 版本要 >= 3.2.0
- 可以和其他<script> 同时使用
- <script setup> 建议放到 <template>前面
<script> function add(a,b){ return a+b } </script> <script setup> import { ref, reactive, toRefs} from 'vue' import Child from './Child' const countRef = ref(100) function addCount(){ countRef.value++ } const state = reactive({name:'zhangsan',age:20}) const {name} = toRefs(state) console.log(add(10,20)) // 30 </script> <template> <p @click="addCount">{{countRef}}</p> <p>{{name}}</p> </template>
- 用defineProps 和 defineEmits 来定义参数 和定义事件
//Child.vue <script setup> import {defineProps,defineEmits} from 'vue' //定义属性--数组形式 //defineProps(['name','age']) // 定义属性--对象形式,可以定义类型 defineProps({ name: String, age: Number }) //定义事件 const emit = defineEmits(['change',delete]) function deleteHandler(){ emit('delete','bbb') // 触发事件 } </script> <template> <p>{{name}} - {{age}}</p>
<!---模板中可以使用$emit触发事件--> <button @click="$emit('change',123)">change</button> <button @click="deleteHandler">delete</button> </template> //Parent.vue <script setup> import Child from './Child.vue' inport {reactive, toRefs } from 'vue' const state = reactive({name:'zhangsan',age:12}) const {name,age} = toRefs(state) function onChange(info){ console.log(info) } function onDelte(info){ console.log(info) } </script> <template> <Child :name="name" :age="age" @change="onChange" @delete="onDlete" /> </template>
- 用defineProps 和 defineEmits 来定义参数 和定义事件
- defineExpose :暴露数据给父组件
//Child.vue <script setup> import {ref, defineExpose} from 'vue' const countRef = ref(100) function add(a,b){ return a+b } defineExpos({ countRef, add }) </script> <template> <p>子组件:{{a}}</p> </template> //Parent.vue <script setup> import Child from './Child' import {ref, onMounted} from 'vue' const childRef = ref(null) onMounted(()=>{ // 拿到Child 组件的一些数据 console.log(childRef.value) // 可以获取到组件暴露的所有数据 console.log(childRef.value.a) console.log(child.value.add.add) }) </script> <template> <Child ref="childRef"> </template>
1. watch
watch(data,()=>{},{})可以传三个参数
-
- 第一个参数:数据源
- 数据源是一个ref
<script setup> import {ref,watch} from 'vue' const count = ref(0) setTimeout(()=>{ count.value = 1 },1000) //直接监听一个ref watch(count,(newvalue,oldvalue)=>{ console.log(newvalue,oldvalue) }) </script>
- 数据源是一个响应式对象reactive时,会隐士创建一个深度监听器(即多层嵌套的对象修改都可以监听到)
<script setup> import {reactive, watch} from 'vue' //reactive 响应式对象 const obj = reactive({ name:'zhangsan', age:20, info:{ city: '北京' } }) setTimeout(()=>{ obj.age = 22; obj.info.city = 'changchun' },1000) //会隐士创建一个深度监听 watch(obj,(value)=>{ console.log(value) }) </script>
- 数据源是一个getter函数,用来监听对象的某个属性,如果监听的属性仍是个对象类型,需要手动开启深度监听(即deep:true)
<script setup> import {reactive, watch} from 'vue' const obj = reactive({ age:20, info:{ city: '北京' } }) setTimgout(()=>{ obj.age = 21; obj.info.city = '长春' },1000) //监听obj.age属性时,需要返回一个getter函数形式,否则不会被监听 watch( ()=>obj.age, (value,oldvalue) =>{ console.log(value,oldvalue) } ) //监听obj.info属性时,需要返回一个getter函数形式,并且需要开启深度监听,否则不会监听到info内部属性的变化,除非替换整个info对象 watch( ()=>obj.info, (value) =>{ console.log(value) }, { deep:true //开启深度监听 } ) //如果监听的是obj.info.city 则不需要深度监听,因为obj.info.city是基本类型 watch( () => obj.info.city, ///需要写成getter函数形式 (value,oldvalue)=>{ console.log(value,oldvalue,'5555') } ) </script>
- 数据源是一个ref
- 第二个参数:监听触发的回调函数 (newvalue,oldvalue)=> {}
- 如果监听的是基本类型的,会返回newvalue和 oldvalue
- 若果监听的是对象类型的,返回的newvalue 和oldvalue 是相同的,都是新值
- 第三个参数:配置对象,可选的
- immediate:true 立即执行
- deep:true 深度监听
- once:true 仅触发一次
- 第一个参数:数据源
2.watchEffect
watchEffect(()=>{})
-
- 接受一个函数作为参数
- 回调函数会在组件渲染时立即触发一次,即使是空函数(为了收集监听的数据)
- 自动追踪所能访问到的响应式属性(必须是在回调函数中用到的属性改变了,才会触发回调函数)
- 无法获取新值和就值
<script setup> import {ref,reactive} from 'vue' const countRef = ref(0) const obj = reactive({age:20,info:{city:'beijing'}}) watchEffect(()=>{ //初始化时一定会执行一次,为了收集监听的数据 console.log(countRef,'countRef') //countRef 会被收集进行监听 console.log(obj.info.city,'city') // city属性会被收集,一旦city改变就会触发回调函数 })
setTimout(()=>{ countRef.value = 2 // 这个改变会触发watchEeffect ,因为watchEffect 中收集的该属性的监听(watchEffect 回调函数中用到) },1000) setTimout(()=>{ obj.age = 21 // 这个改变不会触发watchEeffect ,因为watchEffect 中没有用到改属性 },1000) setTimout(()=>{ obj.info.city ='长春' // 这个改变会触发watchEeffect },1000) </script>
- 接受一个函数作为参数
3.watch 与 watchEffect 区别?
-
-
- 两者都可以监听data 属性变化
- watch 需要明确监听那个属性;而watchEffect 会根据其中的属性自动监听变化
- watch默认情况是惰性执行,只有数据发生变化才会执行。除非设置immmediate:true;而 watchEffect 初始化时立即执行一次
-
4.watch 和 watchEffect 适用场景:
-
- watchEffect 的适用场景
- 适用于需要立即执行响应数据变化的场景
- 监听多个数据源,并且不想明确指定每个数据源时,使用watchEffect 更为方便
- 不需要获取新值和旧值,或者逻辑上不需要区分新旧值
- 适用于需要立即执行响应数据变化的场景
- watch的适用场景
- 需要数据变化时获取新旧值,以便进行对比或执行依赖于新旧值的操作时
- 不需要立即执行,而是在数据变化时才需要响应的场景
- 适用需要精确控制监听哪些数据变化的场景
- watchEffect 的适用场景
vue2.x中数据响应式是通过Object.defineProperty()来实现了
Object.defineProperty 的缺点:
-
-
- 深度监听需要一次行递归
- 无法监听新增属性/删除属性(所以有了 Vue.set、Vue.delete)
- 无法监听数组API 方法,需要特殊处理(vue2中重写了数组原型上的方法)
-
vue3采用Proxy 代替 vue2.x Object.defineProperty() 来实现的。
-
-
- Proxy 是ES6引入的一个新特性,Proxy是一个构造函数,用于实现对对象的代理操作,避免直接操作原数据对象
- Proxy 是对整个对象进行监听和拦截(Object.definProperty() 只能监听单个属性
const objProxy = new Proxy(obj,{ get(target,key,receiver){ console.log('拦截对象属性的读取') }, set(target,key,value,receiver){ console.log('拦截对象属性的设置') }, has(target,key){ console.log('判断对象是否含有该属性,返回一个布尔值') }, ownKeys(target){ console.log('拦截Object.keys(target)、for...in等循环,返回一个数组') }, deleteProperty(target,key){ console.log('删除某个属性的操作,返回一个布尔值') }, defineProperty(target,key,desc){ }, getPrototypeOf(target){ console.log('获取对象的原型') }, setPropertyOf(target,proto){ } })
- Reflect:在Proxy内部,调用对象的默认行为时,建议使用Reflect。
- Reflect 是ES6为了操作对象而提供的新Api,类似于Object的操作方法。
-
- Object 对象的一些明显语言内部方法(如Object.defineProperty)放到Reflect对象上。未来的对象的新方法都会往Relfect对象上部署,用于解决Object 方法过于臃肿的问题
- 让Object 操作变成函数形式,这样更符合语言的规范
const obj = { name: 'zhangsan' } //旧的写法 'name' in obj // true delete obj['name'] //删除name 属性 //新的写法 Reflect.has(obj,'name') // true Reflect.deleteProperty(obj,'name')
- Reflect 对象的方法 与 Proxy 对象的方法一一对应,这样Proxy 对象的拦截操作用调用相应的Reflect方法来完成,保证了原生行为能够正常执行
let obj = {name:'zhangsan',age:20} new Proxy(obj,{ get(target,name){ return Reflect.get(target,name) }, set(target,name,val){ return Reflect.set(target,name,val) }, deleteProperty(target,name){ return Reflect.deleteProperty(target,name) }, has(target,name){ return Reflect.has(target,name) } })
-
-
vue3 用Proxy 响应式的实现:
1. 件单的Proxy代理实现响应式
function reactive(target = {}){ if(typeof target !== 'object'){ return target } const proxyConf = { get(target,key,receiver){ //只处理本身(非原型的)属性 const ownKeys = Reflect.ownKeys(target)
console.log(key) if(ownKeys.includes(key)){ console.log('get',key) } const result = Reflect.get(target,key,receiver) return result }, set(target,key,val,receiver){
console.log(key)
//重复数据,不处理 if(val === target[key]){ return true } const result = Reflect.set(target,key,val,receiver) return result // 设置是否成功 }, deleteProperty(target,key){ const result = Reflect.deleteProperty(target,key) return result //是否删除成功 } } // 生成代理对象 const observed = new Proxy(target,proxyConf) return observed }
测试一下简单数据操作,都能劫持
const state = reactive({ name:'zhangsan',
info:{
city:'beijing'
}
}) //获取 console.log(state.name) // get name //设置已存在的属性 state.name = 'lisi' // 设置不存在的属性 state.age = 20 // 删除属性 delete state.age //true
如果是嵌套对象的情况,就会有点问题,当你拦截一个深层对象的属性访问时,实际上拦截的是最外层的属性访问
const state = reactive({ name:'zhangsan', info:{ city:'北京' } }) console.log(state.name ) // get name. zhangsan console.log(state.info). // get info {city:'北京'} console.log(state.info.city) // get info 北京
上面state.info.city 的时候,拦截的的是info,
解决方案:在get中判断如果是对象类型,则再进行一层代理
function reactive(target = {}){ if(typeof target !== 'object'){ // 如果不是对象类型,直接返回 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) } } // 生成代理对象 const observed = new Proxy(target,proxyConf) return observed }
vue2 和 vue3 响应式实现的区别:
-
-
- vue2 通过 Object.defineProperty() 实现响应式的,vue3 是通过reactive 是通过Proxy 实现的
- Object.defineProperty 是监听对象的属性,需要遍历对象,对每个属性进行监听;Proxy 是对整个对象进行监听,不需要再遍历属性了
- Object.defineProperty 对新增的属性和删除的属性不能见听到;而Proxy 可以拦截到
- 嵌套对象,Object.defineProperty需要递归实现;而proxy 是在get 读取的时候递归
- Object.defineProperty 不支持数组,数组的方法需要重写;Proxy可以拦截到对数组的操作
-
vue3 在编译阶段做了进一步优化。主要有:
- diff算法优化
- 编译模板时,为动态节点添加一个标记
<div>HelWorldlo </div> <div>{{message}}</div> <div :class="name"></div>
上面的代码会变编译成:
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, normalizeClass as _normalizeClass, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock(_Fragment, null, [ _cache[0] || (_cache[0] = _createElementVNode("div", null, "HelWorldlo ", -1 /* HOISTED */)), _createElementVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */), _createElementVNode("div", { class: _normalizeClass(_ctx.name) }, null, 2 /* CLASS */) ], 64 /* STABLE_FRAGMENT */)) } // Check the console for the AST
_createElementVNode()最后面的数组1、2 就是静态标记 path flag (-1:表示静态的,永不会用做diff,1:代表动态文本节点,2:代表动态的calss).
- 标记分为不同类型,静态类型枚举如下:
export const enum PatchFlags { TEXT = 1,// 动态的文本节点 CLASS = 1 << 1, // 2 动态的 class STYLE = 1 << 2, // 4 动态的 style PROPS = 1 << 3, // 8 动态属性,不包括类名和样式 FULL_PROPS = 1 << 4, // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较 HYDRATE_EVENTS = 1 << 5, // 32 表示带有事件监听器的节点 STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的 Fragment KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment NEED_PATCH = 1 << 9, // 512 DYNAMIC_SLOTS = 1 << 10, // 动态 solt HOISTED = -1, // 特殊标志是负整数表示永远不会用作 diff BAIL = -2 // 一个特殊的标志,指代差异算法 }
- diff 算法时,根据类型标记(patch flag)数字来检查来确定不同类型的动态节点,来做相应的操作。
- 编译模板时,为动态节点添加一个标记
由上图可知,vue3中,静态的文本将不会进行比较,只比较打了静态标记的动态文本
- 静态提升 hoistStatic
-
- vue3 在编译过程中会对静态内容(模板中永远不会改变的内容)进行提升
- 将创建静态内容的vnode函数,提升到这个模板的渲染函数之外,缓存起来,在重新渲染时重用这个节点,例如:
<div> <div>foo</div> <!-- 需提升 --> <div>bar</div> <!-- 需提升 --> <div>{{ dynamic }}</div> </div>
上面代码
foo
和bar
这两个 div 是完全静态的,没有必要在重新渲染时再次创建,所以编译时对完全静态的元素进行了缓存import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock(_Fragment, null, [ _cache[0] || (_cache[0] = _createElementVNode("div", null, "foo ", -1 /* HOISTED */)), _cache[1] || (_cache[1] = _createElementVNode("div", null, "HelWorldlo ", -1 /* HOISTED */)), _createElementVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */) ], 64 /* STABLE_FRAGMENT */)) } // Check the console for the AST
_cache[0] ||_cache[0] = _createElementVNode("div", null, "foo ", -1 /* HOISTED */) 将静态文本的创建给缓存起来,如果有直接取缓存的内容
- 事件监听缓存 cachHandler
- 绑定事件行为会被视为动态绑定,每次都会重新定义
- vue3 编译时将事件定义赋值给一个变量缓存起来,下次重新染时直接复用
没开启事件监听缓存
export const render = /*#__PURE__*/_withId(function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [ _createVNode("button", { onClick: _ctx.onClick }, "点我", 8 /* PROPS */, ["onClick"]) // PROPS=1<<3,// 8 //动态属性,但不包含类名和样式 ])) })
开启事件侦听器缓存
export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [ _createVNode("button", { onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args))) }, "点我") ])) }
- 服务端渲染(SSR)方面也进行了优化
- 静态节点直接输出,绕过了vdom
- 动态节点,还是需要动态渲染
<div> <div> <span>你好</span> </div> ... // 很多个静态属性 <div> <span>{{ message }}</span> </div> </div>
上面代码中编译后:
import { mergeProps as _mergeProps } from "vue" import { 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)) }><div><span>你好</span></div> ... // 很多个静态属性 <div><span>${ _ssrInterpolate(_ctx.message) }</span></div></div>`) } // Check the console for the AST
从上面可以看出,静态节点直接输出了。这样减少了服务端的计算量,提升了SSR的速度
- 移除一些不常用的API(如: filter、inline-template)
- Tree shaking 优化
- vue2 中,vue源代码是使用CommonJS 格式编写的,所以不支持tree shaking。这意味着,即使你用了vue一部分功能,最总打包仍会包含整个vue库代码
- vue3中,源代码被重写为使用 ES Module 格式,支持Tree Shaking。
- Tree shanking 在编译阶段利用 ES6 Moudule (import 、 export)判断哪些模块已经加载,哪些模块和变量未被使用或引用,进而删除对应的代码
- 如:ref、reactive、computed等等 仅仅在用到的时候才引入,没有用到不会被引入。
- 这样就会减少最终的打包体积
-
<script setup> import {ref,computed} from 'vue' const countRef = ref(0) const ReadCount = computed(()=> countRef.value++) </script>
上面代码在打包时,会将ref、computed 功能函数打包进去,其他没有用到的则去掉
什么是Vite
-
-
- 前端打包工具,Vue 作者发起的项目
- 借助Vue的影响力,和webpack 竞争
- 优势:开发环境下无需打包,启动快。(注意:生产环境还是需要打包的)
-
Vite 为什么启动非常快
-
-
- 开发环境使用 ES6 Module,无需打包。所以非常快
- 生产环境使用rollup,并不会快很多
-
- 一个模块就是一个独立的文件,模块的作用域只在模块内,外面无法访问
- 只有通过export、import 申明的才可以外部访问,有很好的保护作用
- 在HTML中使用模块,需要<script type="module">来告诉浏览器这里需要当作模块来对待(包括内联代码和外部引用)
<script type="module" src="./foo.js"> <script type="module"> // .... </script>
- export 导出
- 单个导出
export const a = 1 export function b() {}
- 集体导出
const a = 1 function add(a,b){ return a-b } function subtract (a,b){ return a - b } export { a, add, subtract }
- 默认导出 export default:
- 导入时不需要花括号
- 只暴露一个主要的接口,比如一个构造函数 或 一个对象
- default 只能用一次
calss User{ constructor(name,age){ this.name = name this.age = age } greet (){ } } export default User
- 导出时可以重命名:使用as关键字给导出的内容重新命名
const a = 1 function add(me){} export { a as newName, add }
- 单个导出
- import 导入
- 导入具体的导出
import {a,add} from './module'
- 导入所有导出:使用 * as ,将整个模块的所有导出, 导入为一个对象
import * as myModule from './module'
console.log(myModule.add)
console.log(myModule.a)将 moudle 中的所有导出(a、add、subtract),导入到module 这个对象中
- 导入 默认导出(export default )时:不需要使用大括号
//a.js. 默认导出 const str = "export default的内容"; export default str //在另一个文件中的导入方式: import StrFile from 'a'; //导入的时候没有花括号 //本质上,a.js文件的export default输出一个叫做default的变量,然后系统允许你引入的时候为其重命名
- 导入可以重命名:使用 as 关键字 给倒入的变量或函数 重命名
import { newA as a, add, min as subtract } from './module'
- 导入具体的导出
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!