发布时间:2023-02-27 文章分类:编程知识 投稿人:王小丽 字号: 默认 | | 超大 打印

本章将继续和大家分享Vue的一些基础知识。话不多说,下面我们直接上代码:

本文内容大部分摘自Vue的官网:https://v2.cn.vuejs.org/v2/guide/

首先我们先来看一下Demo的目录结构,如下所示:

Vue的基础知识(三)

一、侦听器

<!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';
                },
            }
        },
    };
});

运行结果如下:

Vue的基础知识(三)

三、组件注册

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 值就是这个组件的选项对象。

注意局部注册的组件在其子组件中不可用例如,如果你希望ComponentAComponentB中可用,则你需要这样写:

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的模式触发事件取而代之。举个例子,在一个包含titleprop 的假设的组件中,我们可以用以下方法表达对其赋新值的意图:

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-onv-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('组件钩子被调用')
  }
})
// => "混入对象的钩子被调用"
// => "组件钩子被调用"

值为对象的选项,例如methodscomponentsdirectives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

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

版权声明:如有雷同纯属巧合,如有侵权请及时联系本人修改,谢谢!!!