文章目录
- 写在前面
- Vue
- 一、什么是 Vue
- 二、相关技术栈
- 前端
- 后端
- 关于前后端分离
- 三、入门使用
- 3.1、Hello,Vue
- 3.2、Mustache 语法
- 3.3、插值操作
- 3.4、属性绑定 v-bind
- 3.5、条件插值
- 3.5.1、v-if、v-else、v-else-if
- 3.5.2、v-show
- 3.5.3、使用 key 来管理复用元素
- 3.6、列表渲染 v-for
- 3.6.1、遍历数组
- 3.6.2、遍历对象
- 3.6.3、v-for 添加 key
- 3.6.4、数组更新检测
- 3.7、绑定事件 v-on
- 3.7.1、修饰符
- 3.8、MVVM 模型
- 3.9、表单绑定 v-model
- 3.9.1、基本使用
- 3.9.2、值绑定
- 3.9.3、修饰符
- 三、(补充)Vue 的生命周期
- 四、组件化
- 4.1、全局组件与局部组件
- 4.2、父子组件
- 4.3、Template 分离写法
- 4.4、组件复用的问题
- 4.5、父子组件通信之 props
- 4.6、父子组建通信之$emit
- 4.7、父子组件访问
- 五、(补充)计算属性
- 5.1、基本使用
- 5.2、复杂使用
- 5.3、计算属性 VS. Methods
- 5.4、计算属性 VS. 侦听属性
- 六、插槽 slot
- 6.1、基本使用
- 6.2、编译作用域
- 6.3、具名插槽
- 6.4、作用域插槽
- >> 请转到学习模块开发与Webpack >>
- 九、Vue-CLI
- 9.1、CLI? what? why?
- 9.2、Vue-CLI 使用
- 9.3、模板编译与渲染函数(提高)
- >> 请转到学习Vue-Router >>
- 十二、动态组件
- 12.1、入门案例
- 12.2、keep-alive
- 十三、混入(Mixin)
- 13.0、基础介绍
- 13.1、选项合并
- 13.2、全局mixin
- 13.3、局限性
- 13.2、全局mixin
- 13.3、局限性
写在前面
本博文仅作为个人学习过程的记录,可能存在诸多错误,希望各位看官不吝赐教,支持错误所在,帮助小白成长!
Vue
一、什么是 Vue
- 开发者:尤雨溪(中国)
- 一套用于构建用户界面的渐进式框架,发布于 2014 年 2 月,与其他大型框架不同的是,Vue 被设计为可以自顶向下逐层应用。
- Vue 核心库只关注视图层(HTML+CSS)。
- 便于与第三方库(网络通信:axios,页面跳转:vue-router , 状态管理:vuex)或者既有项目整合。
二、相关技术栈
前端
-
HTML(容易)
-
CSS(难点、重点)
企业中开发,多用 CSS 预处理器,用编程的方式来自动生成输出 CSS
-
JS(重点)
JS 框架:
-
jQuery
-
Angular(Java 程序员开发)
将 MVC 搬到了前端,增加了模块化开发的理念,采用 TypeScript(微软)开发
-
React(Facebook 出品)
提出了虚拟 Dom(Visual Dom)的概念
需要学 JSX 语言
-
Vue
渐进式:不要求完全使用全部功能,可以只在项目中只嵌入一部分。
综合了 Angular 和 React
特色:属性计算
强调模块化
-
Axios(前端通信框架)
-
-
UI 框架
- ElementUI(饿了么)
- AmazeUI
- Bootstrap(Twitter)
- Ant-Design(阿里巴巴)
后端
-
NodeJS
由于过于笨重,作者声称已经放弃了 NodeJS,开始开发新的架构Deno
-
NodeJS 及项目管理工具
-
Express: NodeJS 框架
-
NPM:项目综合管理工具,类似于 Java 开发中的 Maven
-
关于前后端分离
模式也好,技术也罢,没有好坏优劣之分,只有合不合适;
前后端分离的开发思想主要是基于SoC(关注度分离原则)
,让前后端职责更清晰,分工合作更高效。
三、入门使用
3.1、Hello,Vue
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><title>Hello Vue</title></head><body><div id="app">{{message}}</div><script src="../js/vue.js"></script><script>let app = new Vue({ el: '#app', data: { message: 'Hello, Vue!' } })</script></body>
</html>
声明式编程范式!显示与数据分离!
响应式:页面显示会随着数据的改变而改变!
3.2、Mustache 语法
Mustache
语法:代码中我们使用{{message}}
,进行元素插值。双大括号就是 Mustache 语法的标志!
大括号内支持使用运算符对数据进行处理后显示!
3.3、插值操作
v-once
只会进行一次渲染,后面数据的更新不会触发页面的刷新,但是对象的数据还是会变化!
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><title>V Once</title></head><body><div id="app"><span v-once>{{message}}</span></div><script src="../js/vue.js"></script><script>const app = new Vue({el: '#app',data: { message: 'this is a readonly message' },})</script></body>
</html>
代码效果:
v-html
将数据按照 html 代码做解析,然后渲染。
与之相反的是:v-pre,显示原生的文本内容,不做任何解析!
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><title>V Html</title></head><body><div id="app"><h1>v-html:</h1><span v-html="message"></span><h1>v-pre</h1><span v-pre>{{message}}</span><h1>Mustache</h1><span>{{message}}</span></div><script src="../js/vue.js"></script><script>const app = new Vue({el: '#app',data: { message: '<a href="https://www.baidu.com/">百度一下</a>' },})</script></body>
</html>
代码效果:
v-text
其作用与 mustache 相似,接收一个字符串变量,然后渲染,还是利用上面那个例子,效果如图
v-cloak
当 Mustache 语法未被正确解析时,用户可能看到类似{{message}}的文本元素。
使用 v-cloak,可以判断元素是否渲染成功:{
当数据渲染成功前:v-cloak 作为标签属性存在,
数据渲染成功后:v-cloak 从标签属性清除
}
因此我们可以借助这个特殊属性,使用 css 来装饰对应标签选择不让用户看到原生内容
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><title>V-Cloak</title><style>[v-cloak] {display: none;}</style></head><body><div id="app"><h1>Message:</h1><span v-cloak>{{message}}</span></div><script src="../js/vue.js"></script><script>// 设置延时,2s后创建VuesetTimeout(function () {const app = new Vue({el: '#app',data: {message: 'Hello World!',},})}, 2000)</script></body>
</html>
3.4、属性绑定 v-bind
像控制元素内容一样,动态控制属性值,使用v-bind:xxx="??"
将标签内的 xxx 属性绑定到 data 中的??数据上!
<div id="app"><a v-for="movie in movies" v-bind:href="movie.src">{{movie.name}}<br /></a>
</div>
<script src="../js/vue.js"></script>
<script>const app = new Vue({el: '#app',data: {movies: [{ name: '钢铁侠', src: 'xxxx' },{ name: '美国队长', src: 'xxxx' },{ name: '雷神', src: 'xxxx' },],},})
</script>
可以看到我们的 a 标签的 href 是与 movie 的 src 值绑定的!
v-bind 使用的频率极高,于是官方为其提供了简写版::xxx=??
。就可以完成 xxx 属性与 data 中??的值的绑定!
同时官方文档提到了在 2.6 版本后,还提供了动态参数(v-bind、v-on 是支持接收参数的!其绑定的内容就是参数)
也就是说以后绑定的属性或者行为也不用“写死”了,可以通过 data 中的参数进行指定:
<div id="app"><a v-bind:[link.attr]="link.attrValue" v-text="link.linkName"></a>
</div>
<script src="../js/vue.js"></script>
<script>const app = new Vue({el: '#app',data: {link: {linkName: '百度一下',attr: 'href',attrValue: 'https://www.baidu.com',},},})
</script>
可以看到,我们直接通过数据就为 a 标签动态绑定了 href 属性!
再来看看 v-bind 的高阶使用:结合对象动态修改 class:
<style>.red {color: red;}.yello {color: yellow;}</style
><!--version 1.0-->
<body><div id="app"><h1 :class="{'red': true, 'yello': false}">Hello, World</h1></div><script src="../js/vue.js"></script><script>const app = new Vue({ el: '#app' })</script>
</body>
通过一个:class = {classname: boolean, classname2: boolean}
,会将 boolean 为 true 的 class 加入 class 列表。这种写法我们称其为对象语法!
<!--version 2.0-->
<div id="app"><h1 :class="{'red': isRed, 'yello': isYello}">Hello, World</h1>
</div>
<script src="../js/vue.js"></script>
<script>const app = new Vue({ el: '#app', data: { isRed: true, isYello: false } })
</script>
这里进行简单优化后,class 的列表的增删与否通过具体的数据isRed
、isYello
进行控制
<!--version 3.0-->
<div id="app"><h1 :class="getClasses()">Hello, World</h1></div>
<script src="../js/vue.js"></script>
<script>const app = new Vue({el: '#app',data: {isRed: true,isYello: false,},methods: {getClasses: function () {this.isRed = falsethis.isYello = truereturn { red: this.isRed, yello: this.isYello }},},})
</script>
继续优化后,我们的 class 属性值通过一个方法返回一个 object,然后判断是否加入 class。我们还可以通过 v-on 来通过界面交互改变 isRed、isYello 的数据值,以达到改变界面显示的效果!
这种使用会经常应用在实际开发中,请务必熟悉掌握!
v-bind 动态绑定 style
其使用方式和上面的绑定 class 大同小异,我们通过一个 kv 集合对象来动态为标签添加样式!这种操作在组件化开发中很常见!
<div id="app"><span :style="{fontSize: '50px', backgroundColor: 'blue'}">{{message}}</span>
</div>
<script src="../js/vue.js"></script>
<script>const app = new Vue({ el: '#app', data: { message: 'Hello World!' } })
</script>
这样写就感觉很鸡肋,然而其真正的用法应该是利用数据控制样式表的变化,或者使用 method 获取样式表。
<div id="app"><span :style="{fontSize: spanFontSize + 'px'}">{{message}}</span><span :style="getBgColor()">{{message}}</span>
</div>
<script src="../js/vue.js"></script>
<script>const app = new Vue({el: '#app',data: { message: 'Hello World!', spanFontSize: 20, spanBgColor: 'blue' },methods: {getBgColor: function () {return { backgroundColor: this.spanBgColor }},},})
</script>
有几个注意点:
- 传统样式表中带
-
的样式属性(例如font-size
等)在进行属性绑定时,属性名作为 key,要使用小驼峰命名法(lower-Camel-Case)。例如fontSize、backgroundColor
- k-v 在表示时一定要分清是变量还是字符串!key 作为属性名可以不使用字符串表示,但是 value 如果不加以区分,会导致 vue 解析失败。(例如:
:style={fontSize: 50px}
,很明显我们希望 50px 作为属性值被解析,但是 vue 会认为 50px 就是一个变量名,所以需要改写成::style={fontSize: '50px'}
)
3.5、条件插值
3.5.1、v-if、v-else、v-else-if
<body><!--根据绑定ViewModel中的数据判断显示哪个标签--><div id="p1"><h1 v-if="message==='A'">A</h1><h1 v-else-if="message==='B'">B</h1><h1 v-else>C</h1></div><script src="../vue-js/vue.js"></script><script>// vm 绑定 id=p1的Dom元素 var vm = new Vue({ el: "#p1", data: { message: "A" } });</script>
</body>
3.5.2、v-show
v-show 使用效果与 v-if 相同,但是 v-show 是使用 css 的 display 属性来控制元素的显示:(v-show 在 template 中不能使用!)
<div id="app"><!-- v-if则是直接通过增删标签来达到显示、隐藏 --><h1 v-if="display">{{message}}</h1><!-- v-show会保证标签始终在DOM中,使用css的display属性来控制它的显示与否。不可用于template--><h1 v-show="display">{{message}}</h1>
</div>
<script src="../js/vue.js"></script>
<script>const app = new Vue({el: '#app',data: { message: 'hello world', display: false },})
</script>
3.5.3、使用 key 来管理复用元素
Vue 在进行页面渲染时,为了保证效率会最大程度上复用页面元素。
我们修改了数据值导致页面重新渲染时,首先会在内存中创建虚拟 DOM,虚拟 DOM 使用
diff
算法会将旧页面和新页面都有的元素进行复用,然后将渲染完成的虚拟 DOM 渲染回浏览器上。
我们用一个案例来演示一下:
<div id="app"><form v-if='loginType === "username"'><label>Username</label><input type="text" placeholder="Please input your username" /></form><form v-else><label>Email</label><input type="text" placeholder="Please input your email" /></form><button @click="toggleLoginType">Toggle Login Type</button>
</div>
<script src="../js/vue.js"></script>
<script>const app = new Vue({el: '#app',data: { loginType: 'username' },methods: {toggleLoginType() {if (this.loginType === 'username') {this.loginType = 'email'} else {this.loginType = 'username'}},},})
</script>
即使我们使用按钮进行登录类型的切换,我们输入框中的内容依旧没有消失。这就很好地说明了我们的输入框(即 input 元素)被复用了,那么 label 元素也就必然被复用了。
可是如果我们想要切换了登录类型后,就清除原来输入的内容,简单说就是不复用这个 input 元素。我们就需要为这个元素加上特定的标识,在 Vue 中 元素的 key 属性是用于区分元素的关键。所以我们只需要给 input 元素绑定一个独一无二的 key 属性,告诉 Vue“它们是两个独立的东西,请不要复用!”
<form v-if='loginType === "username"'><label>Username</label><inputtype="text"placeholder="Please input your username":key="'usernameInput'"/>
</form>
<form v-else><label>Email</label><inputtype="text"placeholder="Please input your email":key="'email-input'"/>
</form>
<button @click="toggleLoginType">Toggle Login Type</button>
修改后,去试试效果!
3.6、列表渲染 v-for
3.6.1、遍历数组
遍历时,第二个可选参数 index 表示元素在数组中的下标!
<body><div id="p1"><ul v-for="(letter, index) in letters" :key="index"><li>{{letter.element}}-->{{index}}</li></ul></div><script src="../vue-js/vue.js"></script><script>// vm 绑定 id=p1的Dom元素var vm = new Vue({el: '#p1',data: {letters: [{ element: 'A' }, { element: 'B' }, { element: 'C' }],},})</script>
</body>
v-for 也会监听列表的变化,如果列表发生了变化会触发页面的更新!
v-for 还支持for-of
,更接近原生 JavaScript 的遍历。
3.6.2、遍历对象
除了遍历数组,还支持使用 v-for 遍历数组,遍历时有三个参数:{value, name, index}
,依次为属性值、属性名、属性下标!后两个参数为可选参数!
遍历的顺序与对象属性声明的顺序相同!
<div v-for="(value, name, index) in object">{{ index }}. {{ name }}: {{ value }}
</div>
<script src="../js/vue.js"></script>
<script>new Vue({el: '#v-for-object',data: {object: {title: 'How to do lists in Vue',author: 'Jane Doe',publishedAt: '2016-04-10',},},})
</script>
3.6.3、v-for 添加 key
这是 Vue 推荐我们在使用 v-for 的时候所做的操作。
当我们不为遍历元素绑定 key 的时候(默认),当列表插入值后,导致列表的数据顺序变化后,不会移动 DOM 元素来匹配列表的数据项。即列表的数据与 DOM 中的元素是没有绑定的!它会从发生了变化的位置挨个更新元素的值。
但是当我们使用:key
为元素绑定一个 key 以后,并且确保 key 与数据是可以做到一一对应的(index 无效),列表数据就可以与元素进行绑定。可以便于 Vue 追踪这个元素。当列表变化后,Vue 就可以通过元素进行复用和元素重排序来完成更高效的渲染工作。
注意一下,key 尽量不要使用 index,因为 index 无法保证数据变化时与数据保持对应关系!
3.6.4、数组更新检测
并不是所有的数组更新都会触发页面重渲染(例如直接通过下标修改数组)。(响应式更新)
支持更新检测的数组方法有:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
以上的方法属于数组变更方法,即是在原数组的数据上进行变动。当然也有非变更方法:
例如:filter()
、concat()
和 slice()
。这些方法调用后会返回一个新的数组,当我们将新数组赋值给原数据时,**页面会在保证元素重用最大化下进行重新渲染!**即相同的 DOM 元素会被保留重用!而不是丢弃原有所有 DOM 元素,将新的数组进行渲染到整个列表。
3.7、绑定事件 v-on
<body><div id="p1"><!--绑定按钮点击事件--><button v-on:click="showMsg">Click Me</button></div><script src="../vue-js/vue.js"></script><script type="text/javascript">var vm = new Vue({el: '#p1',data: { message: 'hello Vue' },methods: {showMsg: function () {alert(this.message)},},})</script>
</body>
v-on: click=“xxx” 可缩写为 @click="xxx"
我们的事件监听回调函数是可以接收参数的!
如果我们的回调函数是有参数要求的,当你的绑定事件时,省略了函数调用的小括号,那么将默认将触发的事件作为第一个参数传入:
<div id="app"><!--这里省略了小括号,当鼠标点击时,会将事件作为首个参数传入--><button @click="showMessage">点我</button>
</div>
<script src="../js/vue.js"></script>
<script>const app = new Vue({el: '#app',methods: {showMessage(message) {console.log(message)},},})
</script>
于是控制台的输出效果就是:
3.7.1、修饰符
一个事件我们也可以分为很多状态,修饰符就用于指出一个指令以特殊的方式进行绑定。通过在事件后加上.xxx
来为事件加上修饰符。
@click.stop
:停止事件冒泡
<div id="app" @click="clickDiv">text-content <button @click="clickBtn">点我</button>
</div>
<script src="../js/vue.js"></script>
<script>const app = new Vue({el: '#app',methods: {clickDiv() {console.log('div was clicked!')},clickBtn() {console.log('btn was clicked!')},},})
</script>
这样的代码,通常情况下当我们点击按钮的时候,会触发 clickBtn 回调函数执行,然后事件会向上冒泡,然后引起 clickDiv 回调函数执行!
可是我们希望我们的按钮点击事件不要向上冒泡,就仅仅只触发 clickBtn 回调函数。那就对绑定click
事件做一个特殊的修饰:绑定click.stop
事件,就会有效阻止事件冒泡:
<!--修改前-->
<button @click="clickBtn">点我</button>
<!--修改后-->
<button @click.stop="clickBtn">点我</button>
@click.prevent
阻止事件的默认操作。
对于一个表单来说,submit 按钮按下后会触发默认动作,将数据提交到对应的 web 服务器
<form action="https://www.baidu.com/" method="post"><label for="username"><input type="text" id="username" placeholder="Please Input Username..." /></label><input type="submit" value="拦截,我要手动提交" @click.prevent="onSubmit" /><input type="submit" value="自动提交" />
</form>
而我们这里的.prevent
标签就可以阻止这个默认动作,然后我们就可以选择将数据进行手动的提交。
{keyCode|keyAlias}
监听键盘的某一个键的具体事件(指定一个键可以使用前对应的键码或者键别名)。tips: 一般键盘的事件有:键被按下
keydown
、键弹起keyUp
、键按住keypress
所以说
@keyup.enter
(enter 是回车键的 Alias),就是监听 enter 键被按下的事件。
<inputtype="text"placeholder="Please Input Anything"@keyup.enter="inputFinished()"
/>
<script src="../js/vue.js"></script>
<script>const app = new Vue({el: '#app',methods: {inputFinished() {console.log('Input finished!')},},})
</script>
当我们完成输入后,按下回车并松开时,就会执行这个回调函数。除此以外,鼠标键、系统修饰键的事件也可以被监听!
<!-- Alt + C (C的键码是67) -->
<input v-on:keyup.alt.67="clear" />
<!--或者直接用C键alias-->
<input v-on:keyup.alt.c="clear" />
对于一些特殊的键,可以通过 Vue 进行全局配置 keyCode 的别名:
// 可以使用 `v-on:keyup.f1`
Vue.config.keyCodes.f1 = 112
鼠标按键:
除了这些以外,还有很多修饰符,在未来的学习应用中我们会慢慢遇到!以下是官方给出的案例:
<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a><!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form><!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a><!-- 只有修饰符 -->
<form v-on:submit.prevent></form><!-- 添加事件监听器时使用事件捕获模式 --><!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理-->
<div v-on:click.capture="doThis">...</div><!-- 只当在 event.target 是当前元素自身时触发处理函数 --><!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>
3.8、MVVM 模型
- Model-View-ViewMode,一种软件架构设计模式。
- 事件驱动编程方式
- 源自于 MVC 模式
将前端的视图层(View)[Html,CSS,Template],与 ViewModel[JavaScript]实现双向绑定,ViewModel 又可以通过 Ajax 和 Json 与服务端建立联系,从而从后端拿到数据,并动态修改前端视图,而不再需要频繁去修改前端的 View 的模板。
视图状态和行为都封装在 ViewModel 里,这样使得 ViewModel 可以完整的去描述 View 层。由于实现了双向绑定,又得益于 JS 的即时编译运行的动态特性,View 的内容会由 ViewModel 实时地展现,而不必再使用原生的 JS 去操作 Dom 元素去更新 View。
MVVM 的核心就是:DOM 监听与数据绑定
它是连接 view 和 model 的桥梁。它有两个方向:
一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。
二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。
这两个方向都实现的,我们称之为数据的双向绑定。
3.9、表单绑定 v-model
前面的例子只是实现了 View 绑定 ViewModel 实时更新,而在有一些用户可以操作的地方可以实现,Model 随 View 变化实时更新,例如文本框输入,下拉框,单选框,多选框等…
使用 v-model 于 ViewModel 对象实现双向绑定。
3.9.1、基本使用
<body><div id="div1"><input type="text" id="text" value="1111" v-model="message" />输入的是:{{message}}<p>性别 <input type="radio" value="男" v-model="sex" />男<input type="radio" value="女" checked v-model="sex" />女选择的是:{{sex}}</p><p>省份<select v-model="province"><option value=""></option><option value="湖北" selected>湖北</option><option value="湖南">湖南</option><option value="上海">上海</option><option value="广东">广东</option><option value="江苏">江苏</option></select>{{province}}</p></div><script src="../vue-js/vue.js"></script><script type="text/javascript">var vm = new Vue({el: '#div1',data: { message: '输入名字', sex: '男', province: '' },})</script>
</body>
注意点:
当使用了 v-model 双向绑定时,表单的一些默认值都会失效,例如 text 的 value,radio 的 checked,select 中 option 的 selected 都会被忽略 ,而是从绑定的 Vue 实例中的 data 进行取值渲染。
使用 v-model 绑定表单后,input 的输入元素会抛出事件:
- text 和 textarea 元素使用
value
property 和input
事件; - checkbox 和 radio 使用
checked
property 和change
事件; - select 字段将
value
作为 prop 并将change
作为事件。
使用类似@input
可以绑定一个输入事件,然后调用具体的回调方法。
还有一个注意点:当使用通过输入法组合文字的语言中(例如:日文、韩文、中文),在通过输入法组合文字的过程中是不会触发 v-model 刷新的!如果要处理这个过程,请使用
input
事件!
在使用下拉框进行绑定时,若 data 中输出的初始值没有匹配到任何一个可选值,就会被渲染为“未选中”的状态:
因此推荐在候选项目中加上一个空值的禁用选项:
<div id="app"><form><label for="ans">select a letter<select id="ans" v-model="answer"><!--值为空的禁用选项--><option value="" disabled>请选择</option><option>A</option><option>B</option><option>C</option><option>D</option></select></label></form>
</div>
<script src="../js/vue.js"></script>
<script>const app = new Vue({ el: '#app', data: { answer: '' } })
</script>
当 select 支持多选的时候,绑定的数据应该是一个数组!!
并且结合我们之前的v-for
、v-bind
还可以实现动态显示候选项!
3.9.2、值绑定
上面的单选框、多选框、下拉框的选项值都是我们“写死”的。而我们大多情况下,我们是希望选项值是动态绑定我们数据中以及准备好的数据,或者是更多样的写法(绑定的 value 不一定是字符串!)。
如果要使选项值动态绑定 Vue 实例中的数据,使用 v-bind、v-for 就可以实现了:
<div id="app"><h2>您的性别是: {{sexSelect.sex}}</h2><h2>您的爱好有: {{hobbies}}</h2><h2>您的职业是: {{identity}}</h2><div style="border: black solid 2px; display: inline-block"><form style="margin: 10px">请选择您的性别:<label><input type="radio" :value="sexOptions[0]" v-model="sexSelect" />男<input type="radio" :value="sexOptions[1]" v-model="sexSelect" />女</label><br />请勾选您的爱好:<label v-for="item of optionHobbies" :for="item"><inputtype="checkbox":value="item":id="item"v-model="hobbies"/>{{item}}</label><br />请选择您的职业:<label for="identity"><select id="identity" v-model="identity"><option value="" disabled>请选择</option><option v-for="option of optionIdentities" :value="option">{{option}}</option></select></label></form></div>
</div>
<script src="../js/vue.js"></script>
<script>const app = new Vue({el: '#app',data: {sexSelect: '',sexOptions: [{ sex: '男' }, { sex: '女' }],hobbies: [],optionHobbies: ['音乐', '电影', '运动', '编程', '游戏', '绘画', '阅读'],identity: '',optionIdentities: ['学生', '教育从业者', '职员', '自由职业者'],},})
</script>
代码中有很多看似花哨无用的写法(例如性别的 value 绑定使用了对象),但是其实在实际开发中某些情况下大有用处,这里只是演示一些特殊用法!
3.9.3、修饰符
好的,我们又见面了。之前我们说过 v:on 在绑定事件时,可以通过特殊的方式控制事件。我们表单的输入也可以通过修饰符来控制数据绑定的过程。
.lazy
类似于“懒加载”,默认情况下 text 的输入会触发 input 事件(排除输入法组合文字的情况),页面会实时进行数据渲染。但是 v-model 加上
.lazy
修饰符后,只有当触发了 change 事件后才会同步并渲染数据。
<h2>您的输入的内容是: {{inputContent}}</h2>
<div><form>请输入:<label for="textInput"><input type="text" id="textInput" v-model.lazy="inputContent" /></label></form>
</div>
当输入窗口失焦、或者我们按下回车。就会触发 change 事件,然后才会进行数据同步并渲染。略微减轻了浏览器的压力。
.number
在表单输入中,我们通常使用 input 元素并将 type 设置为 number,在输入时就会限制只能输入数字。但是存在一个问题就是,表单在解析数据的时候会将用户的输入转为字符创,所以我们通过 v-model 取到的数据虽然值是数字但是类型是字符串!
使用
.number
修饰符就可以轻松解决这个问题。
<div><form>请输入一个数字<label> <input type="number" v-model.number="number" /> </label></form>
</div>
<h2>{{number}}==>{{typeof number}}</h2>
会自动将你的输入转化为 number 类型。在开发中这非常有用!
.trim
不用多说了,会将输入内容的左右两端多余空格去除
代码略。
所有的修饰符都是可以串联使用的!!!
三、(补充)Vue 的生命周期
上图就是 Vue 官网给出我们的 Vue 的完整生命周期,红色框则是我们可以定义钩子函数的位置!
例如beforeMount
就会 Vue 实例被挂载前,触发执行:
挂载 mount:将 Vue 实例装载到对应的 DOM 元素的动作,我们称其为挂载。挂载时会替换所有 vue 使用 el 属性控制 dom 标签内的数据。挂载完成后,页面会一致监听数据变化,当数据变化页面也会实时刷新。
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><title>Vue Mount</title></head><body><div id="app"><span>{{year}}</span></div><script src="../js/vue.js"></script><script>const app = new Vue({el: '#app',data: { year: 2020 },beforeMount: function () {this.year++console.log(`Now is ${this.year}`)},})</script></body>
</html>
我们这里的 beforeMount,在生命周期图中处于挂载之前,所以 year 数据会先自增,然后被挂载显示到页面上!
四、组件化
一张图看懂组件化
组件化开发的步骤:
- 创建组件构造器:
Vue.extend()
(Vue2.0 后很少见) - 注册组件:
Vue.component()
- 使用组件:在 Vue 实例作用范围内使用已注册的组件。
直接使用 Vue.component() 注册组件
<div id="app"><list-component></list-component></div>
<script src="../js/vue.js"></script>
<script>// 自定义的组件Vue.component('list-component', {template: `<ul> <li v-for="item of balls">{{item}}</li> </ul>`,data() {return { balls: ['篮球', '网球', '乒乓球'] }},})const app = new Vue({el: '#app',})
</script>
演示效果:
需要注意的几点:官方给组件的定义是可复用的 Vue 实例。
所以我们的注册工作主要是将一个自定义 Vue 对象变为可复用的。
Vue.component()
需要两个参数:
- 组件名,即我们以后希望通过什么标签来使用这个组件(代码风格指南中,强烈要求组件名使用多个单词,并使用连词符连接!或者也可以使用大驼峰命名法,但是在 DOM 中(非字符串模板)使用时只有前者是有效的!)
- 组件的参数。其中的内容与我们创建的 Vue 实例对象类似,data、computed、methods 等(除 el,为根实例独有),但是他们都是服务于当前组件的 template 的,并不是通过 el 挂载到某个 DOM 元素上进行使用!所以要求有所不同,后面再说。可以使用一个 JavaScript 对象来定义组件元素
var ComponentA = {/* ... */
}
var ComponentB = {components: {'component-a': ComponentA,},//...
}
组件的使用注意事项:
**一定是在 Vue 实例的作用范围内使用,组件才是生效的!**例图中第二个位置我们使用组件,并没有生效,就因为其位置超出 Vue 实例的作用范围即<div id='app'>...</div>
4.1、全局组件与局部组件
上面我们提到了组件的有效范围,那就不得不提一下全局组件和局部组件。
首先,无论是全局组件还是局部组件都只能在 Vue 实例的作用范围内生效!!
这里就引出了我们的全局组件。注意到我们开始定义组件的时候,是直接在 js 文件的根下创建的。所以说这个组件就是一个全局组件,在所有 new 的 Vue 实例控制范围内下均可使用!并且可以被其他组件直接使用!
但是有时候,我们希望组件只能在我们特定的 Vue 实例控制范围内使用!(例如,只想 list-component 只能在 id 为 app 的 div 内使用,其他位置均无法使用。即这个组件只受限在一个 Vue 实例内使用)那就要使用我们的局部组件了。
注册局部组件:
const app = new Vue({el: '#app',components: {'app-component': {template: `<div><h3>我是app独有的组件!</h3></div>`,},},
})
我们直 接在 Vue 实例内部的components
属性下注册模板,那么这个模板的就只能在当前的 Vue 实例的作用范围内使用!
**同属于一个实例对象的组件是不能相互引用的!**如果要使用,需要结合我们下面要学习的父子组件!
4.2、父子组件
前面我们在注册局部组件时,是在 Vue 实例中 components 属性中进行注册的。那么组件内部应该也可以再注册组件。
那么两个组件的关系就变为了父子关系:
<div id="app"><parent-component></parent-component><hr /><!-- 子组件不可以单独使用,已经被编译到父组件的模板中了 --><child-component></child-component>
</div>
<script src="../js/vue.js"></script>
<script>Vue.component('parent-component', {template: `<div> <h2>SuperHeroes</h2> <child-component></child-component> </div>`,components: {'child-component': {template: `<ul> <li>钢铁侠</li> <li>雷神</li> <li>美国队长</li> </ul>`,},},})const app = new Vue({ el: '#app' })
</script>
演示效果:
==注意点:==子组件不可单独使用,当其在父组件进行注册使用时,就已经将 template 与父组件的 template 编译为一个整体了。
4.3、Template 分离写法
有没有感觉我们在写注册组件的 template 时特别费力?!下面我们来使用集中 template 分离的写法。
在写之前,需要注意一个 template 的编写规则:==template 只能有一个根元素!==即一个 template 代码的所有元素必须被包含在一个根元素内:
<!--有效template-->
<div><h1></h1><form>...</form><!--略-->
</div><!--无效template:有两个根元素!-->
<div><h1></h1>
</div>
<div><p>....</p>
</div>
这样错误的写法,会导致页面在渲染的时候丢失内容!
解决方法:就是在所有元素的最外层套上一个根标签。
下面我们正式开始学习 template 分离写法:
<script type="text/x-handlebars-template" id="xxx"></script>
<script type="text/x-handlebars-template" id="template01"><div> <h2>分离Template01</h2> <p>爽爽爽!!</p> </div>
</script>
<script>const app = new Vue({el: '#app',components: { template01: { template: '#template01' } },})
</script>
在注册组件的时候,template 属性直接使用 id 选择器,选中对应的模板代码即可,但是会报红。或者可以直接写 id, 并且不用引号包裹(请看下面的代码示例)
直接使用
<template id="xxx"></template>
<template id="template02"><div><h2>分离Template02</h2><p>冲冲冲!!</p></div></template
>
<script>const app = new Vue({el: '#app',components: {mytemplate02: {// 直接通过id引用template: template02,},},})
</script>
引用方法同样有两种:
- id 选择器
- 直接用 id 作为 template 属性值
4.4、组件复用的问题
为什么 data 必须是函数?!(直击灵魂)
如果我们 data 不使用函数,在运行的时候,控制台会显示 Vue 的警告信息:
官网也给出了为什么 Vue 需要这条规则的原因:[为什么组件的 data 必须是函数?](组件基础 — Vue.js (vuejs.org))
我们抽取组件的初心本就是提高代码的复用率,让组件化的代码更易于管理。那么我们就应该保证组件在使用时的独立性,一个组件的变化尽量不去影响其他外部组件的变化。所以在复用组件的时候,我们更希望的是每个组件维护一份数据的独立拷贝!
所以我们使用函数来定义数据,那么组件每次复用时拿到的 data 都是一份份独立的拷贝。每个组件对数据的操作不会影响到其他组件!
<div id="app"><counter></counter><hr /><counter></counter><hr /><counter></counter>
</div>
<script src="../js/vue.js"></script>
<script>Vue.component('counter', {template: `<div> <h4>current number:{{number}}</h4> <button @click="increment">+1</button> </div>`,data() {return { number: 0 }},methods: {increment() {this.number++},},})const app = new Vue({ el: '#app' })
</script>
示例效果:
4.5、父子组件通信之 props
现在我们了解到了在组件中数据 data 是以函数的方式出现。但是 data 只能是在我们组件内部使用,一般我们的组件是需要从外面拿数据进来显示的,那么就涉及到父子组件的通信问题。
说一个特别常见的场景:我们现在有一个大的组件,其作用是显示一个文章列表。在这组件内部,我们要通过复用一个小的组件来显示每篇文章的标题、与简述内容。
我们不可能针对每一篇文章去写一个组件,然后将数据在 data 中写死。我们更希望通过组件复用来完成这个任务。所以最好的实现就是我们的文章数据都存放在外部这个大组件中,然后通过特殊方式依次传递给内部的小组件进行显示。
那么就要说一说组件中一个重要的属性了props
,在这个属性中可以存放一些组件 template 中需要使用的属性名。当外部为这个属性传值以后,template 中使用了属性的位置都会被替换为传入的属性值!在使用属性的时候可以像使用 data 中的数据一样利用 mustache 语法。例如:
我们可以先将属性名就想象为真实数据直接在 template 中使用,当父组件向这两个属性传值的时候,template 中使用属性名的位置就会直接替换为对应的真实值!
下面我们演示父组件如何对子组进行传值:
<div id="app"><blog-componentv-for="blog in blogs":key="blog.id":title="blog.title":content="blog.content"></blog-component>
</div>
<script src="../js/vue.js"></script>
<template id="blog-template"><div style="margin: 10px; border: black solid 2px; display: inline-block"><h3>{{title}}</h3><hr /><p>{{content}}</p></div></template
>
<script>const app = new Vue({el: '#app',data: {blogs: [{id: 1,title: '菜鸟的成长之路',content: '讲述一个菜鸟在互联网艰辛耕作的历程...',},{id: 2,title: '大牛带你学Vue',content: '大厂leader手把手教你学Vue,从入门到精通...',},{id: 3,title: '数据库调优的伤心泪',content: '资深数据库开发大佬让你从数据库小白进阶调优能手...',},],},components: {'blog-component': {template: '#blog-template',props: ['title', 'content'],},},})
</script>
数据在我们的父组件中,我们在调用子组件时,通过v:bind:xxx="yy.zz"
将属性值绑定到了组件的属性上。实现了父组件向子组件传递数据,子组件动态绑定外部数据。
实现效果:
刚才也说了在进行传值的时候,一个属性是可接收任何值的,那我们把上面的例子修改一下:
<div id="app"><blog-component v-for="blog in blogs" :key="blog.id" :blog="blog"></blog-component>
</div>
<script src="../js/vue.js"></script>
<template id="blog-template"><div style="margin: 10px; border: black solid 2px; display: inline-block"><h3>{{blog.title}}</h3><hr /><p>{{blog.content}}</p></div>
</template>
<script>const app = new Vue({el: '#app',data: {blogs: [// 略,同上],},components: {'blog-component': {template: '#blog-template',props: ['blog'],},},})
</script>
可以看到我们直接给 blog 属性传了一个对象,在 template 内部,解析这个对象的具体内容。
上面我们在声明props
使用的字符串数组,对于组件来说这些属性就只是一个单纯的数组,具体拿到什么数据我们压根都不知道,这导致别人来用我们的组件的时候,不知道如何传值。所以官方更推荐我们对属性进行具体的描述,例如加上属性值类型:
更加高阶的操作还包括,我们对属性加上一些验证选项:[Props 验证](Prop — Vue.js (vuejs.org))
这里只截取了小部分,更详细的用法请参考官方文档。
单向数据流
简单来说就是,当父子组件的 props 存在依赖关系时,父组件的 props 会影响子组件的 props 变化。但是反过来则不行,是为了防止内部数据的变化影响了父级组件的转态,导致分析难度增加。
并且在父组件更新后,所有子组件的 props 都会刷新,所以不推荐在内部组件修改 props!
首先我们来验证一下这个说法:(肯定是对的,但是眼见为实)
<div id="app"><h2 style="display: inline-block">父组件中的值为:{{pageIndex}}</h2><button @click="pageIndex--" :disabled="pageIndex === 1">-1</button><button @click="pageIndex++">+1</button><hr /><child-component :page="pageIndex"></child-component>
</div>
<script src="../js/vue.js"></script>
<template id="overview-template"><div>当前在<button @click="page--" :disabled="page === 1">上一页</button>第<span>{{page}}</span>页<button @click="page++">下一页</button></div>
</template>
<script>Vue.component('child-component', {template: '#overview-template',props: {page: Number,},})const app = new Vue({el: '#app',data: {pageIndex: 1,},})
</script>
这里 Vue 实例可以视为父组件,而我们注册的组件视为子组件。
下图中,我们尝试通过按钮来修改父组件的数据,并且成功引起了子组件中 props 的变化:
可是当我们试图通过子组件修改 props 从而影响父级组件时:
发现父组件并没有受到影响,并且控制台中出现了 Vue 给出的警告信息:
原文:Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop’s value. Prop being mutated: “page”
告诉我们**避免直接修改一个 props 的值,因为它会在父组件重新渲染的时候被覆盖掉!**下图就演示一下这个覆盖过程:
并且 Vue 给出了建议:
而是使用一个基于 prop‘s value 的 data 或者计算属性。
官方的文档上也给出了这个解决方案的实现:
使用 data:
使用计算属性:
那么我们基于这个解决方案来改一改我们的代码:
<template id="overview-template"><div>当前在<button @click="pageNum--" :disabled="page === 1">上一页</button>第<span>{{pageNum}}</span>页 <button @click="pageNum++">下一页</button></div></template
>
<script>Vue.component('child-component', {template: '#overview-template',props: {page: Number,},// 使用基于prop的datadata() {return {pageNum: this.page,}},})const app = new Vue({el: '#app',data: {pageIndex: 1,},})
</script>
使用了 data 作为中间值隔断与父组件中数据的联系,这样父子组件中的对数据的修改阻隔了。
所以如果你希望父组件的修改能影响子组件,就请限制子组件内对 prop 进行修改!
有一个容易忽略的问题是:当 prop 传递的数据是对象或者数组的时候,由于 JavaScript 中传递对象和数组是传递的引用,那么子组件中对其的修改,将会影响到父组件中数据。但是不会引起 Vue 警告?!
非 Props 的 Attribute
之前我们都通过 v-bind 向组件 props 进行传值,大多都是用来进行数据传递。但是有些属性与数据相关性不大,或者说我们不需要对它做过多的逻辑处理,我们可以直接以将属性键值对写在组件标签上,会自动添加到组件的根元素上:
我们希望对每个组件都使用不同的背景颜色。通过 props 传值过程太复杂,我们完全可以这样做:
<div id="app"><blog-component style="background: orange"></blog-component><blog-component style="background: pink"></blog-component><blog-component style="background: skyblue"></blog-component>
</div>
<script src="../js/vue.js"></script>
<template id="blog-template"><div style="display: inline-block; margin: 10px"><h3>我是子组件</h3></div></template
>
<script>const app = new Vue({el: '#app',components: { 'blog-component': { template: '#blog-template' } },})
</script>
属性(Attribute)的替换与合并
通过上面的例子,你应该能看出来:我们写在组件标签上的style
attribute 和组件根元素上的 style 进行了合并。除此以外class
attribute 也会会进行属性合并。
但是有些特殊的属性,则不会进行合并,而是将根元素中的属性直接替换!例如type
!(组件标签上的 type=’text‘ 会替换根元素的 type=’date’)
4.6、父子组建通信之$emit
上面我们学习了父组件通过 props 向子组件传递信息,但是由于单向数据流的控制,禁止我们通过对子组件的修改来影响父组件。可是我们又难免由于一些场景,需要子组件向父组件传递信息。
例如导航栏和展示框,往往同时隶属于同一个父组件,当我们点击导航栏的时候,我们就需要利用外部的父组件来间接控制我们的展示框。那么中间就涉及导航栏向父组件传递数据的需求!(如图)
为了解决这需求,我们可以通过子组件向父组件发送事件,并携带参数来完成子组件与父组件的通信。当子组件触发了某一个监听事件后,在事件的回调函数中可以使用this.$emit
来发送一个自定义的事件,父组件只需要在外部使用 v-on 监听并处理这个事件就可以啦!
我们先用一个小 demo 来演示一下:通过子组件内的按钮,来修改父组件的中 data 值。呈代码上来!
<div id="app"><h2>当前父组件中number: {{number}}</h2><hr /><child-component @number-incr="add" @number-desc="sub"></child-component>
</div>
<script src="../js/vue.js"></script>
<template id="button-template"><div><button @click="decrement">-1</button><button @click="increment">+1</button></div></template
>
<script>const childComponent = {template: '#button-template',methods: {increment() {this.$emit('number-incr')},decrement() {this.$emit('number-desc')},},}const app = new Vue({el: '#app',data: {number: 0,},methods: {add() {this.number++},sub() {this.number--},},components: {'child-component': childComponent,},})
</script>
效果演示:
发出携带参数的事件
但是这个 demo 并不能完全实现我们的需求,我们需要通过事件向父组件传值。官方当然也想到了这个问题,下面我们来一更具体的来演示一下:
<div id="app"><nav-componentstyle="text-align: center"@to-page="currentPage = $event"></nav-component><hr /><div style="border: black solid 2px" v-if="!(currentPage === '')"><h2 style="text-align: center">您当前的位置: {{currentPage}}</h2></div>
</div>
<script src="../js/vue.js"></script>
<template id="button-template"><div><button @click="$emit('to-page', '主页')">主页</button><button @click="$emit('to-page', '热门')">热门</button><button @click="$emit('to-page', '排行')">排行</button><button @click="$emit('to-page', '我的')">我的</button></div></template
>
<script>const navComponent = {template: '#button-template',}const app = new Vue({el: '#app',data: { currentPage: '' },components: { 'nav-component': navComponent },})
</script>
这里注意下,代码中使用了一些简化写法:
- 子组件中直接使用
$emit(xx, yy)
抛出带有数据的时间。其中 xx 为事件名,yy 为携带的参数。 - 在父组件中对子组件自定义事件进行监听的时候,我们使用
$event
直接取到了数据。使用$event
直接取数据,个人感觉只适用于单个事件参数(因为我并不知道如何使用$event 取出其他参数…)
在事件参数 大于 1 或者需要复杂处理时,你可以将监听事件的回调操作使用函数完成:
<div id="app"><nav-componentstyle="text-align: center"@to-page="jumpToPage"></nav-component><hr /><div style="border: black solid 2px" v-if="!(currentPage === '')"><h2 style="text-align: center">您当前的位置: {{currentPage}}</h2></div>
</div>
<script src="../js/vue.js"></script>
<template id="button-template"><div><button @click="$emit('to-page', '主页', 1)">主页</button><button @click="$emit('to-page', '热门', 2)">热门</button><button @click="$emit('to-page', '排行', 3)">排行</button><button @click="$emit('to-page', '我的', 4)">我的</button></div></template
>
<script>const navComponent = {template: '#button-template',}const app = new Vue({el: '#app',data: {currentPage: '',},methods: {// 函数接收处理多个事件参数jumpToPage(pageName, pageIndex) {this.currentPage = pageIndex + '. ' + pageName},},components: { 'nav-component': navComponent },})
</script>
演示效果:
在自定义组件上使用
v-model
先利用官方的例子来演示一下,
默认情况下,例如我们对 input text 使用 v-model:
<!--使用v-model-->
<input type="text" v-model="inputContent" /><!--等同于下面的写法-->
<inputtype="text":value="inputContent"@input="inputContent = $event.target.value"
/>
但是当你自定义了一个输入组件时,我要如何让其中的输入值绑定到父组件的 data 上呢?直接使用 v-model?!(显然走不通,你都不能访问到父组件的 data,只能父组件向 props 传值。)而父组件又不能直接访问到组件内部的 input 元素的 value 属性。
我们不妨转变一个思路:在子组件内部我们能监控到 input 元素的 value 变化,然后我们可以再通过自定义事件将变化后的 value 作为事件参数携带出来!!
<div id="app"><input-component v-model="inputContent"></input-component><h3>{{inputContent}}</h3>
</div>
<script src="../js/vue.js"></script>
<template id="input-template"><div><inputtype="text":value="inputValue"@input="$emit('input', $event.target.value)"style="border-radius: 5px"/></div
></template>
<script>const inputComponent = {template: '#input-template',props: { inputValue: String },}const app = new Vue({el: '#app',data: { inputContent: '' },components: { 'input-component': inputComponent },})
</script>
- 首先,我们在组件内部将 input 元素的 value 绑定到一个 prop 上。
- 然后,在组件内部的 input 元素触发 input 事件时,同时组件向外抛出携带 input 元素的新值一个自定义事件。(==注意这个自定义事件名一定要是
input
,因为 v-model 的默认通过 input 事件来进行监听数据改变的!!==否则步骤 3 你得改为组合使用v-bind
和@xxx="yy = $event"
,xxx 为组件内抛出的自定义事件名,yyy 为 data 中你绑定的数据名,$event 则是事件携带的值,即最新的输入值) - 在组件上使用
v-model
绑定 data 中的数据。
效果演示:
自定义 checkbox 等需要使用v-model
请参考官方文档:[自定义组件使用 v-model](自定义事件 — Vue.js (vuejs.org))
关于事件命名的问题:官方是更推荐我们使用短横线命名法。
props 在命名时,采用驼峰命名,由于在 HTML 中是不区分大小写的,在进行绑定的时候会自动转化为“连字符命名”。
但是在自定义事件中,是没有这个效果的,需要保证你发出的事件名和外部监听的事件名一模一样!否则不会生效:
以上是关于子组件事件监听的基础使用,后续会遇到更高级的应用。
4.7、父子组件访问
除了父子组件的通信之外,Vue 还提供了父子组件相互访问的机制。分别通过$children
、$refs
、$parent
、$root
我们将页面抽为组件化是希望各个组件独立性、复用性更强。所以说我们通过使用这些方式访问父/子组件的机会很少。但是我们还是来看一些他们的使用!
$children
:访问子组件
<div id="app"><child-component></child-component> <child-component></child-component><child-component></child-component> <button @click="getChildren">按钮</button>
</div>
<script src="../js/vue.js"></script>
<template id="child-template"><div><h2>我是子组件~</h2></div></template
>
<script>const childComponent = { template: `#child-template` }Vue.component('child-component', childComponent)const app = new Vue({el: '#app',methods: {getChildren() {console.log('所有子组件:', this.$children)console.log('第一个子组件:', this.$children[0])},},})
</script>
通过这种方式(下标)依次访问子组件特别呆板。来看看另一种:
$refs
,有一个特殊要求:需要组件上添加ref
属性,并且属性值是唯一的!
<div id="app"><child-component ref="child1"></child-component><child-component ref="child2"></child-component><child-component ref="child3"></child-component><button @click="getChildren">按钮</button>
</div>
<script src="../js/vue.js"></script>
<template id="child-template"><div><h2>我是子组件~</h2></div></template
>
<script>const childComponent = {template: `#child-template`,}Vue.component('child-component', childComponent)const app = new Vue({el: '#app',methods: {getChildren() {console.log('所有子组件:', this.$refs)console.log('第一个子组件:', this.$refs.child1)},},})
</script>
在使用$refs 访问时,我们得到一个 object,里面有若干属性,每一条属性代表一个子组件,属性名对应子组件上的ref
值,属性值则为一个 VueComponent 对象即子组件本身。
这样我们访问某一个组件的时候,就不用呆呆地用下标了。而是直接使用组件的 ref 就能找到了!
$parent
访问父组件
<div id="app"><child-component></child-component></div>
<script src="../js/vue.js"></script>
<template id="child-template"><div><h2>我是子组件~</h2><button @click="getParent">按钮</button></div></template
>
<script>const childComponent = {template: `#child-template`,methods: {getParent() {console.log(this.$parent)},},}Vue.component('child-component', childComponent)const app = new Vue({ el: '#app' })
</script>
演示效果:
因为我们的组件是在 Vue 实例下的挂载块下面使用的,所以我们使用$parent
就取到了其父级组件也即根实例!为了与下面的$root
区分,建议再嵌套一层进行测试。。这里就不做过多演示了
$root
访问根组件
我就不过多演示了,效果与前面的一样(只是巧合~)。
我们通过以上方式获取到父/子组件后是可以访问其他内容的,例如 data 等,但是并不推荐这样做。因为这样的代码出现在组件化中会增加组件之间的耦合,有违我们进行组件化的初心。
五、(补充)计算属性
5.1、基本使用
当我们经常需要用到的一些属性,是需要在已有属性上做一些修改得到的属性,或者说是不经常改变的属性,我们可以使用属性,如果在视图中的模板中放入过多的逻辑,会让代码难以维护,可以尝试利用计算属性,来创建一个与已有属性相关的属性。
例如当我们需要频繁使用到 message 的倒序串时
不使用计算属性:
<div id="app">message<h3>{{message}}</h3>Reverse message<h3>{{message.split('').reverse().join('')}}</h3>
</div>
<script src="../vue-js/vue.js"></script>
<script type="text/javascript">var vm = new Vue({el: '#app',data() {return { message: 'Hello' }},})
</script>
我们没调用一次就需要在模板中添加相应的逻辑,大量的使用会提高代码维护的难度。
使用计算属性:
<body><div id="app">message<h3>{{message}}</h3><!-- 直接使用计算属性 -->reverse message<h3>{{reverseMsg}}</h3></div><script src="../vue-js/vue.js"></script><script type="text/javascript">var vm = new Vue({el: '#app',data() {return {message: 'Hello',}},// 使用计算属性computed: {reverseMsg: function () {return this.message.split('').reverse().join('')},},})</script>
</body>
使用了 Vue 实例对象的
computed
属性,并声明了 reverseMsg 属性,并为其增加了一个函数用做 reverseMsg 属性的 getter 函数,且这个属性依赖于 message,一旦 message 发生变化,相应的 reverseMsg 也会发生变化。
5.2、复杂使用
也许你会认为当计算属性的复用价值不大的时候,直接在 Mustache 表达式中直接写数据处理逻辑比较简单。但是当你需要结合 data 中的数据做统计处理,然后将结果渲染到页面上的时候,计算属性的高效性就不言而喻!
<div id="app"><h2>当前购物车总价格:{{total.price}}</h2></div>
<script src="../js/vue.js"></script>
<script>const app = new Vue({el: '#app',data: {goods: [{ name: 'iPhone 12', price: 6199 },{ name: '码出高效', price: 59 },{ name: 'FL 980', price: 699 },],},computed: {total: function () {return this.goods.reduce((previousValue, currentValue, currentIndex) => {return {name: 'total',price: previousValue.price + currentValue.price,}})},},})
</script>
5.3、计算属性 VS. Methods
说到这,你可能会疑问为什么不使用methods
属性,视图直接调用方法就可以,当然最终效果是一样的。
那就要来说说计算属性的另一个好处:计算属性是基于它们的响应式依赖进行缓存的
<div id="app">message<h3>{{message}}</h3><!-- 使用Method -->reverse message<h3>{{reverseMsg2()}}</h3>
</div>
<script src="../vue-js/vue.js"></script>
<script type="text/javascript">var vm = new Vue({el: '#app',data() {return { message: 'Hello' }},methods: {reverseMsg2: function () {return this.message.split('').reverse().join('')},},})
</script>
但是两者之间有一个最大的不同就是,计算属性是基于它们的响应式依赖进行缓存的,而是同方法则是每次刷新页面都需要调用方法重新计算。而所谓响应式依赖进行缓存,意思就是当计算属性所依赖的属性不发生变化时,可以直接从缓存中直接取出值,而不需要执行函数,唯有当依赖属性变化后,才会执行函数重新求值。
而使用 method,在每次触发页面重新渲染时总是需要重新调用方法计算!
什么时候会触发页面重新渲染呢?任何数据变化都会触发页面的重新渲染。不妨来看一个例子:
<div id="app"><h2>{{message}}</h2><h2>计算属性结果:{{reversedMessage}}</h2><h2>执行方法的结果:{{getReversedMessage()}}</h2><h2>无关数据:{{name}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>const app = new Vue({el: '#app',data: {message: 'Hello World!',name: 'sakura',},computed: {reversedMessage: function () {console.log('执行了一次属性计算!')return this.message.split('').reverse().join('')},},methods: {getReversedMessage: function () {console.log('执行了一次反转方法!')return this.message.split('').reverse().join('')},},})
</script>
因为计算属性是响应式依赖的,也就是说我们修改了 message,控制台就会同时输出“执行了一次属性计算!”和“执行了一次反转方法!”。
但是如果我们修改了无关数据 name,就会触发页面的重新渲染:
- 由于计算属性所依赖的数据没有变化,所以是不会引起属性计算的方法调用的,而是直接从缓存中拿上一次的数据。
- 但是 methods 就没这么“聪明”了,它会被重新调用。
可以试想一下,如果我们计算属性的过程是一个十分耗时的操作时,当我们使用 methods 来实现,就算无关数据的变化也会重复执行这个耗时的操作,造成大量的资源浪费!
当然使用方法也可以控制数据不存在缓存,在控制不应该出现缓存的数据时,请使用 methods!
5.4、计算属性 VS. 侦听属性
Vue 提供了一种更通用的方式来观察和响应 Vue 实例上的数据变动:侦听属性。会通过侦听数据的变动,来执行对应的回调函数!但是通常情况下,更推荐使用计算属性来代替侦听属性!
同样来看一个案例:
使用侦听属性:
<div id="app"><h2>{{fullName}}</h2></div>
<script src="../js/vue.js"></script>
<script>const app = new Vue({el: '#app',data: {firstName: 'Tony',lastName: 'Stark',fullName: 'Tony Stark',},watch: {// 当lastName变化后,会触发此回调函数lastName: function (newLastName) {console.log('lastName was changed!')this.fullName = this.firstName + ' ' + newLastName}, // 当firstName变化后,会触发此回调函数firstName: function (newFirstName) {console.log('firstName was changed!')this.fullName = newFirstName + ' ' + this.lastName()},},})
</script>
使用 watch
来监听两个属性的变化,通过不同的回调函数来调整数据。显得十分繁琐!
使用计算属性:
<div id="app2"><h2>{{fullName}}</h2></div>
<script src="../js/vue.js"></script>
<script>const app2 = new Vue({el: '#app2',data: { firstName: 'Tony', lastName: 'Stark' },computed: {fullName: function () {return this.firstName + ' ' + this.lastName},},})
</script>
在保证功能一致的情况下,大大缩减了代码量并简化了逻辑。
六、插槽 slot
有没有发现我们在使用组件时,有一个很大问题?!我们在使用我们注册的组件的时候,就仅仅是一对对标签。标签内部什么都没有。组件在复用时拉出来除了数据外都“长得”一模一样!
有没有想过通过在组件标签内加上一些自定义的元素,让即使同一个组件也可以有很多模样!?Vue 的**插槽(Slot)**机制,就帮助我们实现了这个需求。
什么是插槽?!
从名字来看其实以及能猜到它的用处了。比如我们的电脑就有很多各种插槽(USB、HDMI、耳机孔),这些插槽设计就是为了用户可能会连接各种设备到 PC 上来进行定制化。那 Vue 的插槽的设计也是相同的思想:
在组件中预留位置(插槽),供给用户进行个性化使用。
6.1、基本使用
<div id="app"><slot-cpn> <button>我是一个按钮</button> </slot-cpn><slot-cpn> <input type="text" placeholder="我是一个输入框" /> </slot-cpn><slot-cpn> <a href="#">我是一个超链接</a> </slot-cpn> <slot-cpn></slot-cpn>
</div>
<script src="../js/vue.js"></script>
<template id="slot-template"><div style="border: black solid 2px; margin: 10px; display: inline-block"><div style="margin: 5px"><h3>我是一个子组件</h3><slot>默认内容</slot></div></div>
</template>
<script>Vue.component('slot-cpn', { template: `#slot-template` }) const app = new Vue({ el: '#app' })
</script>
在组件模板中使用<slot></slot>
为组件创建插槽。在使用组件时,组件标签内包裹的元素会被替换到插槽的位置!(slot 标签在编译后代码中不可见!)
使用效果:
插槽内除了写这些基础的 html 标签,还可以使用其他组件!?!!
<slot-cpn><login-cpn></login-cpn>
</slot-cpn>
当组件内没有预留插槽的话,组件标签内所有内容都会被抛弃!
但是即使只有一个插槽,也会保留所有组件标签内的所有元素!!当 slot 设置了后备内容(即默认内容),若外部不传递内容,将会对默认内容进行渲染。这是一个好选择,可以减少因为缺少内容导致错误显示的尴尬!!
6.2、编译作用域
当你想在插槽中使用组件内的数据时,例如:
<user-cpn url="xxxx"> 当前登录的用户是:{{user.name}}</user-cpn>
你需要了解,插槽可以与组件内其他位置一视同仁,他们可以访问的范围相同。(即作用域是相同的)但是无法访问user-cpn
的作用域,例如其中的url
属性是无法访问到的,因为插槽的内容是传给 user-cpn 组件的,而非在其内部定义的!
<div id="app"><slot-cpn> <h4>当前登录的用户是{{user.name}}</h4> </slot-cpn>
</div>
<script src="../js/vue.js"></script>
<template id="slot-template"><div style="border: black solid 2px; margin: 10px; display: inline-block"><h3>我是一个子组件</h3><slot></slot></div
></template>
<script>Vue.component('slot-cpn', {template: `#slot-template`,data() {return {user: {uid: 123,name: 'admin',},}},})const app = new Vue({el: '#app',data: { user: { uid: 170312, name: 'sakura' } },})
</script>
演示效果表明,slot 是可以访问到组件内部数据值的!
官方给出了一句规则:(初次读起来有些生涩,后续对其进行详细的说明)
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
在理解这句话之前,我们通过上面这个例子来学习一下作用域:
上述例子的结果,就说明了一个问题:h4
元素即使是写在<slot-cpn>
标签内,但是在获取数据进行渲染的时候,依然是使用 Vue 实例的数据。而非组件内的数据!
因为在进行编译渲染数据时,所谓的组件标签<slot-cpn>
只是一个普普通通的标签。所以通通归由 Vue 实例进行管理,即 Vue 实例挂载的<div>
就是 Vue 实例的作用域!这就是父级模板在父级作用域内编译!
而我们的组件呢?它也有自己的作用域,而它的数据仅在<template>
内的代码有效!这也就是为什么我要想组件内部能拿到父级的数据需要 props,而不能直接用 data。当作为组件使用的时候,它会先在其作用域下进行编译后,然后替换到指定位置(即 使用了组件标签的位置。)这就是子模板在子作用域中编译。
由于我们希望嵌入到插槽的内容是在写在父级作用内的,并不是在组件内部,所以理所当然只能用父级组件的数据。
那么我们有没有办法使用组件内部的数据呢?!答案是可以,但是要借助后面要学习的作用域插槽
6.3、具名插槽
首先提醒一下:自 Vue 2.6.0 开始弃用了 slot 作为属性写法。
即<cpn slot="xxx"></cpn>
以及被废弃了!取而代之的是v-slot
对 slot 进行绑定
为什么我们需要具名插槽?!
为了保证我们组件定制化更高,我们往往会在组件中设置多个插槽。这样在定制的时候灵活性就更高!组件的利用率也更高。例如:
两张图,我们完全可以视为同一个插槽,不过是使用了三个插槽,然后三个插槽上放置了不同的组件。
现在问题是我们如何告诉 Vue,我们的要使用那个插槽呢?!
<div id="app"><slot-cpn> <button>返回</button> </slot-cpn>
</div>
<script src="../js/vue.js"></script>
<template id="slot-template"><div style="border: black solid 2px; margin: 10px; display: inline-block"><slot></slot><slot></slot><slot></slot></div>
</template>
结果是:
具名插槽的使用
所以我们要预先给每个插槽取好名字(使用name
属性, 默认名为“default”)然后在插入内容的时候,也指定插槽(使用v-slot:name
进行指定!)。
在使用的时候要注意一下:我们要想具名插槽传入的内容必须使用<template>
包裹,通过在 template 标签上使用v-slot:xx
对插槽进行指定!
所有写在没有 v-slot 属性的 template下的内容,或者其他位置的内容都会被渲染到**默认插槽(没有使用 name 取名的)**中。
**当只有一个默认插槽时,v-slot 可以直接写在组件标签内!!**当出现了多个插槽时,必须使用完整的 template 写法来指定插槽!
<div id="app"><slot-cpn><template v-slot:left> <button>菜单</button> </template><template v-slot:center><inputtype="text"placeholder="请输入搜索内容..."style="border-radius: 5px"/></template><template v-slot:right> <button>搜索</button> </template> 默认内容</slot-cpn>
</div>
<script src="../js/vue.js"></script>
<template id="slot-template"><div style="border: black solid 2px; margin: 10px; display: inline-block"><slot name="left"></slot> <slot name="center"></slot><slot name="right"></slot> <slot></slot></div>
</template>
<script>Vue.component('slot-cpn', {template: `#slot-template`,})const app = new Vue({ el: '#app' })
</script>
与 v-bind、v-on 一样,v-slot 也有缩写:
#
,在选择默认插槽时请使用:#default
!!不允许不带参数
除此以外,自 2.6.0 开始,支持使用动态插槽名:(具体使用请参考[指令动态参数](模板语法 — Vue.js (vuejs.org)))
<template v:slot:[dynamicSlotName]></template>
6.4、作用域插槽
这节内容的学习之前,请确保以及掌握了编译作用域相关知识。
使用案例引入作用域插槽的作用:
现在我们有一个组件<current-user>
,其模板内容是:
<span> <slot>{{ user.lastName }}</slot></span>
模板中 slot 的后备内容使用的是组件内部的数据user.lastName
。我们现在再使用模板的时候,我们希望改变一下,但是还是使用组件内部的数据,改换为user.firstName
,于是我们这样写:
<current-user> {{ user.firstName }}</current-user>
根据编译作用域的规则,我们知道组件标签内使用的 user 和组件模板内的使用 user 分别处于父子作用域,而由于他们是单独编译的。所以我们最终看到的,并不是预期效果!
那么有没有办法**使得组件标签内访问到组件内部的数据呢?**那么现在就是作用域插槽大显身手的时间了!
我们可以将组件内部的数据绑定(v-bind)到插槽上的一个属性上,我们称其为插槽Prop
。
我们在父级使用<v-slot:xx>
时,可以带上一个值,作为对应插槽的插槽Prop
的名字。然后在<template>
内用这个名字访问绑定在组件 slot 上的数据了。
例如我们对上面的代码进行调整:
<!--current-user组件模板-->
<span><!--将data中的user绑定到插槽上,我们的插槽Prop中就多了一个名为user数据--><slot :user="user">{{ user.lastName }}</slot>
</span>
<!--父级作用域中使用(#default 表示选择默认插槽), 并为此插槽的插槽Prop取名slotProps-->
<template #default="slotProps"><!--直接使用插槽Props的名字,取出对应的数据进行使用-->{{slotProps.user.firstName}}
</template>
我们现在用我们的实际代码来测试一下:
不使用作用域插槽:
<div id="app"><current-user><template #default> <span>{{user.firstName}}</span> </template></current-user><br /><hr /><current-user></current-user>
</div>
<script src="../js/vue.js"></script>
<template id="user-template"><div style="border: black solid 2px; margin: 10px; display: inline-block">当前用户为:<slot>{{user.lastName}}</slot></div></template
>
<script>Vue.component('current-user', {template: `#user-template`,data() {return {user: {uid: 123,firstName: 'Leopold',lastName: 'Fitz',},}},})const app = new Vue({el: '#app',data: { user: { uid: 170312, firstName: 'Tony', lastName: 'Stark' } },})
</script>
显然,组件内和我们写在父级作用域内的 user 并不是同一个!分别指向了各自所在编译作用域内的 user!
下面我们加上作用域插槽:
<div id="app"><current-user><!--选择default插槽,并为其插槽Prop取名为slotProp--><template #default="slotProp"><!--通过’slotProp‘ 访问插槽上的属性及其绑定的数据--><span>{{slotProp.user.firstName}}</span></template></current-user><br /><hr /><current-user></current-user>
</div>
<script src="../js/vue.js"></script>
<template id="user-template"><div style="border: black solid 2px; margin: 10px; display: inline-block"><!--将user绑定到slot的user属性上,作为一个插槽Prop-->当前用户为:<slot :user="user">{{user.lastName}}</slot></div>
</template>
效果展示:
关于插槽 Prop 官方给出了其作用原理:[解构插槽 Prop](插槽 — Vue.js (vuejs.org))
>> 请转到学习模块开发与Webpack >>
九、Vue-CLI
9.1、CLI? what? why?
前面我们练手的代码,基本上没有结构可言。而当你准备开发一个项目时,项目的结构与配置将会是使你头疼的关键所在。(上面手动配 Webpack,试问多少人能在写配置的时候做到面面俱到,并且长期坚持?!)所以我们急需一个便捷,而又省心的东西来辅助我们完成项目基础内容(项目结构、基础配置)的构建。
你可能需要的就是CLI(Command Line Interface,命令行界面,俗称脚手架)。
而 Vue-CLI 是众多脚手架中的一个,它可以帮助我们快速地构建 Vue 的开发环境,并为我们自动生成必须的 webpack 配置!
9.2、Vue-CLI 使用
环境要求:
Vue CLI 4.x 需要 Node.js v8.9 或更高版本 (推荐 v10 以上)。
全局安装:
npm install -g @vue/cli
查看版本:
vue -V@vue/cli 4.5.13
版本升级:
npm update -g @vue/cli
目前我们已经很少使用 vue-cli2 来创建项目,直到此笔记撰写时,vue-cli 版本已经更新到 4.x 了!从 vue-cli 3.x 开始我们创建项目使用:
vue create 项目名
并且提供了图像化界面:
vue ui
创建项目时,默认会 Preset(预配置) Babel、ESLint ,你也可以手动进行管理。
手动配置后当你选择了为后续项目保存此预设,此次配置的内容会以文件形式存储到用户目录下,文件名为.vuerc
。
旧版本 vue-cli 2.0 使用
如果想在高版本下还想使用旧版本的 vue-cli,需要额外安装@vue/cli-init
npm install -g @vue/cli-init
初始化项目:
vue init webpack 项目名
关于旧版本(Vue CLI 2.0)中项目初始化选项:
runtimeonly
与runtime-compiler
区别
在旧版本完成项目的初始创建,中间有一个选项让人琢磨不透:
脚手架中对两者的描述分别是:
- Runtime + Compiler: 推荐大多数用户使用
- Runtime-only: 比前者要小 6kb,但是 template 只允许写在
.vue
文件中,取而代之的是一个 render 函数!
一头雾水,说了的啥?!
通过查阅一些资料,发现 GitHub 上的一个 Vue 源码分析项目的一篇 Issue 逐渐揭开了答案:
Runtime Compiler
跟 Runtime Only
的区别在于: Runtime Compiler
调用 $mount
的时候需要通过 compileToFunctions
方法编译成 Runtime Only
需要的 render
方法。其实最终还是调用了 Runtime Only
的 $mount
方法。
来自Runtime Only VS Runtime+Compiler · Issue #2 · jd-smart-fe/vue-analysis (github.com)
结合上面脚手架的说明,我们知道 render 函数与 template 肯定有千丝万缕的关系。而这个回答让我们知道我们在使用 Runtime Compiler 时,使用具体的方法将我们所写的 template 编译成了对应的 render 函数?!
百听不如一见,我们分别使用两个选项来创建项目,来看看里面的一些区别:两者在创建完成后,唯一的区别就在 main.js 中:
果然在使用runtime + compiler
时会多将 template 编译为runtime-only
中 render 函数的步骤。这也是为什么 runtime-only 更小的原因,并且性能更高(免去了编译的步骤~)。而这个过程的完整版我们称其为模板编译。
9.3、模板编译与渲染函数(提高)
如何将一个 Template 转换为像普通 Html 标签显示到页面上的?!这就是我们要学习的模板编译与渲染函数。
这里找到了一张 Vue2 的模板渲染全过程图:
结合这张图和我们找到的答案,你可以描述为以下过程:
-
当你 new Vue 实例,并使用
$mount
函数进行挂载后,会调用compileToFunctions()
方法。 -
在执行 compileToFunctions()函数时,其核心应该是
compile()
函数。在项目 lib 中应该能找到这个(vue/src/compiler/)文件夹,然后里的 to-functions.js 就能看到这个 compileToFunctions()函数的代码。
应该一眼就能看到那个最复杂但是一笔带过的:
compile(template, options)
。毕竟我们只是学习,我们就不展开深入了。感兴趣可以抽空好好看看~。 -
在执行 compile 函数时,会对 template 进行解析,将其先转换为
AST
(Abstract Syntax Tree,抽象语法树)。 -
然后使用
optimise()
对静态内容进行优化 -
然后使用
generator()
函数生成render()
函数然鹅,我们说了这么久的
render()
函数,还不知道它是个啥呢?!这是官方 API 中给出的解答:
核心:替换字符串模板(即 template)函数返回值是一个 VNode
上面那张图中还有一个VDOM
,这就是我们马上要见面的Virtual DOM
即虚拟 DOM。
建议先阅读官方文档学习:渲染函数 & JSX — Vue.js (vuejs.org)
DOM 树,各位应该都熟悉。可是我们来手动更新 DOM 树是很大的难题,然后我们将修改 DOM 节点的工作交给 Vue 完成,我们可以选择使用template
<h1>{{ blogTitle }}</h1>
或者是render()
函数:
render: function (createElement) { return createElement('h1', this.blogTitle)
}
Vue 会使用 watcher 会监听数据的变化,而 render 函数就是数据监听的回调函数。
不过我们常规的 DOM 树上放着都是 Node,你这返回我一个 VNode 算怎么回事?!其实我们使用 template 也好还是 render()都不会直接影响 DOM 树,而是在修改一刻全部由 VNode 组成的 DOM 树,我们亲切地叫它 VDOM,也即虚拟 DOM!!
然后我们对修改前后的两颗新旧虚拟 DOM 使用Diff
算法(DFS、并记录差异)。记录下所有的变更内容。然后将修改内容再映射到真实 DOM 上,这样就完成了页面的更新!
总结一下:我们 template 应用到页面上的过程:
template -> AST -> render() -> VDOM -> 实际 DOM
Tips: 官方文档中提供 template 实时编译成 render 函数的小工具
最后我们回到起点,runtime + compile 和 runtime-only,什么区别?!
前者多了将 template 编译为 render()函数的过程,后者我们只能在 js 文件中写 render 函数。但是免去了编译的过程。所以后者性能更更高!!
但是
.vue
文件还是该怎么写就怎么写,因为最终使用 webpack 打包时,一样会使用 vue-loader、vue-template-compiler 进行编译处理,template 通通都会变成 render 函数!
>> 请转到学习Vue-Router >>
十二、动态组件
这部分原本是要写在基础里面的,这里进行补充。
动态地在组件之间进行切换是非常高效的,在component标签中使用is
属性,可以指定对应的component的name或者是一个组件对象,组件的template将会被渲染到component标签的位置!
12.1、入门案例
我们先看官方给的首个案例:
通过三个个tab标签,完成在三个组件之间动态切换:
<!DOCTYPE html>
<html><head><title>Dynamic Components Example</title><script src="https://unpkg.com/vue"></script><style>.tab-button {padding: 6px 10px;border-top-left-radius: 3px;border-top-right-radius: 3px;border: 1px solid #ccc;cursor: pointer;background: #f0f0f0;margin-bottom: -1px;margin-right: -1px;}.tab-button:hover {background: #e0e0e0;}.tab-button.active {background: #e0e0e0;}.tab {border: 1px solid #ccc;padding: 10px;}</style></head><body><div id="dynamic-component-demo" class="demo"><buttonv-for="tab in tabs"v-bind:key="tab.name"v-bind:class="['tab-button', { active: currentTab.name === tab.name }]"v-on:click="currentTab = tab">{{ tab.name }}</button><component v-bind:is="currentTab.component" class="tab"></component></div><script>var tabs = [{name: "Home",component: {template: "<div>Home component</div>"}},{name: "Posts",component: {template: "<div>Posts component</div>"}},{name: "Archive",component: {template: "<div>Archive component</div>"}}];new Vue({el: "#dynamic-component-demo",data: {tabs: tabs,currentTab: tabs[0]}});</script></body>
</html>
演示效果:
个人感觉代码应该很容易看懂。
首先渲染三个按钮,将三个组价的名字绑定到按钮上,同时绑定按钮点击事件,当按钮按下后将Vue实例中的data里面的currentTab
修改为button对应的对象,然后在component标签中使用v-bind:is
将is
绑定到currentTab.component
(这个是一个组件对象!)然后就有了我们所看到的效果!!
12.2、keep-alive
我们在使用动态组件的时候,组件之前的来回切换会导致失活的组件“丢失状态”,如果反复地切换渲染,而当组件渲染的工作量很大的时,这效率是极低的并且用户体验极差(我只是手抖点了一下,我切回来又要重新开始)。那么有没有什么手段可以将动态组件的状态暂存起来?!
答案是:<keep-alive>
标签
使用keep-alive标签包裹的动态组件的状态都会缓存起来。但是要求被包裹的动态组件都要有自己的名字!!
关于keep-alive的更多属性,请查看API — Vue.js (vuejs.org)|keep-alive
具体案例,各位还是参照官方文档中的案例进行学习,我就截取中间部分:
<div id="dynamic-component-demo"><buttonv-for="tab in tabs"v-bind:key="tab"v-bind:class="['tab-button', { active: currentTab === tab }]"v-on:click="currentTab = tab">{{ tab }}</button><keep-alive><component v-bind:is="currentTabComponent" class="tab"></component></keep-alive>
</div>
这里我们仿造官方案例写了一个demo:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Keep-Alive</title><style>.tab-button {padding: 6px 10px;border-top-left-radius: 3px;border-top-right-radius: 3px;border: 1px solid #ccc;cursor: pointer;background: #f0f0f0;margin-bottom: -1px;margin-right: -1px;}.tab-button:hover {background: #e0e0e0;}.tab-button.active {background: #e0e0e0;}.tab {border: 1px solid #ccc;padding: 10px;}input {display: block;}</style>
</head>
<body>
<div id="keep-alive"><buttonv-for="tab in tabs":class="['tab-button', {active: tab.name === currentTab.name}]"@click="currentTab = tab">{{tab.name}}</button><keep-alive><component :is="currentTab.component" class="tab"></component></keep-alive></div><template id="loginTemplate"><div><label for="login-username">登录用户名<input type="text" id="login-username" placeholder="Input your username"></label><label for="login-password">登录密码<input type="password" id="login-password" ></label></div>
</template><template id="registerTemplate"><div><label for="register-username">注册用户名<input type="text" id="register-username" placeholder="Input your username"></label><label for="register-password">注册密码<input type="password" id="register-password"></label><label for="repeat-password">确认密码<input type="password" id="repeat-password"></label></div>
</template>
<script src="../js/vue.js"></script>
<script>let tabs = [{name: 'login',component: {template: `#loginTemplate`},},{name: 'register',component: {template: `#registerTemplate`}}]const app = new Vue({el: '#keep-alive',data: {tabs: tabs,currentTab: tabs[0]},})
</script></body>
</html>
当不对动态组件使用keep-alive标签进行包裹的话,切换组件后,你的输入内容将会丢失!
而使用keep-alive以后,输入内容会被暂存。(请与前面第三章说的key管理复用元素区分开!)
十三、混入(Mixin)
13.0、基础介绍
什么是混入?为我们提供了什么?
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
官方的这句话,已经解释了大部分。其重点是:分发Vue组件中可复用的功能!我们可能在多个Vue组件实例中使用同一个功能,我们可以选择将其抽离出来单独定义然后通过混入加入到各个组件实例中!
// 可复用功能,抽离单独定义
const myMixin = {// 钩子函数created() {this.hello()},// 方法methods: {hello() {console.log('hello from mixin!')}}
}// Vue组件实例混入
const app = Vue.createApp({mixins: [myMixin]
})app.mount('#mixins-basic') // => "hello from mixin!"
13.1、选项合并
当组件选项中和混入内容中存在重名冲突的内容,会以恰当的方式进行合并!
data选项会进行递归合并,出现冲突的数据优先以组件选项为准!
注意:所说的递归合并,是值data中每一个数据项都会做递归比较。并不是指会替换对象数据的内部属性!
【例如分别定义了两个对象都命名为
user
,即使他们内部存在数据差异,实例数据会直接覆盖混入的】
const myMixin = {created() {this.hello()},methods: {hello() {console.log('hello from mixin!')}},data() {const user = {college: 'hbue',grade: {class: 1,year: 2018}}return {user}},
}createApp(App).mount('#app')
const app = createApp({App,mixins: [myMixin],data() {const user = {name: 'sakura',age: 21,sex: 'male',grade: {major: 'computer science and technology',class: 2}}return { user }}
})
组件实例中的data和混入内容的data中都有user
数据项,那么选项合并的意思是:**以组件实例中的数据项为准!**并不会进行单个数据项内部的属性合并!【即被混入的user数据项,并不会和组件中的数据项user进行属性合并!】
除此以外:methods
、components
、computed
也都遵守这个标准进行内容合并!
但是钩子函数有所不同:
同名钩子函数将合并为一个数组,因此都将被调用。另外,mixin 对象的钩子将在组件自身钩子之前调用。
const myMixin = {created() {console.log('hello from mixin!')}
}createApp(App).mount('#app')
const app = createApp({App,mixins: [myMixin],created() {console.log("hello from component!")}
})// 运行:
// hello from mixin!
// hello from component!
13.2、全局mixin
前面我们使用的选项式进行组件配置混入,只会影响到使用了mixins
选项的组件!下面我们要学习的是全局mixin,它会影响到组件中注册的其他组件!
main.ts
import { createApp } from 'vue'
import App from './App.vue'const app = createApp(App)// 全局混入
app.mixin({created() {// 从实例选项中取出messagelet msg = this.$options.messageif (msg) {console.log(msg)}}
})app.mount('#app')
App.vue
<template><h1>this is the app template!</h1><sub-component></sub-component>
</template><script lang="ts">
import { defineComponent, h } from 'vue'export default defineComponent({name: 'App',// app 组件的message选项message: 'message from app',components: {'sub-component': {render: () => h('h1', 'this is a sub-component!'),// 其子组件的messagemessage: 'message from sub-components'}}
})
</script><style></style>
我们为一个组件实例使用全局混入app.mixin({ ... })
加入了一个钩子函数,功能是输出组件实例中的message
选项的值。
如果我们使用mixins
选项进行混入的话,那么这个钩子函数只会对此选项所在的组件实例生效,但是如果使用全局混入的话组件中注册的子组件也是会受到影响的!
那么最后的运行结果就是:
因为模板中使用了子组件,所以也会触发钩子函数!
13.3、局限性
在Vue2中,我们使用mixin完成逻辑块的抽取和重用。但是他相较于Vue3中的组合式API存在一些局限性。
- Mixin 很容易发生冲突:因为每个 mixin 的 property 都被合并到同一个组件中,所以为了避免 property 名冲突,你仍然需要了解其他每个特性。
- 可重用性是有限的:我们不能向 mixin 传递任何参数来改变它的逻辑,这降低了它们在抽象逻辑方面的灵活性。
mixin!’)
}
}
}
// Vue组件实例混入
const app = Vue.createApp({
mixins: [myMixin]
})
app.mount(’#mixins-basic’) // => “hello from mixin!”
## 13.1、选项合并当组件选项中和混入内容中存在重名冲突的内容,会以恰当的方式进行合并!data选项会进行递归合并,出现冲突的数据**优先以组件选项为准**!> 注意:所说的递归合并,是值data中每一个数据项都会做递归比较。并不是指会替换对象数据的内部属性!
>
> 【例如分别定义了两个对象都命名为`user`,即使他们内部存在数据差异,实例数据会直接覆盖混入的】```typescript
const myMixin = {created() {this.hello()},methods: {hello() {console.log('hello from mixin!')}},data() {const user = {college: 'hbue',grade: {class: 1,year: 2018}}return {user}},
}createApp(App).mount('#app')
const app = createApp({App,mixins: [myMixin],data() {const user = {name: 'sakura',age: 21,sex: 'male',grade: {major: 'computer science and technology',class: 2}}return { user }}
})
组件实例中的data和混入内容的data中都有user
数据项,那么选项合并的意思是:**以组件实例中的数据项为准!**并不会进行单个数据项内部的属性合并!【即被混入的user数据项,并不会和组件中的数据项user进行属性合并!】
除此以外:methods
、components
、computed
也都遵守这个标准进行内容合并!
但是钩子函数有所不同:
同名钩子函数将合并为一个数组,因此都将被调用。另外,mixin 对象的钩子将在组件自身钩子之前调用。
const myMixin = {created() {console.log('hello from mixin!')}
}createApp(App).mount('#app')
const app = createApp({App,mixins: [myMixin],created() {console.log("hello from component!")}
})// 运行:
// hello from mixin!
// hello from component!
13.2、全局mixin
前面我们使用的选项式进行组件配置混入,只会影响到使用了mixins
选项的组件!下面我们要学习的是全局mixin,它会影响到组件中注册的其他组件!
main.ts
import { createApp } from 'vue'
import App from './App.vue'const app = createApp(App)// 全局混入
app.mixin({created() {// 从实例选项中取出messagelet msg = this.$options.messageif (msg) {console.log(msg)}}
})app.mount('#app')
App.vue
<template><h1>this is the app template!</h1><sub-component></sub-component>
</template><script lang="ts">
import { defineComponent, h } from 'vue'export default defineComponent({name: 'App',// app 组件的message选项message: 'message from app',components: {'sub-component': {render: () => h('h1', 'this is a sub-component!'),// 其子组件的messagemessage: 'message from sub-components'}}
})
</script><style></style>
我们为一个组件实例使用全局混入app.mixin({ ... })
加入了一个钩子函数,功能是输出组件实例中的message
选项的值。
如果我们使用mixins
选项进行混入的话,那么这个钩子函数只会对此选项所在的组件实例生效,但是如果使用全局混入的话组件中注册的子组件也是会受到影响的!
那么最后的运行结果就是:
因为模板中使用了子组件,所以也会触发钩子函数!
13.3、局限性
在Vue2中,我们使用mixin完成逻辑块的抽取和重用。但是他相较于Vue3中的组合式API存在一些局限性。
- Mixin 很容易发生冲突:因为每个 mixin 的 property 都被合并到同一个组件中,所以为了避免 property 名冲突,你仍然需要了解其他每个特性。
- 可重用性是有限的:我们不能向 mixin 传递任何参数来改变它的逻辑,这降低了它们在抽象逻辑方面的灵活性。