1-导学
1-1-技术面试流程
基础------>高级特性+原理(框架)------->设计+工作经验
1-2-知识点介绍
- vue和react:基本使用、高级特性、原理
- 工具: webpack的配置、性能优化、babel
- 项目设计: 状态设计、组件设计、组件通信
1-3-浅看面试题
-
Vue
-
v-show和v-if的区别
-
为什么v-for会用key
-
描述vue组件生命周期(有父子组件的情况下)
-
Vue组件如何通信
-
描述组件渲染和更新的过程
-
双向数据绑定和v-model的实现原理
-
-
React
- React组件如何通信
- JSX本质是什么
- context是什么,有什么用途
- shouldComponentUpdate的用途
- 描述redux单项数据流
- setState是同步还是异步
-
框架综合应用
-
基于React设计一个todolist(组件结果,redux state数据结构)
-
基于Vue设计一个购物车(组件结果,vuex State数据结构)
- webpack面试题
- 前端代码为何要进行构建和打包
- module chunck bundle分别是什么意思,有什么区别?
- loader和plugin的区别?
- webpack如何实现懒加载
- webpack常见的性能优化
- babel-runtime和babel-polyfill的区别
-
1-4-如何应对上述面试题
- 框架的使用(基本使用、高级特性、周边插件)
- 框架的原理(基本原理的了解、热门技术的深度、全面性)
- 框架的实际运用,即设计能力(组件结构,数据结构)
2-Vue2.0
2-1-导学
-
先学vue2再学vue3
- vue3不是从0做出来的,而是从vue进化的
- vue2还会被继续使用,面试会继续考察
- vue2的语法,绝大部分是被vue3支持的
-
vue和react越来越接近
- vue3的Options API对应React class Component
- vue3的Composition API 对应 React Hooks
2-2-Vue的使用
2-2-1-基本使用,组件使用
01-基本使用
-
差值
{{message}}
、表达式{{flage?'yes':'no'}}(只能是表达式,不能是js语句)
-
指令、动态属性
:id="myid"
-
v-html: 会有xss风险,会覆盖子组件
-
computed和watch
- computed有缓存,data不变则不会重新计算
- watch如何深度监听?
- watch监听引用类型,拿不到oldValue
-
class和style
-
使用动态属性、使用驼峰式写法
<p :class="{ black: isBlack, yellow: isYellow }">使用 class</p> <p :class="[black, yellow]">使用 class (数组)</p> <p :style="styleData">使用 style</p> data() { return { isBlack: true, isYellow: true, black: 'black', yellow: 'yellow', styleData: { fontSize: '40px', // 转换为驼峰式 color: 'red', backgroundColor: '#ccc' // 转换为驼峰式 } } }
-
-
条件渲染
- v-if v-else的用法,可以使用变量,也可以使用 === 表达式
- v-if 和 v-for 的区别?
- v-if 和 v-for 的使用场景?更新不频繁就 if, 频繁就show。
-
循环渲染
- 如何遍历对象?-----也可以用 v-for
- key 的重要性。key不能乱写(如random或者index)
- v-for 和 v-if 不能一起使用!for 比 if 计算优先级高一些,先用 for 进行循环,然后用 if 进行判断。如有三项数据,进行三项次循环,每一次循环进行 if 判断,做了三次重复的判断。
-
事件
-
event 参数,自定义参数。1、event是原生对象;2、事件被挂载到当前元素。
<button @click="increment1">+1</button> <button @click="increment2(2, $event)">+2</button>
-
事件修饰符,按键修饰符
<!-- 阻止事件继续传播 --> <a v-on:click.stop=""></a> <!-- 提交事件不再重载页面 --> <from v-on:submit.prevent=""></from> <!-- 修饰符可以串联 --> <a v-on:click.stop.prevent=""></a> <!-- 只有修饰符 --> <from v-on:click.submit.prevent></from> <!-- 添加事件监听器时使用事件捕获模式 --> <!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 --> <div v-on:click.capture=""></div> <!-- 只当在event.target 是当前元素自身时触发处理函数 --> <!-- 即事件不是从内部元素触发的 --> <div v-on:click.self=""></div>
<!-- 即使 Alt 或 Shift 被一同按下,也会触发 --> <button @click.ctrl=""></button> <!-- 有且只有Ctrl被按下时,才会触发 --> <button @click.ctrl.exact=""></button> <!-- 没有任何系统修饰符被按下,才会触发 --> <button @click.exact=""></button>
-
【观察】事件被绑定到哪里?
-
-
表单
-
修饰符 lazy number trim
<input type="text" v-model.trim="name"/> <input type="text" v-model.lazy="name"/> <!-- 输入完成后才会变化 --> <input type="text" v-model.number="age"/>
-
常见表单项 textarea checkbox radio select
<!-- checkedNames为数组 --> <p>多个复选框 {{checkedNames}}</p> <input type="checkbox" id="jack" value="Jack" v-model="checkedNames"> <label for="jack">Jack</label> <input type="checkbox" id="john" value="John" v-model="checkedNames"> <label for="john">John</label> <input type="checkbox" id="mike" value="Mike" v-model="checkedNames"> <label for="mike">Mike</label> <!-- gender为具体值的字符串--> <p>单选 {{gender}}</p> <input type="radio" id="male" value="male" v-model="gender"/> <label for="male">男</label> <input type="radio" id="female" value="female" v-model="gender"/> <label for="female">女</label>
-
02-组件使用
-
props 和 $emit
父到子,通过props传属性。子到父,通过$emit触发父的方法。
<List :list="list" @delete="deleteHandler"/>
props: { // prop 类型和默认值 list: { type: Array, default() { return [] } } }, methods: { deleteItem(id) { this.$emit('delete', id) } },
-
组件间通讯 - 自定义事件
// 绑定自定义事件 event.$on('onAddTitle', this.addTitleHandler) // 调用自定义事件 event.$emit('onAddTitle', this.title) // 及时销毁,否则可能造成内存泄露 event.$off('onAddTitle', this.addTitleHandler)
-
组件生命周期
- 挂载阶段
- 更新阶段
- 销毁阶段
2-2-2-高级特性
01-自定义v-model
父组件:
<template>
<div>
<p>{{name}}</p>
<CustomVModel v-model="name"/>
</div>
</template>
<script>
import CustomVModel from './CustomVModel'
export default {
components: {
CustomVModel
},
data() {
return {
name: '双越'
}
}
}
</script>
子组件:
<template>
<!-- 例如:vue 颜色选择 -->
<input type="text"
:value="text1"
@input="$emit('change1', $event.target.value)"
>
<!--
1. 上面的 input 使用了 :value 而不是 v-model
2. 上面的 change1 和 model.event1 要对应起来
3. text1 属性对应起来
-->
</template>
<script>
export default {
model: {
prop: 'text1', // 对应 props text1
event: 'change1'
},
props: {
text1: String,
default() {
return ''
}
}
}
</script>
02-$nextTick和refs
- Vue是异步渲染
- data改变之后,DOM不会立刻渲染
- $nextTick 会在DOM渲染之后被触发,以获得最新的DOM节点
- 页面渲染时会将 data 的修改做整合,多次 data 修改只会渲染一次
03-slot
-
基本使用
<!-- 父组件 --> <SlotDemo :url="website.url"> {{website.title}} </SlotDemo>
<template> <a :href="url"> <slot> 默认内容,即父组件没设置内容时,这里显示 </slot> </a> </template> <script> export default { props: ['url'] } </script>
-
作用域插槽
<template> <a :href="url"> <slot :slotData="website"> {{website.subTitle}} <!-- 默认值显示 subTitle ,即父组件不传内容时 --> </slot> </a> </template> <script> export default { props: ['url'], data() { return { website: { url: 'http://wangEditor.com/', title: 'wangEditor', subTitle: '轻量级富文本编辑器' } } } } </script>
<!-- 父组件 --> <ScopedSlotDemo :url="url"> <template v-slot="slotProps"> {{slotProps.slotData.title}} </template> <script>
-
具名插槽
<div class="container"> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer> </div>
<!-- 父组件 --> <base-layout> <template v-slot:header> <h1>插入到header 的slot中</h1> </template> <p>插入到 main slot 中, 即未命名的slot</p> <template v-slot:footer> <p>插入到 footer 的slot中</p> </template> </base-layout>
04-动态、异步组件
01-动态属性
-
:is=“component-name” 用法
-
需要根据数据,动态渲染的场景。即组件类型不确定。
<template> <div> <!-- 动态组件 --> <component :is="NextTickName"/> <!-- 必须得是动态的属性 --> </div> </template> <script> import NextTick from './NextTick' export default { components: { NextTick }, data() { return { NextTickName: "NextTick", } } } </script>
02-异步组件
<template>
<div>
<FormDemo v-if="showFormDemo"/>
<button @click="showFormDemo = true">show form demo</button>
</div>
</template>
<script>
export default {
components: {
FormDemo: () => import('../BaseUse/FormDemo'), // 异步渲染
},
data() {
return {
showFormDemo: false
}
}
}
</script>
05-keep-alive
-
缓存组件
-
频繁切换,不需要重复渲染
-
Vue常见性能优化
带有层级的,或者tab,用v-show不方便
<template> <div> <button @click="changeState('A')">A</button> <button @click="changeState('B')">B</button> <button @click="changeState('C')">C</button> <keep-alive> <!-- tab 切换 --> <KeepAliveStageA v-if="state === 'A'"/> <!-- v-show --> <KeepAliveStageB v-if="state === 'B'"/> <KeepAliveStageC v-if="state === 'C'"/> </keep-alive> </div> </template> <script> import KeepAliveStageA from './KeepAliveStateA' import KeepAliveStageB from './KeepAliveStateB' import KeepAliveStageC from './KeepAliveStateC' export default { components: { KeepAliveStageA, KeepAliveStageB, KeepAliveStageC }, data() { return { state: 'A' } }, methods: { changeState(state) { this.state = state } } } </script>
06-mixin
- 多个组件都有相同的逻辑,抽离出来
- mixin并不是完美的解决方案,会有一些问题
- Vue3提出的Composition API 在解决这些问题
- mixin 的问题
- 变量来源不明确,不利于阅读
- 多mixin可能引起命名冲突
- mixin 和组件可能出现多对多的关系,复杂度较高
mixin.js:
export default {
data() {
return {
city: '北京'
}
},
methods: {
showName() {
// eslint-disable-next-line
console.log(this.name)
}
},
mounted() {
// eslint-disable-next-line
console.log('mixin mounted', this.name)
}
}
组件:
<template>
<div>
<p>{{name}} {{major}} {{city}}</p>
<button @click="showName">显示姓名</button>
</div>
</template>
<script>
import myMixin from './mixin'
export default {
mixins: [myMixin], // 可以添加多个,会自动合并起来
data() {
return {
name: '双越',
major: 'web 前端'
}
},
methods: {},
mounted() {
// eslint-disable-next-line
console.log('component mounted', this.name)
}
}
</script>
2-2-3-VueX和Vue-router 使用
01-VueX
所有的异步操作都在Actions里完成
02-Vue-Router
- 路由模式(hash、H5 history)
- 路由配置(动态路由,懒加载)
01-路由模式
-
hash模式(默认),如
http://abc.com/#/user/10
-
H5 hostorty模式,如
http://abc/user/20
-
后者需要server端支持,因此无特殊需求可选择前者
02-路由配置
- 动态路由
- 懒加载
component:()=>import("./a")
总结:vueX和vue-router懂怎么配置就行,重点在vue
2-3-回顾题目
2-3-1-v-show和v-if的区别
v-if会频繁的操作dom,v-show只会频繁控制样式的显示隐藏,如果组件用于频繁切换的场景那么就用v-show。
2-3-2-为何v-for中要用key
key不能用index,只能用业务中不能重复的值。
2-3-3-描述Vue组件生命周期(有父子组件的情况下)
1,单个组件:挂载(beforeCreate,created,beforeMount,mounted),更新(beforeUpdate,updated),销毁(beforeDestroy,destroyed)。
2,父子组件:
- 父created -> 子created -> 子mounted -> 父mounted
- 父beforeUpdated -> 子beforeUpdated -> 子updated -> 父updated
创建是从外到内的,渲染是从内到外的。
2-3-4-vue组件如何通讯
- 属性和触发事件的方式(props和this.$emit)
- 自定义事件的方式(两个组件没有关系或者关系深)
- vueX的通讯
2-3-5-描述组件渲染和更新的过程
暂时不会,接下来可以会
2-3-6-双向数据绑定v-model实现原理
暂时不会,接下来可以会
2-4-Vue的原理(大厂必考)
-
面试为何会考察原理?
- 知其然知其所以然。
- 了解原理才能更好运用,竞争激烈。
- 大厂造轮子(有钱有资源,业务定制,技术KPI)
-
面试中如何考察?以何种方式考察?
- 考察重点,非细节,掌握好2/8原则。
- 和使用相关的原理,例如 vdom、模板渲染。
- 整体流程是否全面?热门技术是否有深度?
-
vue原理包括那些?
- 组件化
- 响应式
- vdom,diff
- 模板编译
- 渲染过程
- 前端路由
2-4-1-组件化的基础
-
“很久以前”就有组件化
- asp jsp php已经有组件化了
- nodejs中也有类似的组件化
-
数据驱动视图(MVVM,setState)
-
传统组件,只是静态渲染,更新还要依赖操作DOM
-
数据驱动视图 - vue MVVM(model-view-viewModel)
-
数据驱动视图 - React setState
-
2-4-2-Vue的响应式
01-导学
- 组件data的变化,立刻触发视图的更新。
- 实现数据驱动视图的第一步。
- 考察vue原理的第一题。
- 核心API - Object.defineProperty
- 如何实现响应式,代码演示。
- Object.defineProperty的一些缺点(Vue3.0启用proxy)
- Proxy有兼容性的问题
- Proxy兼容性不太好,且无法用polyfill。
- Vue2.X还会存在一段时间,所以都得学。
- Vue3.0下章讲。
02-Object.defineProperty的基本用法
const data = {};
const name = "zhangsan";
Object.defineProperty(data,"name",{
get: function() {
console.log("get")
return name;
},
set: function(newVal) {
console.log("set");
name = newVal;
}
})
console.log(data.name)
// get
// zhangsan
data.name = "lisi";
//set
03-Object.defineProperty 实现响应式
- 监听对象,监听数组
- 复杂对象,深度监听
- 几个缺点
- 深度监听,需要递归到底,一次性计算量大。
- 无法监听新增/删除属性(Vue.set Vue.delete)。
- 无法监听原生数组,需要特殊处理。
// 触发更新视图
function updateView() {
console.log('视图更新')
}
// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function () {
updateView() // 触发视图更新
oldArrayProperty[methodName].call(this, ...arguments)
// Array.prototype.push.call(this, ...arguments)
}
})
// 重新定义属性,监听起来
function defineReactive(target, key, value) {
// 深度监听
observer(value)
// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
// 深度监听
observer(newValue)
// 设置新值
// 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
value = newValue
// 触发更新视图
updateView()
}
}
})
}
// 监听对象属性
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象或数组
return target
}
// 污染全局的 Array 原型
// Array.prototype.push = function () {
// updateView()
// ...
// }
if (Array.isArray(target)) {
target.__proto__ = arrProto
}
// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}
// 准备数据
const data = {
name: 'zhangsan',
age: 20,
info: {
address: '北京' // 需要深度监听
},
nums: [10, 20, 30]
}
// 监听数据
observer(data)
// 测试
// data.name = 'lisi'
// data.age = 21
// // console.log('age', data.age)
data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组
2-4-3-虚拟DOM(Virtual DOM)和diff
01-导学
1、基础
- vdom是实现vue和react的基石。
- diff算法是vdom中最核心、最关键的部分。
- vdom是一个热门话题,也是面试中的热门问题。
2、背景
- DOM操作操作非常耗费性能
- 以前JQuery,可以自行控制DOM操作时机,手动调整。
- vue和react是数据驱动视图,如何有效控制DOM操作?
3、解决方案 - vdom
-
有了一定复杂度,想要减少计算次数比较困难
-
能不能把计算放到js中计算?因为JS执行速度很快
-
vdom - 用js模拟DOM结构,计算出最小变更,操作DOM
<div id="div1" class="container"> <p>vdom </p> <ul style="font-size: 20px"> <li>a</li> </ul> </div>
{ tag: "div", props: { className: "container", id: "div1" }, children: [ { tag: "p", children: [] }, { tag: "ul", props: {style: "font-size:20px;"}, children: [ { tag: "li", children: "a" } ] } ] }
4、通过snabbdom学习vdom
-
简介强大的dom库,易学易用
-
Vue参考它实现的vdom和diff
-
https://github.com/snabbdom/snabbdom
-
Vue3.0重写了vdom的代码,优化了性能
-
但vdom的基本理念不变,面试考点也不变
-
React和vdom的具体实现也不相同,但不妨统一学习
5、snabbdom重点总结
- h函数
- vnode结构
- patch函数
6、vdom总结
- 用js模拟Dom结构(vnode)
- 新旧vnode对比,得出最小的更新范围,最后更新DOM
- 数据驱动视图模式下,有效控制DOM操作
-
02-diff算法
- diff算法是vdom中最核心,最关键的部分
- diff算法能在日常中使用Vue React 中体现出来(如key)
- diff算法是前端热门话题,面试宠儿
01-diff算法的概述
- diff即对比,是一个广泛的概念,如linux diff命令、git diff等
- 两个js对象也可以做diff,如https://github.com/flitbit/diff
- 两棵树做diff,如这里的vdom diff
1、树的diff时间复杂度O(n^3)
- 第一,遍历tree1;
- 第二,遍历tree2;
- 第三,排序
- 1000个节点,要计算1亿次,算法不可用
2、优化时间复杂度到O(n)
- 只比较同一层级,不跨级比较
- tab不相同,则直接删掉重建,不在深度比较
- tag和key,两者相同,则认为是相同节点,不再深度比较
02-snabbdom - 源码解读
1、示例:
import { init, classModule, propsModule, styleModule, eventListenersModule, h,} from "snabbdom";
const patch = init([
// 通过传入模块初始化 patch 函数
classModule, // 开启 classes 功能
propsModule, // 支持传入 props
styleModule, // 支持内联样式同时支持动画
eventListenersModule, // 添加事件监听
]);
const container = document.getElementById("container");
const vnode = h("div#container.two.classes", { on: { click: someFn } }, [
h("span", { style: { fontWeight: "bold" } }, "This is bold"),
" and this is just normal text",
h("a", { props: { href: "/foo" } }, "I'll take you places!"),
]);
// 传入一个空的元素节点 - 将产生副作用(修改该节点)
patch(container, vnode);
const newVnode = h(
"div#container.two.classes",
{ on: { click: anotherEventHandler } },
[
h(
"span",
{ style: { fontWeight: "normal", fontStyle: "italic" } },
"This is now italic type"
),
" and this is still just normal text",
h("a", { props: { href: "/bar" } }, "I'll take you places!"),
]
);
// 再次调用 `patch`
patch(vnode, newVnode); // 将旧节点更新为新节点
2、要素:
- vnode = h(标签和选择器,本身的属性,孩子);
- patch(dom/vnode, vnode);
3、源码:
-
h函数
- 返回的还是一个函数,
return vnode(sel, data, children, text, undefind)
。
- 返回的还是一个函数,
-
vnode 导出一个函数
- 这个函数内返回一个对象,
return (sel, data, children, text, elm, key)
。
- 这个函数内返回一个对象,
-
patch函数
-
来源:
var patch = snabbdom.init([相关参数])
。 -
init函数返回了一个patch函数。
-
patch函数参数
function patch(oldVnode: VNode|Element, vnode: VNode):VNode { /** 略 */ // cbs就是callbacks,执行pre hooks。pre属于生命周期第一个周期 for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); // 第一个参数不是vnode if(!isVnode(oldVnode)){ // 创建一个空的vnode,关联到这个DOM元素 oldVnode = emptyNodeAt(oldVnode); } // 相同的 vnode,sameVnode返回的是:vnode1.key===vnode2.key && vnode1.sel===vnode2.sel,key和sel都相同,如果没有传key,则都是undefind,undefind===undefind是true。 if(sameVnode(oldVnode, vnode)) { // 进行vnode对比 patchVnode(oldVnode, vnode, insertedVnodeQueue) }else{ // 不相同的 vnode,直接删掉重建 /** 略 */ // 重建 createdElm(vnode, insertedVnodeQueue) } }
-
patchVnode
function patchVnode(oldValue: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) { // 执行prepatch hook const hook = vnode.data?.hook; hook?.prepatch?.(oldVnode, vnode); // 设置vnode.elem, 把新的vnode上挂上dom const elm = (vnode.elm = oldVnode.elm)!; // 旧的children const oldCh = oldVnode.children as VNode[]; // 新的children const ch = vnode.children as VNode[]; if(oldVnode === vnode) return; /** hook相关,略 */ // vnode.text === undefind (vnode.children一般有值) if(isUndef(vnode.text)){ if (isDef(oldCh) && isDef(ch)) { // 新旧都有 children if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue); } else if (isDef(ch)) { // 新children有 if (isDef(oldVnode.text)) api.setTextContent(elm, ""); addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); } else if (isDef(oldCh)) { // 没有新的,有旧的 removeVnodes(elm, oldCh, 0, oldCh.length - 1); } else if (isDef(oldVnode.text)) { api.setTextContent(elm, ""); } } else if(oldValue.text !== vnode.text) {// vnode.text !== undefind (vnode.children无值) // 移除旧children if(isdef(oldCh)) { // 移除旧children removeVnodes(elm, oldCh, 0, oldCh.length-1); } // 设置新 text api.setTextContent(elm, vnode.text); } }
-
-
updateChildren
这个只是snabbdom的实现方式,vue和react可能对比方式有所不同。
function updateChildren (parentElm: Node, oldCh: VNode[], newCh: VNode[], insertedVnodeQueue: VNodeQueue) {
let oldStartIdx = 0;
let newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndIdx = newCh.length - 1;
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
let oldKeyToIdx: KeyToIndexMap | undefined;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx];
// 开始和开始对比
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
// 结束和结束对比
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
// 开始和结束对比
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(
parentElm,
oldStartVnode.elm!,
api.nextSibling(oldEndVnode.elm!)
);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
// 结束和开始对比
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
// 没对比上
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// 拿新节点的key,能否对应上 oldCh 中某个节点的key
idxInOld = oldKeyToIdx[newStartVnode.key as string];
// 如果没有对应上
if (isUndef(idxInOld)) {
// New element
// 新的,没有对应,直接插入
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!
);
// 对应上了
} else {
elmToMove = oldCh[idxInOld];
// sel 是否相等(sameVnode的条件)
if (elmToMove.sel !== newStartVnode.sel) {
// New element
// 新的,没有对应,直接插入
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!
);
// select 相等,key相等
} else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any;
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
if (newStartIdx <= newEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
addVnodes(
parentElm,
before,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
);
}
if (oldStartIdx <= oldEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
- 不用key的话,老版本ABCD全部删掉重渲染,如果有key的话就可以直接移动过来。
- 如果key使用随机数也不行,随机数都是新的,对应不上。
- 如果key是index也不行,如果abcd有排序的变化也会出问题。
03-vdom和diff的总结
- 细节不重要,updateChildren的过程也不重要,不要深究。
- vdom核心概念很重要:h, vnode, patch, dff, key等。
- vdom存在价值更重要:数据驱动视图,控制DOM操作。
2-4-4-模板编译
- 模板是vue开发常用的部分,即与使用相关联的原理。
- 它不是html,有指令、差值、js表达式,到底是什么?
- 面试不会直接问,但是会通过“组件渲染和更新的过程”考察
01-前置知识:JS的with语法
const obj = {a:100, b:200}
console.log(obj.a)
console.log(obj.b)
console.log(obj.c) // undefined
// 使用with,能改变{}内自由变量的查找方式
// 将{}内的自由变量,当做obj的属性来找
with(obj) {
console.log(a)
console.log(b)
console.log(c) // 会报错!!
}
- 改变了{}内自由变量的查找规则,当做obj的属性来查找
- 如果找不到匹配的obj属性,就会报错
- with要慎用,它打破了作用于规则,易读性变差
02-vue template complier 将模板编译为render函数
- 模板不是html,有指令、差值、JS表达式,能实现判断、循环。
- html是标签语言,只有JS才能实现判断、循环(图灵完备的)。
- 因此,模板一定是转换成某种js代码,即编译模板。
const compiler = require('vue-template-compiler')
// 插值
// const template = `<p>{{message}}</p>`
// with(this){return createElement('p',[createTextVNode(toString(message))])}
// h -> vnode
// createElement -> vnode
// // 表达式
// const template = `<p>{{flag ? message : 'no message found'}}</p>`
// // with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}
// // 属性和动态属性
// const template = `
// <div id="div1" class="container">
// <img :src="imgUrl"/>
// </div>
// `
// with(this){return _c('div',
// {staticClass:"container",attrs:{"id":"div1"}},
// [
// _c('img',{attrs:{"src":imgUrl}})])}
// // 条件
// const template = `
// <div>
// <p v-if="flag === 'a'">A</p>
// <p v-else>B</p>
// </div>
// `
// with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}
// 循环
// const template = `
// <ul>
// <li v-for="item in list" :key="item.id">{{item.title}}</li>
// </ul>
// `
// with(this){return _c('ul',_l((list),function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}),0)}
// 事件
// const template = `
// <button @click="clickHandler">submit</button>
// `
// with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}
// v-model
const template = `<input type="text" v-model="name">`
// 主要看 input 事件
// with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})}
// render 函数
// 返回 vnode
// patch
// 编译
const res = compiler.compile(template)
console.log(res.render)
// ---------------分割线--------------
// // 从 vue 源码中找到缩写函数的含义
// function installRenderHelpers (target) {
// target._o = markOnce;
// target._n = toNumber;
// target._s = toString;
// target._l = renderList;
// target._t = renderSlot;
// target._q = looseEqual;
// target._i = looseIndexOf;
// target._m = renderStatic;
// target._f = resolveFilter;
// target._k = checkKeyCodes;
// target._b = bindObjectProps;
// target._v = createTextVNode;
// target._e = createEmptyVNode;
// target._u = resolveScopedSlots;
// target._g = bindObjectListeners;
// target._d = bindDynamicKeys;
// target._p = prependModifier;
// }
- 编译模板编译为render函数,执行render函数返回vnode
- 基于vnode再执行patch和diff
- 使用webpack vue-loader,会在开发环境编译模板(重要),会比引入js自己写要快。
03-vue组件中使用render代替template
Vue.component("heading", {
// template: "xxx",
render: function (createElement) {
return createElement('h' + this.level,[ // h1,h2
createElement('a',{
attr: {
name: "headerId",
href: "#" + "headerId"
}
}, "this is a tag")
])
}
})
2-4-5-组件渲染更新过程总结
- 响应式: 监听data属性,setter和getter。
- 模板编译:模板到render函数,再到vnode。
- vdom: patch(elem, vnode)和 patch(vnode, newVnode)。
01-初次渲染过程
- 解析模板为render函数(或在开发环境已完成, vue-loader)。
- 触发响应式,监听data属性getter、setter。
- 执行render函数,生成vnode, patch(elem, vnode)。
02-更新过程
- 修改data,触发setter(此前在getter中已经被监听)。
- 重新执行render函数,生成newVnode。
- patch(vnode, newVnode)。
03-异步渲染
- 回顾$nextTick
- 汇总data的修改,一次性更新视图
- 减少dom操作次数,提高性能
2-5-前端路由的原理
2-5-1-网页url组成部分
// http:127.0.0.1:8881/01-hash.html?a=100&b=20#/aaa/bbb
location.protocol // http
location.hostname // 127.0.0.1
location.host // 127.0.0.1:8881
location.pathname // /01-hash.html
location.search // ?a=100&b=20
location.hash // #/aaa/bbb
2-5-2-hash的特点
- hash变化会触发网页跳转,即浏览器的前进后退(但不会刷新)。
- hash不会刷新页面,SPA必需的特点。
- hash永远不会提交到server端(前端自生自灭)。
// hash 变化,包括:
// a. JS 修改 url
// b. 手动修改 url 的 hash
// c. 浏览器前进、后退
window.onhashchange = (event) => {
console.log('old url', event.oldURL)
console.log('new url', event.newURL)
console.log('hash:', location.hash)
}
// 页面初次加载,获取 hash
document.addEventListener('DOMContentLoaded', () => {
console.log('hash:', location.hash)
})
// JS 修改 url
document.getElementById('btn1').addEventListener('click', () => {
location.href = '#/user'
})
2-5-3-H5 history
- url规范的路由,但跳转时不刷新页面
- history.pushState
- window.onpopState
- 正常页面浏览
- https://github.com/XXX 刷新页面
- https://github.com/XXX/yyy 刷新页面
- https://github.com/XXX/yyy/zzz 刷新页面
- 改造为H5 history模式
- https://github.com/XXX 刷新页面
- https://github.com/XXX/yyy 前端跳转,不刷新页面
- https://github.com/XXX/yyy/zzz 前端跳转,不刷新页面
// 页面初次加载,获取 path
document.addEventListener('DOMContentLoaded', () => {
console.log('load', location.pathname)
})
// 打开一个新的路由
// 【注意】用 pushState 方式,浏览器不会刷新页面
document.getElementById('btn1').addEventListener('click', () => {
const state = { name: 'page1' }
console.log('切换路由到', 'page1')
history.pushState(state, '', 'page1') // 重要!!
})
// 监听浏览器前进、后退
window.onpopstate = (event) => { // 重要!!
console.log('onpopstate', event.state, location.pathname)
}
// 需要 server 端配合,可参考
// https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90
2-5-4-总结
- 小结
- hash - window.onhashchange
- H5 history - history.pushState 和 window.onpopState
- H5 history 需要后端支持
- 两者选择
- to B 的系统推荐使用hash,简单易用,对url规范不敏感
- to C的系统,可以考虑选择H5 history,但需要服务器支持
- 能选简单的就选简单的,要考虑成本和收益
2-6-Vue原理总结
- 组件化
- 组件化的历史
- 数据驱动视图
- MVVM
- 响应式
- Object.defineProperty
- 监听对象(深度),监听数组
- Object.defineProperty的缺点(Vue3 用proxy,后面会讲)
- vdom和diff
- 应用背景
- vnode结构
- snabbdom的使用:vnode h patch
- 模板编译
- with语法
- 模板编译为render函数
- 执行render函数生成vnode
- 渲染过程
- 初次渲染过程
- 更新过程
- 异步渲染
- 前端路由
- hash
- H5 history
- 两者的对比
3-面试真题演练
- 是自己个人觉得的面试重点
- 网上收集的面试题
- 热门技术和知识点
- 以下真题将分为几组,每组以最重要的题为组名
3-1-v-for为何使用key
3-1-1-v-show和v-if的区别?
- v-show是通过css display控制显示和隐藏
- v-if是组件真正的渲染和销毁,而不是显示和隐藏
- 频繁切换显示状态用v-show,否则用v-if
3-1-2-为何在v-for中用key?
- 必须使用key,且不能是index和random
- diff算法中通过tag和key来判断,是否是sameNode
- 减少渲染次数,提升渲染性能
3-1-3-描述Vue组件的生命周期(父子组件)
-
单组件生命周期图(官网)
-
父子组件生命周期的关系
-
1,单个组件:挂载(beforeCreate,created,beforeMount,mounted),更新(beforeUpdate,updated),销毁(beforeDestroy,destroyed)。
2,父子组件:
- 父created -> 子created -> 子mounted -> 父mounted
- 父beforeUpdated -> 子beforeUpdated -> 子updated -> 父updated
创建是从外到内的,渲染是从内到外的。
3-1-4-Vue组件如何通讯(常见)
- 父子组件
props
和this.$emit
- 自定义事件
event.$on
、event.$off
、event.$emit
- vuex
3-1-5-描述组件渲染和更新的过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-upWPvX1k-1681782975724)(image/20221122.PNG)]
3-2-组件data为何是函数
3-2-1-双向数据绑定v-model的实现原理
- input元素的
value = this.name
- 绑定input事件
this.name = $event.target.value
- data更新触发re-render
3-2-2-对MVVM的理解
3-2-3-computed有何特点?
- 缓存,data不变不会重新计算
- 提高技能
3-2-4-为何组件data必须是一个函数?
export default {
name: "app"
data() {
return {
name: "vue"
}
}
}
- export default 导出的对象实际上被编译后是一个class类,对组件的使用相当于实例化了一个类。
- 所以实例化的时候执行data,如果data不是函数的话,在一个地方修改了name,其他name的值也会变。
- 如果是函数,两个都在闭包之中,不会相互影响
3-2-5-ajax请求应该放在哪个生命周期?
- mounted
- js是单线程的,ajax异步获取数据
- 反正mounted之前没有用,只会让逻辑更加混乱。
3-3-何时使用keep-alive
3-3-1-如何将组件所有props传递给子组件?
$props
<User v-bind="$props">
- 细节知识点,优先级不高
3-3-2-如何自己实现v-model
-
父组件绑定值:
<Children v-model=“name”></Children>
-
子组件接收:
-
<input type="text" :value="name" @input="$emit('change',$event.target.value)" />
-
props接收:
props:{ name: String }
-
model里绑定:
model:{ prop: "name", event: "change" }
-
-
补充:其实主要是通过
$emit
触发去更新父组件的值,而model里绑定的event值就是$emit
的事件名称。 -
其他:
-
关于学习的时候发现双向更新数据的好用方法:
.sync
修饰符。官网:https://v2.cn.vuejs.org/v2/guide/components-custom-events.html#sync-%E4%BF%AE%E9%A5%B0%E7%AC%A6 -
父组件绑定:
<template> // 方法一:统一绑定 <Children v-bind.sync="appType"></Children> // 方法二:分开绑定 <Children :path.sync="appType.path" :idList.sync="appType.idList"></Children> </template> <script> export default { data() { return { appType: { path: [], idList: [] } } } } </script>
-
子组件的接收:
<template> {{ path }} {{ idList }} </template> <script> export default { props: { path: Array, idList: idList } methods: { update: { // 请注意!事件名称必须是 update: 加上你要更新的data this.$emit("update:path", [1,2,3]); this.$emit("update:idList", [3,2,1]) } } } </script>
-
3-3-3-多个组件有相同的逻辑,如何抽离?
- mixin
- mixin的一些缺点
3-3-4-何时要使用异步组件?
- 加载大组件
- 路由异步加载
3-3-5-何时需要使用keep-alive?
- 缓存组价,不需要重复渲染。
- 如多个静态tab页的切换。
- 优化性能。
3-4-何时需要使用beforeDestory
3-4-1-何时需要使用beforeDestory
- 解绑自定义事件
event.$off
- 清除定时器
- 解绑自定义的dom事件,如 window srcoll 等
3-4-2-什么是 作用域插槽
// 父组件
<template>
<a :href="url">
<slot :slotData="website">
{{website.subTitle}} <!-- 默认值显示 subTitle ,即父组件不传内容时 -->
</slot>
</a>
</template>
<script>
export default {
props: ['url'],
data() {
return {
website: {
url: 'http://wangEditor.com/',
title: 'wangEditor',
subTitle: '轻量级富文本编辑器'
}
}
}
}
</script>
// 子组件
<scopedSlotDemo>
<template v-slot="slotProps">
{{slotProps.website.title}}
</template>
</scopedSlotDemo>
3-4-3-Vuex中的action和mutation有何区别?
- action中可以处理异步,mutation不可以。
- mutation做原子操作(每次只做一个操作)。
- action可以整合多个mutation.
3-4-4-Vue-router 常用的路由模式
- hash模式
- H5 history(需要服务端支持)
- 两者比较
3-4-5-如何配置Vue-router 异步加载
export default new VueRouter{
routes: [
path: "/",
component: () => import(
'./../components/Navigater'
)
]
}
3-5-diff算法时间复杂度
3-5-1-请用vnode描述一个DOM结构
<div id="div1" class="container">
<p>vdom</p>
<ul style="font-size: 20px;">
<li>a</li>
</ul>
</div>
{
tag: "div",
props: {
id: "div1",
className: "container"
}
children: [
{
tag: "p",
children: "vdom"
},
{
tag: "ul",
props: {style: "font-size: 20px;"},
children: [
{
tag: "li",
children: "a"
}
]
}
]
}
3-5-2-监听data变化的核心API是什么?
- Object.defineProperty
- 以及深度监听、监听数组
- 有何缺点
3-5-3-Vue如何监听数组变化
- Object.defineProperty不能监听数组变化
- 重新定义原型,重写push pop等方法,实现监听
- Proxy 可以支持监听数组变化
3-5-4-请描述响应式原理
- 监听数组变化
- 组件渲染和更新的流程
3-5-5-diff算法的时间复杂度
- O(n)
- 在O(n^3)基础上做了一些调整
3-6-Vue常见性能优化
3-6-1-简述diff算法过程
- patch(elem, vnode) 和 patch(vnode, newVnode)
- patchVnode 和 addVnodes 和 removeVnodes
- updateChildren(key的重要性)
3-6-2-Vue为何是异步渲染,$nextTick何用?
- 异步渲染(以及合并data修改),以提高渲染性能
- $nextTick在DOM更新完之后,触发回调
3-6-3-Vue常见性能优化方式
- 合理使用v-show和v-if
- 合理使用computed
- v-for时加key,以及避免和v-if同时使用
- 自定义事件、DOM事件及时销毁
- 合理使用异步组件
- 合理使用keep-alive
- data层级不要太深
- 使用vue-loader在开发环境做模板编译(预编译)
- webpack层面的优化
- 前端通用的性能优化,如图片懒加载
- 使用SSR
4-Vue3.0
对vue3进行全面讲解,包括vue3升级变动,Vue3核心知识点,compositionAPI如何使用,以及vue3的一些基础原理
- 产出
- vue3常考知识和面试题
- 主要内容
- vue3的基本使用
- vue3相比于vue2的升级
- ref相关知识点(比较难理解)
- compositionAPI
- vue3原理讲解
- 关键字
- vue3
- ref
- CompositionAPI
- Proxy
- 学习方法
- Vue3课程示例,一定要亲自动手练习
- 学习原理要关注重点,核心的部分,关注流程,不要关注细节
4-1-考点概述
(思维导图)
4-2-面试题
- Vue3比Vue2有什么优势?
- 描述Vue3的生命周期
- 如何看待CompositionAPI和OptionsAPI?
- 如何理解red toRef 和 toRefs?
- Vue3升级了哪些重要的功能?
- CompositionAPI如何实现代码复用?
- Vue3如何实现响应式
- watch和watchEffect的区别是什么?
- setup中如何获取组件实例?
- vue3为何比vue2快?
- Vite是什么?
- CompositionAPI和ReactHooks的对比
4-3-vue3对vue2有什么优势?
- 性能更好
- 体积更小
- 更好的ts支持
- 更好的代码组织
- 更好的逻辑抽离
- 更多新功能
4-4-Vue3生命周期
4-4-1-Option API生命周期
- beforeDestroy改为beforeUnmount
- destoryed改为unmounted
- 其他沿用Vue2的生命周期
4-4-2-Composition API生命周期
<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')
})
},
// 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>
4-5-如何理解CompositionAPI和OptionsAPI
- compositionAPI带来了什么?
- compositionAPI和optionAPI如何选择?
- 别误解compositionAPI
4-5-1-CompositionAPI带来了什么?
- 更好的代码组织
-
每种颜色是一种逻辑,compositionAPI在代码多、应用复杂的时候更好用
-
更好的逻辑复用(有一道专门的面试题)
-
更好的类型推导
{ data() { return { a:10 } }, methods: { fn1() { const a = this.a } }, mounted() { this.fn1(); } }
- 如果是通用的js语法应该是this.methods.fn1(),这种语法糖不利于一些工具的使用和类型推导
4-5-2-compositionAPI和optionAPI如何选择?
- 不建议共用,会引起混乱
- 小型项目,业务逻辑简单,用OptionAPI
- 中大型项目,逻辑复杂,用CompositionAPI
4-5-3-别误解CompositionAPI
- CompositionAPI是高阶技巧,不是基础必会
- CompositionAPI是为了解决复杂业务逻辑而设计
- CompositionAPI就行Hooks在React中的地位
4-5-3-小结
- compositionAPI带来了什么?
- compositionAPI和optionAPI如何选择?
- 别误解compositionAPI
4-6-如何理解ref toRef和toRefs
- 是什么
- 最佳使用方式
- 进阶和深入理解
4-6-1-是什么?
-
ref
-
生成值类型的响应式数据
-
可用于模板和reactive
-
通过
.value
修改值 -
代码示例
<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 { ref, toRef, reactive } from 'vue' export default { name: 'ToRef', setup() { const state = reactive({ age: 20, name: '双越' }) const age1 = computed(() => { return state.age + 1 }) // // 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 { ref, toRef, 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; // 如果这样的话<p>toRefs demo {{state.age}} {{state.name}}</p> // 如果直接解构,属性更改不会触发相应 return {...state} } } </script>
-
4-6-2-ref toRef和toRefs的最佳使用方式
-
合成函数返回响应式对象
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
4-6-3-进阶,深入理解
- 为何需要ref?
- 为何需要.value?
- 为何需要toRef toRefs
-
为何需要ref?
-
返回值类型,会丢失响应式
-
如在setup、computed、合成函数,都有可能返回值类型
-
如果vue不定义ref,用户将自造ref,反而混乱
<template> <p>why ref demo {{state.age}} - {{age1}}</p> </template> <script> import { ref, toRef, toRefs, reactive, computed } from 'vue' function useFeatureX() { const state = reactive({ x: 1, y: 2 }) return toRefs(state) } export default { name: 'WhyRef', setup() { const { x, y } = useFeatureX() const state = reactive({ age: 20, name: '双越' }) // computed 返回的是一个类似于 ref 的对象,也有 .value const age1 = computed(() => { return state.age + 1 }) setTimeout(() => { state.age = 25 }, 1500) return { state, age1, x, y } } } </script>
-
-
为何需要.value?
-
ref是一个对象(不丢失响应式),value存储值
-
通过.value属性的get和set 实现响应式
-
用于模板,reactive时,不需要.value,其他情况都需要
// computed 返回的是一个类似于 ref 的对象,也有 .value const age1 = computed(() => { return state.age + 1 }) // 错误 function computed(getter) { let value watchEffect(()=>{ // 测试的时候可以把watchEffect代替到setTimeout value = getter() }) return value } // let a = 100 // b = a; // a = 200; // 等价于 a = computed(()=>100) // b // 100 // 正确 function computed(getter) { let ref = { value: null } watchEffect(()=>{ // 测试的时候可以把watchEffect代替到setTimeout ref.value = getter() }) return ref } // const obj1 = {x:100} // const obj2 = obj1 // obj1.x = 200; // 等价于 obj1.x = computed(()=>200) // boj2.x // 200
-
-
为何需要toRef toRefs
- 初衷:不丢失响应式的情况下,吧对象数据分散/扩散
- 前提:针对的是响应式对象(reactive封装的)非普通对象
- 注意: 不创造响应式,而是延续响应式
4-7-vue3升级了哪些重要功能
- createApp
- emits属性
- 生命周期
- 多事件
- Fragment
- 移除.sync
- 异步组件的写法
- 移除filter
- teleport
- Supense
- Composition API
4-7-1-createApp
// vue2.x
const app = new Vue({/* 选项 */})
// vue3.x
const app = Vue.createApp({/* 选项 */})
// vue2.x
Vue.use(/*...*/)
Vue.mixin(/*...*/)
Vue.component(/*...*/)
Vue.directive(/*...*/)
// vue3.x
app.use(/*...*/)
app.mixin(/*...*/)
app.component(/*...*/)
app.directive(/*...*/)
4-7-2-emits属性
<!-- 父组件 -->
<HelloWorld :msg="msg" @onSayHello="onSayHello" />
export default {
name: "helloWorld",
props: {
msg: String
},
emits: ["onSayHello"], // 把sayHello改成onSayHello
setup(props,{emit}) {
emit("onSayHello", "vue3")
}
}
4-7-3-多事件处理
<!-- 在methods里定义one,two两个函数 -->
<button @click="one($event), two($event)">
Submit
</button>
4-7-4-Fragment
<!-- Vue2.x 组件模板 -->
<template>
<div class="blog-post">
<h3>{{ title }}</h3>
<div v-html="content"></div>
</div>
</template>
<!-- Vue3.x 组件模板 -->
<template>
<h3>{{ title }}</h3>
<div v-html="content"></div>
</template>
4-7-5-移除.sync
<!-- vue2.x -->
<MyComponent v-bind:title.sync="title"></MyComponent>
<!-- vue3.x -->
<MyComponent v-model:title="title"></MyComponent>
4-7-6-异步组件
// vue2.x
new Vue({
components: {
"mycomponent": ()=>import("./my-async-component.vue")
}
})
// vue3.x
import {createApp, defineAsyncComponent} from 'vue'
createApp({
components: {
AsyncComponent: defineAsyncComponent(()=>import("./my-async-component.vue"))
}
})
4-7-7-移除filter
<!-- 以下filter在vue3中不可用了! -->
<!-- 在双花括号中 -->
{{ message | capitalize}}
<!-- 在 v-blind z中 -->
<div v-blind:id="rawId | formatId"></div>
4-7-8-Teleport
<!-- data中设置modalOpen: false -->
<button @click="modalOpen=true">
Open full screen modal!(With teleport)
</button>
<teleport to="body"> <!-- vue3可以直接放在body上去 -->
<div v-if="modalOpen" class="modal">
<div>
teleport 弹窗 (父元素是 body)
<button @click="modalOpen=false">close</button>
</div>
</div>
</teleport>
4-7-9-Suspense
<Suspense>
<template>
<Test1 /> <!-- 是一个异步组件 -->
</template>
<!-- #fallback 就是一个具名插槽
即 Suspens组件内部,有两个slot,
其中一个具名为 fallback -->
<template #fallback>
Loading...
</template>
</Suspense>
4-7-10-CompositionAPI
- reactive
- ref相关
- readonly
- watch和watchEffect
- setup
- 生命周期钩子函数
4-8-CompositionAPI如何实现逻辑复用
-
抽离逻辑代码到一个函数
-
函数命名约定为 useXxxx格式(React Hook也是)
-
在setup中引用useXxxx函数
<template> <p>mouse position {{x}} {{y}}</p> </template> <script> import { reactive } from 'vue' 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>
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
4-9-Vue3如何实现响应式
- 回顾Vue2.0的Object.defineProperty
- 学习Proxy语法
- Vue3如何用Proxy实现响应式
- 两者对比
4-9-1-回顾Vue2.0的Object.defineProperty
// 触发更新视图
function updateView() {
console.log('视图更新')
}
// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function () {
updateView() // 触发视图更新
oldArrayProperty[methodName].call(this, ...arguments)
// Array.prototype.push.call(this, ...arguments)
}
})
// 重新定义属性,监听起来
function defineReactive(target, key, value) {
// 深度监听
observer(value)
// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
// 深度监听
observer(newValue)
// 设置新值
// 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
value = newValue
// 触发更新视图
updateView()
}
}
})
}
// 监听对象属性
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象或数组
return target
}
// 污染全局的 Array 原型
// Array.prototype.push = function () {
// updateView()
// ...
// }
if (Array.isArray(target)) {
target.__proto__ = arrProto
}
// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}
// 准备数据
const data = {
name: 'zhangsan',
age: 20,
info: {
address: '北京' // 需要深度监听
},
nums: [10, 20, 30]
}
// 监听数据
observer(data)
// 测试
// data.name = 'lisi'
// data.age = 21
// // console.log('age', data.age)
data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组
- 缺点
- 深度监听需要一次性递归
- 无法监听新增和删除属性(Vue.set Vue.delete)
- 无法监听原生数组,需要特殊处理
4-9-2-实现响应式
-
基本使用
const data = { name: "zhangsan", age: "20" } //const data = ['a', 'b', 'c']; //data.push("d"); // 依次打印:"get push" "get length" "set 3 d" "result true" "set length 4" "result true"
const ProxyData = new Proxy(data, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
console.log("get", key)
return result; // 返回结果
},
set(target, key, val, receiver) {
const result = Reflect.get(target, key, val, receiver)
console.log("set", key, val)
console.log("result", result)
return result; // 是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.get(target, key)
console.log("delete Property", key)
console.log("result", result)
return result; // 是否删除成功
},
})
// const data = {
// name: 'zhangsan',
// age: 20,
// }
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 // 是否删除成功
}
})
-
Reflect
-
和Proxy能力一一对应
-
规范化、标准化、函数式
const obj = {a:100, b:200} "a" in obj Reflect.has(obj, "a") delete obj.a Reflect.deleteProperty(obj, "a")
-
代替掉Object上的工具函数
const obj = {a:100, b:200} Obejct.getOwnPropertyNames(obj) // [a, b] Reflect.ownKeys(obj)
-
-
实现响应式
// 创建响应式
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)
- 深度监听,性能更好
- 可监听 新增/删除 属性
- 可监听数组变化
4-9-3-总结
- Proxy能规避Object.defineProperty的问题
- Proxy无法兼容所有浏览器,无法plofill
4-10-v-model参数的用法
<template>
<p>{{name}} {{age}}</p>
<user-info
v-model:name="name"
v-model:age="age"
></user-info>
</template>
<script>
import { reactive, toRefs } from 'vue'
import UserInfo from './UserInfo.vue'
export default {
name: 'VModel',
components: { UserInfo },
setup() {
const state = reactive({
name: '双越',
age: '20'
})
return toRefs(state)
}
}
</script>
<template>
<input :value="name" @input="$emit('update:name', $event.target.value)"/>
<input :value="age" @input="$emit('update:age', $event.target.value)"/>
</template>
<script>
export default {
name: 'UserInfo',
props: {
name: String,
age: String
}
}
</script>
4-11-watch和watchEffect的区别
- 两者都可监听data属性变化
- watch需要明确监听哪个属性
- watchEffec会根据其中的属性,自动监听其变化
<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() {
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)
// 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>
4-12-setup中如何获取组件的实例
- 在setup和其他compositionAPI中没有this
- 可通过getCurrentInstance获取当前实例
- 若使用OptionAPI可照常使用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)
onMounted(() => {
console.log('this in onMounted', this)
console.log('x', instance.data.x)
})
const instance = getCurrentInstance()
console.log('instance', instance)
},
mounted() {
console.log('this2', this)
console.log('y', this.y)
}
}
</script>
4-13-Vue3为何比Vue2快
- Proxy响应式
- PatchFlag
- hoistStatic
- catchHandler
- SRR优化
- tree-shaking
4-13-1-PatchFlag
-
编译模板是,动态节点做标记
-
标记,分为不同的类型,如TEXT PROPS
代码演示:https://vue-next-template-explorer.netlify.app/#eyJzcmMiOiI8ZGl2PlxyXG4gIDxzcGFuPkhlbGxvIFdvcmxkPC9zcGFuPlxyXG4gIDxzcGFuPnt7IG1zZyB9fTwvc3Bhbj5cclxuICA8c3BhbiA6Y2xhc3M9XCJuYW1lXCI+5ZCN56ewPC9zcGFuPlxyXG4gIDxzcGFuIDppZD1cIm5hbWVcIj7lkI3np7A8L3NwYW4+XHJcbiAgPHNwYW4gOmlkPVwibmFtZVwiPnt7IG1zZyB9fTwvc3Bhbj5cclxuICA8c3BhbiA6aWQ9XCJuYW1lXCIgOm1zZz1cIm1zZ1wiPuWQjeensDwvc3Bhbj5cclxuPC9kaXY+Iiwib3B0aW9ucyI6e319
<div> <span>Hello World</span> <span>{{ msg }}</span> <span :class="name">名称</span> <span :id="name">名称</span> <span :id="name">{{ msg }}</span> <span :id="name" :msg="msg">名称</span> </div>
编译后:
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, normalizeClass as _normalizeClass, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock("div", null, [ _createElementVNode("span", null, "Hello World"), _createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */), _createElementVNode("span", { class: _normalizeClass(_ctx.name) }, "名称", 2 /* CLASS */), _createElementVNode("span", { id: _ctx.name }, "名称", 8 /* PROPS */, ["id"]), _createElementVNode("span", { id: _ctx.name }, _toDisplayString(_ctx.msg), 9 /* TEXT, PROPS */, ["id"]), _createElementVNode("span", { id: _ctx.name, msg: _ctx.msg }, "名称", 8 /* PROPS */, ["id", "msg"]) ])) } // Check the console for the AST
-
diff算法时,可以区分静态节点,以及不同的类型的动态节点
4-13-2-HoistStatic
-
将静态节点的定义,提升到父作用域,缓存起来(典型的拿空间换时间的优化策略)
代码演示:https://vue-next-template-explorer.netlify.app/#eyJzcmMiOiI8ZGl2PlxyXG4gIDxzcGFuPkhlbGxvIFZ1ZTM8L3NwYW4+XHJcbiAgPHNwYW4+SGVsbG8gVnVlMzwvc3Bhbj5cclxuICA8c3Bhbj5IZWxsbyBWdWUzPC9zcGFuPlxyXG4gIDxzcGFuPnt7IG1zZyB9fTwvc3Bhbj5cclxuPC9kaXY+Iiwib3B0aW9ucyI6eyJob2lzdFN0YXRpYyI6dHJ1ZX19
<div> <span>Hello Vue3</span> <span>Hello Vue3</span> <span>Hello Vue3</span> <span>{{ msg }}</span> </div>
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, "Hello Vue3", -1 /* HOISTED */) const _hoisted_2 = /*#__PURE__*/_createElementVNode("span", null, "Hello Vue3", -1 /* HOISTED */) const _hoisted_3 = /*#__PURE__*/_createElementVNode("span", null, "Hello Vue3", -1 /* HOISTED */) export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock("div", null, [ _hoisted_1, _hoisted_2, _hoisted_3, _createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */) ])) } // Check the console for the AST
-
多个相邻静态节点,会被合并起来
代码演示:https://vue-next-template-explorer.netlify.app/#eyJzcmMiOiI8ZGl2PlxyXG4gIDxzcGFuPkhlbGxvIFZ1ZTM8L3NwYW4+XHJcbiAgPHNwYW4+SGVsbG8gVnVlMzwvc3Bhbj5cclxuICA8c3Bhbj5IZWxsbyBWdWUzPC9zcGFuPlxyXG4gIDxzcGFuPkhlbGxvIFZ1ZTM8L3NwYW4+XHJcbiAgPHNwYW4+SGVsbG8gVnVlMzwvc3Bhbj5cclxuICA8c3Bhbj5IZWxsbyBWdWUzPC9zcGFuPlxyXG4gIDxzcGFuPkhlbGxvIFZ1ZTM8L3NwYW4+XHJcbiAgPHNwYW4+SGVsbG8gVnVlMzwvc3Bhbj5cclxuICA8c3Bhbj5IZWxsbyBWdWUzPC9zcGFuPlxyXG4gIDxzcGFuPkhlbGxvIFZ1ZTM8L3NwYW4+XHJcbiAgPHNwYW4+e3sgbXNnIH19PC9zcGFuPlxyXG48L2Rpdj4iLCJvcHRpb25zIjp7ImhvaXN0U3RhdGljIjp0cnVlfX0=
<div> <span>Hello Vue3</span> <span>Hello Vue3</span> <span>Hello Vue3</span> <span>Hello Vue3</span> <span>Hello Vue3</span> <span>Hello Vue3</span> <span>Hello Vue3</span> <span>Hello Vue3</span> <span>Hello Vue3</span> <span>Hello Vue3</span> <span>{{ msg }}</span> </div>
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, createStaticVNode as _createStaticVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<span>Hello Vue3</span><span>Hello Vue3</span><span>Hello Vue3</span><span>Hello Vue3</span><span>Hello Vue3</span><span>Hello Vue3</span><span>Hello Vue3</span><span>Hello Vue3</span><span>Hello Vue3</span><span>Hello Vue3</span>", 10) export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock("div", null, [ _hoisted_1, _createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */) ])) } // Check the console for the AST
4-13-3-cacheHandler
-
缓存事件
代码演示:https://vue-next-template-explorer.netlify.app/#eyJzcmMiOiI8ZGl2PlxyXG4gIDxzcGFuIEBjbGljaz1cImNsaWNrSGFuZGxlclwiPkhlbGxvIFZ1ZTM8L3NwYW4+XHJcbjwvZGl2PiIsIm9wdGlvbnMiOnsiY2FjaGVIYW5kbGVycyI6dHJ1ZX19
<div> <span @click="clickHandler">Hello Vue3</span> </div>
import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" // export function render(_ctx, _cache, $props, $setup, $data, $options) { // return (_openBlock(), _createElementBlock("div", null, [ // _createElementVNode("span", { onClick: _ctx.clickHandler }, "Hello Vue3", 8 /* PROPS */, ["onClick"]) // ])) // } export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock("div", null, [ _createElementVNode("span", { onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.clickHandler && _ctx.clickHandler(...args))) }, "Hello Vue3") ])) } // Check the console for the AST
4-13-4-SSR优化
- 静态节点直接输出,绕过了vdom
- 动态节点,还是需要渲染
代码演示: https://vue-next-template-explorer.netlify.app/#eyJzcmMiOiI8ZGl2PlxyXG4gIDxzcGFuPkhlbGxvIFZ1ZTM8L3NwYW4+XHJcbiAgPHNwYW4+SGVsbG8gVnVlMzwvc3Bhbj5cclxuICA8c3Bhbj5IZWxsbyBWdWUzPC9zcGFuPlxyXG4gIDxzcGFuPnt7IG1zZyB9fTwvc3Bhbj5cclxuPC9kaXY+Iiwic3NyIjp0cnVlLCJvcHRpb25zIjp7fX0=
<div>
<span>Hello Vue3</span>
<span>Hello Vue3</span>
<span>Hello Vue3</span>
<span>{{ msg }}</span>
</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))
}><span>Hello Vue3</span><span>Hello Vue3</span><span>Hello Vue3</span><span>${
_ssrInterpolate(_ctx.msg)
}</span></div>`)
}
// Check the console for the AST
4-13-5-tree shaking
-
编译时,根据不同的情况,引入不同的API
代码演示:https://vue-next-template-explorer.netlify.app/#eyJzcmMiOiI8ZGl2PlxyXG4gIDxzcGFuIHYtaWY9XCJtc2dcIj5IZWxsbyBWdWUzPC9zcGFuPlxyXG4gIDxpbnB1dCB2LW1vZGVsPVwibXNnXCIgLz5cclxuPC9kaXY+Iiwic3NyIjpmYWxzZSwib3B0aW9ucyI6e319
<div> <span v-if="msg">Hello Vue3</span> <input v-model="msg" /> </div> <script> import { openBlock as _openBlock, createElementBlock as _createElementBlock, createCommentVNode as _createCommentVNode, vModelText as _vModelText, createElementVNode as _createElementVNode, withDirectives as _withDirectives } from "vue" export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock("div", null, [ (_ctx.msg) ? (_openBlock(), _createElementBlock("span", { key: 0 }, "Hello Vue3")) : _createCommentVNode("v-if", true), _withDirectives(_createElementVNode("input", { "onUpdate:modelValue": $event => ((_ctx.msg) = $event) }, null, 8 /* PROPS */, ["onUpdate:modelValue"]), [ [_vModelText, _ctx.msg] ]) ])) } </script>
<div> <span>Hello Vue3</span> </div> <script> import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock("div", null, [ _createElementVNode("span", null, "Hello Vue3") ])) } </script>
4-14-Vite为什么启动非常快
vite是什么?
- 一个前端打包工具,Vue作者发起的项目。
- 借助Vue的影响力,发展较快,和webpack竞争。
- 优势:开发环境下无需打包,启动快。
vite为何启动快?
- 开发环境使用ES6 Module,无需打包----非常快。
- 生产环境使用rollup,并不会快很多。
ES6 Module
基础代码演示:
<!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>
<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>
</body>
</html>
// add.js
import print from './print.js'
export default function add(a, b) {
print('print', 'add')
return a + b
}
// print.js
export default function (a, b) {
console.log(a, b)
}
// math.js
export function add(a, b) {
return a + b
}
export function multi(a, b) {
return a * b
}
外链演示:
<!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="./index.js"></script>
</body>
</html>
// index.js
import { add, multi } from './math.js'
console.log('add res', add(10, 20))
console.log('multi res', multi(10, 20))
远程引用:
<!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' // mjs: module js
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>
4-15-CompositionAPI和ReactHooks对比
- 前者setup只会调用一次,而后者函数会被多次调用
- 前者无需useMemo useCallback(就是不用缓存数据),因为setup只调用一次
- 前者无需顾虑调用顺序,而后者需要保证hooks的顺序一致
- 前者reactive + ref 比后者useState要难理解
5-Vue3和JSX
- Vue3中的JSX的基本应用
- JSX和template的区别
- JSX和slot(体会JSX的优越性)
- 注意
- JSX最早是React提出的概念(现在已经发展壮大)
- 如果不了解JSX语法,可以先学习React
5-1-Vue3中JSX的基本使用
- 基本使用
- 使用.jsx格式文件和defineComponent
- 引用自定义组件,传递属性
代码演示:
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>
demo.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
})
// defineComponent(props, context)=>{}
// 1. setup 函数: defineComponent(()={setup函数内容})
// 2. 组件的配置: defineComponent({配置的map})
// 可以用.jsx
//可以用.tsx typescript的形式
child.jsx
import { defineComponent } from 'vue'
export default defineComponent({
props: ['a'],
setup(props) {
const render = () => {
return <p>Child {props.a}</p>
}
return render
}
})
5-2-JSX和template的区别
- 语法上有很大的区别
- JSX本质就是js代码,可以使用js的任何能力
- template只能嵌入简单的js表达式,其他需要指令,如v-if
- JSX已经成为ES规范,template还是Vue自家规范
- 本质上是相同的,都会编译为js代码(render函数)
- 具体示例:插槽,自定义组件,属性和事件,条件和循环(在上面的代码演示里)
5-3-JSX和slot(插槽)
- slot是vue发明的概念,为了完善template的能力
- slot一直是Vue初学者的噩梦,特别是作用域slot
- 但是使用JSX将很容易理解,因为JSX的本质就是js
5-3-1-Template与slot代码演示
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>
TablePanel.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> <!-- 如何控制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>
5-3-2-JSX与slot代码演示
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
}
})
5-3-3-scoped-slot-template代码演示
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>
Child.vue
<template>
<p>child</p>
<slot :msg="msg"></slot>
</template>
<script>
export default {
data() {
return {
msg: '作用域插槽 Child'
}
}
}
</script>
5-3-4-scoped-slot-jsx代码演示
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>
</>
}
})
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>
}
}
})
5-4-Vue3 script setup(vue3.2更新)
- Vue3引入了compositionAPI
- compositionAPI最重要的是setup函数
script
只有一个setup函数太孤单,如何简化一下?
5-4-1-基本使用
- 顶级变量,自定义组件,可以直接用于模板
- 可以正常使用ref reactive computed等功能
- 和其他
<script>
同时使用
5-4-2-属性和事件
- defineProps
- defineEmits
5-4-3-defineExpose
- 暴露数据给父组件
5-4-4-代码演示
demo.vue
<script>
function add(a, b) { return a + b }
</script>
<script setup> // 标签setup
import { ref, reactive, toRefs, onMounted } from 'vue'
import Child1 from './Child1'
import Child2 from './Child2'
import Child3 from './Child3'
// 顶级变量 可以直接用于template
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)
}
const child3Ref = ref(null) // ref="child3Ref"
onMounted(() => {
// 拿到 Child3 组件的一些数据
console.log(child3Ref.value)
console.log(child3Ref.value.a)
console.log(child3Ref.value.b)
})
</script>
<template>
<p @click="addCount">{{countRef}}</p>
<p>{{name}}</p>
<child-1></child-1>
<hr>
<child-2 :name="name" :age="countRef" @change="onChange" @delete="onDelete"></child-2>
<hr>
<child-3 ref="child3Ref"></child-3>
</template>
Child1.vue
<script setup>
</script>
<template>
<p>Child1</p>
</template>
Child2.vue
<script setup>
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>
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>
总结:
- 基本使用,
<script>
写在<template>
前面,看起来更直观 - 定义属性defineProps,定义事件defineEmits
- defineExpose暴露数据给父组件