Vue 3.0
Vue 3与 Vue 2的区别
源码组织方式的变化,Vue3的源码全部采用TypeScript方式重写,项目组织的方式也有变化,使用Monorepo的方式组织项目的结构,把独立的功能模块提取到不同的包中,Vue3虽然代码重写了,但是90%以上的api还是兼容2.x,并根据社区的反馈添加了Composition API即组合式api,它是用来解决2.x开发大型项目时遇到超大组件使用options不好拆分和重用的问题,
学习Composition API最好的方式是查看官方的RFC(Request For Comments),Composition API RFC文档
为什么要有Composition API呢?Vue2开发中小型项目很好用,但是开发大型项目需要长期迭代维护会有一些限制,例如会有功能复杂的组件,组件复杂的时候阅读起来比较困难,因为vue2使用的是Options API,Options API指的是包含一个描述组件选项(data、methods、props等)的对象,Options API开发复杂组件,同一个功能逻辑的代码被拆分到不同选项,为了看懂同一个功能需要不同的上下滚动滚动条,例如:
export default {
data(){
return {
position:{
x: 0,
y: 0
}
}
},
created(){
window.addEventListener('mousemove',this.handle)
}
destroyed(){
window.removeEventListener('mousemove',this.handle)
},
methods:{
handle(e){
this.position.x = e.pageX
this.position.y = e.pageY
}
}
}
而Composition API是vue3新增的一组基于函数的API,可以更灵活的组织组件的逻辑。例如:
import { reactive, onMounted, onUnmounted} from 'vue'
function useMousePosition(){
const position = reactive({
x: 0,
y: 0
})
const update = (e)=>{
position.x = e.pageX
position.y = e.pageY
}
onMounted(()=>{
window.addEventListener('mousemove',update)
})
onUnmounted(()=>{
window.addEventListener('mousemove',update)
})
return position
}
export default{
setup(){
const position = useMousePosition()
return {
position
}
}
}
在性能方面Vue3有很大幅度的提升,主要包括响应式系统升级、编译优化、源码体积的优化。
vue2中响应式系统的核心是defineProperty,而Vue3使用proxy对象来重写响应式系统,它可以监听动态新增的属性、监听属性的删除以及监听数组的索引和length属性的操作。
编译优化,vue2中通过标记静态根节点,优化diff的过程,而vue3中标记和提升所有的静态根节点,diff的时候只需要对比动态节点内容。另外vue3中新引入了Fragments特性,模板中不需要再创建唯一的根节点。通过patch flag在diff过程中跳过静态节点,只需要更新动态节点的内容,提升了diff性能。然后缓存事件处理函数,减少了不必要的更新操作。
优化打包体积,vue3中移除了一些不常用的API,例如inline-template、filter等,可以让代码的体积变小。另外vue3对Tree-shaking的支持更好,Tree-shaking依赖ES Module,通过编译时的静态分析,将没有使用的模块,打包过滤掉。
随着vue3的发布,官方提供了开发工具Vite,使用Vite在开发阶段测试的时候不需要打包,可以直接去运行项目,提升了开发的效率。
vite的vue-cli的区别是vite在开发模式下不需要打包可以直接运行,vue-cli开发模式下必须对项目打包才可以运行。vite在生产环境下使用Rollup基于ES Module的方式打包,vue-cli使用Webpack打包。
Composition API
Composition API仅仅是vue3新增多的api,像createApp函数用来创建Vue对象,setup函数是Composition API的入口,reactive函数用来创建响应式对象。
import { createApp, reactive } from 'vue'
const app = createApp({
setup(){
//第一个参数props
//第二个参数context,attrs、emit、slots
const position = reactive({
x: 0,
y: 0
})
return {
position
}
},
mounted(){
this.position.x = 100
}
})
app.mount('#app')
生命周期钩子函数
在setup中可以使用组件生命周期中的钩子函数,但是需要在生命周期钩子函数前面加上on,然后首字母大写,比如mounted在setup函数中对应onMounted。另外setup是在组件初始化之前执行的,是在beforeCreate和created之间执行的,所以在beforeCreate和created的代码都可以在setup中。
Options API | Hook inside inside setup |
---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
reactive/toRefs/ref
这三个函数都是用来创建响应式数据的。
toRefs可以将响应式对象中的所有属性都转换成响应式的,toRefs要求传入的参数必须是一个代理对象,如果不是代理对象则会报警提示。它内部会创建一个新的对象,然后遍历代理对象的所有属性,把所有属性的值都转换成响应式对象,然后挂载到新创建的对象上并返回。它内部会为代理对象的每一个属性创建一个具有value属性的对象,该对象是响应式的,value属性具有get和set,这一点和ref类似,get返回代理对象中对应属性的值,set中给代理对象的属性赋值,所以返回的每一个属性都是响应式的。
import { createApp, ref , toRefs} from 'vue'
createApp({
setup(){
const position = {
x: 0,
y: 0
}
const update = e=>{
position.x = e.pageX
position.y = e.pageY
}
onMounted(()=>{
window.addEventListener('mousemove',update)
})
onUnmounted(()=>{
window.removeEventListener('mousemove',update)
})
return toRefs(position)
}
}).mount('#app')
ref的作用是把普通的数据转换成响应式数据,而reactive的作用是把一个对象转换成响应式数据,ref可以把基本类型数据包装成响应式对象。
import { createApp, ref } from 'vue'
function useCount(){
const count = ref(0)
return {
count,
increase:()=>{
count.value++
}
}
}
createApp({
setup(){
return {
...useCount()
}
}
}).mount('#app')
ref如果传入对象,则内部会调用reactive返回代理对象。如果是基本类型的值,则内部会创建有value属性的对象,该对象的value属性具有get和set,在get中收集依赖,在set中触发更新。
Computed
两种用法,第一种是传入获取值得函数,函数的内部依赖响应式的数据,当依赖的数据发生变化后会重新执行该函数获取数据。computed函数返回一个不可变的响应式对象,类似于使用ref创建的对象,只有一个value属性,获取计算属性的值可以通过value来获取。
computed(()=>count.value+1)
第二种用法是传入一个对象,对象具有set和get,返回不可变的响应式对象。
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: val=>{
count.value = val - 1
}
})
Watch
Watch函数有三个参数,第一个参数是要监听的数据,第二个参数是监听到数据变化后执行的函数,这个函数有两个参数分别是新值和旧值,第三个参数是选项对象,deep和immediate。
Watch的返回值是取消监听的函数。
import { createApp, ref ,watch} from 'vue'
createApp({
setup(){
const question = ref('')
const answer = ref('')
watch(question,async (newValue,oldValue)=>{
const response = await fetch('http://www.xxx.com')
answer.value = await response.json()
})
return {
question,
answer
}
}
}).mount('#app')
Vue3中提供了WatchEffect,它是watch函数的简化版本,也用来监视数据的变化。它接收一个函数作为参数,监听函数内响应式数据的变化。
import { createApp, ref ,watchEffect } from 'vue'
createApp({
setup(){
const count = ref(0)
const stop = watchEffect(()=>{
console.log(count.value)
})
return {
count,
stop,
increase:()=>{
count.value++
}
}
}
}).mount('#app')
响应式原理
和Vue2相比Vue3响应式系统底层使用Proxy对象实现,在初始化的时候不需要遍历所有的属性再把属性通过defineProperty转换成get和set。另外在有属性嵌套的时候,在访问属性的过程中处理下一级属性。所以Vue3的响应式性能比Vue2好。Vue3可以监听动态添加的属性,监听属性的删除操作,监听数组索引和length属性的修改操作,另外也可以将响应式系统作为单独的模块使用。
先看一下Proxy对象的使用
const target = {
foo: 'xxx',
bar: 'yyy'
}
// Reflect.getPrototypeOf()
// Object.getPrototypeOf()
const proxy = new Proxy(target,{
get(target,key,receiver){
//return target[key]
return Reflect.get(target,key,receiver)
},
set(target,key,value,receiver){
//target[key]=value
Reflect.set(target,key,value,receiver)
},
deleteProperty(target,key){
//delete target[key]
Reflect.deleteProperty(target,key)
}
})
上面有两个注意的问题,第一个问题是set和deleteProperty中需要返回布尔类型的值,在严格模式下,如果返回false的话会出现TypeError的异常。第二个问题是Proxy和Reflect中使用的receiver,Proxy中receiver是Proxy或者继承Proxy的对象,Reflect中receiver是如果target对象中设置了getter,getter中的this指向receiver。
const obj = {
get foo(){
console.log(this)
return this.bar
}
}
const proxy = new Proxy(obj,{
get(target,key,receier){
if(key === 'bar'){
return 'value - bar'
}
return Reflect.get(target,key)
}
})
reactive的模拟实现
它接收一个参数,判断这参数是否是对象,然后创建拦截器对象,设置get/set/deleteProperty,返回Proxy对象。
const isObject = val => val!==null && typeof val === 'object'
const convert = target => isObject(target) ? reactive(target):target
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (target,key) => hasOwnProperty.call(target,key)
export function reactive(target){
if(!isObject(target)) return target
const handler = {
get(target,key,receiver){
//收集依赖
console.log('get',key)
const result = Reflect.get(target,key,receiver)
return convert(result)
},
set(target,key,value,receiver){
const oldValue = Reflect.get(target,key,receiver)
let result = true
if(oldValue!==value){
result = Reflect.set(target,key,value,receiver)
//触发更新
console.log('set',key,value)
}
return result
},
deleteProperty(target,key){
const hadKey = hasOwn(target,key)
const result = Reflect.deleteProperty(target,key)
if(hadKey && result){
//触发更新
console.log('delete',key)
}
}
}
return new Proxy(target,handler)
}
收集依赖
在依赖收集的过程中会创建三个集合,分别是targetMap、depsMap、dep。targetMap的作用是记录目标对象和一个字典,也就是中间的这个map。targetMap使用的类型是WeakMap,key是目标对象,值是depsMap。depsMap是一个字典Map,key是目标对象的属性名称,值是set集合。set集合中元素不会重复,里面存储的是effect函数。通过这种方式可以收集目标对象和目标对象的属性以及Effect函数。
const isObject = val => val!==null && typeof val === 'object'
const convert = target => isObject(target) ? reactive(target):target
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (target,key) => hasOwnProperty.call(target,key)
export function reactive(target){
if(!isObject(target)) return target
const handler = {
get(target,key,receiver){
//收集依赖
console.log('get',key)
track(target,key)
const result = Reflect.get(target,key,receiver)
return convert(result)
},
set(target,key,value,receiver){
const oldValue = Reflect.get(target,key,receiver)
let result = true
if(oldValue!==value){
result = Reflect.set(target,key,value,receiver)
//触发更新
console.log('set',key,value)
}
return result
},
deleteProperty(target,key){
const hadKey = hasOwn(target,key)
const result = Reflect.deleteProperty(target,key)
if(hadKey && result){
//触发更新
console.log('delete',key)
}
}
}
return new Proxy(target,handler)
}
let activeEffect = null
export function effect(callback){
activeEffect = callback
callback() //访问响应式对象属性,去收集依赖
activeEffect = null
}
let targetMap = new WeakMap()
export function track(target,key){
if(!activeEffect) return
let depsMap = targetMap.get(target)
if(!depsMap){
targetMap.set(target,(depsMap=new Map()))
}
let dep = depsMap.get(key)
if(!dep){
depsMap.set(key,(dep = new Set()))
}
dep.add(activeEffect)
}
依赖收集之后就是触发更新trigger
const isObject = val => val!==null && typeof val === 'object'
const convert = target => isObject(target) ? reactive(target):target
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (target,key) => hasOwnProperty.call(target,key)
export function reactive(target){
if(!isObject(target)) return target
const handler = {
get(target,key,receiver){
//收集依赖
console.log('get',key)
track(target,key)
const result = Reflect.get(target,key,receiver)
return convert(result)
},
set(target,key,value,receiver){
const oldValue = Reflect.get(target,key,receiver)
let result = true
if(oldValue!==value){
result = Reflect.set(target,key,value,receiver)
//触发更新
trigger(target,key)
console.log('set',key,value)
}
return result
},
deleteProperty(target,key){
const hadKey = hasOwn(target,key)
const result = Reflect.deleteProperty(target,key)
if(hadKey && result){
//触发更新
trigger(target,key)
console.log('delete',key)
}
}
}
return new Proxy(target,handler)
}
let activeEffect = null
export function effect(callback){
activeEffect = callback
callback() //访问响应式对象属性,去收集依赖
activeEffect = null
}
let targetMap = new WeakMap()
export function track(target,key){
if(!activeEffect) return
let depsMap = targetMap.get(target)
if(!depsMap){
targetMap.set(target,(depsMap=new Map()))
}
let dep = depsMap.get(key)
if(!dep){
depsMap.set(key,(dep = new Set()))
}
dep.add(activeEffect)
}
export function trigger(target,key){
const depsMap = targetMap.get(target)
if(!depsMap) return
const dep = depsMap.get(key)
if(dep){
dep.forEach(effect=>{
effect()
})
}
}
ref
ref可以把基本数据类型数据,转成响应式对象。ref返回的对象重新赋值成对象也是响应式的。reactive返回的对象,重新赋值丢失响应式,reactive返回的对象不可以解构。
export function ref(raw){
//判断raw是否是ref创建的对象,如果是的话直接返回
if(isObject(raw) && raw.__v__isRef){
return
}
let value = convert(raw)
const r = {
__v__isRef: true,
get value(){
track(r,'value')
return value
},
set value(newValue){
if(newValue!==value){
raw = newValue
value = convert(raw)
trigger(r,'value')
}
}
}
return r
}
toRefs
export function toRefs(proxy){
const ret = proxy instanceof Array ? new Array(proxy.length) : {}
for(const key in proxy){
ret[key] = toProxyRef(proxy,key)
}
return ret
}
function toProxyRef(proxy,key){
const r = {
__v__isRef: true,
get value(){
return proxy[key]
},
set value(newValue){
proxy[key] = newValue
}
}
return r
}
computed
export function computed(getter){
const result = ref()
effect(()=>(result.value = getter()))
return result
}