Vue3 知识点总结

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 带来了什么?

  • 更好的代码组织
  • 更好的逻辑复用
  • 更好的类型推导
  1. 更好的代码组织、更好的逻辑复用
    Option API 代码逻辑是分散的;组合式API 代码逻辑是组合到一起的。

出处:https://coding.imooc.com/lesson/419.html
在这里插入图片描述

  1. 更好的类型推导
    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 ?
  • 返回值类型,会丢失响应式
  • 如在 setupcomputed、合成函数,都有可能返回值类型
  • Vue 如果不定义 ref,用户将自造 ref,反而混乱
为何需要 .value ?
  • ref 是一个对象,才能不丢失响应式,value 用来存储值
  • 通过 .value 属性的 getset 实现响应式
  • 用于模板、reacttive 时,不需要 .value,其他情况都需要

ref 也是靠 reactive 实现的。类似于 ref = reactive({ value: '张三' }) ;

为什么需要 toRef 和 toRefs ?

初衷:在不丢失响应式的情况下,把对象数据分解/扩散
前提:针对的是 reactive 封装的响应式对象,并非普通对象。

reactive、ref 是创造响应式;toReftoRefs 是延续响应式。

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

watchwatchEffect 的区别:

  • 两者都可监听 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 APIsetup 只会被调用一次,而 React Hooks 函数会被多次调用
  • Composition API 无需 useMemouseCallback,因为 setup 只调用一次
  • Composition API 无需顾虑调用顺序,而 React Hooks 需要保证 hooks 的顺序一致
  • Composition API 的 reacttive + refReact HooksuseState 要难理解

生命周期

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:等于 beforeCreatecreated
  • 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.setVue.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 静态标记

  • 编译模板时,动态节点做标记
  • 标记 分为不同的类型,如 TEXTPROPS
  • diff 算法时,可以区分静态节点,以及不同类型的动态节点

可以区分出动态和静态节点,静态节点就不用比较了,因为静态节点是不会变的,这样就可以优化 diff 算法。(从输入来源做的优化,并不是针对 diff 算法这个流程做的优化)

出处:https://coding.imooc.com/lesson/419.html
在这里插入图片描述

hoistStatic 静态提升

  • 将静态节点的定义,提升到父作用域,缓存起来
  • 多个相邻的静态节点,会被合并起来
  • 典型的拿空间换时间的优化策略
    在这里插入图片描述
    以上三个静态节点定义到父作用域,以后这个函数怎么执行,都不会重新定义这个三个变量。(空间换时间)
    在这里插入图片描述
    在这里插入图片描述
    如果相邻的静态节点很多,到一定程度后,就会合并起来做一个静态的集合,只定义一个变量。(不是空间换时间,类似于合并,提前帮我们做的操作)
    在这里插入图片描述

cacheHandler 缓存事件

  • 缓存事件
    在这里插入图片描述
    有缓存就去拿缓存,没有缓存就把这个事件定义并缓存。
    在这里插入图片描述

SSR 优化

在这里插入图片描述
开启 ssr 后直接渲染字符串,没有经过 vdom 的一个转换。
在这里插入图片描述

Tree-shaking 优化

  • 编译时,根据不同的情况,引入不同的 API

根据模板的指令,选择需要 importapi,需要才引入
编译时尽可能的减少体积,从而进行优化
在这里插入图片描述
在这里插入图片描述

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 的区别

  1. Webpack
  • 支持的模块规范:ES ModulesCommonJSAMD Modules
  • Dev Server:通过 webpack-dev-server 托管打包好的模块
  • 生产环境构建:webpack
  1. 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 进行简化

基本使用

  • 顶级变量、自定义组件,可直接用于模板
  • 可正常使用 refreactivecomputed 的能力
  • 和其他 <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

  1. Vue3Vue2 有什么优势?

    • 性能更好
    • 体积更小
    • 更好的 ts 支持
    • 更好的代码组织
    • 更好的逻辑抽离
    • 更多新功能
  2. 描述 vue3 的生命周期。

    • Options API 生命周期:
      • beforeDestroy 改为 beforeUnmount
      • destroyed 改为 unmounted
      • 其他沿用 vue2 的生命周期
    • Composition API 生命周期:
      • setup:等于 beforeCreatecreated
      • onBeforeMount
      • onMounted
      • onBeforeUpdate
      • onUpdated
      • onBeforeUnmount
      • onUnmounted
  3. 如何看待 Composition APIOptions API ?

    • composition API 带来了什么?
      • 更好的代码组织
      • 更好的逻辑复用
      • 更好的类型推导
    • 如何选择?
      • 不建议共用,会引起混乱
      • 小型项目、业务逻辑简单,用 Options API
      • 中大型项目、逻辑复杂,用 Composition API
    • 别误解 Composition API
      • Composition API 属于高阶技巧,不是基础必会
      • Composition API 是为解决复杂业务逻辑而设计
      • Composition API 就像 HooksReact 中的地位
  4. 如何理解 reftoReftoRefs

    • ref
      • 生成值类型的响应式数据
      • 可用于模板和 reactive
      • 通过 .value 修改值
      • 可获取 dom 节点
    • toRef
      • 针对一个reactive 封装的响应式对象的 属性
      • 常见一个 ref,具有响应式
      • 两者保持引用关系
    • toRefs
      • reactive 封装的响应式对象转换成普通对象
      • 对象的每个属性都是对应的 ref
      • 两者保持引用关系
      • 合成函数返回响应式对象
  5. 关于 ref 为什么是一个对象。
    因为 Object.definePropertyproxy 都只能监听复杂数据类型,只不过 vue2 中是放在 data 中进行监听,而 vue3 不存在这样一个数据收集的地方,所以只能单个去收集数据。本质上 vue2vue3 都是需要外面“包裹”一层对象作为被监听数据的“承载容器”来进行 definePropertyproxy 监听。

  6. 为什么需要 ref
    不管是 Object.definePropety 还是 proxy,都是用来监听复杂数据类型的,objectarray,不能对简单数据类型 string,number,boolean 这些的变化进行直接监听。vue2 中是把监听内容放在 data 的返回对象中,在初始化的时候使用 Object.definePropety 对该对象进行监听,所以在对 data 的返回对象新添加数据的时候,需要使用特殊方法,而不能直接 this.xxx 这样添加,因为 vue2 这时候不会对该对象进行重新监听,数据会不具有响应式特性。
    vue3 中没有 vue2 这种一个特定地方(即 vue2 中的 data 函数返回的那个对象)去对所有数据进行统一监听处理,所以我们需要告诉 vue3,哪些是我们需要使用响应式属性的数据。本质上 refreactive 是在收集这些数据。关于 ref,因为 proxy 无法直接监听简单数据类型,所以需要包裹成一个对象的形式来进行监听,proxy 就是去监听 ref 定义的这个对象,以此来监听 ref.value 这个值的变化。

  7. vue3 升级了哪些重要功能?

    • 由原来的 new Vue() 变成 createApp 创建实例
    • emits 属性:在子组件中使用 emits 属性声明父组件传递的事件
    • 多事件处理:v-on 可以传递多个事件
    • Fragmentvue2 模板里面只能是单一节点; vue3 里面可以是多个节点
    • 移除 .sync 改为 v-model 参数
    • 改变了异步组件的引用方式:使用 defineAsyncComponent
    • 移除了 filter
    • 新增 Teleport 可以把弹窗放到组件外面去
    • 新增 Suspense:具有深层异步依赖的组件,使用 Suspense,在初始渲染时,Suspense 将在内存中渲染其默认的插槽内容
    • 新增了 Composition API
    • 生命周期:提供两种生命周期方式,Options APIComposition APIOptions APIvue2beboreDestroy和destroyed更名为beforeUnmountunmounted,其余沿用vue2的生命周期;Composition API 生命周期包括 onBeforeMount、onMounted、onBeforeUpdate、onUpdated、onBeforeUnmout、onUnmountedsetup 等于 beforeCreatecreated
  8. Composition API 如何实现代码逻辑复用?

    • 抽离逻辑代码到一个函数
    • 函数命名约定为 useXxxx 格式(React Hooks 也是)
    • setup 中引用 useXxxx 函数
  9. Vue3 如何实现响应式?
    使用 Proxy 实现响应式:

    • 深度监听,性能更好:在 get 中进行递归,获取到哪一层才进行递归。
    • 可监听 新增/删除 属性:有 deleteProperty 属性监听删除;可在 set 中判断出是已有属性还是新增属性。
    • 可监听数组变化

    缺点:

    • Proxy 无法兼容所有浏览器,无法 polyfill
  10. watchwatchEffect 的区别是什么?

    • 两者都可监听 data 属性变化
    • watch 需要明确监听哪个属性
    • watchEffect 会根据其中的属性,自动监听其变化
  11. setup 中如何获取组件实例?

    • setup 和其他 Composition API 中没有 this
    • 可通过 getCurrentInstance 获取当前实例
  12. Vue3 为何比 Vue2 快?

    • Proxy 实现响应式:
      能规避 Object.defineProperty 的问题。

      • 深度监听,性能更好
      • 可监听 新增/删除 属性
      • 可监听数组变化
    • PatchFlag 静态标记:

      • 编译模板时,动态节点做标记
      • 标记 分为不同的类型,如 TEXTPROPS
      • diff 算法时,可以区分静态节点,以及不同类型的动态节点
    • hoistStatic 静态提升:
      将静态节点提升到父作用域,只定义一次。

    • cacheHandler 缓存事件:
      有缓存就去取缓存,没有缓存就定义函数并缓存,不用每次都去定义函数。

    • SSR 优化:
      开启 ssr 后直接渲染字符串,不用经过 vdom 的一个转换。

    • Tree-shaking 优化:
      编译时,根据不同的情况,引入不同的 API,尽可能的减少使体积

  13. Vite 是什么?
    Vite 是一个轻量级的、速度极快的构建工具。
    Vite 是旨在提升开发者体验的下一代 JavaScript 构建工具,核心借助了浏览器的原生 ES Modules 和像 esbuild 这样的将代码编译成 native code 的打包工具。极大的降低了应用的启动和热更新时间。

  14. Composition APIReact Hooks 的对比。

    • Composition APIsetup 只会被调用一次,而 React Hooks 函数会被多次调用
    • Composition API 无需 useMemouseCallback,因为 setup 只调用一次
    • Composition API 无需顾虑调用顺序,而 React Hooks 需要保证 hooks 的顺序一致
    • Composition API 的 reacttive + refReact HooksuseState 要难理解
  • 24
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值