*Vue.js 实战* 基础篇 学习笔记



书本信息:

 << Vue.js 实战 >> 作者: 梁灏

学习笔记 部分内容摘自书 非原创 侵删

第二章 数据绑定


2.1.4 过滤器

推荐用 computed 来实现
在 {{}} 插值尾部添加一个管道运行算符 | 对数据进行过滤

<div id="app">
    {{ data | formatDate }
</div>
<script>
    var padDate = function(value) {
        return value < 10 ? '0' + value : value;
    };

    var app = new Vue({
        el: '#app',
        data: {
            date: new Date()
        },
        filters: {
            formatDate: function(value) {
                var hours = padDate(date.getHours());
                return hours;
            }
        },
        mounted () {
            var _this = this;
            this.timer = setInterval(function() {
                _this.date = new Date();
            }, 1000);
        },
        beforeDestroy () {
            if (this.timer) {
                clearInterval(this.timer);
            }
        }
    })
</script>
  • 串联

{{ message | filterA | filterB }}

  • 接收参数

{{ message | filterA('arg1', 'arg2') }}


第三章 计算属性


3.2 计算属性用法

每一个计算属性都包含一个 gettter 和一个 setter, 在我们需要时, 可以提供一个 setter 函数, 当手动修改计算属性的值就行修改一个普通数据那样时, 就会触发 setter 函数, 执行一些自定义的函数

<div id="app">
    姓名: {{ fullName }}
</div>
<script>
    var app = new Vue({
        el: '#app',
        data: {
            firstName: 'Jack',
            lastName: 'Green'
        },
        computed: {
            fullName: {
                // getter
                get () {
                    return this.firstName + ' ' + this.lastName;
                },
                // setter
                set (newValue) {
                    var names = newValue.split(' ');
                    this.firstName = names[0];
                    this.lastName = names[names.length - 1];
                }
            }
        }
    })
</script>

当执行 app.fullName = 'John Doe'; 时, setter 就会被调用, 数据 firstName 和 'lastName' 都会相对更新, 视图也会更新.


第五章 内置指令


5.1.2 v-once

v-once 不需要表达式, 定义它的组件或组件只渲染一次, 包括元素或组件的所有子节点
首次渲染后, 不再岁数据的变化重新渲染, 将被视为静态内容
<span v-once>{{ message }}</span>


5.2.1 key 属性 元素复用

Vue 在渲染元素时出于效率考虑, 会尽可能的复用已有元素而非重新渲染
如果你不希望这样, 可以使用 key 属性, 可以让你自己决定是否要复用元素, key 的值必须唯一

<div id="app">
    <template v-if="type === 'name">
        <label>用户名: </label>
        <input placeholder="输入用户名" key="name-input">
    </template>
    <template v-else>
        <label>邮箱: </label>
        <input placeholder="输入邮箱" key="mail-input">
    </template>
    <button @click="handleToggleClick">切换输入类型</button>
</div>

5.3 列表渲染 v-for

  • 数组

<li v-for="(book, index) in books">{{ index }} - {{ book.name }}</li>

  • 对象

<li v-for="(value, key, index) in user">{{ index }} - {{ key }}: {{ value }}</li>

  • 整数

<span v-for="n in 10">{{ n }} </span>
结果是 1 2 ...... 10


5.3.2 数组更新

Vue 的核心是数据与视图的双向绑定, 当我们修改数组时, Vue 会检测到数据变化, 所以用 v-for 渲染的也会立即更新. Vue 包含了一组观察数组变异的方法, 使用它们改变数组也会触发视图更新.

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

使用以上方法会改变这些方法调用的原始数组, 有些方法不会改变原始数组

  • filter()
  • concat()
  • slice()

它们返回的是一个新数组, 在使用这些非变异方法时, 可以用新数组来替换原始数组

app.books = app.books.filter(function (item) {
    return item.name.match(/JavaScript/);
    // 对 books 数组做 filter 返回带有 JavaScript 的项
})

Vue 在检测到数组变化时, 并不是直接重新渲染整个列表, 而是最大化地复用 DOM 元素, 替换的数组中, 含有相同元素的项不会被重新渲染, 因此可以大胆地用新数组来替换旧数组, 不用担心性能问题.

需要注意的是, 以下变动的数组中, Vue 是不能检测到的, 也不会触发视图更新

  • 通过索引直接设置项, 比如 `app.books[3] = {...}
  • 修改数组长度, 比如 `app.books.length = 1

解决第一个问题可以用两种方法

  • set 方法
Vue.set(app.books, 3, {
    name: 'changedName'
});

// webpack app.&set
this.&set(app.books, 3, {
    name: 'changedName'
})
  • 用 splice
app.books.splice(3, 1, {
    name: 'changedName'
})

第二个问题也可以直接用 splice 来解决
app.books.splice(1);


5.4.2 修饰符

在@绑定的事件后加小圆点 ".", 再更一个后缀来使用修饰符

  • .stop
  • .prevent
  • .capture
  • .self
  • .once

具体用法

// 阻止单击事件冒泡
<a @click.stop="handle"></a>
// 提交事件不再重载页面
<form @submit.prevent="handle"></form>
// 修饰符可以串联
<a @click.stop.prevent="handle"></a>
// 只有修饰符
<form @submit.prevent></form>
// 添加事件侦听器时使用事件捕获模式
<div @click.capture="handle"></div>
// 只当事件在该元素本身(而不是子元素)触发时触发回调
<div @click.self="handle"></div>
// 只触发一次, 组件同样适用
<div @click.once="handle"></div>

在表单元素上监听键盘事件时, 还可以使用按键修饰符, 比如按下某个键时猜调用方法

// 只有在 keyCode 是13时代用 submit()
<input @keyup.13="submit">

也可以自己配置具体按键

Vue.config.keyCodes.f1 = 112;
// 全局定义后, 就可以使用 @keyup.f1

除了具体的某个 keyCode, 还提供了一些快捷名称

  • .enter
  • .tab
  • .delete (删除和退格)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right

这些按键修饰符也可以组合使用, 或者和鼠标一起配合使用

  • .ctrl
  • .alt
  • .shift
  • .meta ( Mac 下是 Command, Windows 下是窗口键)

第六章 表单与 v-model


6.3 修饰符

.lazy: 懒加载

在输入框中, v-model 默认实在 input 事件中同步输入框的数据 (除了中文输入法输入的时候), 使用修饰符 .lazy 会转变为在 change 事件中同步
<input v-model.lazy="message">

.number: 将输入的 String 转换为 Number

使用 .number 可以将输入转换为 Number 类型, 否则虽然你输入的是数字, 但是它的类型其实是 String, 在输入输入框中比较有用
<input type="number" v-model.number="message">

.trim: 自动过滤输入的首尾空格

<input type="text" v-model.trim="message">


第七章 组件详解


7.1.2 is 属性挂载组件

Vue 组件的模板在某些情况下会受到 HTML 的限制, 比如 <table> 内规定只允许是 <tr>, <td>, <th> 等这些表格元素, 所以直接在 <table> 内使用组件是无效的, 这种情况下, 可以使用特殊的 is 属性来挂载组件
常见的限制元素还有 <ul>, <ol>, <select>

<div id="app">
    <table>
        <tbody is="my-component"></tbody>
    </table>
</div>

7.2.3 数据验证 props

在组件中, 使用选项 props 来声明需要从父级接收的数据, props 的值可以是两种, 一种是字符串数组, 一种是对象
当 prop 需要验证时, 就需要对象写法
一般当你的组件需要提供给别人使用时, 推荐都进行数据验证, 比如某个数据必须是数字类, 如果是 string 就会在控制台弹出警告

props: {
    // 必须是数字类型
    propA: Number,
    // 必须是字符串或者数字
    propB: [String, Number],
    // 布尔值, 如果没有定义, 默认为 true
    propC: {
        type: Boolean,
        default: true
    },
    // 数字, 而且是必传
    propD: {
        type: Number,
        required: true
    },
    // 如果是数组或者对象, 默认值必须是一个函数来返回
    propE: {
        type: Array,
        default: function () {
            return [];
        }
    },
    // 自定义验证函数
    propF: {
        validator: function (value) {
            return value > 10;
        } 
    }
}

验证的 type 类型可以是:

  • String
  • Number
  • Boolean
  • Object
  • Array
  • Function

type 也可以是一个自定义构造器, 使用 instanceof 检测
当 prop 验证失败时, 在开发版本下会在控制台抛出一条警告


7.3.1 自定义事件 v-on .native 监听原生事件, 监听的是该组件的根元素

当给一个 Vue 组件绑定事件的时候需要加上 .native
否则 Vue 会认为这个是一个自定义事件 v-on 来监听子组件的 $emit()

<my-component v-on:click,native="handleClick"></my-component>


7..3.2 自定义组件使用 v-model

<my-component v-model="total"></my-component>
...
methods: {
    handleClick () {
        this.counter++;
        this.$emit('input', this.counter);
    }
}

组件 $emit() 的事件名是特殊的 input, 在使用组件的父级, 并没有在 <my-component> 上使用 @input="handler", 而是直接使用了 v-model 绑定的一个数据 total, 也可以用自定义事件实现

<my-component @input="handleGetTotal"></my-component>
...
methods: {
    handleGetTotal (total) {
        this.total = total;
    }
}

v-model 还可以用来创建自定义的表单输入组件, 进行数据双向绑定

<my-component v-model="total"></my-component>
<button @click="handleReduce">-1</button>
...
<template>
    <input :value="value" @input="updateValue">
</template>
...
methods: {
    updateValue (event) {
        this.$emit('input', event.target.value);
    }
}

实现这样一个具有双向绑定的 v-model 组件需要满足下面两个要求

  • 接收一个 value 属性
  • 在有新的 vaue 时触发 input 事件

7.3.3 非父子组件通信

组件通信几种方式

  • $emit() $on() 只能父子组件通信, 子组件 props 接收父组件数据
  • bus 空 Vue 实例
  • vuex
  • 父链和子组件索引

父链

在子组件中, 使用 this.$parent 可以直接访问该组件的父实例或组件, 父组件也可以通过 this.$children 访问他所有的子组件, 而且可以递归向上或者向下无限访问, 一直到根实例或最内层的组件

子组件索引

当子组件较多时, 通过 this.$children 来一一遍历出我们需要的一个组件实例是比较困难的, 尤其是当组件动态渲染时, 他们的序列不是固定的. Vue 可以使用特殊的属性 ref 来为子组件指定一个索引名称

<div id="app">
    <button @click="handleRef">通过ref获取子组件实例</button>
    <compoent-a ref="comA"></compoent-a>
</div>
...
methods: {
    handleRef () {
        var msg = this.$refs.comA.message;
        console.log(msg);
    }
}

在父组件模板中, 子组件标签上使用 ref 指定一个名称, 并在父组件内通过 this.$refs 来访问指定名称的子组件

注意: $refs 只在组件渲染完成后才填充, 并且他是非响应式的, 它仅仅作为一个直接访问子组件的应急方案, 应当避免在模板或计算属性中使用


7.4.4 作用域插槽

作用域插槽是一种特殊的 slot, 使用一个可以复用的模板替换已渲染元素

// 列表组件, 允许组件自定义应该如何渲染列表每一项
<div id="app">
    <my-list :books="books">
        // 作用域插槽也可以是具名的 slot
        <template slot="book" scope="props">
            <li>{{ props.bookName }}</li>
        </template>
    </my-list>
</div>
<script>
    Vue.component('my-list', {
        props: {
            books: {
                type: Array,
                default () {
                    reutrn [];
                }
            }
        },
        template: '\
        <ul>\
            <slot name="book"\
                  v-for="book in books"\
                  :book-name="book.name"\
                  //这里也可以写默认 slot 内容 \
            </slot>\
        </ul>'
    });

    var app = new Vue({
        el: '#app',
        data: {
            books: [
                { name: 'abc1'},
                { name: 'abc2'},
                { name: 'abc3'}
            ]
        }
    })
</script>

父组件当中 scope="props", 这里的 props 只是一个临时变量, 就像 v-for="item in items" 里面的 item 一样. template 可以通过临时变量 props 访问来自子组件插槽的数据

在这个例子当中, 子组件 my-list 接收父级的 prop 数组 books, 并且将它在 name 为 book 的 slot 上使用 v-for, 同时暴露一个变量 bookName

这个作用域插槽的使用场景就是即可以复用子组件的slot, 又可以使 slot 内容不一致. 如果上例还在其他组件内使用, <li> 的内容渲染权是由使用者掌握的, 而数据却可以通过临时变量 (比如 props) 从子组件内获取


7.4.5 访问 slot

this.$slots.name

this.$slots.default     //包括了所有没有被包含在具名 slot 中的节点


7.5.3 动态组件

Vue 提供了一个特殊元素 <component> 来动态地挂载不同的组件, 使用 is 特性来选择要挂载的组件

<div id="aap">
    <component :is="currentView"></component>
    <button @click="handleChangeView('B')">点击切换到组件B</button>
</div>
<script>
    ...
    components: {
        comA: {...},
        comB: {...}
    },
    date: {
        currentView: 'comA'
    },
    methods: {
        handleChangeView (component) {
            this.currentView = 'com' + component;
        }
    }
</script>

7.6.1 $nextTick

Vue 中异步更新队列

Vue在观察到数据变化时并不是直接更新 DOM, 而是开启一个队列, 并缓冲在同一事件循环中发生的所有数据改变. 在缓冲时会去除重复数据, 从而避免不必要的计算和 DOM 操作.

然后, 在下一个事件循环 tick 中, Vue 刷新队列并执行实际 (已去重) 工作. 所以如果你用一个 for 循环来动态改变数据100次, 其实它只会应用最后一次改变, 如果没有这种机制, DOM 就要重绘100次, 这显然是一个很大的开销.
Vue 会根据当前浏览器环境优先使用原生的 Promise.then 和 MutationObserver, 如果都不支持, 就会采用 setTimeout 代替.
知道了 Vue 异步更新 DOM 的原理, 上面示例的报错也就不难理解了. 事实上, 在执行 this.showDiv=true; 时, div 仍然还是没有被创建出来, 直到下一个 Vue 事件循环时, 才开始创建. $nextTick 就是用来知道什么时候 DOM 更新完成的

<div id="app">
    <div id="div" v-if="showDiv">这是一段文本</div>
    <button @click="getText">获取 div 内容</button>
</div>
<script>
    var app = new Vue({
        el: '#app',
        data: {
            showDiv: false
        },
        methods: {
            getText () {
                this.showDiv = true;
                this.$nextTick(function () {
                    var text = document.getElementById('div').innerHTML;
                    console.log(text);
                });
            }
        }
    })
</script>

这时候点击按钮, 控制台会打印出 div 的内容 "这是一段文本"

理论上, 我们应该不用主动操作DOM, 因为 Vue 的核心思想就是数据驱动 DOM, 但在很多业务里, 我们避免不了会使用一些第三方库, 比如 popper.js, swiper 等, 这些基于原生 js 的库都有创建和更新及销毁的哇证生命周期, 与 Vue 配合使用时, 就要利用好 $nextTick

vue nextTick深入理解-vue性能优化、DOM更新时机、事件循环机制


7.6.3 手动挂载实例

动态地创建 Vue 实例, Vue 提供了 Vue.extend 和 $mount 两个方法来手动挂载一个实例.

Vue.extend 是基础 Vue 构造器, 创建一个 子类 , 参数是一个包含组件选项的对象.

如果 Vue 实例在实例化时没有收到 el 选项, 它就处在 '未挂载' 状态, 没有关联的 DOM 元素. 可以使用 $mount() 手动地挂载一个未挂载的实例. 这个方法返回实例自身, 因而可以链式调用其他实例方法.

<div id="mount-div">
    
</div>
<script>
    var MyComponent = Vue.extend({
        template: '<div>Hello: {{ name }}</div>',
        data () {
            return {
                name: 'abc'
            }
        }
    });

    new MyComponent().$mount('#mount-div');
</script>

除了这种写法外, 以下两种写法也是可以的

new MyComponent().$mount('#mount-div');
// 同上
new MyComponent({
    el: '#mount-div'
});
// 或者, 在文档之外渲染并且随后挂载
var component = new MyComponent().$mount();
document.getElementById('mount-div').appendChild(component.$el);

手动挂载实例 (组件) 是一种比较极端的高级用法, 在业务中几乎用不到, 只在开发一些复杂的独立组件时可能会使用, 这边只做了解.

Vue2 几种常见开局方式


第八章 自定义指令


8.1 基本用法

exp. 注册一个 v-focus 的指令, 用于在 <input>, <textarea> 元素初始化时自动获得焦点

  • 全局注册
Vue.directive('focus', {
        //指令选项
});
  • 局部注册
var app = new Vue({
    el: '#app',
    directives: {
        focus: {
            //指令选项
        }
    }
})

下面具体介绍自定义指令的各个选项

自定义指令的选项是由几个钩子函数组成的, 每个都是可选的

  • bind:
    只调用一次, 指令第一次绑定到元素时调用, 用这个钩子函数可以定义一个在绑定时执行一次的初始化动作
  • inserted:
    被绑定元素插入父节点时调用 (父节点存在即可调用, 不必存在于 document 中)
  • update:
    被绑定元素所在的模板更新时调用, 而不论绑定值是否变化. 通过比较更新前后的绑定值, 可以忽略不必要的模板更新
  • componentUpdated:
    被绑定元素所在模板完成一次更新周期时调用
  • unbind:
    只调用一次, 指令与元素解绑时调用

可以根据需求在不同的钩子函数内完成逻辑代码, 例如上面的 v-focus, 我们希望在元素插入父节点时就调用, 可以用 inserted

<div id="app">
    <input type="text" v-focus>
</div>
<script>
    Vue.directive('focus', {
        inserted (el) {
            // 聚焦元素
            el.focus();
        }
    });

    var app = new Vue({
        el: '#app'
    })
</script>

浏览器中打开这个页面, input 输入框就自动获得了焦点, 成为可输入状态

每个钩子函数都有几个参数可用, 比如上面我们用到了 el

  • el 指令所绑定的元素, 可以用来直接操作 DOM
  • binding 一个对象, 包含以下属性
    • name 指令名, 不包括 v- 前缀
    • value 指令的绑定值, 例如 v-my-directive="1+1", value 的值是2
    • oldValue 指令绑定的前一个值, 仅在 update 和 componentUpdated 钩子中可用, 无论值是否改变都可用
    • expression 绑定值的字符串形式. 例如 v-my-directive="1+1", expression 的值是 "1+1"
    • arg 传给指令的参数, 例如 v-my-directive:foo, arg 的值是 foo
    • modifiers 一个包含修饰符的对象, 例如 v-my-directive.foo.bar, 修饰符对象 modifiers 的值是 { foo: true, bar: true }
  • vnode Vue 编译生成的虚拟节点
  • oldVnode 上一个虚拟节点仅在 update 和 componentUpdated 钩子中可用

下面是结合了以上参数的一个具体实例

<div id="app">
    <div v-test:msg.a.b="message"></div>
</div>
<script>
    Vue.directive('test', {
        bind (el, binding, vnode) {
            var keys = [];
            for (var i in vnode) {
                keys.push(i);
            }
            el.innerHTML =
                'name: ' + binding.name + '<br>' +
                'value: ' + binding.value + '<br>' +
                'expression: ' + binding.expression + '<br>' +
                'argument: ' + binding.arg + '<br>' +
                'modifiers: ' + JSON.stringify(binding.modifiers) + '<br>' +
                'vnode keys: ' + keys.join(', ')
        }
    });

    var app = new Vue({
        el: '#app',
        data: {
            message: 'some text'
        }
    })
</script>

执行后, <div> 的内容会使用 innerHTML 重置

vnode keys:
tag, data, children, text, elm, ns, context, functionalContext, key, componentOptions,
componentInstance, parent, raw, isStatic, isRootInsert, isComment,isCloned, isOnce

在大多数场景下, 我们会在 bind 钩子里绑定一些事件, 比如在 document 上用 addEventListener 绑定, 在 unbind 上用 removeEventListener 解绑, 比较典型的示例就是让这个元素随着鼠标拖拽

如果需要多个值, 自定义指令也可以传入一个 js 对象字面量, 只要是合法类型的 js 表达式都是可以的

<div id="app">
    <div v-test="{msg: 'hello', name: 'abc'}"></div>
</div>
<script>
    Vue.directive('test', {
        bind (el, binding, vnode) {
            console.log(binding.value.msg);
            console.log(binding.value.name);
        }
    });

    var app = new Vue({
        el: '#app'
    })
</script>

推荐阅读更多精彩内容