一、本单元教学目标
(Ⅰ)重点知识目标
1. 自定义指令 2. vue3的过渡效果 3. Teleport传送门 4. 两个重要的实例API 5. 自定义过滤器 6. 混入mixins 7. webpack
二、本单元知识详讲
4.1 自定义指令
除了 Vue 内置的一系列指令 (比如 v-model
或 v-show
) 之外,Vue 还允许你注册自定义的指令 (Custom Directives)。
一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。
4.1.1 指令的作用
1.对复杂D0M操作的封装,方便使用,
V-XXx
。比如表单默认聚焦<1nput type="text" v-focus>
; 2.优雅的实现页面的性能优化,比如我们可以封装懒加载指令<img srca="v-Lazy>
、防抖节流等等指令<button v-throttle>
; 3.小功能的实现,比如页面上的数字我们加“千分符",<p V-thounddep>3234234234</p>
,展示3,234,234,234; 4.有一些插件(比如拖拽插件vue draggable)它就是提供了一个指令让我们使用,<p v-drag>
,
学习自定义指令,就能了解这种插件底层是如何实现的。 一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。
4.1.2 自定义指令定义和使用
分类:全局 局部
学习:app.directive()、directives 选项
当一个 input 元素被 Vue 插入到 DOM 中后,它会被自动聚焦:
完整案例:59_自定义指令.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>59_自定义指令</title> </head> <body> <div id="app"> <input type="text" v-focus/> </div> </body> <script src="lib/vue.global.js"></script> <script> const app = Vue.createApp({ directives: {// 局部自定义指令 focus: { mounted (el) { el.focus() } } } }) // 全局自定义指令 // app.directive('focus', { // 一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。 // mounted (el) { // el 就是当前指令对应的DOM节点 // el.focus() // } // }) app.mount('#app') </script> </html>
4.1.3 自定义指令钩子
一个指令的定义对象可以提供几种钩子函数 (都是可选的):
vue3相比 vue2,钩子函数做了更新
vue2中一个指令定义对象可以提供如下几个钩子函数 (均为可选):
bind
:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted
:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update
:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
componentUpdated
:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind
:只调用一次,指令与元素解绑时调用。
// vue3钩子函数 const myDirective = { // 在绑定元素的 attribute 前 // 或事件监听器应用前调用 created(el, binding, vnode, prevVnode) { // 下面会介绍各个参数的细节 }, // 在元素被插入到 DOM 前调用 beforeMount(el, binding, vnode, prevVnode) {}, // 在绑定元素的父组件 // 及他自己的所有子节点都挂载完成后调用 mounted(el, binding, vnode, prevVnode) {}, // 绑定元素的父组件更新前调用 beforeUpdate(el, binding, vnode, prevVnode) {}, // 在绑定元素的父组件 // 及他自己的所有子节点都更新后调用 updated(el, binding, vnode, prevVnode) {}, // 绑定元素的父组件卸载前调用 beforeUnmount(el, binding, vnode, prevVnode) {}, // 绑定元素的父组件卸载后调用 unmounted(el, binding, vnode, prevVnode) {} }
4.1.4 指令的钩子参数:
-
el
:指令绑定到的元素。这可以用于直接操作 DOM。 -
binding
:一个对象,包含以下属性。 -
value
:传递给指令的值。例如在v-my-directive="1 + 1"
中,值是2
。 -
oldValue
:之前的值,仅在beforeUpdate
和updated
中可用。无论值是否更改,它都可用。 -
arg
:传递给指令的参数 (如果有的话)。例如在v-my-directive:foo
中,参数是"foo"
。 -
modifiers
:一个包含修饰符的对象 (如果有的话)。例如在v-my-directive.foo.bar
中,修饰符对象是{ foo: true, bar: true }
。 -
instance
:使用该指令的组件实例。 -
dir
:指令的定义对象。 -
vnode
:代表绑定元素的底层 VNode。 -
prevNode
:之前的渲染中代表指令所绑定元素的 VNode。仅在beforeUpdate
和updated
钩子中可用。
给一个元素设置颜色 v-red v-color="green",设置手机号正确为绿色,不正确为红色
4.1.5 简化形式
对于自定义指令来说,一个很常见的情况是仅仅需要在 mounted
和 updated
上实现相同的行为,除此之外并不需要其他钩子。这种情况下我们可以直接用一个函数来定义指令,如下所示:
<div v-color="color"></div>
app.directive('color', (el, binding) => { // 这会在 `mounted` 和 `updated` 时都调用 el.style.color = binding.value })
4.1.6 对象字面量
-
如果自定义指令需要多个值,可以传入一个 JS 对象字面量。指令函数能够接受所有合法的 JS 表达式。
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
Vue.directive('demo', function (el, binding) { console.log(binding.value.color) // => "white" console.log(binding.value.text) // => "hello!" })
4.1.7 模拟v-show
// 绑定的值为false,display为none,值为true,display为"",""是显示的 app.directive('myshow', { created (el, binding) { var display = binding.value ? '' : 'none'; el.style.display = display; }, mounted (el, binding) { //一旦绑定上就可以设置上style var display = binding.value ? '' : 'none'; el.style.display = display; } })
4.1.8 案例 v-color/v-tel
完整案例60_directives_demo.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>60_自定义指令demo</title> </head> <body> <div id="app"> <div v-red>自定义指令 无参数 设置为红色</div> <!-- green需要添加 '' 否则被视为 变量 --> <div v-color="'green'">自定义指令 有参数 设置为绿色</div> <div v-color="'blue'">自定义指令 有参数 设置为蓝色</div> <input type="text" v-model="tel" v-tel/> </div> </body> <script src="lib/vue.global.js"></script> <script> const app = Vue.createApp({ data () { return { tel: '' } }, directives: { tel: { // 手机号输入时判断是否符合规则,如果符合 显示为绿色 否则为红色 // 确保输入 使用 updated 钩子 updated (el) { if (/^(?:(?:\+|00)86)?1[3-9]\d{9}$/.test(el.value)) { el.style.color = 'green' } else { el.style.color = 'red' } } } } }) app.directive('red', { mounted (el) { el.style.color = 'red' } }) app.directive('color', { mounted (el, binding) { // binding.value 指令传递的值 el.style.color = binding.value } }) app.mount('#app') </script> </html>
4.1.8 跟随鼠标滑动小案例。
注意看下面这个效果图: 当我右键中间这个小方框,在大盒子内移动鼠标的话,我想让这个小方块跟着鼠标移动,回想一下,原来我们用js是怎么实现的?
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> * { padding: 0; margin: 0; } #app { width: 600px; height: 600px; margin: auto; background-color: #e9e9e9; position: relative; } .box { height: 100px; width: 100px; background-color: skyblue; position: absolute; top: 0; left: 0; cursor: pointer; } h1 { text-align: center; line-height: 100px; } </style> <script src="./js/vue3.js"></script> </head> <body> <h1>小拖拽的案例</h1> <div id="app"> <!-- drag 是指令的名称 --> <!-- .box 是我们这个小盒子 --> <div class="box" v-drag></div> </div> <script> const { createApp } = Vue; let vm = createApp({ data() { return { } }, directives: { // directives 是指令的选项,因为在这里面我们可以定义多个指令,所以 directive 是加了一个s。 drag: { mounted(el) { // mounted 是他的钩子函数。// el --> dom节点 // 在绑定元素的父组件及他自己的所有子节点都挂载完成后调用。 let x = 0; // 盒子x轴偏移的距离 let y = 0; // 盒子y轴偏移的距离 // onmousedown 鼠标按下事件 el.onmousedown = function (e) { // sx 小盒子相对大盒子的x轴距离 let sx = el.offsetLeft; // sy 小盒子相对大盒子的y轴距离 let sy = el.offsetTop; // onmousemove 鼠标移动事件 document.onmousemove = function (et) { // et.pageX 鼠标相对页面左上角x轴的距离; // et.pageX 鼠标相对页面右上角y轴的距离; x = sx + (et.pageX - e.pageX); y = sy + (et.pageY - e.pageY); x = x >= 0 && x <= 500 ? x : x < 0 ? 0 : 500; // 设置x轴的边界条件 y = y >= 0 && y <= 500 ? y : y < 0 ? 0 : 500; // 设置y轴的边界条件 el.style.top = y + "px"; el.style.left = x + "px"; } } // onmouseup 鼠标抬起事件 el.onmouseup = function () { // 鼠标抬起 onmousemove 清空移动事件 document.onmousemove = null; }; } } } }) vm.mount("#app") </script> </body> </html>
4.2 vue3过渡效果
学习:Vue3过渡效果开发(内置组件 <Transition>
、内置组件 <TransitionGroup>
)
Vue 提供了两个内置组件,可以帮助你制作基于状态变化的过渡和动画:
-
<Transition>
会在一个元素或组件进入和离开 DOM 时应用动画。本章节会介绍如何使用它。 -
<TransitionGroup>
会在一个v-for
列表中的元素或组件被插入,移动,或移除时应用动画。
除了这两个组件,我们也可以通过其他技术手段来应用动画,比如切换 CSS class 或用状态绑定样式来驱动动画。
4.2.1 Transition 组件
<Transition>
是一个内置组件,这意味着它在任意别的组件中都可以被使用,无需注册。它可以将进入和离开动画应用到通过默认插槽传递给它的元素或组件上。进入或离开可以由以下的条件之一触发:
-
由
v-if
所触发的切换 -
由
v-show
所触发的切换 -
由特殊元素
<component>
切换的动态组件
以下是最基本用法的示例:
<button @click="show = !show">Toggle</button> <Transition> <p v-if="show">hello</p> </Transition>
/* 下面我们会解释这些 class 是做什么的 */ .v-enter-active, .v-leave-active { transition: opacity 0.5s ease; } .v-enter-from, .v-leave-to { opacity: 0; }
注意:
<Transition>
仅支持单个元素或组件作为其插槽内容。如果内容是一个组件,这个组件必须仅有一个根元素。
完整的代码:
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>63_过渡效果</title> <style> .v-enter-from { opacity: 0; transform: translateX(100px);} .v-enter-active { transition: all 5s; } .v-enter-to { opacity: 1; transform: translateX(0); } .v-leave-from { opacity: 1; transform: translateX(0); } .v-leave-active { transition: all 5s; } .v-leave-to { opacity: 0; transform: translateX(-100px); } </style> </head> <body> <div id="app"> <button @click="show = !show">切换</button> <!-- 默认需要实现6个样式 v-enter-from v-enter-active v-enter-to v-leave-from v-leave-active v-leave-to --> <Transition> <h1 v-if="show">显示</h1> </Transition> </div> </body> <script src="lib/vue.global.js"></script> <script> Vue.createApp({ data () { return { show: false } } }).mount('#app') </script> </html>
4.2.2 CSS 过渡 class
一共有 6 个应用于进入与离开过渡效果的 CSS class。
-
v-enter-from
:进入动画的起始状态。在元素插入之前添加,在元素插入完成后的下一帧移除。 -
v-enter-active
:进入动画的生效状态。应用于整个进入动画阶段。在元素被插入之前添加,在过渡或动画完成之后移除。这个 class 可以被用来定义进入动画的持续时间、延迟与速度曲线类型。 -
v-enter-to
:进入动画的结束状态。在元素插入完成后的下一帧被添加 (也就是v-enter-from
被移除的同时),在过渡或动画完成之后移除。 -
v-leave-from
:离开动画的起始状态。在离开过渡效果被触发时立即添加,在一帧后被移除。 -
v-leave-active
:离开动画的生效状态。应用于整个离开动画阶段。在离开过渡效果被触发时立即添加,在过渡或动画完成之后移除。这个 class 可以被用来定义离开动画的持续时间、延迟与速度曲线类型。 -
v-leave-to
:离开动画的结束状态。在一个离开动画被触发后的下一帧被添加 (也就是v-leave-from
被移除的同时),在过渡或动画完成之后移除。
v-enter-active
和 v-leave-active
给我们提供了为进入和离开动画指定不同速度曲线的能力
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>64_过渡效果_name</title> <style> .slide-enter-from { opacity: 0; transform: translateX(100px);} .slide-enter-active { transition: all 5s; } .slide-enter-to { opacity: 1; transform: translateX(0); } .slide-leave-from { opacity: 1; transform: translateX(0); } .slide-leave-active { transition: all 5s; } .slide-leave-to { opacity: 0; transform: translateX(-100px); } /* .fade-enter-from { opacity: 0;} .fade-enter-active { transition: all 5s; } .fade-enter-to { opacity: 1; } .fade-leave-from { opacity: 1; } .fade-leave-active { transition: all 5s; } .fade-leave-to { opacity: 0; } */ .fade-enter-from, .fade-leave-to { opacity: 0; } .fade-enter-active, .fade-leave-active { transition: all 5s; } .fade-enter-to, .fade-leave-from { opacity: 1; } </style> </head> <body> <div id="app"> <button @click="show = !show">切换</button> <!-- 默认需要实现6个样式 v-enter-from v-enter-active v-enter-to v-leave-from v-leave-active v-leave-to --> <Transition name="slide"> <h1 v-if="show">显示</h1> </Transition> <Transition name="fade"> <h1 v-if="show">显示</h1> </Transition> </div> </body> <script src="lib/vue.global.js"></script> <script> Vue.createApp({ data () { return { show: false } } }).mount('#app') </script> </html>
更多信息请参考:Transition | Vue.js
4.2.3 为过渡效果命名
我们可以给 <Transition>
组件传一个 name
prop 来声明一个过渡效果名:
<Transition name="fade"> ... </Transition>
对于一个有名字的过渡效果,对它起作用的过渡 class 会以其名字而不是 v
作为前缀。比如,上方例子中被应用的 class 将会是 fade-enter-active
而不是 v-enter-active
。这个“fade”过渡的 class 应该是这样:
.fade-enter-active, .fade-leave-active { transition: opacity 0.5s ease; } .fade-enter-from, .fade-leave-to { opacity: 0; }
可以结合 css3中的
transition
以及animation
实现动画效果
4.2.4 TransitionGroup 内置组件
<TransitionGroup>
是一个内置组件,用于对 v-for 列表中的元素或组件的插入、移除和顺序改变添加动画效果。
和 Transition 的区别:
-
<TransitionGroup> 支持和 <Transition> 基本相同的 props、CSS 过渡 class 和 JavaScript 钩子监听器,但有以下几点区别:
-
默认情况下,它不会渲染一个容器元素。但你可以通过传入 tag prop 来指定一个元素作为容器元素来渲染。
-
过渡模式在这里不可用,因为我们不再是在互斥的元素之间进行切换。
-
列表中的每个元素都必须有一个独一无二的 key attribute。
-
CSS 过渡 class 会被应用在列表内的元素上,而不是容器元素上。
小案例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> .list-enter-active, .list-leave-active { transition: all 0.5s ease; } .list-enter-from, .list-leave-to { opacity: 0; transform: translateX(30px); } </style> <script src="./js/vue3.js"></script> </head> <body> <div id="app"> <div> <button @click="list.push('哈哈哈')">在任意位置添加一项</button> <button @click="list.pop()">在任意位置删除一项</button> </div> <transition-group name="list" tag="ul"> <li v-for="item in list" :key="item">{{item}}</li> </transition-group> </div> <script> const { createApp } = Vue; let vm = createApp({ data() { return { list: ['赵子龙', '张飞', '刘备', '关羽'] } } }) vm.mount("#app") </script> </body> </html>
更多信息请参考:TransitionGroup | Vue.js
4.3 Teleport传送门
学习:开发modal组件(<Teleport />
内置组件)
vue3 新增,模仿了 react中 Portal
<Teleport>
是一个内置组件,它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。
有时我们可能会遇到这样的场景:一个组件模板的一部分在逻辑上从属于该组件,但从整个应用视图的角度来看,它在 DOM 中应该被渲染在整个 Vue 应用外部的其他地方。
这类场景最常见的例子就是全屏的模态框。理想情况下,我们希望触发模态框的按钮和模态框本身是在同一个组件中,因为它们都与组件的开关状态有关。但这意味着该模态框将与按钮一起渲染在应用 DOM 结构里很深的地方。这会导致该模态框的 CSS 布局代码很难,祖先元素有transform样式的时候,会破坏 position: fixed,的布局这样的话,样式就更难写了。
65_model.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>65_teleport传送门</title> <style> .modal { position: fixed; left: 0; right: 0; bottom: 0; top: 0; background-color: rgba(0,0,0,0.4); } .modal .box { width: 50%; min-height: 300px; background-color: #fff; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } </style> </head> <body> <div id="app"> <button @click="open=true">打开模态框</button> <!-- 审查元素还在 div#app 内部 --> <!-- <my-modal v-if="open" @close="close"></my-modal> --> <!-- 审查元素得知 位置发生了改变,放大了 body 标签的最底下 --> <teleport to="body"> <my-modal v-if="open" @close="close"></my-modal> </teleport> </div> </body> <template id="modal"> <div class="modal" @click.self="$emit('close')"> <div class="box"> <p>Hello from the modal!</p> <button @click="$emit('close')">Close</button> </div> </div> </template> <script src="lib/vue.global.js"></script> <script> const Modal = { template: '#modal' } Vue.createApp({ components: { MyModal: Modal }, data () { return { open: false } }, methods: { close () { this.open = false } } }).mount('#app') </script> </html>
通过给组件模板添加
<teleport >
标签审查元素查看结果
4.4 两个重要的实例API
4.4.1 $forceUpdate()
强制该组件重新渲染。
鉴于 Vue 的全自动响应性系统,这个功能应该很少会被用到。唯一可能需要它的情况是,你使用高阶响应式 API 显式创建了一个非响应式的组件状态。
vue2中对象以及数组的操作 数据改变视图没有更新,可以使用 forceUpdate 进行强制更新视图
由于vue2响应式原理的问题,导致对数据[数组、对象深层次]的监听与更新有问题。
不添加 forceUpdate
和添加之后对比查看效果
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>66_forceupdate_vue2</title> </head> <body> <div id="app"> <button @click="change(2)">改变3的数据</button> <button @click="clear">清空数组</button> <button @click="test">改变obj</button> <ul> <li v-for="(item, index) of list" :key="index">{{item}}</li> </ul> {{ arr[2].c }} <ul> <li v-for="(item, index) of arr" :key="index">{{item.a}} - {{ item.b }}</li> </ul> {{ obj.foo }} - {{ obj.bar }} </div> </body> <script src="lib/vue.js"></script> <script> new Vue({ data: { list: ['a', 'b', 'c'], arr: [ { a: 1, b:2 }, { a: 10, b: 20 }, { a: 100, b: 200 } ], obj: { foo: 'foo' } }, methods: { change (index) { this.arr[index].c = 300 // 给对象数组添加额外的属性 this.$forceUpdate() }, clear () { this.list.length = 0 // 数组长度设置为0 清空数组 this.$forceUpdate() }, test () { this.obj.bar = 'bar' // 对象添加额外的属性 console.log(this.obj) this.$forceUpdate() } } }).$mount('#app') </script> </html>
4.4.2 vue2中的$set
在vue2中如果只是为了数据改变更新视图,还可以使用 $set 将响应式数据中添加额外的属性也变为响应式
66_set_vue2.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>66_vue $set</title> </head> <body> <div id="app"> <button @click="change(2)">改变3的数据</button> <button @click="clear">清空数组</button> <button @click="test">改变obj</button> <ul> <li v-for="(item, index) of list" :key="index">{{item}}</li> </ul> {{ arr[2].c }} <ul> <li v-for="(item, index) of arr" :key="index">{{item.a}} - {{ item.b }}</li> </ul> {{ obj.foo }} - {{ obj.bar }} </div> </body> <script src="lib/vue.js"></script> <script> new Vue({ data: { list: ['a', 'b', 'c'], arr: [ { a: 1, b: 2 }, { a: 10, b: 20 }, { a: 100, b: 200 } ], obj: { foo: 'foo' } }, methods: { change(index) { // this.arr[index].c = 300 // 给对象数组添加额外的属性 this.$set(this.arr[index], 'c', 300) // 给对象数组添加额外的属性 }, clear() { // this.list.length = 0 // 数组长度设置为0 清空数组 this.list = [] }, test() { // this.obj.bar = 'bar' // 对象添加额外的属性 this.$set(this.obj, 'bar', 'bar') // 需要使用$set进行更新 console.log(this.obj) } } }).$mount('#app') </script> </html>
说明: vue3中的forceUpdate 一般都用不到,在vue2中实现不了的以上案例,vue3全部支持
4.4.3 $nextTick()
注意:Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。$nextTick 是在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后使用 $nextTick,则可以在回调中获取更新后的 DOM
绑定在实例上的 nextTick() 函数。
和全局版本的 nextTick()
的唯一区别就是组件传递给 this.$nextTick()
的回调函数会带上 this
上下文,其绑定了当前组件实例。
什么时候需要用
1、Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中,原因是在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。与之对应的就是mounted钩子函数,因为该钩子函数执行时所有的DOM挂载已完成。
2、当项目中你想在改变DOM元素的数据后基于新的dom做点什么,对新DOM一系列的js操作都需要放进Vue.nextTick()的回调函数中;通俗的理解是:更改数据后当你想立即使用js操作新的视图的时候需要使用它
3、在使用某个第三方插件时 ,希望在vue生成的某些dom动态发生变化时重新应用该插件,也会用到该方法,这时候就需要在 $nextTick 的回调函数中执行重新应用插件的方法。
** 案例:**
67_nextTick.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>67_nextTick</title> </head> <body> <div id="app"> <div ref="oDiv">{{ count }}</div> <button @click="add">加1</button> </div> </body> <script src="lib/vue.global.js"></script> <script> const app = Vue.createApp({ data () { return { count: 1 } }, methods: { add () { this.count++ console.log(this.count) // 2 console.log(this.$refs.oDiv.innerHTML) // 1 this.$refs.oDiv获取指定DOM,输出:原始值 this.$nextTick(() => { // 可以获取真实的DOM节点的值,瀑布流布局 console.log(this.$refs.oDiv.innerHTML) // 2 }) } } }) app.mount('#app') </script> </html>
原理
因为Vue是异步执行dom更新的,一旦观察到数据变化,Vue就会开启一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和DOM操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。 当你设置 vm.someData = 'new value',DOM 并不会马上更新,而是在异步队列被清除,也就是下一个事件循环开始时执行更新时才会进行必要的DOM更新。如果此时你想要根据更新的 DOM 状态去做某些事情,就会出现问题。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。
4.5 渲染函数
4.5.1 h()
创建虚拟 DOM 节点 (vnode)。
Vue 提供了一个 h()
函数用于创建 vnodes:
import { h } from 'vue' const vnode = h( 'div', // type { id: 'foo', class: 'bar' }, // props [ /* children */ ] )
h()
是 hyperscript 的简称——意思是“能生成 HTML (超文本标记语言) 的 JavaScript”。这个名字来源于许多虚拟 DOM 实现默认形成的约定。一个更准确的名称应该是 createVnode()
,但当你需要多次使用渲染函数时,一个简短的名字会更省力。
h()
函数的使用方式非常的灵活:
创建原生元素:
// 除了类型必填以外,其他的参数都是可选的 h('div') h('div', { id: 'foo' }) // attribute 和 property 都能在 prop 中书写 // Vue 会自动将它们分配到正确的位置 h('div', { class: 'bar', innerHTML: 'hello' }) // 类与样式可以像在模板中一样 // 用数组或对象的形式书写 h('div', { class: [this.foo], style: { color: 'red' } }) // 事件监听器应以 onXxx 的形式书写 h('div', { onClick: () => {} }) // children 可以是一个字符串 h('div', { id: 'foo' }, 'hello') // 没有 props 时可以省略不写 h('div', 'hello') h('div', [h('span', 'hello')]) // children 数组可以同时包含 vnodes 与字符串 h('div', ['hello', h('span', 'hello')])
创建组件:
const Header = { template: `<header>1头部</header>` } const Content = { template: `<div>1内容</div>` } const Footer = { template: `<footer>1底部</footer>` } // h也可以创建组件 const Container = { render () { return [ h('div', { class: 'box' }, [ h(Header, { class: 'header' }), h(Content, { class: 'content' }), h(Footer, { class: 'footer' }), ]) ] } }
68_h.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>68_h</title> </head> <body> <div id="app"> <my-com>11</my-com> <my-container> <my-header></my-header> <my-content></my-content> <my-footer></my-footer> </my-container> </div> </body> <template> <div class="container"> <!-- <header class="header">头部</header> --> <header class="header"> <slot></slot> </header> <div class="content" id="content">内容</div> <footer class="footer"> <ul> <li @click="myAlert(1)">首页</li> <li @click="myAlert(2)">分类</li> <li @click="myAlert(3)">购物车</li> <li @click="myAlert(4)">我的</li> </ul> </footer> </div> </template> <script src="lib/vue.global.js"></script> <script> const { createApp, h } = Vue // h 创建HTML元素 const Com = { render () { return [ // h 第一个参数是必须的,第二表示属性,如果没有,可以省略,如果有写为对象,第三个参数 子元素 h('div', { class: 'container' }, [ // h('header', { class: 'header' }, '头部'), h('header', { class: 'header' }, this.$slots.default()), h('div', { class: 'content', id: 'content' }, '内容'), h('footer', { class: 'footer' }, [ h('ul', [ h('li', { onClick: () => { this.myAlert(1) } }, '首页'), h('li', { onClick: () => { this.myAlert(2) } }, '分类'), h('li', { onClick: () => { this.myAlert(3) } }, '购物车'), h('li', { onClick: () => { this.myAlert(4) } }, '我的') ]) ]) ]) ] }, methods: { myAlert (num) { console.log(num) } } } const Header = { template: `<header>1头部</header>` } const Content = { template: `<div>1内容</div>` } const Footer = { template: `<footer>1底部</footer>` } // h也可以创建组件 const Container = { render () { return [ h('div', { class: 'box' }, [ h(Header, { class: 'header' }), h(Content, { class: 'content' }), h(Footer, { class: 'footer' }), ]) ] } } const app = createApp({ components: { MyCom: Com, MyContainer: Container } }) app.mount('#app') </script> </html>
4.5.2 mergeProps()
合并多个 props 对象,用于处理含有特定的 props 参数的情况。
mergeProps()
支持以下特定 props 参数的处理,将它们合并成一个对象。
-
class
-
style
-
onXxx
事件监听器——多个同名的事件监听器将被合并到一个数组。
如果你不需要合并行为而是简单覆盖,可以使用原生 object spread
语法来代替
import { mergeProps } from 'vue' const one = { class: 'foo', onClick: handlerA } const two = { class: { bar: true }, onClick: handlerB } const merged = mergeProps(one, two) /** { class: 'foo bar', onClick: [handlerA, handlerB] } */
69_mergeProps.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>69_mergeProps</title> </head> <body> <div id="app"> <div :class="test.class" @click="test.onClick">测试</div> </div> </body> <script src="lib/vue.global.js"></script> <script> const { createApp, mergeProps } = Vue const one = { class: 'foo', onClick: () => { console.log(1) } } const two = { class: { bar: true }, onClick: () => { console.log(2) } } const test = mergeProps(one, two) console.log(test) // { class: 'foo bar', onClick: [() => {console.log(1)}, () => {console.log(2)}]} const app = createApp({ data () { return { test } } }) app.mount('#app') </script> </html>
4.5.3 cloneVNode()
克隆一个 vnode。
返回一个克隆的 vnode,可在原有基础上添加一些额外的 prop。
Vnode 被认为是一旦创建就不能修改的,你不应该修改已创建的 vnode 的 prop,而应该附带不同的/额外的 prop 来克隆它。
Vnode 具有特殊的内部属性,因此克隆它并不像 object spread 一样简单。cloneVNode()
处理了大部分这样的内部逻辑。
import { h, cloneVNode } from 'vue' const original = h('div') const cloned = cloneVNode(original, { id: 'foo' })
70_cloneVNode.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>70_cloneVNode</title> </head> <body> <div id="app"> <my-header></my-header> <hr /> <!-- Invalid VNode type: undefined (undefined) --> <new-header></new-header> </div> </body> <script src="lib/vue.global.js"></script> <script> const { createApp, cloneVNode, h } = Vue const old = h('div') // 克隆元素 const cloned = cloneVNode(old, { id: 'box'}) console.log('原始的', old) //{ props: null} console.log('克隆的元素', cloned) // {props: { id: 'box' }} // 克隆组件--失败 --- cloneVNode 克隆组件的返回值不是一个组件 const Header = { template: `<header>1头部</header>` } console.log('原始组件', Header) const NewHeader = cloneVNode(Header) console.log('克隆组件', NewHeader) const app = createApp({ components: { MyHeader: Header, NewHeader: NewHeader } }) app.mount('#app') </script> </html>
cloneVNode 函数不可以克隆组件,返回值不是一个组件
4.6 自定义过滤器
vue3移除了了自定义过滤器 过滤器 — Vue.js
页面展示 男 女 数据库中存 1 0,接口返回为 1 和 0,如何展示
国际化,中国 ¥ 美国 $
71_vue2_filters.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>71_vue2_过滤器</title> </head> <body> <div id="app"> {{ sex }} - {{ sex | sexFilter }} - {{ price }} - {{ price | priceFilter('$') }} - {{ price | priceFilter('¥') }} </div> </body> <script src="lib/vue.js"></script> <script> // 全局过滤器 Vue.filter('sexFilter', (val) => { return val === 1 ? '男' : '女' }) new Vue({ data: { sex: 1, // 1 男 0 女 price: 100 }, filters: { priceFilter (val, type) { return type + val } } }).$mount('#app') </script> </html>
vue3 没有过滤器
72_change_filters.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>72_vue3_代替过滤器</title> </head> <body> <div id="app"> {{ sex }} - {{ newSex }} - {{ price }} - {{zhPrice }} - {{ enPrice }} </div> </body> <script src="lib/vue.global.js"></script> <script> Vue.createApp({ data(){ return { sex: 1, // 1 男 0 女 price: 100 } }, computed: { newSex () { return this.sex === 1 ? '男' : '女' }, zhPrice () { return '¥' + this.price }, enPrice () { return '$' + this.price } } }).mount('#app') </script> </html>
4.7 混入mixins
一个包含组件选项对象的数组,这些选项都将被混入到当前组件的实例中。
mixins
选项接受一个 mixin 对象数组。这些 mixin 对象可以像普通的实例对象一样包含实例选项,它们将使用一定的选项合并逻辑与最终的选项进行合并。举例来说,如果你的 mixin 包含了一个 created
钩子,而组件自身也有一个,那么这两个函数都会被调用。
Mixin 钩子的调用顺序与提供它们的选项顺序相同,且会在组件自身的钩子前被调用。
在 Vue 2 中,mixins 是创建可重用组件逻辑的主要方式。尽管在 Vue 3 中保留了 mixins 支持,但对于组件间的逻辑复用,Composition API 是现在更推荐的方式。
73_mixins.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>73_mixins</title> </head> <body> <div id="app"> <my-child1></my-child1> <hr/> <my-child2></my-child2> </div> </body> <template id="child1"> <div> <h1>child1组件</h1> {{ count }} <button @click="add">加1</button> </div> </template> <template id="child2"> <div> <h1>child2组件</h1> {{ count }} <button @click="add">加1</button> </div> </template> <script src="lib/vue.global.js"></script> <script> const { createApp } = Vue const myMixin = { data () { return { count: 10 } }, methods: { add () { this.count++ } }, mounted () { console.log('1', this.count) } } const Child1 = { // 局部混入 mixins: [myMixin], template: '#child1', data () { // 初始化数据相同,组件的 优先级高于 混入 return { count: 100 } }, mounted () { console.log('2', this.count) } } const Child2 = { // 局部混入 mixins: [myMixin], template: '#child2', methods: { add () { // 函数名相同, 组件的优先级高于 混入 this.count += 10 } }, mounted () { // 相同的生命周期的钩子函数,先执行混入的代码,再执行组件的代码 console.log('3', this.count) } } const app = createApp({ components: { MyChild1: Child1, MyChild2: Child2 } }) // 全局混入 // 不建议使用全局混入,会给所有的组件都加入部分代码 // app.mixin(myMixin) app.mount('#app') </script> </html>
除了生命周期钩子函数外,所有的代码都以组件为优先
生命周期钩子函数先执行 混入的,后执行组件的
4.8 webpack
4.8.1 webpack 简介
为什么需要打包工具?
-
开发时,我们会使用框架(React、Vue),ES6 模块化语法,Less/Sass 等 css 预处理器等语法进行开发。
-
这样的代码要想在浏览器运行必须经过编译成浏览器能识别的 JS、Css 等语法,才能运行。
-
所以我们需要打包工具帮我们做完这些事。
-
除此之外,打包工具还能压缩代码、做兼容性处理、提升代码性能等。
有哪些打包工具?
-
Grunt
-
Gulp
-
Parcel
-
Webpack
-
Rollup
-
Vite
-
...
目前市面上最流量的是 Webpack,所以我们主要以 Webpack 来介绍使用打包工具,不过vue现在开始推荐vite,有可能在未来会超过webpack。
4.8.2 基本使用
Webpack 是一个静态资源打包工具
它会以一个或多个文件作为打包的入口,将我们整个项目所有文件编译组合成一个或多个文件输出出去。
输出的文件就是编译好的文件,就可以在浏览器段运行了。
我们将 Webpack 输出的文件叫做 bundle。
功能介绍
Webpack 本身功能是有限的:
-
开发模式:仅能编译 JS 中的 ES Module 语法
-
生产模式:能编译 JS 中的 ES Module 语法,还能压缩 JS 代码
开始使用
1. 资源目录
webpack_code # 项目根目录(所有指令必须在这个目录运行) └── src # 项目源码目录 ├── js # js文件目录 │ ├── count.js │ └── sum.js └── main.js # 项目主文件
** 2.创建文件**
-
count.js
export default function count(x, y) { return x - y; }
-
sum.js
export default function sum(...args) { return args.reduce((p, c) => p + c, 0); }
-
main.js
import count from "./js/count"; import sum from "./js/sum"; console.log(count(2, 1)); console.log(sum(1, 2, 3, 4));
3. 下载依赖
打开终端,来到项目根目录。运行以下指令:
-
初始化package.json
npm init -y
此时会生成一个基础的 package.json
文件。
需要注意的是 package.json
中 name
字段不能叫做 webpack
, 否则下一步会报错
-
下载依赖
npm i webpack webpack-cli -D
4. 启用 Webpack
-
开发模式
npx webpack ./src/main.js --mode=development
-
生产模式
npx webpack ./src/main.js --mode=production
npx webpack
: 是用来运行本地安装 Webpack
包的。
./src/main.js
: 指定 Webpack
从 main.js
文件开始打包,不但会打包 main.js
,还会将其依赖也一起打包进来。
--mode=xxx
:指定模式(环境)。
5. 观察输出文件 默认 Webpack 会将文件打包输出到 dist 目录下,我们查看 dist 目录下文件情况就好了
4.8.3 基本配置
在开始使用 Webpack 之前,我们需要对 Webpack 的配置有一定的认识。
5 大核心概念**
-
entry(入口)
指示 Webpack 从哪个文件开始打包
-
output(输出)
指示 Webpack 打包完的文件输出到哪里去,如何命名等
-
loader(加载器)
webpack 本身只能处理 js、json 等资源,其他资源需要借助 loader,Webpack 才能解析
-
plugins(插件)
扩展 Webpack 的功能
-
mode(模式)
主要由两种模式:
-
开发模式:development
-
生产模式:production
准备 Webpack 配置文件
在项目根目录下新建文件:webpack.config.js
module.exports = { // 入口 entry: "", // 输出 output: {}, // 加载器 module: { rules: [], }, // 插件 plugins: [], // 模式 mode: "", };
Webpack 是基于 Node.js 运行的,所以采用 Common.js 模块化规范
修改配置文件
-
配置文件
// Node.js的核心模块,专门用来处理文件路径 const path = require("path"); module.exports = { // 入口 // 相对路径和绝对路径都行 entry: "./src/main.js", // 输出 output: { // path: 文件输出目录,必须是绝对路径 // path.resolve()方法返回一个绝对路径 // __dirname 当前文件的文件夹绝对路径 path: path.resolve(__dirname, "dist"), // filename: 输出文件名 filename: "main.js", }, // 加载器 module: { rules: [], }, // 插件 plugins: [], // 模式 mode: "development", // 开发模式 }; 2. 运行指令 ```:no-line-numbers npx webpack
此时功能和之前一样,也不能处理样式资源
4.8.4 处理样式资源
-
Webpack 本身是不能识别样式资源的,所以我们需要借助 Loader 来帮助 Webpack 解析样式资源
-
我们找 Loader 都应该去官方文档中找到对应的 Loader,然后使用
-
官方文档找不到的话,可以从社区 Github 中搜索查询
处理 Css 资源
1. 下载包
npm i css-loader style-loader -D
注意:需要下载两个 loader
说明:
-
css-loader
:负责将 Css 文件编译成 Webpack 能识别的模块 -
style-loader
:会动态创建一个 Style 标签,里面放置 Webpack 中 Css 模块内容
此时样式就会以 Style 标签的形式在页面上生效
2. 配置
const path = require("path"); module.exports = { entry: "./src/main.js", output: { path: path.resolve(__dirname, "dist"), filename: "main.js", }, module: { rules: [ { // 用来匹配 .css 结尾的文件 test: /\.css$/, // use 数组里面 Loader 执行顺序是从右到左 use: ["style-loader", "css-loader"], }, ], }, plugins: [], mode: "development", };
3. 添加 Css 资源
-
src/css/index.css
.box1 { width: 100px; height: 100px; background-color: pink; }
-
src/main.js
import count from "./js/count"; import sum from "./js/sum"; // 引入 Css 资源,Webpack才会对其打包 import "./css/index.css"; console.log(count(2, 1)); console.log(sum(1, 2, 3, 4));
-
public/index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>webpack5</title> </head> <body> <h1>Hello Webpack5</h1> <!-- 准备一个使用样式的 DOM 容器 --> <div class="box1"></div> <!-- 引入打包后的js文件,才能看到效果 --> <script src="../dist/main.js"></script> </body> </html>
4. 运行指令
npx webpack
打开 index.html 页面查看效果
4.8.5 处理 Less 资源
1. 下载包
npm i less-loader -D
2. 功能介绍
-
less-loader
:负责将 Less 文件编译成 Css 文件
3. 配置
const path = require("path"); module.exports = { entry: "./src/main.js", output: { path: path.resolve(__dirname, "dist"), filename: "main.js", }, module: { rules: [ { // 用来匹配 .css 结尾的文件 test: /\.css$/, // use 数组里面 Loader 执行顺序是从右到左 use: ["style-loader", "css-loader"], }, { test: /\.less$/, use: ["style-loader", "css-loader", "less-loader"], }, ], }, plugins: [], mode: "development", };
4. 添加 Less 资源
-
src/less/index.less
.box2 { width: 100px; height: 100px; background-color: deeppink; }
-
src/main.js
import count from "./js/count"; import sum from "./js/sum"; // 引入资源,Webpack才会对其打包 import "./css/index.css"; import "./less/index.less"; console.log(count(2, 1)); console.log(sum(1, 2, 3, 4));
-
public/index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>webpack5</title> </head> <body> <h1>Hello Webpack5</h1> <div class="box1"></div> <div class="box2"></div> <script src="../dist/main.js"></script> </body> </html>
5. 运行指令
npx webpack
打开 index.html 页面查看效果;