本章将继续和大家分享Vue的一些基础知识。话不多说,下面我们直接上代码:
本文内容大部分摘自Vue的官网:https://v2.cn.vuejs.org/v2/guide/
首先我们先来看一下Demo的目录结构,如下所示:
一、侦听器
<!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>Vue中的侦听器</title> <script src="/js/lib/vue.js"></script> <script src="/js/lib/axios.js"></script> <script src="/js/lib/lodash.js"></script> </head> <body> <div id="app"> <div desc="侦听属性"> <p> 请输入您的问题: <input v-model="question"> </p> <p>{{ answer }}</p> </div> </div> <script> var vm = new Vue({ el: '#app', //挂载点 data: { question: '', answer: '在您提出问题之前,我不能给您答案!', student: { name: '张三', age: 18 } }, watch: { //简单监听 //如果 `question` 发生改变,这个函数就会运行 question: function (newQuestion, oldQuestion) { var _this = this; _this.answer = '正在等待您停止输入...' _this.debouncedGetAnswer(); }, //对对象进行深度监听 //普通的watch方法无法监听到对象内部属性的变化 student: { handler(newValue, oldValue) { // 注意:在嵌套的变更中, // 只要没有替换对象本身, // 那么这里的 `newValue` 和 `oldValue` 相同,都是新值 console.log(newValue); console.log(oldValue); }, deep: true, // 深度监听 immediate: true // 强制立即执行回调(一般用于父组件向子组件动态传值时) }, //对对象的某一个属性进行深度监听 //如果想要监听对象的某一个属性,并且希望获取该属性变化前后的值则需要用该方式进行监听 'student.age': { handler(newValue, oldValue) { console.log(newValue); console.log(oldValue); }, deep: true, // 深度监听 immediate: true // 强制立即执行回调(一般用于父组件向子组件动态传值时) } }, created: function () { // `_.debounce` 是一个通过 Lodash 限制操作频率的函数。 // 在这个例子中,我们希望限制访问 接口 的频率 // AJAX 请求直到用户输入完毕才会发出。想要了解更多关于 // `_.debounce` 函数 (及其近亲 `_.throttle`) 的知识, // 请参考:https://lodash.com/docs#debounce var _this = this; _this.debouncedGetAnswer = _.debounce(_this.getAnswer, 1500); //debouncedGetAnswer 方法名可自定义 }, methods: { getAnswer: function () { var _this = this; if (_this.question.indexOf('?') === -1) { _this.answer = '问题通常包含问号!'; return; } _this.answer = '数据获取中...'; axios.get('https://autumnfish.cn/api/joke') .then(function (response) { _this.answer = response.data; }) .catch(function (error) { _this.answer = '请求接口异常:' + error; }); } } }); </script> </body> </html>
二、Vue组件基础
自定义组件<button-counter> 代码如下:
define([ 'axios' ], function (axios) { /* 因为组件是可复用的 Vue 实例,所以它们与 new Vue 接收相同的选项,例如 data、computed、watch、methods 以及生命周期钩子等。 仅有的例外是像 el 这样根实例特有的选项。 */ return { template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>', props: [], //一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝 data: function () { return { count: 0 } }, mounted: function () { }, methods: { }, watch: { } }; });
自定义组件 <blog-post> 代码如下:
define([ 'axios' ], function (axios) { /* 因为组件是可复用的 Vue 实例,所以它们与 new Vue 接收相同的选项,例如 data、computed、watch、methods 以及生命周期钩子等。 仅有的例外是像 el 这样根实例特有的选项。 */ return { //每个组件必须只有一个根元素 template: ` <div class="blog-post" desc="根元素"> <h3>{{ post.title }}</h3> <p>子组件中的titleNew:<input type="text" v-model="titleNew"></p> <slot></slot> <button @click="handleEnlargeText">Enlarge text</button> <div v-html="post.content"></div> </div> `, /* 1、通过 Prop 向子组件传递数据。 2、一个组件默认可以拥有任意数量的 prop,任何值都可以传递给任何 prop。 在上述模板中,你会发现我们能够在组件实例中访问这个值,就像访问 data 中的值一样。 3、所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。 这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。 */ props: ['post', 'title'], //一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝 data: function () { return { enlargeFontSize: 0.1, //需要放大字体的大小 titleNew: this.title, //初始值为props中父组件传递过来的值 } }, mounted: function () { }, methods: { //处理放大文本字体 handleEnlargeText: function () { var _this = this; //子组件可以通过调用内建的 $emit 方法并传入事件名称来触发一个父组件的事件 //第二个参数为调用父组件事件所需传的参数 //enlarge-text为自定义事件 _this.$emit('enlarge-text', _this.enlargeFontSize); } }, watch: { //监听器完整写法 title: { handler(newValue, oldValue) { this.titleNew = newValue; }, //deep: true, // 深度监听 immediate: true // 强制立即执行回调(一般用于父组件向子组件动态传值时) }, //监听器简写,当需要设置 deep 或者 immediate 时需使用完整写法 titleNew: function (newValue, oldValue) { /* 注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说, 在子组件中改变变更这个对象或数组本身将会影响到父组件的状态。 这种情况下就不需要以 update:myPropName 的模式触发更新事件了。 */ this.$emit('update:title', newValue); //更新父组件title属性绑定的值 /* 自定义事件 .sync 修饰符: 在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。 不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件两侧都没有明显的变更来源。 这也是为什么我们推荐以 update:myPropName 的模式触发事件取而代之。 举个例子,在一个包含 title prop 的假设的组件中,我们可以用以下方法表达对其赋新值的意图: this.$emit('update:title', newTitle) 然后父组件可以监听那个事件并根据需要更新一个本地的数据 property。例如: <text-document v-bind:title="doc.title" v-on:update:title="doc.title = $event"> </text-document> 为了方便起见,我们为这种模式提供一个缩写,即 .sync 修饰符: <text-document :title.sync="doc.title"></text-document> */ } } }; });
Vue组件基础.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>Vue组件基础</title> </head> <body> <div id="app"> <button-counter></button-counter> <button-counter></button-counter> <button-counter></button-counter> <div :style="{ fontSize: postFontSize + 'em' }"> <blog-post v-for="post in posts" :key="post.id" :title.sync="post.title" :post="post" v-on:enlarge-text="onEnlargeText"> <!-- 默认插槽的内容 --> <template v-slot:default> <p>父组件中的post.title:<input type="text" v-model="post.title" /></p> </template> </blog-post> </div> </div> <script src="/js/lib/require.js"></script> <script src="/js/common/require_config.js"></script> <script src="/js/ComponentsDemo.js"></script> </body> </html>
其中 ComponentsDemo.js 代码如下:
//Vue组件基础 require(['../common/base', '../components/blogPost'], function (base, blogPost) { let axios = base.axios; var vm = new base.vue({ el: '#app', //挂载点 mixins: [base.mixin], //混入,类似基类的概念 components: { 'blog-post': blogPost //局部注册组件,注意局部注册的组件在其子组件中不可用。 }, data: { posts: [ { id: 1, title: 'My journey with Vue' }, { id: 2, title: 'Blogging with Vue' }, { id: 3, title: 'Why Vue is so fun' } ], postFontSize: 1 }, //created钩子函数 created: function () { console.log('This is index created'); }, //mounted钩子函数 mounted: function () { console.log('This is index mounted'); }, //方法 methods: { //放大文本 onEnlargeText: function (enlargeFontSize) { var _this = this; _this.postFontSize += enlargeFontSize } } }); });
其中require_config.js 代码如下:
//主要用来配置模块的加载位置(设置短模块名) require.config({ baseUrl: '/js/lib', //设置根目录 paths: { //如果没有设置根目录则需要填写完整路径 'vue': 'vue', 'axios': 'axios', 'jquery': 'jquery-3.6.3', //paths还有一个重要的功能,就是可以配置多个路径,如果远程cdn库没有加载成功,可以加载本地的库,如下: //'jquery': ['http://libs.baidu.com/jquery/2.0.3/jquery', '/js/lib/jquery-3.6.3'], } });
其中base.js 代码如下:
//define用来自定义模块 //第一个参数:加载依赖模块,可以是require_config中定义的短模块名,也可以是完整的模块路径(去掉.js后缀名) //第二个参数:执行加载完后的回调函数 define(['vue', 'axios', '../components/buttonCounter'], function (vue, axios, buttonCounter) { //TODO 此处可以处理一些公共的逻辑 //vue.component('component-a', { /* ... */ }); //全局注册组件 //vue.mixin({...}); //全局混入 /* 定义组件名的方式有两种: 1、使用 kebab-case (短横线分隔命名) 当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如 <my-component-name> 2、使用 PascalCase (首字母大写命名) 当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。 也就是说 <my-component-name> 和 <MyComponentName> 都是可接受的。 注意,尽管如此,直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的。 */ //Vue.component(...) 的第一个参数为组件名。 vue.component('button-counter', buttonCounter); //全局注册 return { vue: vue, axios: axios, //Vue混入 mixin: { //数据 data: function () { return { domain: '', //域名 } }, //组件 components: { }, //created钩子函数 created: function () { console.log('This is base created'); }, //mounted钩子函数 mounted: function () { console.log('This is base mounted'); }, //方法 methods: { //测试 doTest: function () { console.log('This is base doTest'); }, //获取域名 getDomain: function () { var _this = this; _this.domain = 'https://www.baidu.com'; }, } }, }; });
运行结果如下:
三、组件注册
1、组件名大小写
定义组件名的方式有两种:
1)使用 kebab-case
Vue.component('my-component-name', { /* ... */ })
当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如<my-component-name>
。
2)使用 PascalCase
Vue.component('MyComponentName', { /* ... */ })
当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说<my-component-name>
和<MyComponentName>
都是可接受的。注意,尽管如此,直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的。
2、全局注册
到目前为止,我们用过Vue.component
来创建组件:
Vue.component('my-component-name', { // ... 选项 ... })
这些组件是全局注册的。也就是说它们在注册之后可以用在任何新创建的 Vue 根实例 (new Vue
) 的模板中。
3、局部注册
全局注册往往是不够理想的。比如,如果你使用一个像 webpack 这样的构建系统,全局注册所有的组件意味着即便你已经不再使用一个组件了,它仍然会被包含在你最终的构建结果中。这造成了用户下载的 JavaScript 的无谓的增加。
在这些情况下,你可以通过一个普通的 JavaScript 对象来定义组件:
var ComponentA = { /* ... */ } var ComponentB = { /* ... */ } var ComponentC = { /* ... */ }
然后在components
选项中定义你想要使用的组件:
new Vue({ el: '#app', components: { 'component-a': ComponentA, 'component-b': ComponentB } })
对于components
对象中的每个 property 来说,其 property 名就是自定义元素的名字,其 property 值就是这个组件的选项对象。
注意局部注册的组件在其子组件中不可用。例如,如果你希望ComponentA
在ComponentB
中可用,则你需要这样写:
var ComponentA = { /* ... */ } var ComponentB = { components: { 'component-a': ComponentA }, // ... }
四、组件中的Prop
1、Prop 的大小写 (camelCase vs kebab-case)
HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名:
Vue.component('blog-post', { // 在 JavaScript 中是 camelCase 的 props: ['postTitle'], template: '<h3>{{ postTitle }}</h3>' })
<!-- 在 HTML 中是 kebab-case 的 --> <blog-post post-title="hello!"></blog-post>
重申一次,如果你使用字符串模板,那么这个限制就不存在了。
2、Prop 类型
到这里,我们只看到了以字符串数组形式列出的 prop:
props: ['title', 'likes', 'isPublished', 'commentIds', 'author']
但是,通常你希望每个 prop 都有指定的值类型。这时,你可以以对象形式列出 prop,这些 property 的名称和值分别是 prop 各自的名称和类型:
props: { title: String, likes: Number, isPublished: Boolean, commentIds: Array, author: Object, callback: Function, contactsPromise: Promise // or any other constructor }
这不仅为你的组件提供了文档,还会在它们遇到错误的类型时从浏览器的 JavaScript 控制台提示用户。
3、单向数据流
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。
额外的,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。
这里有两种常见的试图变更一个 prop 的情形:
1)这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。
在这种情况下,最好定义一个本地的 data property 并将这个 prop 用作其初始值:
props: ['initialCounter'], data: function () { return { counter: this.initialCounter } }
2)这个 prop 以一种原始的值传入且需要进行转换。
在这种情况下,最好使用这个 prop 的值来定义一个计算属性:
props: ['size'], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } }
注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变变更这个对象或数组本身将会影响到父组件的状态。
五、自定义事件
1、事件名
不同于组件和 prop,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称。举个例子,如果触发一个 camelCase 名字的事件:
this.$emit('myEvent')
则监听这个名字的 kebab-case 版本是不会有任何效果的:
<!-- 没有效果 --> <my-component v-on:my-event="doSomething"></my-component>
不同于组件和 prop,事件名不会被用作一个 JavaScript 变量名或 property 名,所以就没有理由使用 camelCase 或 PascalCase 了。并且v-on
事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以v-on:myEvent
将会变成v-on:myevent
——导致myEvent
不可能被监听到。
因此,我们推荐你始终使用 kebab-case 的事件名。
2、.sync 修饰符
在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件两侧都没有明显的变更来源。
这也是为什么我们推荐以update:myPropName
的模式触发事件取而代之。举个例子,在一个包含title
prop 的假设的组件中,我们可以用以下方法表达对其赋新值的意图:
this.$emit('update:title', newTitle)
然后父组件可以监听那个事件并根据需要更新一个本地的数据 property。例如:
<text-document v-bind:title="doc.title" v-on:update:title="doc.title = $event" ></text-document>
为了方便起见,我们为这种模式提供一个缩写,即.sync
修饰符:
<text-document v-bind:title.sync="doc.title"></text-document>
六、插槽
1、具名插槽
有时我们需要多个插槽。例如对于一个带有如下模板的<base-layout>
组件:
<div class="container"> <header> <!-- 我们希望把页头放这里 --> </header> <main> <!-- 我们希望把主要内容放这里 --> </main> <footer> <!-- 我们希望把页脚放这里 --> </footer> </div>
对于这样的情况,<slot>
元素有一个特殊的 attribute:name
。这个 attribute 可以用来定义额外的插槽:
<div class="container"> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer> </div>
一个不带name
的<slot>
出口会带有隐含的名字“default”。
在向具名插槽提供内容的时候,我们可以在一个<template>
元素上使用v-slot
指令,并以v-slot
的参数的形式提供其名称:
<base-layout> <template v-slot:header> <h1>Here might be a page title</h1> </template> <template v-slot:default> <p>A paragraph for the main content.</p> <p>And another one.</p> </template> <template v-slot:footer> <p>Here's some contact info</p> </template> </base-layout>
现在<template>
元素中的所有内容都将会被传入相应的插槽。
最终渲染结果如下所示:
<div class="container"> <header> <h1>Here might be a page title</h1> </header> <main> <p>A paragraph for the main content.</p> <p>And another one.</p> </main> <footer> <p>Here's some contact info</p> </footer> </div>
注意 v-slot 只能添加在 <template> 上 (只有一种例外情况),这一点和已经废弃的 slot attribute 不同。
2、后备内容
有时为一个插槽设置具体的后备 (也就是默认的) 内容是很有用的,它只会在没有提供内容的时候被渲染。例如在一个<submit-button>
组件中:
<button type="submit"> <slot></slot> </button>
我们可能希望这个<button>
内绝大多数情况下都渲染文本“Submit”。为了将“Submit”作为后备内容,我们可以将它放在<slot>
标签内:
<button type="submit"> <slot>Submit</slot> </button>
现在当我在一个父级组件中使用<submit-button>
并且不提供任何插槽内容时:
<submit-button></submit-button>
后备内容“Submit”将会被渲染:
<button type="submit"> Submit </button>
但是如果我们提供内容:
<submit-button>
Save
</submit-button>
则这个提供的内容将会被渲染从而取代后备内容:
<button type="submit"> Save </button>
3、动态插槽名
动态指令参数也可以用在 v-slot 上,来定义动态的插槽名:
<base-layout> <template v-slot:[dynamicSlotName]> ... </template> </base-layout>
4、具名插槽的缩写
跟v-on
和v-bind
一样,v-slot
也有缩写,即把参数之前的所有内容 (v-slot:
) 替换为字符#
。例如v-slot:header
可以被重写为#header
:
<base-layout> <template #header> <h1>Here might be a page title</h1> </template> <template #default> <p>A paragraph for the main content.</p> <p>And another one.</p> </template> <template #footer> <p>Here's some contact info</p> </template> </base-layout>
七、混入
1、基础
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
例子:
// 定义一个混入对象 var myMixin = { created: function () { this.hello() }, methods: { hello: function () { console.log('hello from mixin!') } } } // 定义一个使用混入对象的组件 var Component = Vue.extend({ mixins: [myMixin] }) var component = new Component() // => "hello from mixin!"
2、选项合并
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
比如,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
var mixin = { data: function () { return { message: 'hello', foo: 'abc' } } } new Vue({ mixins: [mixin], data: function () { return { message: 'goodbye', bar: 'def' } }, created: function () { console.log(this.$data) // => { message: "goodbye", foo: "abc", bar: "def" } } })
同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。
var mixin = { created: function () { console.log('混入对象的钩子被调用') } } new Vue({ mixins: [mixin], created: function () { console.log('组件钩子被调用') } }) // => "混入对象的钩子被调用" // => "组件钩子被调用"
值为对象的选项,例如methods
、components
和directives
,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
var mixin = { methods: { foo: function () { console.log('foo') }, conflicting: function () { console.log('from mixin') } } } var vm = new Vue({ mixins: [mixin], methods: { bar: function () { console.log('bar') }, conflicting: function () { console.log('from self') } } }) vm.foo() // => "foo" vm.bar() // => "bar" vm.conflicting() // => "from self"
注意:Vue.extend()
也使用同样的策略进行合并。
3、全局混入
混入也可以进行全局注册。使用时格外小心!一旦使用全局混入,它将影响每一个之后创建的 Vue 实例。使用恰当时,这可以用来为自定义选项注入处理逻辑。
// 为自定义的选项 'myOption' 注入一个处理器。 Vue.mixin({ created: function () { var myOption = this.$options.myOption if (myOption) { console.log(myOption) } } }) new Vue({ myOption: 'hello!' }) // => "hello!"
请谨慎使用全局混入,因为它会影响每个单独创建的 Vue 实例 (包括第三方组件)。大多数情况下,只应当应用于自定义选项,就像上面示例一样。推荐将其作为插件发布,以避免重复应用混入。
Demo源码:
链接:https://pan.baidu.com/s/1jc5SGf3_8qb6pZT4T-5mxA 提取码:broa
此文由博主精心撰写转载请保留此原文链接:https://www.cnblogs.com/xyh9039/p/17139196.html
版权声明:如有雷同纯属巧合,如有侵权请及时联系本人修改,谢谢!!!