Vue + Webpack

Vue简介

Vue是一款用于动态构建用户界面渐进式JavaScript框架, 核心功能是基于 MVVM 架构模式的视图模板引擎

MVVM

MVC ( Model-View-Controller )和 MVVM (Model-View-ViewModel) 都是前端开发的架构模式, MVVM 的本质是 MVC改进版

MVVM的出现

在HTML5兴起前, 标准的Web应用程式由三部分组成:

  1. View: 把数据以某种方式呈现给用户
  2. Model: 数据模型, 就是从后台接收到的数据
  3. Controller: 接收并处理用户的请求, 返回对应的 Model 给用户

当时Web应用的 View 层相对比较简单, 前端所需要的数据基本在后端可以处理好 ( 这也是一些 老项目前后端不分离 的原因 ), View 层主要负责数据展示, 而复杂的业务逻辑由 Controller 来处理, MVC架构模式是当时Web应用的最佳实践

随着H5的兴起, H5为移动设备提供了一些非常有用的功能( API ), 使得H5具备开发App的能力, 实现跨平台, 快速迭代和上线, 节省人力成本并提高开发效率, 但同时也使得 View 层不再是简单的数据展示, 还得管理复杂的数据状态以及处理移动设备的各种操作行为, MVC发展成:

  1. View: 展示数据, 整体UI布局
  2. Model: 接收并管理数据
  3. Controller: 响应用户操作, 并将 Model 更新到 View 上

这种 MVC 架构模式对于简单的应用是OK的, 也符合软件架构的分层思想, 但随着H5的发展, 前端应用的复杂程度越发复杂, 前后端逐渐分离, 而前端开发暴露出三个痛心的问题:

  1. 开发者在代码中大量调用相同的 DOM API, 处理繁琐, 操作冗余, 代码难以维护
  2. 大量的DOM操作是的页面渲染性能降低, 加载速度变慢, 影响用户体验
  3. 当 Model 频繁变动, 开发者需要主动更新到 View; 同样, 当用户操作导致数据发生变化, 开发者需要主动将更新后的数据同步到 Model 中, 工作繁琐且难以维护复杂多变的数据状态

此时, 早期提倡的 “Write less, do more”的 jQuery 就是为了简化DOM操作而设计的, 但这仅仅解决了第一个问题, 前端开发依旧繁琐而艰难

MVVM 的出现, 完美解决了以上三个问题

MVVM 由 Model, View, ViewModel 组成:

  1. Model: 数据模型, 定义修改数据和处理业务逻辑
  2. View: 视图(UI组件), 负责将数据模型转化成UI展现给用户
  3. ViewModel 是一个同步 View 和 Model 的对象(核心), 通过双向数据绑定把 View 和 Model 连接起来, 且 View 和 Model 的同步工作是完全自动的, 无需人为干涉, 因此开发者只需关注业务逻辑, 不再需要主动操作DOM, 也无需担心数据状态的同步问题, 所有繁琐艰难的操作全交给 ViewModel 实现

MVVM架构模式

ViewModel 是 Vue.js 的核心, 是Vue的一个实例

DOM Listeners (DOM监听器) 和 Data Bindings (数据绑定) 是 View Model 实现双向绑定的关键:

  1. DOM Listeners 会监测页面上 DOM 元素的变化, 更新 Model 中的数据
  2. Model 中数据更新时, Data Bindings 会刷新页面中的 DOM 元素

ViewModel核心实现

在MVVM架构模式下, ViewModel 之间没有 直接的联系, 而是通过 ViewModel ( 简称: VM ) 进行交互, 而 VM 可以通过 观察者模式 或者 订阅发布模式 2种模式实现

Vue.jsvm实例 的核心是 对象劫持, 采用 Object.definePropertygettersetter, 结合 观察者模式 实现 对象劫持 ( defineProperty )模板编译 ( complier ) 的响应

Vue核心

  1. Observer 数据监听器( 观察者 ), 内部通过 Object.definePropertygettersetter实现, 对劫持对象的所有属性进行监听, 如有变动获取最新值并通知订阅者( Watcher )
  2. Compile 指令解析器, 对每个DOM元素节点的指令进行扫描和解析, 根据指令模板替换数据, 并绑定响应的更新( 回调 )函数
  3. Watcher 订阅者, 连接 ObserverCompile, 订阅并接收每个属性的变动通知, 执行对应的更新( 回调 )函数
  4. Dep 消息订阅器( 公众号 ), 内部维护一个数组, 收集所有订阅者( Watcher ), 数据变动执行Dep.notify()函数, 再调用订阅者的Watcher.update()方法
Vue 工作流程

当执行new Vue()时, VM实例进入初始化阶段, 一方面遍历实例data配置项中的属性并劫持, 监听劫持属性; 另一方面, Compile 编译器对元素节点的指令进行扫描和解析, 初始化视图, 并订阅 Watcher 来更新视图, Watcher 将自己添加到 Dep 管理的数组中, 初始化完毕

当数据发生变化时, Ovserversetter 方法被触发, setter 会立即调用Dep.notify(), Dep开始遍历所有订阅者(Watcher), 并调用Watcher.update(), 订阅者接收到哦通知后对视图进行响应的更新

了解Vue的渐进式

符合MVVM架构模式的前端三大框架: AngularReactVue

其中 Vue 是国内中小型项目首选的框架, Vue 的实现:

  1. 借鉴了 Angular模板数据绑定技术
  2. 借鉴了 React组件化虚拟DOM技术

Vue 是一款渐进式框架, 即本身有一个轻量级的核心库( Vue.js Core ), 核心库只关注视图层( View ), 开发者在构建界面时可根据需要添加插件( Vue 全家桶)或者第三方库进行拓展, 而且 Vue 全家桶是互相独立的功能, 在核心库的基础上可任意选用, 不一定要全部整合在一起

Vue的渐进式

声明式渲染

Vue 和 jQuery 对比

使用对比1

需求: 数据回显, 将后台返回的数据( 伪数据 )渲染到页面上

jQuery

<body>
姓名: <span id="name"></span><br>
年龄: <span id="age"></span><br>
班级: <span id="className"></span><br>
</body>
<script src="js/jquery.min.js"></script>
<script>
    let data = {
        name: 'Tenderness',
        age: 18,
        className: 'electronic commerce'
    };
    $('#name').text(data.name);
    $('#age').text(data.age);
    $('#className').text(data.className);
</script>

Vue

<body>
<div id="box">
    姓名: {{name}}<br>
    年龄: {{age}}<br>
    班级: {{className}}<br>
</div>
</body>
<script src="js/vue.min.js"></script>
<script>
    let data = {
        name: 'Tenderness',
        age: 18,
        className: 'electronic commerce'
    };
    new Vue({
        el: '#box',
        data
    })
</script>

使用对比2

需求: 数据绑定, 将用户输入信息直接回显到页面上

jQuery ( 在以下demo中, jQuery只能做到储存数据和回显数据, 并未实现数据绑定 )

<body>
<input type="text" id="user"><br>
你输入的是: <span id="text"></span>
</body>
<script src="js/jquery.min.js"></script>
<script>
    let data = {
        msg: ''
    };
    $('#user').on('input', function () {
        data.msg = $(this).val();
        let user = data.msg
        $('#text').text(user);
    })
</script>

Vue

<body>
<div id="box">
    <input type="text" v-model="msg"><br>
    你输入的是: {{msg}}
</div>
</body>
<script src="js/vue.min.js"></script>
<script>
    new Vue({
        el: '#box',
        data: {
            msg: ''
        }
    })
</script>

对比结论

通过以上两简单的使用对比可以发现, 使用 Vue 能大量减少 DOM操作 并实现 数据双向绑定

Vue实例

语法: var vm = new Vue({ /*选项*/ })

在创建Vue实例时, 可以传入一个 配置对象, 以下列表是配置对象可选的 配置项

数据相关配置项

配置项 接收类型 描述
data Object/Function Vue实例的数据对象
props Array/Object 接收来自父组件的数据
computed {[key: String]: Function} 储存计算属性的对象
methods {[key: String]: Function} 储存实例方法的对象
watch {[key: String]: Function/Object} 储存监测函数的对象

注意点:

  1. 在定义 组件 时, data 只能接收Function, 该函数返回真正的数据对象, 原因是引用数据共享问题

  2. 对于 props 的描述, 放在 Vue组件 部分

  3. computed 对象中的方法需要有返回值, 该返回值就是计算属性值

  4. watch 对象中的key键在 data 中必须有对应的key键一一对应, 绑定的监测函数接收2参数, 第一个参数是监测属性的当前值(变化后的值), 第二个参数表示监测属性旧值(变化前的值)

  5. methodscomputedwatch 三者的区别:

    a) methods 中的方法是经常 被实例主动调用 的方法

    b) computed 中的方法在 实例初始化 时会 立即执行, 得到返回值(计算属性), 并混入到实例中缓存 起来, 当依赖的 响应式属性 发生变化, 会 自动执行 对应方法, 并把最新计算属性值缓存(更新)

    c) watch 中的方法在 实例初始化 后会 自动监测 data 中的数据变化, 当监听的数据发生变化 自动执行 对应监测函数, 实现相关业务逻辑( 如数据存储等 )

  6. methodscomputedwatch 对象中的方法不建议使用 箭头函数, 原因是this指向发生变化, 不再指向Vue实例
  7. computed 中的对象用法( 即gettersetter)

    // 首先, 计算属性默认只有getter, 不过可以通过对象的写法为计算属性提供一个setter
    var vm = new Vue({
        data: {
            firstName: 'Tenderness',
            lastName: 'Chen'
        },
        computed: {
            fullName: {
                get() {
                    return this.firstName + ' ' + this.lastName
                },
                set(newVal) {
                    // 当计算属性fullName放生改变时, vm.firstName和vm.lastName也会响应更新
                    let newName = newVal.split(' ');
                    this.firstName = newName[0];
                    this.lastName = newName[newName.length - 1]
                }
            }
        }
    })
    
  8. watch 中的键值对(k:v), 值为对象的使用方法

    var vm = new Vue({
        data: {
            a: '基本数据类型',
            b: [
                {name: '对象1', age: 16},
                {name: '对象2', age: 17},
                {name: '对象3', age: 18},
            ]
        },
        watch: {
            a: {
                handler(val, oldVal) {
                    console.log('val: ', val);
                    console.log('old: ', oldVal);
                },
                immediate: true // 绑定监测函数后, 立即执行一次
            },
            b: {
                handler(val, oldVal) {
                    // 引用类型数据的性质(都是指针), val和oldVal是一样的值
                    console.log('val: ', val);
                    console.log('old: ', oldVal);
                },
                deep: true // 深度(递归)监测, 当数组b中对象内属性发生变化时会执行监测函数
            }
        }
    })
    

DOM相关配置项

配置项 接收类型 描述
el String/Element 提供 一个 页面中已存在的DOM元素作为 Vue 实例的挂载目标
template String 提供一个字符串模板作为 Vue 实例的标识使用
render Function 使用render函数创建虚拟DOM

注意点

  1. el 相关:

    a) el 只在由new关键字创建的实例中遵守, 建议使用 id选择器 与实例一一对应

    b) 提供的元素只作为 挂载点, 所有的挂载元素会被 Vue 生成的 DOM 替换, 因此 禁止 将实例挂载到<html>或者<body>

    c) 当 templaterender 都不存在, 提供的挂在元素会被提取出来作为模板, 此时需要使用 Runtime + Compiler (运行时 + 编译器) 构建的 Vue 库

    d) 当 实例挂载 完成, 根元素 可以通过vm.$el访问

    e) 如果有 el 配置项, 实例会立即进入编译过程, 否则需要调用vm.$mount()手动开启编译

  2. template相关

    a) 模板会 替换 挂载元素, 挂载元素的内容将被忽略, 除非模板内容有分发 插槽

    b) 模板内容需要有一个 根元素 包裹着

    c) 建议模板和 JavaScript 代码分离, 常用技巧:

    <!-- 方式1 -->
    <script type="text/x-template" id="myTemp">
        <div>
            <!-- 模板内容 -->
        </div>
    </script>
    
    <!-- 方式2 -->
    <template id="myTemp">
        <div>
            <!-- 模板内容 -->
        </div>
    </template>
    
    <!-- 
    注意: 移上2中方法中, script 和 template 标签都只做环境, 不会在页面上渲染, 模板内容需要有一层根元素包裹 
    -->
    
    <script>
        var vm = new Vue({
            template: `#myTemp`
        })
    </script>
    

    d) 如果 render 函数存在, template 配置项将被忽略

    e) 如果 render 函数不存在, 需要使用 Runtime + Compiler (运行时 + 编译器) 构建的 Vue 库

  3. render 相关

    a) 设置了 render 函数, Vue实例就不会从配置项 template 或者 el 提取的 innerHTML 作为模板编译渲染函数

    b) 对于 render 函数的更多描述, 放在 Vue+Webpack 部分.

其他配置项

在创建Vue实例时, 还可以接受更多配置项, 如 生命周期钩子自定义指令组件混入 等, 对于这些配置项的描述, 将在对应部分描述

模板语法

Vue.js 使用了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据, 实现动态 html 页面, 主要包含2类: a) Mustache语法(双大括号), 放在元素的文本内容中 b) 指令, 以v-开头放置在元素标签属性

Mustache语法

往元素文本内容中 插值

<body>
<div id="app">
    <p>姓名: {{name}}</p>
    <p>年龄: {{age}}</p>
    <!-- 支持简单的JS表达式, 但不支持语句 -->
    <p>性别: {{age?'男':'女'}}</p>

    <!-- 显示结果:
            姓名: Tenderness
            年龄: 18
            性别: 男
    -->
</div>
<script src="js/vue.min.js"></script>
<script>
    var vm = new Vue({
        el: '#app',
        data: {
            name: 'Tenderness',
            sex: 1,
            age: 18
        }
    })
</script>
</body>

指令

  1. v-htmlv-text, 与innerHTMLinnerText用法类似

    注意点:

    a) 在网络较差环境下, 使用v-text代替Mustache语法能避免未编译的双大括号表达式直接渲染在页面上的问题, 但不推荐, 建议使用v-cloak解决

    b) 谨慎使用v-html, 原因和innerHTML一样, 容易导致 XSS 攻击

    c) v-html内部结构内容不会作为 Vue 模板进行编译, 因此在单文件组件( 即 .vue 文件)中, scoped的样式不会应用在v-html内部

  2. v-cloak这个指令保持在元素上直到关联实例结束编译

    注意点:

    a) 不需要表达式, 相当于元素的一个属性

    b) 配合CSS使用可以隐藏未编译的Mustache标签

    <head>
        <meta charset="UTF-8">
        <title>v-cloak演示</title>
        <style>
            [v-cloak] {
                display: none;
            }
        </style>
    </head>
    <body>
    <div id="app" v-cloak>
        <p>姓名: {{name}}</p>
    </div>
    <script src="js/vue.min.js"></script>
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                name: 'Tenderness'
            }
        })
    </script>
    </body>
    
  3. v-show控制元素的显示隐藏

    注意点:

    a) v-show="true" 显示( 使用元素默认的display样式 ), v-show="false"隐藏(display: none)

  4. v-once只渲染元素或组件一次

    注意点:

    a) Vue实例挂载时, 渲染一次, 然后该元素或组件将被视为静态内容, 即响应性数据发生变化时该元素不会发生变化

  5. v-for列表渲染, 基于源数据多次渲染元素或模板块

    用法:

    <!-- 遍历数组数据进行渲染 -->
    <div v-for="(item, index) in array"></div>
    <!-- 遍历对象数据进行渲染 -->
    <div v-for="(val, key, index) in object"></div>
    <!-- 接收整数多次渲染 -->
    <div v-for="n in number"></div>
    

    注意点:

    a) 在组件中使用v-for, 必须加上key

    b) 在同一元素中v-forv-if不建议一起使用

  6. v-if条件渲染系列

    用法:

    a) v-if根据表达式的布尔值判断是否渲染该元素

    b) v-elsev-else-if配合v-if使用, 规则与if-else语句一样

    注意点:

    a) Vue会尽可能高效地渲染元素, 对已有元素会进行复用而不是重新渲染, 如果想让复用的元素重新渲染( 如用户切换登录方式时, input输入框重新渲染清空已输入值 ), 可以为复用元素加上key, 让元素重新渲染

    b) v-ifv-show, v-if条件渲染, v-show只是简单的 控制CSS样式 (元素已经被渲染); 两者相比, v-if有更高的切换开销, 而v-show有更高的初始渲染开销; 因此, 如果需要频繁切换使用v-show否则使用v-if

    c) v-ifv-for一起使用, 统一元素中v-for优先级比v-if高, 即每次循环都会进行v-if判断

  7. v-bind动态绑定特性或者组件prop到表达式

    语法糖: :

    用法:

    <!-- 绑定属性, Vue实例中data的imageSrc -->
    <img v-bind:src="imageSrc">
    <img :src="imageSrc">
    
    <!-- 绑定属性, 简单表达式 -->
    <img :src="'/basePath/images/'+imageSrc">
    
    <!-- 绑定class, 表达式为对象 -->
    <!-- 根据isActive布尔值判断是否加类名 -->
    <div :class="{active: isActive}"></div>
    
    <!-- 绑定class, 表达式为数组 -->
    <!-- 一次性添加多个类名 -->
    <div :class="[active, otherClass]"></div>
    <!-- 结合三目条件判断 -->
    <div :class="[isActive ? active : '', otherClass]"></div>
    <!-- 结合对象条件判断 -->
    <div :class="[{active: isActive}, otherClass]"></div>
    
    <!-- 绑定style, 表达式为对象 -->
    <div :style="{width: widthSize + 'px'}"></div>
    <!-- 绑定style, 表达式为数组 -->
    <div :style="[styleObjectA, styleObjectB]"></div>
    
    <!-- 绑定prop, "propName"需在my-comp组件中声明 -->
    <my-comp :propName="someData"></my-comp>
    <!-- 绑定$prop, 将父组件的props传给子组件 -->
    <sub-comp :propName="$props"></sub-comp>
    
  8. v-on绑定DOM事件

    语法糖:@

    常见用法:

    <!-- 绑定单击事件, doSomething为methods中方法 -->
    <button v-on:click="doSomething"></button>
    <button @click="doSomething"></button>
    
    <!-- 事件对象, $event -->
    <button @click="doSomething($event)"></button>
    <!-- 不使用()括号, 默认会把$event传到dosomething的第一个参数中 -->
    <button @click="doSomething"></button>
    
    <!-- 绑定单次点击事件 -->
    <button @click.once="doSomething($event)"></button>
    
    <!-- 事件修饰符, 阻止默认行为 -->
    <input type="file" @click.prevent="prevent($event)">
    
    <!-- 事件修饰符, 阻止默认行为, 没有表达式 -->
    <form type="file" @submit.prevent>
    
    <!-- 事件修饰符, 阻止冒泡 -->
    <div type="file" @click.stop="stop($event)"></div>
    
    <!-- 键修饰符, enter/键代码(keyCode) -->
    <input type="text" @keydown.enter="onEnter($event)">
    <!-- Vue封装的键修饰符
            .enter
            .tab
            .delete (捕获delete键和backspace键)
            .esc
            .space
            .up
            .down
            .left
            .right
    -->
    
    <!-- 系统修饰符
            .ctrl
            .alt
            .shift
            .meta(Mac系统的Command键, Windows系统的WIN键)
    -->
    
    <!-- 鼠标修饰符
            .left
            .right
            .middle
    -->
    
    <!-- .exact修饰符
            允许你控制由精确的系统修饰符组合触发的事件
    -->
    

    组件中的用法:

    <!-- 自定义事件, 父组件不传参数时不能加(), 否则接收不了子组件传来的参数, 
         具体请查看 组件数据通信 章节 -->
    <my-comp @myEvent="handleEvent"></my-comp>
    
    <!-- 给组件根元素绑定事件 -->  
    <my-comp @click.native="onClick"></my-comp>
    

    其他用法(事件捕获, 移动端优化等):

    <!-- 添加事件监听器时使用事件捕获模式 -->
    <!-- 即元素自身触发的事件先在此处理,然后才交由内部元素进行处理 -->
    <div v-on:click.capture="doThis"></div>
    
    <!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
    <!-- 即事件不是从内部元素触发的 -->
    <div v-on:click.self="doThat"></div>
    
    <!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
    <!-- 而不会等待 `onScroll` 完成  -->
    <!-- 一般用于提升移动端性能 -->
    <!-- 不要和 .prevent 一起使用, 因为 .passive 会忽略 .prevent -->
    <div v-on:scroll.passive="onScroll"></div>
    
  9. v-model表单元素双向数据绑定

    对于不同元素的表现:

    a) text类型 和 textarea元素 使用 value 属性和 input 事件

    b) checkbox类型 和 radio类型 使用 checked 属性和 change 事件

    c) select 元素将 value 作为 prop 并将 change 作为事件

    注意点:

    a) 绑定了v-model的表单元素将忽视valuecheckedselected初始值并始终与Vue实例的数据双向数据绑定

    b) 对于文本输出框, 可通过.lazy修饰符把input事件转为change事件进行数据同步

    c) 文本输入框.trim修饰符会自动删除用户输入的value字符串的首尾空白字符

    d) 文本输入框.number修饰符会尝试将用户输入的value字符串通过parseFloat()方法转成数字, 如果无法转换, 返回原始值

数组和对象的更新监测(响应性修改)

数组

由于 JavaScript 的限制, Vue 不能监测以下数组的变动:

  1. 利用索引值修改数组元素, vm.array[index] = newValue
  2. 改变数组长度, vm.array.length = newLength

但支持监测数组的一些变异方法( mutation method )以及非变异方法( 返回新数组, 替换数组 )

变异方法

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

替换数组( 非变异方法 )

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

Vue自带方法修改数组

  • Vue.set(vm.items, index, newValue)
  • vm.$set(vm.items, index, newValue)

对象

还是由于 JavaScript 的限制,Vue 不能检测对象属性的添加或删除

var vm = new Vue({
  data: {
    a: 1
  }
})
/*
在控制台中输入
    vm.a = 2
    视图会响应, a是响应属性
    vm.b = 1
    视图不会响应, b不是响应属性
*/

Vue提供2种方法修改对象

  • Vue.set(object, key, newValue)
  • vm.$set(object, key, newValue)

注意: 当Vue实例创建后, 不允许动态添加根级响应属性, 即

var vm = new Vue({
  data: {
    a: 1
  }
});

Vue.set(vm.b, 'b', 2); // 会报错, 不允许添加根级响应属性

如需给已有响应对象添加多个属性:

var vm = new Vue({
  data: {
    person: {name: 'Tenderness'}
  }
});

Object.assign(vm.person, {
    age: 18,
    class: 'H5'
}); // 这样vm.person的引用不会变, 即对原对象 person 添加属性, 不会响应

// 应该这样做
vm.person = Object.assign({}, vm.person, {
    age: 18,
    class: 'H5'
});

Vue自带的delete方法

Vue提供了delete方法, 通过该方法删除响应式的数组或对象中的成员可以更新视图, 但是开发者应该 尽量少用该方法, 使用方法如下:

  • Vue.delete(array, index)vm.$delete(array, index), 删除数组元素
  • Vue.delete(object, key)vm.$delete(object, key), 删除对象成员

Vue组件

模块化与组件化

首先模块化和组件化开发都为了代码复用, 方便维护

  1. 模块化是从 代码逻辑 层面上划分( 一般就是 JS 代码的封装 ), 功能解耦
  2. 组件化是从整个 UI页面布局 层面上划分( 包含HTML, CSS, JavaScript ), 更像是模块化的进一步封装, 封装隔离, 是一个具有一定功能特性的独立个体

组件化历史

  1. 前期的 UI组件库 ( JS组件库 ), 通过JS代码控制HTML+CSS+JavaScript, JS负责做的事情太多( 即大量JS代码 ), 不好维护
  2. 组件化开发, 单文件组件( template 模板编译 + CSS + JavaScript ), 在Vue中即 .vue 文件

Vue组件注册和使用

Vue组件的本质也是 Vue 的一个实例, 创建时可以设置所有 Vue 实例的配置项

全局组件注册

注意, 全局组件需要在实例创建之前注册

// 组件的名称可以用 kebab-case(烤串形式)、PascalCase(大驼峰形式)命名 
Vue.component('gobal-comp', {
    template: `<div>全局组件</div>`
})

局部组件注册

var myComp = {
    template: `<div>局部组件</div>`
}

var vm = new Vue({
    el: '#app',
    components: {
        // 建议使用大驼峰形式命名, 因为对象key键如果含有'-', 需要加上引号
        PrivateComp: myComp // 可以给局部组件起别名
    }
})

组件使用(复用)

<div id="app">
    <!-- 组件标签的使用可以建议用烤串形式书写, 因为HTML元素标签不分大小写 -->
    <!-- 不要使用H5内置标签名作为组件的名字, 防止浏览器识别错误 -->
    <gobal-comp></gobal-comp> <!-- 全局组件使用 -->
    <private-comp></private-comp> <!-- 局部组件使用 -->
    <private-comp></private-comp> <!-- 组件复用 -->
</div>

组件嵌套

局部注册的组件在其子组件中不可用, 如需在子组件中使用其他局部组件, 需要使用组件嵌套

var CompA = {
    template: `<<div>CompA</div>>`
}
var CompB = {
    template: `<<div><comp-a></comp-a></div>>`,
    // CompB组件不能直接使用CompA组件, CompA需要在CompB中注册绑定才能正常使用
    // 即局部组件在哪注册(绑定), 只能在哪使用
    components: {
        CompA
    }
}
var vm = new Vue({
    el: '#app',
    components: {
        CompA,
        CompB
    }
})

组件数据通信

组件父向子传递数据

父组件使用子组件时, 通过v-bind:propName传递数据, 子组件在配置项props中接收数据

<div id="app">
    <!-- 使用MyComp组件, 传递实例data对象中的name和age数据 -->
    <my-comp :name="name" :age="age"></my-comp>
</div>

<template id="myComp">
    <div>
        这是 MyComp 组件, 接收数据 {{name}}, {{age}}
        <!-- 使用SubComp组件, 把MyComp从父组件接收的props对象传递到SubComp的data中 -->
        <sub-comp :data="$props"></sub-comp>
    </div>
</template>

<template id="sub">
    <div>
        这是 SubComp 组件, 接收数据 {{data}}
        <!-- 将接收的data渲染到组件上 -->
        <p>名字: {{data.name}}</p>
        <p>年龄: {{data.age}}</p>
    </div>
</template>

<script src="js/vue.min.js"></script>
<script>
    var SubComp = {
        template: '#sub',
        // SubComp组件接收 data 数据
        props: ['data']
    };

    var MyComp = {
        template: '#myComp',
        // MyComp组件接收 name 和 age 俩数据
        props: ['name', 'age'],
        // MyComp组件绑定(嵌套)子组件SubComp
        components: {
            SubComp
        }
    };

    var vm = new Vue({
        el: '#app',
        components: {
            // 实例绑定局部(子)组件MyComp
            MyComp
        },
        data: {
            name: 'Tenderness',
            age: 18
        }
    })
</script>

props配置项详细描述

首先, 传递的prop(数据名字)命名规则(大小写)与 组件命名 一样, 因为HTML元素特性名大小写不敏感

prop传递数据细节:
  1. prop传递动态数据, 必须加上v-bind:, 表示引号中数据的是一个变量而不是字符串

  2. prop传递静态数据

    <!-- 传递静态简单字符串, 不需要v-bind: -->
    <!-- 将str静态字符串传递给SubComp组件 -->
    <sub-comp str="这是静态字符串"></sub-comp>
    <!-- 当传递的字符串需要拼接, 需要加上v-bind:, 让Vue将数据解析成表达式 -->
    <!-- 静态字符串拼接时, 注意单引号和双引号的使用, 推荐里单外双 -->
    <sub-comp :str="'字符串拼接'+'这是静态字符串'"></sub-comp>
    
    <!-- 传递其他静态数据类型时, 需要加上v-bind:, 让Vue正确解析数据类型 -->
    <!-- 传递数字 -->
    <sub-comp :num="520"></sub-comp>
    <!-- 传递数组 -->
    <sub-comp :arr="['item1', 'item2']"></sub-comp>
    <!-- 传递对象 -->
    <sub-comp :obj="{name: 'Tenderness', age: 18}"></sub-comp>
    <!-- 传递布尔值 -->
    <!-- 特殊情况: prop没有值时, 表示传递true, 不需要v-bind: -->
    <sub-comp is-active></sub-comp>
    <!-- 等同于 -->
    <sub-comp :is-active="true"></sub-comp>
    
  3. prop传递一个对象的所有属性

    如果你想要将一个对象的所有属性都作为 prop 传入,你可以使用不带参数的 v-bind (取代 v-bind:prop-name), 因此 组件父向子传递数据 中代码可以改造成:

    <template id="myComp">
        <div>
            这是 MyComp 组件, 接收数据 {{name}}, {{age}}
            <sub-comp v:bind="$props"></sub-comp>
        </div>
    </template>
    
    <template id="subComp">
        <div>
            这是 SubComp 组件, 接收数据 {{name}}, {{age}}
            <p>名字: {{name}}</p>
            <p>年龄: {{age}}</p>
        </div>
    </template>
    
    <script>
         var SubComp = {
            template: '#sub',
            props: ['name','age']
        };
    </script>
    
props配置项验证数据

在 SubComp 组件中, props配置项接收2数据nameage, 并期望name是 string 类型, age是 number 类型, 但通过 数组 的方式接收, 只能做到 简单接收 , Vue并不会验证父组件所传来的数据, 如需定制 prop 验证方式, 需要通过 对象 的方式验证接收:

var SubComp = {
    template: '#sub',
    props: {
        name: String,
        age: Number
    }
};
/* 类型检查通过构造函数, 本质是通过 instanceof 进行验证, 
    注意: 设置为 null 和 undefined 会通过任何类型检测
    原生构造函数:
    String
    Number
    Boolean
    Array
    Object
    Date
    Function
    Symbol
    自定义构造函数
*/

还可以为prop配置对象, 设置 必须值 (required)或者默认值(default):

var SubComp = {
    template: '#sub',
    props: {
        name: {
            type: String,
            // 必须值
            required: true
        },
        age: {
            // Number 或者 String 类型都能通过
            type: [Number, String],
            // 默认值
            default: 18
        },
        bestFriend: {
            type: Object,
            // 对象或者数组的默认值必须从一个工厂函数中获取
            default(){
                return {
                    name: 'Jacky',
                    age: 18
                }
            }
        },
        class:{
            // 自定义验证函数
            validator(value){
                return ['H5','Java','UI'].indexOf(value) !== -1
            }
        }
    }
};

注意点: props会在 组件实例创建之前 (beforeCreate)验证, 所以所有实例属性(包括datacomputed)在default或者validator中不可用

组件子向父传递数据
单向数据流

所有prop使得父子prop间形成一个 单向下行绑定, 即父级prop的更新会向下流动到子组件中, 但是反过来不行, 这样的处理能 防止 子组件意外改变父级组件的状态, 导致应用的数据流向难以理解

所以, 父级组件发生更新时, 子组件prop刷新保持最新值, 子组件不能直接操作prop

自定义事件

单向数据流就像是水顺流而下的道理, 但是我们可以通过泵让水逆流而上, 这就是 组件子向父传递数据 的原理, 而 自定义事件 就相当于这个泵, 以下是一个简易版的 todo-list 诠释组件子向父传递数据的过程

<div id="app">
    <input type="text" v-model="user" @keydown.enter="add">
    <!-- 父组件使用子组件时, 绑定自定义事件'del' -->
    <news-list :list="list" @del="del"></news-list>
</div>

<template id="list">
    <ul>
        <li v-for="(item,index) in list" :key="index">
            <p>{{item}}</p>
            <span @click="emitDel(index)">&times;</span>
        </li>
    </ul>
</template>

<script src="js/vue.js"></script>
<script>
    var NewsList = {
        template: '#list',
        props: {
            list: {
                type: Array,
                default() {
                    return []
                }
            }
        },
        methods: {
            emitDel(index) {
                // 子组件中触发'del'自定义事件, 并把index传递给父组件
                this.$emit('del', index)
            }
        }
    };

    new Vue({
        el: '#app',
        components: {
            NewsList
        },
        data: {
            user: '',
            list: []
        },
        methods: {
            add() {
                this.user && this.list.push(this.user);
                this.user = '';
            },
            del(index) {
                // 自定义事件处理父组件中list数组
                this.list.splice(index, 1);
            }
        }
    })
</script>
非父子组件传递数据

非父子组件传递数据可通过三种方式实现

  1. 中央事件总线
  2. 混入store模式
  3. vuex插件

本章节只介绍 中央事件总线 , 其余2方法在vuex中介绍

组件数据私有化

在介绍中央事件总线之前, 先说下组件数据私有化, 即 组件的data配置项必须是一个函数

data配置项中设置一个函数, 该函数返回真正的 数据对象, 即每次绑定注册组件时, 该组件实例的data数据对象都是一个新的引用, 即每个组件实例管理一个data数据对象的独立拷贝, 这样的处理能让组件 数据私有化, 复用时组件数据互不影响

组件数据私有化示例

<div id="app">
    <my-comp></my-comp>
    <my-comp></my-comp>
</div>

<template id="my-comp">
    <div style="border: 1px solid #000;display: inline-block;padding: 15px;">
        <p>组件私有num: {{num}}</p>
        <!-- 每个组件实例的add()方法, 只会对当前实例的私有num进行累加 -->
        <button @click="add">自增</button>
    </div>
</template>

<script src="js/vue.js"></script>
<script>
    var MyComp = {
        template: '#my-comp',
        data() {
            return {
                num: 0
            }
        },
        methods: {
            add() {
                this.num++
            }
        }
    };

    new Vue({
        el: '#app',
        components: {
            MyComp
        }
    })
</script>
中央事件总线

中央事件总线 本质也是通过 自定义事件 实现数据传递, 使用过程中 注意事件名字重复

vue bus 中央事件总线

<div id="app">
    <my-comp></my-comp>
    <my-comp></my-comp>
    <other-comp></other-comp>
</div>

<template id="my-comp">
    <div style="border: 1px solid #000;display: inline-block;padding: 15px;">
        <p>组件私有num: {{num}}</p>
        <button @click="add">自增</button>
    </div>
</template>

<template id="other-comp">
    <div>
        <!-- 点击第一个按钮时, 触发bus的addNum自定义事件 -->
        <button @click="emitAdd">MyComp组件的num自增</button>
        <div>
            <!-- input输入框v-model绑定OtherComp组件实例私有数据num -->
            <input type="number" v-model="num">
            <!-- 点击第一个按钮时, 触发bus的changeNum自定义事件 -->
            <button @click="emitChange">修改MyComp组件的num</button>
        </div>
    </div>
</template>

<script src="js/vue.js"></script>
<script>
    var bus = new Vue(); //全局公交车, 当前script环境下任何都能使用

    var MyComp = {
        template: '#my-comp',
        data() {
            return {
                num: 0
            }
        },
        methods: {
            add() {
                this.num++
            }
        },
        // 生命周期钩子函数, 当实例挂载完成后, 执行的函数
        mounted() {
            // 给bus监听addNum自定义事件, 用箭头函数改变this指向(当前实例), MyComp组件实例的私有Num自增
            bus.$on('addNum', () => this.num++);
            // 给bus监听changeNum自定义事件, 接收一个newVal参数, 并赋值给MyComp组件实例的私有Num
            bus.$on('changeNum', (newVal) => this.num = newVal)
        }
    };

    var OtherComp = {
        template: '#other-comp',
        data() {
            return {
                num: 0
            }
        },
        methods: {
            emitAdd() {
                // 触发bus的addNum自定义事件
                bus.$emit('addNum')
            },
            emitChange() {
                // 触发bus的changeNum自定义事件, 并把OtherComp的私有num传递过去
                bus.$emit('changeNum', this.num)
            }
        }
    };

    new Vue({
        el: '#app',
        components: {
            MyComp,
            OtherComp
        }
    })
</script>

插槽

插槽的作用

简单数据通信

在上一章节中, 我们提及了组件间的数据通信问题, 在组件嵌套层次较深的组件中, 父组件向后代组件传递数据时, 如果通过父向子传递, 则需要一层一层逐层传递和接收prop, 逻辑可能会出现混乱并且不好维护

插槽 的出现能 简化 层级较深组件中的父组件向后代组件传递数据的逻辑关系

传递简单数据

使用父向子prop传递
<div id="app" style="width: 500px;background: pink;padding: 15px;">
    <p>#app实例</p>
    <input type="text" v-model="msg">
    <span>v-model绑定#app实例msg</span>
    <!-- 将msg传递到MyComp中 -->
    <my-comp :msg="msg"></my-comp>
</div>

<template id="my-comp">
    <div style="background: skyblue;padding: 15px;margin: 15px;">
        <p>MyComp组件</p>
        <!-- 将接收过来的msg传递到SubComp中 -->
        <sub-comp :msg="msg"></sub-comp>
    </div>
</template>

<template id="sub-comp">
    <div style="background: white;padding: 15px;margin: 15px;">
        <p>SubComp组件</p>
        <p>#app实例传递过来的msg: {{msg}}</p>
    </div>
</template>

<script src="js/vue.js"></script>
<script>
    var SubComp = {
        template: '#sub-comp',
        // 接收MyComp传递过来的msg数据
        props: ['msg']
    };

    var MyComp = {
        template: '#my-comp',
        components: {
            SubComp
        },
        // 接收#app传递过来的msg数据
        props: ['msg']
    };

    new Vue({
        el: '#app',
        components: {
            MyComp
        },
        data: {
            msg: ''
        }
    })
</script>
使用slot插槽简化
<div id="app" style="width: 500px;background: pink;padding: 15px;">
    <p>#app实例</p>
    <input type="text" v-model="msg">
    <span>v-model绑定#app实例msg</span>
    <!-- 往MyComp组件插槽插入#app实例msg, 传递到SubComp组件中 -->
    <my-comp>{{msg}}</my-comp>
</div>

<template id="my-comp">
    <div style="background: skyblue;padding: 15px;margin: 15px;">
        <p>MyComp组件</p>
        <sub-comp>
            <!-- 提供插槽 -->
            <slot></slot>
        </sub-comp>
    </div>
</template>

<template id="sub-comp">
    <div style="background: white;padding: 15px;margin: 15px;">
        <p>SubComp组件</p>
        <!-- 提供插槽 -->
        <p>#app实例传递过来的msg: <slot></slot></p>
    </div>
</template>

<script src="js/vue.js"></script>
<script>
    // SubComp和MyComp组件不再需要props配置项接收msg
    var SubComp = {
        template: '#sub-comp'
    };

    var MyComp = {
        template: '#my-comp',
        components: {
            SubComp
        }
    };

    new Vue({
        el: '#app',
        components: {
            MyComp
        },
        data: {
            msg: ''
        }
    })
</script>
slot传递元素标签或者组件

slot传递元素或者组件

  1. slot传递元素标签

    <div id="app" style="width: 500px;background: pink;padding: 15px;">
        <p>#app实例</p>
        <input type="text" v-model="msg">
        <span>v-model绑定#app实例msg</span>
        <my-comp>
            <!-- 往MyComp组件插槽插入元素, 传递到SubComp组件中 -->
            <div style="background: yellowgreen;">
                <p>这是#app实例传来的p元素</p>
                <p>这是#app实例传来的msg: {{msg}}</p>
            </div>
        </my-comp>
    </div>
    
    <template id="my-comp">
        <div style="background: skyblue;padding: 15px;margin: 15px;">
            <p>MyComp组件</p>
            <sub-comp>
                <slot></slot>
            </sub-comp>
        </div>
    </template>
    
    <template id="sub-comp">
        <div style="background: white;padding: 15px;margin: 15px;">
            <p>SubComp组件</p>
            <slot></slot>
        </div>
    </template>
    
  2. slot传递组件

    <div id="app" style="width: 500px;background: pink;padding: 15px;">
        <p>#app实例</p>
        <input type="text" v-model="msg">
        <span>v-model绑定#app实例msg</span>
        <my-comp>
            <!-- 往MyComp组件插槽中插入SubComp组件 -->
            <sub-comp>
                <!-- 往SubComp组件插槽中插入元素 -->
                <div style="background: yellowgreen;">
                    <p>这是#app实例传来的p元素</p>
                    <p>这是#app实例传来的msg: {{msg}}</p>
                </div>
            </sub-comp>
        </my-comp>
    </div>
    
    <template id="my-comp">
        <div style="background: skyblue;padding: 15px;margin: 15px;">
            <p>MyComp组件</p>
            <slot></slot>
        </div>
    </template>
    
    <template id="sub-comp">
        <div style="background: white;padding: 15px;margin: 15px;">
            <p>SubComp组件</p>
            <slot></slot>
        </div>
    </template>
    
    <script src="js/vue.js"></script>
    <script>
        var SubComp = {
            template: '#sub-comp'
        };
    
        // MyComp 不再需要绑定SubComp组件
        var MyComp = {
            template: '#my-comp'
        };
    
        // #app实例中绑定MyComp和SubComp组件
        new Vue({
            el: '#app',
            components: {
                MyComp,
                SubComp
            },
            data: {
                msg: ''
            }
        })
    </script>
    

插槽的后备内容(默认内容)

prop类似, slot可以设置默认内容

<div id="app">
    <form action="#">
        <input type="text">
        <!-- 显示默认内容提交 -->
        <my-btn></my-btn>
        <!-- 显示插入内容重置 -->
        <my-btn type="reset">重置</my-btn>
    </form>
</div>

<template id="my-btn">
    <button type="type">
        <slot>提交</slot>
    </button>
</template>

<script src="js/vue.js"></script>
<script>
    var MyBtn = {
        template: '#my-btn',
        props: {
            type: {
                type: String,
                default: 'submit'
            }
        }
    };

    new Vue({
        el: '#app',
        components: {
            MyBtn
        }
    })
</script>

具名插槽

在一个组件中, 我们可能需要提供多个插槽, 如一个简单的三栏布局:

<template id="my-header">
    <div style="display: flex;width: 360px;height: 50px;line-height: 50px;text-align: center;">
        <div class="left" style="width: 80px;height: 100%;background: pink">
            <!-- 期待插入左栏内容 -->
            <slot></slot>
        </div>
        <div class="center" style="flex:1;background: skyblue;">
            <!-- 期待插入中间部分内容 -->
            <slot></slot>
        </div>
        <div class="right" style="width: 80px;height: 100%;background: pink">
            <!-- 期待插入右栏内容 -->
            <slot></slot>
        </div>
    </div>
</template>

那么问题来了, 在使用时, 该怎么把对应内容插到对应位置呢

<div id="app">
    <!-- 尝试一下是否按顺序插入 -->
    <my-header>
        <span></span>
        <span></span>
        <span></span>
    </my-header>
</div>

匿名插槽

结果是, 三个<span>元素重复了三次

由以上结果可知, 有多少个匿名插槽, 插入的匿名内容就会重复多少次.

使用 具名插槽 进行改造:

<div id="app">
    <my-header>
        <span slot="left"></span>
        <span></span>
        <span slot="right"></span>
    </my-header>
</div>

<template id="my-header">
    <div style="display: flex;width: 360px;height: 50px;line-height: 50px;text-align: center;">
        <div class="left" style="width: 80px;height: 100%;background: pink">
            <!-- 期待插入左栏内容 -->
            <slot name="left"></slot>
        </div>
        <div class="center" style="flex:1;background: skyblue;">
            <!-- 期待插入中间部分内容 -->
            <slot></slot>
        </div>
        <div class="right" style="width: 80px;height: 100%;background: pink">
            <!-- 期待插入右栏内容 -->
            <slot name="right"></slot>
        </div>
    </div>
</template>

具名插槽

使用具名插槽改造后, 达到我们需要的显示效果

注意点:

  1. 具名插槽是通过<slot>标签的name属性和插入元素的slot特性相关联的
  2. 匿名插槽默认name属性为defalut, 可以省略

作用域插槽

插槽的编译作用域
<div id="app">
    <!-- 不插入内容, 使用默认内容, 在MyComp组件作用域寻找name -->
    <!-- 结果是"MyComp" -->
    <my-comp></my-comp>

    <!-- 不插入内容name, 在当前实例作用域寻找name -->
    <!-- 结果是"Tenderness" -->
    <my-comp>
        {{name}}
    </my-comp>
</div>

<script src="js/vue.js"></script>
<script>
    var MyComp = {
        template: `<div><slot>{{name}}</slot></div>`,
        data() {
            return {
                name: 'MyComp'
            }
        }
    };

    new Vue({
        el: '#app',
        components: {
            MyComp
        },
        data: {
            name: 'Tenderness'
        }
    })
</script>

小结: 父级模板里的所有内容都是在父级作用域中编译的; 子模板里的所有内容都是在子作用域中编译的

作用域插槽

就像组件子向父传递数据一样, 在插槽中, 我们有时也需要 从子组件中获取数据, 而这通信的桥梁就是具名插槽

子组件 <slot>标签中, 通过v-bind:propName传递数据

var MyComp = {
    // 注意, 传递的数据名字不能是H5元素标签的属性名
    template: `<div><slot :msg="msg"></slot></div>`,
    data() {
        return {
            msg: 'MyComp'
        }
    }
}

父组件 插入插槽内容时, 通过slot-scope特性接收数据

使用插槽建议始终使用完整的基于<template>的语法.

<div id="app">
    <my-comp>
        <template slot-scope="slotProps">
            {{slotProps.msg}}
        </template>
    </my-comp>
</div>

slot-scope特性接收的是包含插槽所有prop的对象, 一般取名slotProps接收该对象, 当然你也可以像变量名一样取任意名字

同时, slot-scope特性还支持 对象解构赋值

<div id="app">
    <my-comp>
        <template slot-scope="{msg}">
            {{msg}}
        </template>
    </my-comp>
</div>

v-slot指令

Vue从2.6.0开始, 废弃slotslot-scope特性( 目前还能使用 ), 用v-slot指令代替

注意, v-slot指令只能写在<template>标签上

  1. v-slot指令使用 具名插槽, 对简单三栏布局改造

    <div id="app">
        <my-header>
            <template v-slot:left>
                <span></span>
            </template>
            <!-- 不用v-slot指令, 默认是v-slot:default -->
            <template>
                <span></span>
            </template>
            <template v-slot:right>
                <span></span>
            </template>
        </my-header>
    </div>
    
  2. 使用语法糖#

    <div id="app">
        <my-header>
            <template #left>
                <span></span>
            </template>
            <template>
                <span></span>
            </template>
            <template #right>
                <span></span>
            </template>
        </my-header>
    </div>
    
  3. v-slot指令使用 作用域插槽

    组件模板:

    <template id="my-header">
        <div style="display: flex;width: 360px;height: 50px;line-height: 50px;text-align: center;">
            <div class="left" style="width: 80px;height: 100%;background: pink">
                <slot name="left" msg="左侧"></slot>
            </div>
            <div class="center" style="flex:1;background: skyblue;">
                <slot msg="中间"></slot>
            </div>
            <div class="right" style="width: 80px;height: 100%;background: pink">
                <slot name="right" msg="右侧"></slot>
            </div>
        </div>
    </template>
    

    具名插槽接收数据:

    <div id="app">
        <my-header>
            <template #left="{msg}">
                <span>{{msg}}</span>
            </template>
            <!-- 等同于 v-slot:default="{msg} 或者 #default="{msg} -->
            <template v-slot="{msg}">
                <span>{{msg}}</span>
            </template>
            <template #right="{msg}">
                <span>{{msg}}</span>
            </template>
        </my-header>
    </div>
    

Vue核心补充与进阶

生命周期

实例的生命周期钩子

每个Vue实例在被创建时会经过一系列的初始化过程, 在这些过程执行的函数叫做 生命周期钩子函数, 以下是 Vue 官方文档提供的生命周期图示

生命周期

生命周期钩子 描述和作用 操作数据(data) 操作元素(el)
beforeCreate() 劫持数据对象前 × ×
created() 劫持数据对象后 ×
beforeMount() 实例挂载前 ×
mounted() 实例挂载后
beforeUpdate() 数据对象更新前
updated() 数据对象更新后
beforeDestroy() 实例销毁前
destroyed() 实例销毁后 × ×

常用的钩子函数:

  1. created() 发起 ajax 异步请求, 获取数据
  2. mounted() 设置定时器
  3. beforeDestory() 清除定时器

注意点:

  1. 不要使用箭头函数来定义生命周期钩子函数, 因为生命周期钩子函数this指向当前 Vue 实例

  2. beforeUpdate钩子函数获取的是 更新前 的数据, updated获取的是 最新 的数据

  3. mountedupdated不会等待其 子组件 的挂载/重绘状态, 如需等待整个视图渲染完毕才调用对应的 钩子函数, 需要用vm.$nextTick替换对应钩子函数

    mounted() {
        this.$nextTick(function () {
            /* 全视图挂载后(rendered)执行的代码 */
        })
    },
    created() {
        this.$nextTick(function () {
            /* 全视图重绘(re-rendered)后执行的代码 */
        })
    }
    
  4. activateddeactivated两钩子在 路由 部分描述

生命周期相关实例方法

vm.$mount()

当创建 Vue 时, 没有el配置项, 实例处于 未挂载(beforeMount)状态, 需要调用vm.$mount()方法手动挂载

注意点:

  1. 该方法可以接受 元素(element) 或者 元素选择器(querySelector) 两种参数, 表示挂载(被替换)的元素
  2. 如果 不传参数, 表示模板将渲染成文档之外的元素( 相当于document.createElement() ) , 需要使用原生 DOM API 将它插入文档中( 如 appendChild,insertBefore )
  3. vm.$mount()返回vm实例本身, 所以支持链式操作, 如vm.$mount().$el访问根元素
vm.$destroy()

完全销毁一个实例, 清理它与其他实例的链接, 解绑它身上的所有指令和事件监听器

注意点, 该方法会同时触发beforeDestroydestroy两个钩子

vm.$forceUpdate()

强制让 Vue 实例 重新渲染, 仅影响当前实例及其插槽内容, 自她子组件不受影响, 一般不会使用.

vm.$nextTick([callback])

将回调函数延迟到 DOM 更新循环( 挂载/重绘 )之后执行

访问元素和组件

在使用 Vue 时, 不建议触达另一个组件实例内部或者手动操作 DOM 元素, 但确实存在这需求让我们的操作更便捷, 因此 Vue 也提供了几个Api:

  1. $root, 访问 根实例, 后代组件实例可以通过this.$root访问到根实例

  2. $parent, 访问 父组件, 与$root类似, 子组件实例可通过this.$parent访问其父组件

  3. $children, 访问当前实例的 直接子组件, 基本不用.

  4. $refs, 访问 子组件或子元素, 相对$root$parent而言比较常用, 常用于操作 DOM 元素

    <div id="app">
        <div>普通div</div>
        <!-- 实例模板中通过ref特性绑定需要访问的元素或者组件实例 -->
        <div ref="targetDiv">目标div</div>
    </div>
    
    <script src="js/vue.js"></script>
    <script>
        var vm = new Vue({
            el: '#app',
            mounted() {
                // 在父组件中, 通过vm.$refs访问对应的子元素或子组件
                // 只能在挂载后的生命周期钩子函数中操作访问
                console.log(this.$refs.targetDiv)
            }
        });
    </script>
    

事件接口API

  1. $on(eventName, eventHandler) 监听一个事件

  2. $once(eventName, eventHandler) 监听一个一次性事件

  3. $emit(eventName, [...args]) 触发实例事件

  4. $off([eventName, callback]) 移除事件监听器

    注意点:

    a) 没有参数, 表示移除所有事件监听器

    b) 只提供事件( eventName ), 移除该事件所有监听器

    c) 同时提供事件( eventName )和回调( callback ), 只移除这个回调的监听器

$watch接口进阶监测

语法: vm.$watch(exp|Fn, callback, [options])

示例 Vue 实例:

var vm = new Vue({
    data: {
        groupName: 'Vue电商项目',
        members: [
            {name: '成员1', wages: 6},
            {name: '成员2', wages: 8},
            {name: '成员3', wages: 15},
        ]
    }
});

watch配置项类似的监测

// 第一个参数为字符串, 表示监测的数据名称
// 第二个参数为处理函数
// 第三个参数为配置对象, 如 immediate 和 deep
vm.$watch('groupName', function (newVal, oldVal) {
    console.log('当前值:', newVal);
    console.log('旧值:', oldVal);
}, {immediate: true}); // 立即执行处理函数

vm.$watch('members', function (newVal) {
    console.log(newVal);
}, {deep: true}); // 深度监测

第一个参数接收一个 函数

vm.$watch(function () {
    // 相当于监测一个未定义的计算值( computed )
    return this.members.reduce((tar, cur) => {
        return tar + cur.wages
    }, 0)
}, function (newVal, oldVal) {
    console.log('当前值:', newVal);
    console.log('旧值:', oldVal);
})

$watch返回值

let unwatch = vm.$watch('groupName', handler);
// vm.$watch 返回一个取消监测函数, 用来停止触发回调( handler )
unwatch()

key特殊特性

key主要用在 Vue 的虚拟 DOM 算法, 接收一个number或者string类型的值, 在新旧 nodes 对比时辨识 VNodes. 如果不用key, Vue 会尽可能的复用相同的元素 重新渲染数据; 使用key后, Vue会 基于key的变化 重新排列元素顺序, 并移除key不存在的元素

key特性常与v-for指令搭配使用.

注意: 相同父元素的子元素key不能重复, 否则会造成渲染错误.

key也能用于强制替换元素/组件而不是复用, 比如:

  1. 完整地触发组件的生命周期钩子(beforeCreatedestroyed, v-if指令条件渲染)
  2. 触发过渡(<transition>或者transition-group组件)

自定义指令

除了内置指令外, Vue 支持开发者注册自定义事件对 Vue 进行拓展, 减少DOM操作

全局注册:

Vue.directive('指令名字', {/* 钩子函数 */})

局部注册:

new Vue({
    directives: {
        '指令名字': {/* 钩子函数 */}
    }
})

使用自定义指令:

<div id="#app">
    <div v-自定义指令名字></div>
</div>

注意点: 注册时不需要带v-, 使用时需要带上v-.

钩子函数

  1. bind(), 当指令绑定到元素时调用, 只执行一次, 一般进行 初始化 设置
  2. inserted(), 被绑定元素插入父节点时调用( 父节点需要在内存中存在, 不一定插入文档中 )
  3. update(), 所在组件VNode(虚拟DOM)更新时, 可能在子VNode更新前 调用
  4. componentUpdated(), 所在 组件及其子VNode全部更新后调用
  5. unbind(), 指令与元素解绑时调用, 只调用一次

钩子函数接收的参数

  1. el, 绑定指令的元素, 常用来直接操作DOM
  2. binding, 一个对象, 包含
    • name, 指令名, 不含v-前缀
    • value, 指令的绑定值/对象, 例如:v-my-directive="1 + 1" 中,绑定值为 2
    • oldValue, 指令绑定的旧值, 仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用。
    • expression, 指令绑定的表达式, 例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"
    • arg, 传给指令的参数, 例如 v-my-directive:foo 中,参数为 "foo"
    • modifiers, 包含修饰符的对象, 例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
  3. vnode, Vue 编译生成的虚拟节点
  4. oldVnode, 上一个虚拟节点, 仅在 updatecomponentUpdated 钩子中可用

注意点:

  1. 除了el, 其他参数都应该是只读的, 切勿进行修改.
  2. 如需在各钩子间共享数据, 通过元素的dataset(即元素上的data-*属性)实现, 语法: el.dataset.*

自定义指令实现输入框自动对焦

autofocus特性在 移动版Safari 中失效, 通过自定义指令让<input>输入框自动聚焦:

// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})

获取实例属性API

  1. vm.$data, 获取实例数据对象, 即data配置项
  2. vm.$props, 获取接收的props对象
  3. vm.$options, 获取实例的初始化选项, 一般用于获取自定义配置项
  4. vm.$slots, 访问 插槽分发 的内容, 以及每个具名插槽的相应属性(name)
  5. vm.$scopedSlots, 访问 作用域插槽
  6. vm.$attrs, 获取 父作用域 中, 不被props接收的特性(classstyle除外)
  7. vm.$listeners, 获取 父作用域 中的 事件监听器

过渡与动画

首先, Vue 不适合做过渡或者动画效果, 但 Vue 提供<transition><transition-group>两内置组件实现过渡与动画效果

Vue 在插入、更新、移除 DOM 时, 能触发过渡效果.

单元素/组件过渡

使用<transition>内置组件能让单元素/组件实现过渡效果, 以下是简单的案例

<head>
    <style>
        /* 使用过渡类名控制过渡样式 */
        .myTrans-enter-active, .myTrans-leave-active {
            transition: opacity .5s;
        }

        .myTrans-enter, .myTrans-leave-to {
            opacity: 0;
        }
    </style>
</head>
<body>
<div id="app">
    <button @click="isShow=!isShow">切换</button>
    <!-- v-if 和 v-show 都能触发过渡 -->
    <!-- 给transition组件设置name特性, 自定义组件过渡效果 -->
    <transition name="myTrans">
        <div v-if="isShow">过渡测试</div>
    </transition>
    <transition name="myTrans">
        <div v-show="isShow">过渡测试</div>
    </transition>
</div>
<script src="js/vue.js"></script>
<script>
    new Vue({
        el: '#app',
        data: {
            isShow: true
        }
    })
</script>
</body>

CSS过渡类名

在整个过渡的过程, 元素/组件会进入6个状态, v为对应<transition>组件的name特性值

  1. v-enter, 进场过渡的开始状态
  2. v-enter-active, 进场过渡的生效过程
  3. v-enter-to, 进场过渡的结束状态
  4. v-leave, 退场过渡的开始状态
  5. v-leave-active, 退场过渡的生效过程
  6. v-leave-to, 退场过渡的结束状态

过渡过程

总结:

  1. 一般v-enterv-leave-to状态是一致的, v-enter-tov-leave状态是一致的, 即可用并集选择器设置相同样式
  2. v-enter-activev-leave-active在进场/退场整个过程应用, 在这俩类名选择器中可以设置transition(过渡)或者animation(动画, 外面定义@keyframes) CSS属性

自定义过渡类名

自定义过渡类名 优先级 高于普通类名, 一般配合第三方 CSS 动画库使用(如 Animate.css )

对应的6个自定义类名:

  1. enter-class
  2. enter-active-class
  3. enter-to-class
  4. leave-class
  5. leave-active-class
  6. leave-to-class

配合 Animate.css 使用时, 需需要设置enter-active-classleave-active-class即可, 如

<div id="app">
    <button @click="isShow = !isShow">切换</button>
    <transition enter-active-class="animated fadeIn"
                leave-active-class="animated fadeOut">
        <div v-if="isShow">过渡测试</div>
    </transition>
</div>

过渡钩子

Vue提供的8个过渡钩子

  1. v-on:before-enter, 进场前状态
  2. v-on:enter, 进场过渡时
  3. v-on:after-enter, 进场结束状态
  4. v-on:enter-cancelled, 进场过渡取消时
  5. v-on:before-leave, 退场前状态
  6. v-on:leave, 退场过渡时
  7. v-on:after-leave, 退场结束状态
  8. v-on:leave-cancelled, 退场过渡取消时

注意:

  1. 8个钩子绑定的函数接收的第一个参数都是el, 即当前执行过渡效果的元素
  2. enterleave还接收第二个参数done回调函数, 表示 异步 调用, 当只用 JavaScript 过渡时必须调用, 否则两钩子函数同步调用, 过渡效果失效
  3. 仅适用 JavaScript 过渡时, 建议给需要过渡的元素设置v-bind:css="false"表示跳过CSS监测

列表过渡

<transition>组件只能设置单个元素/组件的过渡, 如需设置列表过渡, 如配合v-for指令, 则需要使用<transition-group>组件

注意点:

  1. <transition>组件在页面中不会被渲染成一个真正的元素, 而<transition-group>会被渲染成一个真正的元素

  2. tag特性, <transition-group>默认会被渲染成<span>元素, 可通过tag特性设置成其他元素, 如

    <!-- 指定为ul元素 -->
    <transition-group tag="ul">
        <li v-for="num in 10" :key="{{num}}">{{num}}</li>
    </transition-group>
    
  3. 内部元素 必须 设置key特性

过渡组件的其他特性

  1. duration设置过渡的持续时长

    // 单位毫秒(ms)
    <transition :duration="1000">...</transition>
    // 定制进场和退场的持续时间
    <transition :duration="{enter: 600, leave: 800}">...</transition>
    
  2. type指定监听的类型, Vue是通过监听transitionendanimationend事件判断过渡完成的, 当同时使用过渡(transition)和动画(animation)时, 需要设置type特性, 明确 Vue 监听的类型以判断过渡完成, 取值transitionanimation

  3. appear设置初次渲染的过渡, 与进场和出场过渡一样, 但只在元素/组件初次在页面中渲染时有效

    a) 自定义类名: appear-class,appear-active-class,appear-to-class

    b) 自定义狗子: @before-appear,@appear,@after-appear,@appear-cancelled

  4. mode设置过渡模式, 用于v-if,v-else-if,v-else渲染的多组件过渡, 取值in-out(新元素进场后当前元素退场), out-in(当前元素退场后新元素进场)

Vue路由

Vue RouterVus.js 官方的路由管理器, 利用 Vue.js + Vue Router 能便捷的创建一个单页面应用( SPA )

<div id="app">
    <!-- router-link组件进行导航 -->
    <!-- to特性设置跳转的路由路径 -->
    <router-link to="/home">首页</router-link>
    <router-link to="/about">关于</router-link>
    <!-- router-view组件设置路由出口 -->
    <!-- 路由匹配的组件将在这里渲染 -->
    <router-view></router-view>
</div>

<!-- 引入Vue核心库 vue.js -->
<script src="js/vue.js"></script>
<!-- 引入Vue Router插件 -->
<script src="js/vue-router.js"></script>
<script>
    // 定义组件
    var Home = {template: `<h1>首页</h1>`};
    var About = {template: `<h1>关于</h1>`};

    // 定义路由
    var router = new VueRouter({
        // 路由设置
        routes: [
            // 根路径处理
            {
                path: '/',
                redirect: '/home' // 网页重定向, 指向对应路由路径(path)
            },
            {
                path: '/home',
                // 将组件映射到对应路由中
                component: Home
            },
            {
                path: '/about',
                component: About
            }
        ]
    });

    var vm = new Vue({
        el: '#app',
        // 引入路由器
        router,
    })
</script>

mode配置项

mode配置项设置 Vue 路由的路径模式, 默认值"hash"

hash模式

hash模式 下, Vue使用 URL 的 hash 来模拟一个完整的 URL, 于是 URL 改变时, 页面不会重新加载

但很显然, 这样的路由路径是比较丑的, 所以 Vue Router 提供了 history模式, 将mode配置项设置为"history"

history模式

history模式 下, 路由路径更像正常的 URL, 但需要 后台配置支持, 否则会报 404 找不到资源

操作hash和history原生JS接口

  1. 操作hash(哈希)

    操作hash, 通过location.hash进行

    操作hash

  2. 操作history(历史记录)

    a) history.back()返回

    b) history.forward()前进

    c) history.go(number|URL)跳转到某历史记录

    d) history.pushState(data, title, URL), 其中data是对象, title是字符串, 添加(压栈)历史记录

    e) history.replaceState(data, title, URL), 用新的历史记录替换当前记录

router-link组件

<router-link>组件在实例挂载时会被渲染成<a>标签, 本质是对<a>标签的封装:

  1. 阻止<a>标签的默认跳转行为(e.preventDefalut = true)
  2. 根据mode配置项, hash模式 修改location.hash, history模式 调用history.pushState()

动态路由

Vue 路由也有符合 RESTful 设计风格的 动态路由, 如在 我的 路由中, 根据不同id匹配不同用户, 使用相同组件渲染页面:

  1. 定义我的组件

    var User = {
        template: `<div>用户</div>`,
        mounted() {
            console.log(this);
        }
    };
    
  2. 添加路由设置

    {
        // :id 就是动态路径参数
        path: '/user/:id',
        component: User
    }
    

设置完之后, 我们在路径上添加路径参数也能访问到对应路由组件了

动态路由

但是现在, 我们获取的仍是一个死的(静态)组件页面, 我们需要获取到 动态路径参数, 并渲染到页面上, 观察控制台打印的this( Vue 实例):

vm.$route

在实例的$route属性中, 我们可以通过vm.$route.params获取到我们需要的 动态路由参数, 于是对组件进行改造

var User = {
    template: `<div>用户 {{$route.params.id}}</div>`,
};

嵌套路由

除了 动态路由 外, 实际的应用界面通常由多层嵌套的组件组合而成, 如http://path/user/1/infohttp://path/user/1/wallet等, 这时候就要用到 嵌套路由

  1. 对我的组件进行改造

    var User = {
        template: `
        <div>
            <p>用户 {{$route.params.id}}</p>
            <router-link :to="'/user/'+$route.params.id+'/info'">详情</router-link>
            <router-link :to="'/user/'+$route.params.id+'/wallet'">钱包</router-link>
            <router-view></router-view>
        </div>`
    };
    
  2. 修改路由设置

    {
        path: '/user/:id',
        component: User,
        // 设置嵌套路由, children配置项和routes配置项是一样的数组
        children: [
            {
                // 注意, 以 / 开头的嵌套路由路径会被当做根路径, 所以不能加 / 
                path: 'info',
                component: Info
            }, {
                path: 'wallet',
                component: Wallet
            },
        ]
    }
    

命名路由

在使用 嵌套路由 时, 我们会发现, 随着嵌套的层次越深, <router-link>to特性就越复杂, 因此建议每个路由都带有name属性, 这就是 命名路由

  1. 对路由进行命名改造

    {
        path: '/user/:id',
        name: 'user',
        component: User,
        children: [
            {
                path: 'info',
                name: 'info',
                component: Info
            }, {
                path: 'wallet',
                name: 'wallet',
                component: Wallet
            },
        ]
    }
    
  2. <router-link>组件进行修改

    <!-- 根实例中 -->
    <!-- 通过动态to特性, 配置一个对象, 设置跳转的命名路由 -->
    <!-- 动态路径参数通过 params:{} 传递 -->
    <router-link :to="{name:'user',params:{id:1}}">我的</router-link>
    
    <!-- user组件中, 直接通过命名路由跳转 -->
    <router-link :to="{name:'info'}">详情</router-link>
    <router-link :to="{name:'wallet'}">钱包</router-link>
    

路由的样式类名

设置完路由后, 我们的页面是这样的

没有样式的路由

不看浏览器地址栏, 用户并不知道自己当前在哪个路由中, 用户希望是这种效果以确认自己的位置

有样式的路由

因此, 我们在跳转到/user/1/info后审查元素

路由类名

切换到/user/1/wallet

路由类名

Vue给我们当前的路由设置router-link-active类名, router-link-exact-active精准定位到当前嵌套路由中, 因此可以通过这2类名设置对应样式, 但是, 这两类名还是比较复杂的, Vue Router 提供了2配置项, 让开发者重命名这2类名

var router = new VueRouter({
    routes: [/* 路由设置 */],
    // 重命名2类名
    linkActiveClass: 'active',
    linkExactActiveClass: 'exActive'
});

重命名后, 我们就可以设置对应样式了

<style>
    .active {
        color: yellowgreen;
    }

    .exActive {
        color: red;
    }
</style>

编程式导航(JS跳转)

除了通过<router-link>组件的to特性实现路由跳转外, 我们还可以通过JS代码进行跳转, vm.$router就是我们当前使用的 router 实例

vm.$router

Vue Router 对 路由跳转 的方法进行了封装(兼容 hash模式history模式 )

  1. go(number), 跳转到具体路由, 参照当前记录前进或者后退多少步
  2. back(), 后退
  3. forward(), 前进
  4. push(location, onComplete, onAbort), 其中location表示路由路径, onCompleteonAbort是可选的2个 回调函数, 表示跳转成功和失败时调用的方法
  5. replace(location, onComplete, onAbort)

给一个按钮绑定点击事件实现跳转

<button @click="jump">跳转</button>

设置跳转方法

methods: {
    jump() {
        this.$router.push('/user/1')
        // 对于命名路由以下效果一样
        this.$router.push({name: 'user', params: {id: 1}})
    }
}

路由传参

除了传递 动态路径参数 外, Vue 路由还可接收类似于 GET 请求参数类似的 query

  1. 修改/home路由配置:

    {path: '/home', name: 'home', component: Home}
    
  2. <router-link>组件传递query对象

    <router-link :to="{name:'home',query:{msg:'hello'}}">首页</router-link>
    
  3. 在 Home 组件中获取query对象

    var Home = {
        template: `<h1>首页</h1>`,
        mounted() {
            console.log(this.$route.query);
        }
    };
    

命名视图

有时候, 我们需要一个路由页面同时显示多个视图组件, 而不是嵌套显示, 如头部搜索框SearchBar需要在路由/home/about中同时显示, 而/user中不需要显示, 此时就需要用到 命名视图

  1. 修改路由

    // 定义头部搜索框组件
    var SearchBar = {template: `<div>头部搜索框</div>`};
    
    var router = new VueRouter({
        routes: [
            {
                path: '/home',
                name: 'home',
                // 定义路由时, 不再只引入一个组件, components
                // 通过对象方式引入多个组件
                // default为必须项, 其他key可自定义, 与命名视图的name特性关联
                components: {
                    default: Home,
                    searchbar: SearchBar
                }
            },
            {
                path: '/about',
                components: {
                    default: About,
                    searchbar: SearchBar
                }
            }
        ]
    });
    
  2. 使用命名视图

    <div id="app">
        <router-link :to="{name:'home',query:{msg:'hello'}}">首页</router-link>
        <router-link to="/about">关于</router-link>
        <router-link :to="{name:'user',params:{id:1}}">我的</router-link>
        <!-- 使用命名视图, name不设置默认为default -->
        <router-view name="searchbar"></router-view>
        <router-view></router-view>
    </div>
    

props配置项

在组件中使用 $route 会使之与其对应路由形成 高度耦合,从而使组件只能在某些特定的 URL 上使用,限制了其灵活性。使用 props 配置项将组件和路由解耦:

var User = {
    // 模板直接使用id
    template: `
<div>
    <p>用户 {{id}}</p>
    <router-link :to="{name:'info'}">详情</router-link>
    <router-link :to="{name:'wallet'}">钱包</router-link>
    <router-view></router-view>
</div>`,
    // 定义组件时, 通过props接收动态路径参数id
    props: ['id'],
};

var router = new VueRouter({
    routes: [
        {
            path: '/user/:id',
            name: 'user',
            component: User,
            // 设置路由时, 配置props: true, route.params将会被接收成为组件实例的属性
            props: true,
            children: [
                {
                    path: 'info',
                    name: 'info',
                    component: Info
                }, {
                    path: 'wallet',
                    name: 'wallet',
                    component: Wallet
                }
            ]
        }
    ]
});

props的其他使用方法

  1. 对于包含 命名视图 的路由, 必须使用对象的形式给每个命名视图设置props

    // 为`/user/:id`路由添加一个订单命名视图
    var Order = {template: `<div>订单</div>`};
    
    //定义路由
    var router = new VueRouter({
        routes: [
            {
                path: '/user/:id',
                name: 'user',
                components: {
                    default: User,
                    order: Order
                },
                props: {
                    // 为每个命名视图配置props
                    default: true,
                    order: falue
                },
                children: [
                    {
                        path: 'info',
                        name: 'info',
                        component: Info
                    }, {
                        path: 'wallet',
                        name: 'wallet',
                        component: Wallet
                    }
                ]
            }
        ]
    });
    
  2. 没有命名视图 的路由可通过对象形式传递 静态参数

    var Test = {
        // 定义组件时接收并使用
        template: `<h1>{{msg}}</h1>`,
        props: ['msg']
    };
    
    var router = new VueRouter({
        routes: [
            {
                path: '/test',
                name: 'test',
                component: Test,
                // 传递静态参数
                props: {
                    msg: 'hello'
                }
            }
        ]
    });
    
  3. 函数 形式返回props, 高级用法

    <div id="app">
        <!-- 跳转路由时, 传入params和query -->
        <router-link :to="{name:'user',params:{id:1},query:{num:520}}">我的</router-link>
    
        <router-view></router-view>
        <router-view name="order"></router-view>
    </div>
    
    <script>
        var User = {
            // 在模板中直接使用
            template: `
        <div>
            <p>用户 {{id}}</p>
            <p>{{msg}}</p>
            <p>{{num}}</p>
            <router-link :to="{name:'info'}">详情</router-link>
            <router-link :to="{name:'wallet'}">钱包</router-link>
            <router-view></router-view>
        </div>`,
            // 定义组件时, 接收路径参数id, 请求参数num以及静态参数msg
            props: ['id', 'msg', 'num']
        };
    
        var router = new VueRouter({
            routes: [
                {
                    path: '/user/:id',
                    name: 'user',
                    components: {
                        default: User,
                        order: Order
                    },
                    props: {
                        // 定义路由时通过函数方式返回props
                        default: route => ({
                            id: route.params.id,
                            num: route.query.num,
                            msg: 'hello'
                        }),
                        order: false
                    },
                    children: [
                        {
                            path: 'info',
                            name: 'info',
                            component: Info
                        }, {
                            path: 'wallet',
                            name: 'wallet',
                            component: Wallet
                        }
                    ]
                }
            ]
        });
    </script>
    

命名视图传参

在命名视图<router-view>组件中通过v-bind:prop可传递参数到对应视图组件中

<div id="app">
    <router-view name="searchbar" msg="头部搜索框"></router-view>
</div>

<script>
    var SearchBar = {
        template: `<div>{{msg}}</div>`,
        props: ['msg']
    };
</script>

keep-alive组件和相关生命周期钩子

使用一个简单的路由系统介绍<keep-alive>组件

<div id="app">
    <!-- 使用导航组件和路由视图组件 -->
    <my-nav></my-nav>
    <router-view></router-view>
</div>

<template id="my-nav">
    <div>
        <router-link v-for="(item,index) in list" :to="{name:item.name}" :key="index">{{item.title}}</router-link>
    </div>
</template>

<script src="js/vue.js"></script>
<script src="js/vue-router.js"></script>
<script>
    // 设置导航组件
    var MyNav = {
        template: '#my-nav',
        data() {
            return {
                list: [
                    {name: 'home', title: '主页'},
                    {name: 'category', title: '分类'},
                    {name: 'topic', title: '话题'},
                    {name: 'cart', title: '购物车'},
                    {name: 'user', title: '我的'}
                ]
            }
        }
    };

    // 设置路由视图组件, 每个组件创建后发一个请求获取数据
    var Home = {
        template: `<div>主页</div>`,
        async created() {
            var res = await fetch('./data/home.json');
            var data = await res.json();
            console.log(data);
        }
    };
    var Category = {
        template: `<div>分类</div>`,
        async created() {
            var res = await fetch('./data/category.json');
            var data = await res.json();
            console.log(data);
        }
    };
    var Topic = {
        template: `<div>话题</div>`,
        async created() {
            var res = await fetch('./data/topic.json');
            var data = await res.json();
            console.log(data);
        }
    };
    var Cart = {
        template: `<div>购物车</div>`,
        async created() {
            var res = await fetch('./data/cart.json');
            var data = await res.json();
            console.log(data);
        }
    };
    var User = {
        template: `<div>我的</div>`,
        async created() {
            var res = await fetch('./data/user.json');
            var data = await res.json();
            console.log(data);
        }
    };

    // 设置简单路由
    var router = new VueRouter({
        routes: [
            {path: '/', redirect: {name: 'home'}},
            {path: '/home', name: 'home', component: Home},
            {path: '/category', name: 'category', component: Category},
            {path: '/topic', name: 'topic', component: Topic},
            {path: '/cart', name: 'cart', component: Cart},
            {path: '/user', name: 'user', component: User},
        ]
    });

    // 定义根实例
    var vm = new Vue({
        el: '#app',
        router,
        components: {
            MyNav
        }
    })
</script>

观察 Vue 官方提供的 Devtools (谷歌浏览器插件)

"主页"页面

"我的"页面

"话题"页面

"购物车"页面

"我的"页面

每次切换路由, <Anoymous Component>匿名组件就是我们<router-view>展示的组件, 再看一下网络请求以及控制台打印结果

网络请求

来回切换路由时, 会 重复 (重新)发起请求, 这无疑会消耗占用用户的宽带, 而且, 重新发起请求证明组件的created钩子函数重新触发, 即组件重新渲染( re-render ), 然而在大多情况下, 同一次会话( session )中, 页面一般不会有太大的改变, 即不需要重复请求获取数据, 此时我们需要用到<keep-alive>组件, 把组件缓存起来, 而不是让它重新渲染

<div id="app">
    <my-nav></my-nav>
    <!-- keep-alive组件和transition组件一样, 内部只能放置一个组件/元素 -->
    <keep-alive>
        <router-view></router-view>
    </keep-alive>
</div>

使用<keep-alive>组件后, 多次切换路由的 Devtools 和 控制台打印结果

keep-alive组件效果

首次切换 路由时, 加载对应视图组件并 缓存 起来; 再次切换 时, 当前视图组件进入activeated( 激活时调用 ), 已缓存的视图组件进入deactivated( 停用时调用 )

Webpack

webpack 是前端自动化构建工具, 核心概念:

  1. 入口( entry ), 工程的入口文件配置
  2. 输出( output ), 打包的输出文件配置
  3. 加载器( loader ), 用于处理不同类型的模块, 可拓展
  4. 插件( plugins ), 在打包过程中执行一些任务, 如清除打包目录, 复制静态文件等
  5. 模式( mode ), development开发模式, production生产模式

在 webpack 中, 所有静态资源都可以被处理为一个 模块, 包括图片、CSS、JS等等.

webpack与gulp、grunt区别

webpack是 模块化 打包工具, gulp、grunt是 流程化 任务工具

webpack 的工作方式: 将所有资源通过commonJS或者ES6模块化形式引入到主(入口)文件main.js中, 通过loader加载器将不同的文件资源处理成模块( 按需加载 ), 最后打包成浏览器识别的JS文件, 打包方式可通过命令行( 开箱即用 )或者 配置文件

gulp 的工作方式: stream流, 必须编写配置文件gulpfile.js, 创建任务task流式(pipe)处理每种资源还有项目依赖, 历遍源文件匹配规则打包( 不能按需加载 )

与webpack类似的工具

基于入口的打包工具: webpack、rollup、parcel

应用场景:

  • webpack 适用于大型复杂前端项目构建
  • rollup 适用于基础库(JS库)打包, 如vue, react
  • parcel 适用于简单的实验性项目, 会自动将使用到的依赖下载到本地

webpack 目前正在往 parcel 方向设计, 真正达到开箱即用

起步

全局安装

npm i -g webpack webpack-cli

本地项目安装

npm init -y
npm i -D webpack webpack-cli

命令行打包

webpack 本地文件 --output 输出目录地址文件, 其中--output可以简写成-o, 如

webpack src/main.js -o dist/bundle.js

配置文件打包

使用命令行打包需要我们每次都去编写并执行命令行, 所以一般我们执行配置文件webpack.config.js进行打包

// 配置文件仅支持common.js
// 需要使用node内置模块进行路径拼接
const path = require('path');

// 封装方法
function resolve(paths) {
    return path.join(__dirname, paths);
}

module.exports = {
    // 入口文件
    entry: resolve('./src/main.js'),
    // 输出目录文件
    output: {
        path: resolve('./dist'),
        filename: "bundle.js"
    }
};

然后在命令行中, 可直接使用webpack执行打包

webpack

或者, 在package.json配置script脚本

"scripts": {
  "start": "webpack"
}

然后在命令行中

npm start

配置文件

main.js就是我们整个项目打包的入口文件, 我们将会在这个文件中引入各种模块(包括图片, css, js), 我们只需在页面index.html引入bundle.js即可

html-webpack-plugin插件

使用html-webpack-plugin插件, 能让webpack在打包过程中自动将bundle.js引入到index.html

  1. 安装

    npm i -D html-webpack-plugin
    
  2. 使用

    const path = require('path');
    // 引入插件
    const HtmlPlugin = require('html-webpack-plugin');
    
    function resolve(paths) {
        return path.join(__dirname, paths);
    }
    
    module.exports = {
        // 开发者模式
        mode: 'development',
        entry: resolve('./src/main.js'),
        output: {
            path: resolve('./dist'),
            filename: "bundle.js"
        },
        // 使用插件
        plugins: [
            new HtmlPlugin({
                template: resolve('./src/index.html'),
                filename: "index.html"
            })
        ]
    };
    
  3. 运行npm start

热更新服务

现在我们每次更新代码都需要手动运行命令行并刷新浏览器才能看到效果, 使用webpack-dev-server可以让 webpack 自动打包并刷新浏览器

  1. 安装

    npm i -D webpack-dev-server
    
  2. 修改package.json

    "scripts": {
      "start": "webpack-dev-server --open --port 3000 --hot"
    }
    
  3. 运行npm start

加载器 loader

在 webpack 打包过程中, 所有静态资源都是通过 ES6模块 引入进main.js中实现打包的

引入 js 模块文件

引入js模块文件不需要加载器, 如

  1. 编写一个工具类模块src/js/utils.js

    /**
     * 数组去重
     * @param arr 需要去重的数组
     * @returns 处理后的新数组
     */
    function duplicate(arr) {
        return arr.reduce((tar, cur) => {
            tar.indexOf(cur) === -1 && tar.push(cur);
            return tar
        }, [])
    }
    
    const utils = {
        duplicate
    };
    
    export default utils
    
  2. main.js中引入模块文件并使用

    import utils from "./js/utils"
    
    console.log('hello world');
    
    let arr = [1, 2, 3, 5, 5, 5, 3, 8];
    let res = utils.duplicate(arr);
    console.log(res);
    
引入 css 文件
  1. 下载style-loader,css-loader

    npm i -D style-loader css-loader
    
  2. 修改配置文件

    module.exports = {
        mode: 'development',
        entry: resolve('./src/main.js'),
        output: {
            path: resolve('./dist'),
            filename: "bundle.js"
        },
        plugins: [
            new HtmlPlugin({
                template: resolve('./src/index.html'),
                filename: "index.html"
            })
        ],
        module: {
            // 编写所有加载器使用规则
            rules: [
                {test: /\.css$/, use: ['style-loader', 'css-loader']}
            ]
        }
    };
    
  3. main.js中引入css文件

    import "./css/reset.css"
    
引入 less 文件
  1. 下载less-loader,less

    npm i -D less less-loader
    
  2. 添加rules

    {test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader']}
    
  3. main.js中引入less文件

    import "./less/style.less"
    
引入 scss 文件
  1. 下载sass-loader,node-sass

    npm i -D sass-loader node-sass
    
  2. 添加rules

    {test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader']}
    
  3. main.js中引入scss文件

    import './scss/style.scss'
    
热更新index.html
  1. 下载raw-loader

    npm i -D raw-loader
    
  2. 添加rules

    {test: /\.html?$/i, use: ['raw-loader']}
    
  3. main.js中引入./index.html

    import "./index.html"
    
处理图片
  1. 下载url-loader,html-withimg-loader

    npm i -D url-loader html-withimg-loader
    
  2. 添加rules

    {test: /\.(png|jpe?g|gif)$/i, use: ['url-loader']},
    // 处理html中的img标签需要html-withimg-loader(国人开发), 与raw-loader冲突, 只能使用一个
    {test: /\.html?$/i, use: ['html-withimg-loader']}
    
url-loader更多配置

url-loader默认将图片路径转成Base64编码, 然而对于一些高清图, 转码起来耗时长且消耗性能多, 所以可以设置一个限制值( 单位字节 bytes ), 大于该值时, 会使用file-loader(默认)打包图片

  1. 下载file-loader

    npm i -D file-loader
    
  2. 修改rules, 一般限制不超过 8kb

    {test: /\.(png|jpe?g|gif)$/i, use: [{loader: 'url-loader', options: {limit: 8192}}]}
    

使用Base64编码时, 我们还可以设置mimetype将图片转成统一格式的Base64编码, 一般设为image/png

{test: /\.(png|jpe?g|gif)$/i, use: [{loader: 'url-loader', options: {limit: 8192, mimetype: 'image/png'}}]}

当图片大小超过limit值时, 我们还可以设置callback用其他加载器打包图片

  1. 下载responsive-loader,jimp(简单压缩), 如需压缩大量图片使用(sharp)

    npm i -D responsive-loader jimp
    
  2. 修改rules, 压缩图片

    {test: /\.(png|jpe?g|gif)$/i, use: [{loader: 'url-loader', options: {limit: 8192, callback: 'responsive-loader'}}]}
    

使用file-loader或者responsive-loader时, 默认文件名是文件内容的MD5哈希值和原拓展名, 可以设置name将图片文件的名字取代MD5哈希值

{test: /\.(png|jpe?g|gif)$/i, use: [{loader: 'url-loader', options: {limit: 8192, name: '[path][name].[ext]'}}]}

为了解决缓存问题, 一般会带上MD5哈希值的前8位

{test: /\.(png|jpe?g|gif)$/i, use: [{loader: 'url-loader', options: {limit: 8192, name: '[path][name]-[hash:8].[ext]'}}]}
使用Babel

为了让我们的js代码兼容各大浏览器, 使用babel将ES6, ES7语法转成ES5语法

  1. 下载babel相关依赖

    npm i -D babel-loader@7.1.5 babel-core babel-preset-env babel-preset-stage-0 babel-plugin-transform-runtime
    

    a) `babel-loader@7.1.5, 指定加载器版本, 为了使用babel-preset-stage-0`, 兼容跟多浏览器

    b) babel-core, babel核心库, 是babel-loader的依赖项

    c) babel-preset-envbabel-preset-stage-0, babel预设, stage有4个阶段, 可按需加载对应阶段功能, 其中0涵盖所有阶段的功能

    d) babel-plugin-transform-runtime, babel插件, 对不同js文件中引用同一模块, 该模块只打包一次

  2. 配置rules

    // exclude 忽视npm或者bower下载的第三方库
    {test: /\.m?js$/, exclude: /(node_modules|bower_components)/, use: ['babel-loader']}
    
  3. 配置.babelrc文件

    {
      "presets": [
        "env",
        "stage-0"
      ],
      "plugins": [
        "transform-runtime"
      ]
    }
    

调试工具 devtool

devtool有多个配置项, 开发时建议使用cheap-module-eval-souce-map, 快速定位到对应模块文件的对应位置

module.exports = {
  mode: 'development',
  entry: ''/* 入口路径 */,
  output: {/* 输出 */},
  plugins: [/* 插件 */],
  module: {/* 模块 */},
  resolve: {/* 问题处理 */}
  devtool: "cheap-module-eval-source-map"
};

Vue Loader

Vue Loader 是一个 webpack 的加载器, 允许你以 单文件组件(SFCs) 格式撰写 Vue 组件

起步

  1. 下载vue-loader,vue-style-loader,vue-template-complier,vue

    npm install -D vue-loader vue-style-loader vue-template-compiler
    npm install -S vue
    
  2. 配置webpack.config.js

    const path = require('path');
    const HtmlPlugin = require('html-webpack-plugin');
    const VueLoaderPlugin = require('vue-loader/lib/plugin');
    
    function resolve(paths) {
        return path.join(__dirname, paths);
    }
    
    module.exports = {
        mode: 'development',
        entry: resolve('./src/main.js'),
        output: {
            path: resolve('./dist'),
            filename: "bundle.js"
        },
        plugins: [
            new HtmlPlugin({
                template: resolve('./src/index.html'),
                filename: "index.html"
            }),
            // 必须使用插件, 让其他loader识别到vue的三种顶级语言块
            new VueLoaderPlugin()
        ],
        module: {
            rules: [
                // 能匹配到普通的`.css`文件以及`.vue`文件的`<style>`
                {test: /\.css$/, use: ['vue-style-loader', 'css-loader']},
                {test: /\.less$/, use: ['vue-style-loader', 'css-loader', 'less-loader']},
                {test: /\.scss$/, use: ['vue-style-loader', 'css-loader', 'sass-loader']},
                {test: /\.(png|jpe?g|gif)$/i, use: [{loader: 'url-loader', options: {limit: 8192, name: '[name]-[hash:8].[ext]'}}]},
                // 能匹配到普通的`.js`文件以及`.vue`文件的`<script>`
                {test: /\.m?js$/, exclude: /(node_modules|bower_components)/, use: ['babel-loader']},
                {test: /\.vue$/, use: ['vue-loader']}
            ]
        }
    };
    

单文件组件

.vue文件是一个自定义的文件类型, 用类 HTML 语法描述一个 Vue 组件, 一个.vue文件包含三种类型的顶级语言块<template>,<script>,<style>

vue-loader 会解析文件, 提取每个语言块, 如有必要会通过其它 loader 处理, 最后将他们组装成一个 ES6模块, 它的默认导出是一个 Vue.js 组件选项的对象

<!-- .vue文件名建议使用大驼峰命名 App.vue -->
<!-- 一个.vue文件只能有一个template -->
<!-- template的内容将被vue-template-compiler预处理为JS渲染函数, 注入到script导出的组件中 -->
<template>
    <!-- 必须有一个父容器包裹模板内容 -->
    <div>
        App
        <!-- 模板内容 -->
    </div>
</template>

<!-- 一个.vue文件只能有一个script -->
<!-- 默认导出组件选项对象, 也可以导出 Vue.extend() 创建的扩展对象 -->
<!-- 匹配/.\js$/的webpack模板规则 -->
<script>
    export default {
        // 具名组件
        name: "App"
        /* 此处编写Vue实例的配置项 */
    }
</script>
<!-- 一个.vue文件可以包含多个style -->
<!-- scoped表示该样式表是对该文件(组件)的元素生效 -->
<!-- 子组件的根节点会受父组件的作用域样式表影响, 这样设计为了让父组件可以以布局角度出发 -->
<!-- 父组件的作用域样式表不会渗透到子组件的模板内容中 -->
<!-- v-html创建的DOM不会受作用域样式表影响 -->
<style scoped>
    /* 作用域样式表 */
    /* 可通过深度选择器渗透到子组件或者v-html创建的DOM中 */
    /* 深度选择器: 在正常的CSS选择器前加 >>> 或者 /deep/ */
</style>
<style>
    /* 全局样式表 */
</style>
<!-- 默认匹配/\.css$/, 设置lang="less"后, 匹配/\.less$/ -->
<style scoped lang="less">
    /* 使用预处理器less编辑样式 */
    /* 注意: 使用less时, 深度选择器只能使用 /deep/ */
</style>

render函数

main.js中使用App.vue

// 引入vue
import Vue from "vue"
// 引入单文件组件App.vue
import App from "./App.vue"

new Vue({
  // 在index定义挂载元素
  el: '#app',
  // 绑定App.vue组件
  components: {App},
  // 在模板中直接使用<app>组件
  template: `<app></app>`
});

但是运行npm start后, 浏览器会报错:

runtime-only

这是因为 Vue 的NPM包dist目录下有多个版本的 Vue.js 构建版本, 在 webpack 等构建工具中有2版本vue.esm.js(完整版),vue.runtime.esm.js(运行时), 完整版包含 编译器, 用来将模板字符串(即<template>)编译成JS渲染函数的代码, 而import引入的默认是运行时vue.runtime.esm.js版本

如需使用 完整版, 需要修改webpack.config.js配置文件

module.exports = {
  // ...其他配置项
  resolve: {
    // 起别名, 将import路径以vue结尾替换成vue/dist/vue.esm.js
    // 即使用完整版
    alias: {
      'vue$': 'vue/dist/vue.esm.js'
    },
    // 在resolve配置项中, 还可以设置import时省略的文件后缀名
    extensions: ['.js', '.vue']
  }
};

因为在绑定组件后立即使用组件作为根节点, 所以main.js可简写为

import Vue from "vue"
import App from "./App"

new Vue(App).$mount('#app');

当然默认使用运行时版本也是有道理的, 因为在打包时, 包的体积会比使用完整版时的小30%, 使用

运行时版本, 编译快, 体积小

使用 运行时 版本, 需要使用 render()函数

import Vue from "vue"
import App from "./App"

new Vue({render: h => h(App)}).$mount('#app');

render函数中接收的h参数其实是createElement别名, createElement函数返回的是VNode(虚拟节点), 它的作用是告诉 Vue 页面上需要渲染什么样的节点( 真实DOM节点 ), 每个VNode包括自身及其子节点的描述信息, 所有VNode组成的树叫做 虚拟DOM

虚拟DOM

首先先了解一下浏览器内核 渲染引擎渲染机制 (各大内核大致相同):

  1. 使用HTML分析器, 扫描文档文本, 提取标签转成DOM节点, 形成 DOM树 (DOM tree)

  2. 使用CSS分析器, 将CSS代码转成 CSSOM ( CSS Object Model )

  3. 结合 DOM 和 CSSOM, 每个DOM节点调用attach方法 接收样式信息, 并返回render对象, 组合形成 Render树 (包含每个节点的视觉信息)

  4. 通过 Render树 生成 布局 ( Layout ), 即为每个 Render树 上的节点确定一个在屏幕显示的准确坐标

  5. 调用每个节点的paint方法, 将 布局 绘制 ( Paint ) 在屏幕上

    渲染引擎工作过程

注意点:

  1. DOM树 的构建是 渐进 的, 不会等文档加载完才开始, 为了提高用户体验, 尽快将内容显示到页面上
  2. DOM树CSSOM树 的基本可以当成是同步开始构建的
  3. DOM树 , CSSOM树Render树, 三者的构建是 交叉 的, 会出现 一边加载、一边解析、一边渲染 的工作现象
  4. CSS的 读取 顺序: UA defaults(浏览器默认样式) → stylesheets (文档和外部样式表) → style=””(元素的行内样式)
  5. CSS的 解析 是从 右往左逆向解析 的( 即选择器从内到外 ), 这样是为了 提高解析效率, 所以 组合选择器层次越复杂, 解析越慢

重排(回流)和重绘

网页的生成, 最耗时的是 “生成布局(排版)”(flow)和”绘制”(paint), 合称 “渲染”(render)

注意点:

  1. 生成一个网页, 至少会渲染一次, 用户的每次访问/操作, 都会引起重新渲染
  2. 以下三种情况会因此重新渲染: a) 修改DOM b) 修改CSS c) 用户事件(鼠标悬停, 页面滚动等)
  3. 重新渲染包括重新生成布局和重新绘制, 前者叫 重排(回流, reflow), 后者叫 重绘(repaint), “重绘”不一定要”重排”, “重排”一定导致”重绘”

JS直接操作真实DOM

浏览器的每次渲染都会从构建 DOM树 开始, 从头到尾 执行一遍, 比如说, 一次操作中, 我们同时修改了10个 DOM 节点, 浏览器在接收到第一个DOM更新请求后并不知道后面还有9次更新操作, 因此它会马上执行重新渲染, 导致一次操作重新渲染了10次

因此, 通过JS直接操作真实DOM 开销非常大, 为了提高页面的性能, 应尽可能降低 “重排”“重绘” 的频率和成本.

提高性能的技巧:
  1. 新增删除 节点, 通过 字符串拼接(有大小限制) 或者 数组操作内存中 执行修改, 然后通过innerHTML执行浏览器 重新渲染

    // 模拟网络请求, 为新闻发布页面已存在 ul#target 新增3条li
    let data = ['内容1', '内容2', '内容3'];
    let str = '';
    let arr = [];
    
    data.forEach(item => {
      // 字符串拼接
      str = `<p>${item}</p>` + str;
      // 数组操作
      arr.unshift(`<p>${item}</p>`);
    });
    
    // innderHTML 重新渲染
    let renderUl = document.getElementById('target');
    // renderUl.innderHTML = str + renderUl.innderHTML;
    renderUl.innderHTML = arr.join('') + renderUl.innderHTML;
    
  2. 修改样式 通过 克隆节点内存中 执行修改, 然后 替换节点 执行 重新渲染

    // 修改页面中 div#target 的样式
    let tarEl = document.getElementById('target');
    let cloneEl = tarEl.cloneNode(true);
    /* 执行样式修改 cloneEl.style = ... */
    // 替换节点, 重新渲染
    document.body.replaceChild(cloneEl, tarEl);
    

虚拟DOM

虽然通过以上操作能减少重新渲染的频率, 然而频繁调用 WEB API, 导致代码量大, 后期难以维护, 虚拟DOM的设计就是为了 解决浏览器性能问题, 减轻维护成本, 每次操作更新, 虚拟DOM不会立即操作真实DOM, 而是将更新的diff内容保存到本地JS对象中, 最终把这个JS对象一次性attach到DOM树上, 再进行渲染

// 描述 <p title="段落">这是p标签 <span>这是span标签</span></p>
// 模拟虚拟DOM设计
let VNode = {
  name: 'p',
  props: {
    title: '段落'
  },
  children: [
    '这是p标签',
    {
      name: 'span',
      props: null,
      children: [
        '这是span标签'
      ]
    }
  ]
}

Diff算法

虚拟DOM 和 真实DOM 是 强映射 关系, 内容更新通过 Diff算法 实现

  • tree diff: DOM级别, 对比新旧DOM树, 逐层对比

  • component diff: 组件级别, 在对比每一层DOM树时, 组件之间的对比, 先对比组件的类型(name), 类型相同, 暂时认为不需要被更新, 类型不同, 用新组件替换旧组件的位置

  • element diff: 元素级别, 在组件中, 对比每个元素

  • key: 元素/组件的key属性可以把 真实DOM虚拟DOM 做一层 关联关系

    Diff算法

createElement接收的参数

createElement(
    /**
     * @param1: [String, Object, Function]
     * required: true
     * HTML标签名字符串 或 组件选项对象
     * Vue 推荐使用 单文件组件(即组件选项对象)
     */
    'div',
    /**
     * @param2: Object
     * 一个与模板中属性对应的数据对象
     */
    {
      // class需要用'', 因为class在JS中是关键词
      // 使用与 v-bind:class 相同, 可以是字符串, 数组或对象
      'class': {},
      // 使用与 v-bind:style 相同, 可以是字符串, 对象或对象组成的数组
      style: {},
      // 普通HTML特性, 如 id 等
      attrs: {},
      // 组件 prop
      props: {},
      // DOM属性
      domProps: {},
      // 事件监听器, 不支持修饰符
      on: {},
      // 自定义指令
      directives: [],
      // 作用域插槽, name: props => VNode
      scopedSlots: {
        default: null
      },
      // 命名插槽
      slot: 'name-of-slot',
      // 其他组件顶层特性
      key: 'myKey',
      ref: 'myRef'
    },
    /**
     * @param3: [String, Array]
     * 文本虚拟节点 或 子集虚拟节点 (VNodes), 通过`createElement()`构建
     */
    []
)

使用JSX

使用JSX能简化渲染函数的编写, 让createElement函数创建虚拟DOM更像编写<template>模板, 如:

new Vue({
  /* 渲染一个简单的虚拟DOM
  <div class="my-div">
      文本节点
      <p title="欢迎">这是p标签<span data-msg="dataset">这是span标签</span></p>
      <label for="user">用户</label>
      <input type="text" id="user">
  </div>
  */
  render: h => h('div', {'class': 'my-div'}, [
    '文本节点',
    h('p', {attrs: {title: '欢迎'}}, [
      '这是p标签',
      h('span', {attrs: {'data-msg': 'dataset'}}, ['这是span标签']),
    ]),
    h('label', {attrs: {for: 'user'}}, ['用户']),
    h('input', {attrs: {type: 'text', id: 'user'}}, [])
  ])
}).$mount('#app');

使用JSX语法糖后:

let msg = '这是msg';
let arr = ['内容1', '内容2', '内容3'];
new Vue({
  // JSX 语法糖还是调用 createElement 函数, 所以 h 形参必须接收
  // 与 template 模板一样, 有且仅有一个根节点
  // 括号提高优先级, 隔行使用JSX, 防止报错, 让JSX编写的虚拟DOM结构更明显
  // 与react不同, vue中的class和for不必担心与js关键词/保留字冲突
  // {} 内部是JS环境, 可书写JS表达式, 如使用变量, 三目表达式, 函数调用等
  // jsx中注释通过 {/* 注释 */} 书写, 使用 // 注释注意转行
  render: h => (
      <div class="my-div">
        文本节点
        <p title="欢迎">这是p标签<span data-msg="dataset">这是span标签</span></p>
        <label for="user">用户</label>
        <input type="text" id="user"/>
        <p>{msg}</p>
        {/*这是注释*/}
        {
            // 使用 // 注释, 不推荐这种注释 
        }
        <div>{arr.map((item, index) => (
            <p key={index}>{item}</p>
        ))}</div>
      </div>
  )
}).$mount('#app');
使用JSX的准备工作
  1. 安装babel预设依赖

    npm install babel-preset-jsx babel-transform-vue-jsx babel-helper-vue-jsx-merge-props
    
  2. 修改.babelrc

    {
      "presets": [
        "env",
        "stage-0"
      ],
      "plugins": [
        "transform-runtime",
        "transform-vue-jsx"
      ]
    }
    

设置路由系统

  1. 安装vue-router

    npm i -S vue-router
    
  2. src目录下新建router文件夹, 文件夹下新建index.js

    import Vue from "vue"
    // 引入vue-router
    import VueRouter from "vue-router"
    // src目录下新建views文件夹, 存放路由页面组件
    import Home from "../views/Home";
    import Topic from "../views/Topic"
    import Category from "../views/Category"
    import Cart from "../views/Cart"
    import Mine from "../views/Mine"
    
    // 安装vue插件, 相当于扩展Vue的原型对象
    Vue.use(VueRouter);
    
    // 定义路由系统
    let router = new VueRouter({
      routes: [
        {path: '/', redirect: '/home'},
        {path: '/home', component: Home},
        {path: '/category', component: Category},
        {path: '/topic', component: Topic},
        {path: '/cart', component: Cart},
        {path: '/mine/:id', component: Mine},
      ]
    });
    
    // 导出路由系统
    export default router;
    
  3. 改造main.js

    import Vue from "vue"
    import App from "./App"
    // 引入路由系统
    import router from "./router"
    
    new Vue({
      router,
      render: h => h(App)
    }).$mount('#app');
    

异步组件(懒加载)

在当前路由系统中, 所有路由页面组件是在访问网页时全部加载到bundle.js中的, 随着项目的增大, 这可能会导致 页面加载速度变慢

同步加载所有组件

此时需要改造路由系统, 让/category,/topic,/cart,/mine路由对应的视图组件, 异步加载, 即 进入路由后再加载

let router = new VueRouter({
  routes: [
    {path: '/', redirect: '/home'},
    {path: '/home', name: 'home', component: Home},
    {path: '/category', name: 'category', component: () => import('../views/Category')},
    {path: '/topic', name: 'topic', component: () => import('../views/Topic')},
    {path: '/cart', name: 'cart', component: () => import('../views/Cart')},
    {path: '/mine/:id', name: 'mine', component: () => import('../views/Mine')}
  ]
});

异步加载组件

网页初次加载时, 使用了异步加载的路由系统的bundle.js会比全部加载的小( 大小与项目大小有关 )

切换路由时, 对应的(0-3).bundle.js 就是我们异步加载的视图组件( 按需加载 )

切换路由时网络请求

为了区分是加载的是哪个组件

  1. webpack.config.js输出output字段中设置chunkFileName

    output: {
      path: resolve('./dist'),
      filename: "bundle.js",
      chunkFilename: "[name].bundle.js"
    },
    
  2. 异步引入时,()=>import(/* webpackChuckName: 'compName' */ URL)

    routes: [
      {path: '/', redirect: '/home'},
      {path: '/home', name: 'home', component: Home},
      {path: '/category', name: 'category', component: () => import(/* webpackChunkName: 'category' */ '../views/Category')},
      {path: '/topic', name: 'topic', component: () => import(/* webpackChunkName: 'topic' */ '../views/Topic')},
      {path: '/cart', name: 'cart', component: () => import(/* webpackChunkName: 'cart' */ '../views/Cart')},
      {path: '/mine/:id', name: 'mine', component: () => import(/* webpackChunkName: 'mine' */ '../views/Mine')}
    ]
    

    webpackChunkName

使用历史(HistoryAPI)模式

vue-router 部分, 我们提及过使用history模式时需要 后台配置支持, 不然刷新页面会报 404 错误, 在webpack中, 我们可以通过devserver配置项实现使用history模式

  1. 路由系统src/router/index.js中配置mode:'history'

  2. 修改webpack.config.js

    module.exports = {
      mode: 'development',
      entry: resolve('./src/main.js'),
      output: {
        path: resolve('./dist'),
        filename: "bundle.js",
        chunkFilename: "[name].bundle.js",
        // 解决动态路由404问题
        publicPath: "/"
      },
      plugins: [/* 插件 */],
      module: {/* 模块处理 */},
      resolve: {/* 问题处理 */},
      devServer: {
        // 以下三项就是 `package.json`中 "script" 字段 "start" 脚本的配置项
        open: true,
        port: 9999,
        hot: true,
        // 开启history API
        historyApiFallback: true
      },
      devtool: "cheap-module-eval-source-map"
    };
    

Vuex

Vuex 是 Vue 官方为 Vue.js 应用程序开发维护的 状态管理模式, 它采用集中式存储管理用用的所有组件的状态, 并以相应的规则保证状态以一种可预测的方式发生变化

状态管理模式

首先, 每个单文件组件(.vue)就是一个 状态自管理应用, 如以下一个简单的 计步器 应用

// view
<template>
  <div>
    <p>当前步数: {{num}}</p>
    <button @click="increment">加一</button>
    <button @click="decrement">减一</button>
  </div>
</template>

<script>
  export default {
    name: "App",
    // state
    data() {
      return {
        num: 0
      }
    },
    // actions
    methods: {
      increment() {
        this.num++
      },
      decrement() {
        this.num--
      }
    }
  }
</script>

一个 状态自管理应用 包含:

  • state, 驱动应用的数据源
  • view, 以声明的方式将 state 映射到视图中
  • actions, 响应在 view 的用户操作导致的 state 变化

三者形成一个 单向数据流

单向数据流

然而, 如果是 多组件共享状态 时, 单向数据流就会被破坏:

  • 多个 view 依赖于同一个 state
  • 不同组件的 actions 更改同一个 state

组件通信 部分, 我们提及到组件间通信, 可通过

  1. 嵌套组件, 父向子 传参
  2. 嵌套组件, 通过 自定义事件 实现子向父通信

然而如果是 多层嵌套 的组件, 传参 或者 自定义事件 都会相当繁琐, 而且对于 兄弟组件 的状态传递无能为力, 这时候您可能会想到使用 中央事件总线, 然而这种方法在 模块化开发 时, 并不好维护:

  1. src目录下创建Bus.js

    import Vue from "vue";
    
    let Bus = new Vue();
    
    export default Bus
    
  2. 计步器 应用拆分成3个组件, 存放在src/components

    a) App.vue

    <template>
      <div>
        <show-num></show-num>
        <incre-btn></incre-btn>
        <decre-btn></decre-btn>
      </div>
    </template>
    
    <script>
      import ShowNum from "./components/ShowNum";
      import IncreBtn from "./components/IncreBtn";
      import DecreBtn from "./components/DecreBtn";
    
      export default {
        name: "App",
        components: {DecreBtn, IncreBtn, ShowNum}
      }
    </script>
    

    b) ShowNum 组件

    <template>
      <p>当前步数: {{num}}</p>
    </template>
    <script>
      // 导入 Bus
      import Bus from "../Bus";
    
      export default {
        name: "ShowNum",
        // 私有数据
        data() {
          return {
            num: 0
          }
        },
        created() {
          // 私有数据初始化完成后, 给Bus绑定2自定义事件, 操作私有数据num
          Bus.$on('add', () => this.num++);
          Bus.$on('subtract', () => this.num--);
        }
      }
    </script>
    
    

    c) IncreBtn 组件

    <template>
      <button @click="increment">加一</button>
    </template>
    
    <script>
      // 导入 Bus
      import Bus from "../Bus";
    
      export default {
        name: "IncreBtn",
        methods: {
          increment() {
            // 触发Bus自定义事件 add
            Bus.$emit('add')
          }
        }
      }
    </script>
    

    d) DecreBtn组件

    <template>
      <button @click="decrement">减一</button>
    </template>
    
    <script>
      import Bus from "../Bus";
    
      export default {
        name: "DecreBtn",
        methods: {
          decrement() {
            Bus.$emit('subtract')
          }
        }
      }
    </script>
    

这样改造后, 能达到不同组件的 actions 影响同一 state 的效果, 要想实现多组件共享同一 state, 需要继续改造:

  1. Bus.js

    import Vue from "vue";
    
    let Bus = new Vue({
      // state
      data: {
        num: 0
      },
      // actions
      methods: {
        add() {
          this.num++
        },
        subtract() {
          this.num--
        }
      }
    });
    
    export default Bus
    
  2. ShowNum.vue

    <template>
      <p>当前步数: {{num}}</p>
    </template>
    
    <script>
      import Bus from "../Bus";
    
      export default {
        name: "ShowNum",
        computed: {
          // 通过computed获取Bus.num
          num: {
            get() {
              return Bus.num
            },
            // 必须加setter, 否则vue会报错
            set(newVal) {
              console.log(newVal);
            }
          }
        }
      }
    </script>
    
  3. IncreBtn.vue

    <template>
      <div>
        <span>{{num}}</span>
        <button @click="increment">加一</button>
      </div>
    </template>
    
    <script>
      import Bus from "../Bus";
    
      export default {
        name: "IncreBtn",
        computed: {
          num: {
            get() {
              return Bus.num
            },
            set(newVal) {
              console.log(newVal);
            }
          }
        },
        methods: {
          increment() {
            // 触发Bus的add方法
            Bus.add()
          }
        }
      }
    </script>
    
  4. DecreBtn.vue

    <template>
      <div>
        <span>{{num}}</span>
        <button @click="decrement">减一</button>
      </div>
    </template>
    
    <script>
      import Bus from "../Bus";
    
      export default {
        name: "DecreBtn",
        computed: {
          num: {
            get() {
              return Bus.num
            },
            set(newVal) {
              console.log(newVal);
            }
          }
        },
        methods: {
          decrement() {
            Bus.subtract()
          }
        }
      }
    </script>
    

显然, 使用Bus实现 多组件共享状态 有以下缺点:

  1. 每个组件需要导入Bus.js
  2. 获取共享 state 需要通过computed, 而且必须有setter

起步

Vuex 应用的核心就是store(仓库), store是一个容器, 包含应用中的 共享状态( state ), 和单纯的全局对象有2点不同:

  1. Vuex 的状态存储是响应式的, 即当 Vue 组件从store中读取状态时, 如果store中状态发生变化, 组件也会相应的高效更新

  2. 你不能直接改变store中的状态, 改变store状态唯一的途径是显式地 提交 (commit) mutation, 这使我们可以方便地追踪每一个状态的变化, 从而让我们能够实现一些工具帮助我们更好地了解我们的应用

    Vuex

创建第一个store

  1. 下载vuex

    npm i -S vuex
    
  2. 创建store, 在src/store目录下创建index.js

    import Vue from "vue"
    import Vuex from "vuex"
    
    // 显式安装Vuex, 相当于当前项目所有Vue实例(组件)共享store
    Vue.use(Vuex);
    
    const store = new Vuex.Store({
      // 详细的配置项即概念在下一节介绍
      state: {
        num: 0
      },
      mutations: {
        increment(state) {
          state.num++
        },
        decrement(state) {
          state.num--
        }
      }
    });
    
    export default store
    
  3. 导入store

    import Vue from "vue";
    import App from "./App";
    import store from "./store";
    
    new Vue({
      store,
      render: h => h(App)
    }).$mount('#app');
    
  4. App.vue中打印this

    <template>
      <div>
        <show-num></show-num>
        <incre-btn></incre-btn>
        <decre-btn></decre-btn>
      </div>
    </template>
    
    <script>
      import ShowNum from "./components/ShowNum";
      import IncreBtn from "./components/IncreBtn";
      import DecreBtn from "./components/DecreBtn";
    
      export default {
        name: "App",
        components: {DecreBtn, IncreBtn, ShowNum},
        mounted() {
          console.log(this);
        }
      }
    </script>
    

    vm.$store

    此时, Vue 实例中, vm.$store就是我们刚创建的仓库store

  5. 修改ShowNum.vue

    <template>
      <p>当前步数: {{num}}</p>
    </template>
    
    <script>
      export default {
        name: "ShowNum",
        computed: {
          // 依然通过computed读取共享state
          num() {
            return this.$store.state.num
          }
        }
      }
    </script>
    
  6. 修改IncreBtn.vue

    <template>
      <div>
        <span>{{num}}</span>
        <button @click="increment">加一</button>
      </div>
    </template>
    
    <script>
      export default {
        name: "IncreBtn",
        computed: {
          num() {
            return this.$store.state.num
          }
        },
        methods: {
          increment() {
            // 通过commit触发mutation修改共享state
            this.$store.commit('increment')
          }
        }
      }
    </script>
    
  7. 修改DecreBtn.vue

    <template>
      <div>
        <span>{{num}}</span>
        <button @click="decrement">减一</button>
      </div>
    </template>
    
    <script>
      export default {
        name: "DecreBtn",
        computed: {
          num() {
            return this.$store.state.num
          }
        },
        methods: {
          decrement() {
            this.$store.commit('decrement')
          }
        }
      }
    </script>
    

核心概念

State

Vuex 使用 单一状态树, 即用一个对象管理一个单页面应用( SPA )所有的共享状态, 因此, 每个应用有且仅有一个store实例, 这样的处理能让我们直接快速地定位到某一特定的状态片段

注意点: 在 Vue 根实例中设置store配置项后, 所有子组件将共享仓库( 即将 store 实例注入到每个组件中 )

Vuex 的状态存储是 响应式 的, 储存到state中, 而组件想获取 store 实例中的状态, 可以通过vm.$store.state获取共享状态对象, 如

// src/components/ShowNum.vue 中
<template>
  <!-- 在模板中直接使用 -->
  <p>当前步数: {{$store.state.num}}</p>
</template>

为了保持模板简洁, 最简单的方法是在 计算属性 (computed)中返回某个状态

<template>
  <p>当前步数: {{num}}</p>
</template>

<script>
  export default {
    name: "ShowNum",
    computed: {
      num() {
        return this.$store.state.num
      }
    }
  }
</script>

然而, 当一个组件要获取 store 实例中多个状态时, 如

// src/store/index.js 中
const store = new Vuex.Store({
  state: {
    num: 0,
    msg: '共享状态',
    list: ['el1', 'el2', 'el3']
  }
});
// src/components/ShowNum.vue 中
computed: {
  num() {
    return this.$store.state.num
  },
  msg() {
    return this.$store.state.msg
  },
  list() {
    return this.$store.state.list
  }
}

随着需要的状态增多, computed中代码就会显得非常臃肿, 不方便维护, 此时, Vue 提供了mapState辅助函数帮助我们生成计算属性

// 引入辅助函数
import {mapState} from "vuex";
// ...省略
computed: mapState({
  // 使用箭头函数, 让代码更简洁  
  num: state => state.num,
  // 传字符串, 相当于 numAlias: state => state.num
  // 重命名
  numAlias: 'num',
  msg: 'msg',
  list: 'list',
  // 返回值需要this时, 必须使用普通函数
  localMsg(state) {
    return this.msg + 'local'
  }
})

当键值对一样时, 如msg: 'msg', 可以给mapState传入一个字符串数组

computed: mapState(['num', 'msg', 'list'])

以上改造可知, mapState函数返回的是一个对象, 我们可以通过ES6的对象扩散符...将它和局部计算属性混合使用

computed: {
  ...mapState(['num', 'msg', 'list']),
  localMsg() {
    return this.msg + 'local'
  }
}

注意点: 使用mapState辅助函数后, 在vue-devtools中的数据展示更直观:

mapState

Getter

与 Vue 实例的computed类似, 相当于store的计算属性, getter的返回值会根据它的依赖被缓存起来, 依赖发生变化才会被重新计算, 如一个简单的 todo list

const store = new Vuex.Store({
  state: {
    // 全部任务
    todo: [
      {id: 1, text: '内容1', done: true, urgent: false},
      {id: 2, text: '内容2', done: false, urgent: true},
      {id: 3, text: '内容3', done: false, urgent: false},
      {id: 4, text: '内容4', done: true, urgent: true},
      {id: 5, text: '内容5', done: false, urgent: true},
    ]
  },
  getters: {
    // 接收 state 作为第一个参数
    // 筛选已完成任务
    done(state) {
      return state.todo.filter(item => item.done)
    },
    // 筛选未完成任务
    undo(state) {
      return state.todo.filter(item => !item.done)
    },
    // 接收 getters 作为第二个参数
    // 筛选紧急的未完成任务
    urgent(state, getters) {
      return getters.undo.filter(item => item.urgent)
    }
  }
})

在组件中打印this.$store

getters

getters的三个列表以存储在this.$store.getters中, 与state一样, 通过computed获取返回三个列表

computed: {
  done() {
    return this.$store.getters.done
  },
  undo() {
    return this.$store.getters.undo
  },
  urgent() {
    return this.$store.getters.urgent
  }
}

Vue 同样提供mapGetters辅助函数, 便捷地将getters对象的属性扩散到局部计算属性中

import {mapGetters} from "vuex";
// ...省略
computed: {
  ...mapGetters(['done', 'undo', 'urgent'])
}

注意点: mapGettersmapState不同, 没有函数的形式, 但可以通过对象形式重命名

Mutation

更改 Vuex 中的状态的唯一方法是提交mutation, Vuex 中的mutation与事件非常类似, 即每个mutation都有一个字符串的 事件类型( type ) 和一个 回调函数( handler ), 这个回调函数就是实际更新状态的地方, 如 计步器

mutations: {
  // 接收 state 作为第一个参数
  increment(state) {
    state.num++
  },
  // 第二个参数 payload(载荷), 在提交 mutation 时, 传入额外的参数 
  // payload 建议是一个对象, 这样处理可以让 mutation 更容易读取
  setNum(state, payload) {
    // state.num = payload
    state.num = payload.nelVal
  }
}

在组件中提交mutation, 必须需要调用commit方法

<template>
  <div>
    {{$store.state.num}}
    <button @click="$store.commit('increment')">加一</button>
    <button @click="$store.commit('setNum',{newVal:0})">重置</button>
    <!-- 还可以直接传入一个对象的, 事件类型通过 type: 'type' 传递 -->
    <button @click="$store.commit({type:'setNum',newVal:10})">重置到10</button>
  </div>
</template>

同样, Vue 也提供mapMutations辅助函数, 将mutations中的方法混入到methods

<template>
  <div>
    <!-- 使用 -->
    {{num}}
    <button @click="increment">加一</button>
    <button @click="setNum({newVal:0})">重置</button>
    <button @click="setNum({newVal:10})">重置到10</button>
  </div>
</template>

<script>
  import {mapMutations, mapState} from "vuex";

  export default {
    name: "ListContent",
    // 使用辅助函数扩散
    computed: {
      ...mapState(['num'])
    },
    methods: {
      // 与mapGetters用法一样
      ...mapMutations(['increment', 'setNum'])
    }
  }
</script>

注意点:

  1. Vuex 中的状态是响应式的, 所以mutation必须遵循 Vue 的响应规则, 具体查看 数组和对象的更新监测
  2. mutation必须是同步函数, Vuex 提供action专门处理异步操作

Action

actionmutation类似, 区别在于:

  1. action提交的是mutation, 而不是直接更改状态
  2. action可以包含任何 异步操作

注册一个action

actions: {
  // 接收一个 context 参数, 表示当前action的上下文环境
  test(context) {
    console.log(context);
  }
}

触发(分发, diapatch )action

<template>
  <div>
    <!-- 直接使用vm.$store.dispatch调用 -->
    <button @click="$store.dispatch('test')">dispatch</button>
    <!-- 使用辅助函数简化 -->
    <button @click="test">辅助函数</button>
  </div>
</template>

<script>
  import {mapActions} from "vuex";

  // ...省略
  methods: {
  // 与mutation相同, 使用辅助函数`mapActions`将`diapatch`映射到局部`method`中
  ...mapActions(['test'])
  }
</script>

分发action, 查看控制台打印结果

context上下文

从打印结果看, context上下文环境与store实例具有相同的方法和属性: state,getters,commitdispatch, 但注意 它不是 store 实例本身, 在下一节 Modules 中介绍

在使用过程中, context参数一般通过 ES6 参数解构简化使用, 如 计步器

actions: {
  test({state, getters, commit, dispatch}) {
    // 之前提及过, 改变vuex中状态只能通过提交(commit) mutation 的方式改变
    // 所以 context 上下文中的 state getters 最好只是读取属性
    // 虽然 state.num++ 也会生效, 但违反了 vuex 的状态管理规则, 强烈不建议这样使用
    // 此处是同步代码, 按顺序运行
    // 因此打印的是 旧的 num
    console.log(state.num);
    commit('increment');
    // 打印的是 更新后的 num
    console.log(state.num);
  }
}

action同样支持载荷和对象方式进行分发:

  1. store实例中定义setNumAsync

    actions: {
      setNumAsync({commit}, payload) {
        commit('setNum', payload)
      }
    }
    
  2. 组件中分发setNumAsync

    <template>
      <div>
        <button @click="$store.dispatch({type:'setNumAsync',newVal:10})">dispatch</button>
        <button @click="setNumAsync({newVal:10})">辅助函数</button>
      </div>
    </template>
    

计步器 案例中, 你可能会觉得使用 分发( dispatch ) action会显得麻烦, 因为incrementsetNum都是同步的, 但 涉及到调用异步API和分发多重mutation时, 就必须使用action, 如

// 根据 payload 判断提交哪个 mutation
setNumAsync({commit}, payload) {
  // payload 注意点
  // 使用 @click="setNumAsync()" 即使用了括号时, 不传参数, payload为undefined
  // 使用 @click="setNumAsync" payload为整个事件对象
  // 直接使用 @click="setNumAsync('setNumAsync')" 不传参, payload为undefined
  // 直接使用 @click="setNumAsync({type:'setNumAsync'})", payload就是{type:'setNumAsync'}
  // mutation的payload同理
  console.log(payload);
  if (payload.newVal) commit('setNum', payload);
  else commit('increment');
}
组合action(高级用法)

因为action通常是处理异步操作, 而且接收当前action的上下文context, 因此可以通过组合action的方式处理复杂的异步操作

const store = new Vuex.Store({
  state: {
    msg: '你好'
  },
  mutations: {
    update(state) {
      state.msg = '我爱你'
    },
    replace(state, payload) {
      state.msg = payload.msg.replace(/(you|u|你|您)/gi, 'Tenderness')
    }
  },
  actions: {
    actionA({state, commit}) {
      // actionA 返回一个Promise
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          commit('update');
          resolve(state.msg);
        }, 2000)
      })
    },
    actionB({commit, dispatch}) {
      // diapatch 处理 actionA 返回的 Promise
      dispatch('actionA').then(res => {
        commit('replace', {msg: res})
      })
    }
  }
});

最后还可以使用async/await让异步代码编写起来像同步代码, 方便维护, 如使用fetch(JS内置的Promise风格接口, 不支持IE)发送请求

actions: {
  async actionA({commit}) {
    let res = await fetch('urlA');
    let data = await res.json();
    commit('saveDataA', data);
  },
  async actionB({commit, dispatch}) {
    // 等待 actionA 完成再发起新的请求
    await dispatch('actionA');
    let res = await fetch('urlB');
    commit('saveDataB', await res.json());
  }
}

Module

由于 Vuex 使用单一状态树, 应用的所有共享状态都会集中在一个比较大的对象中, 因此随着项目的增大, store实例将会变得越来越臃肿, 为了解决这个问题, Vuex 允许我们将store分割成 模块( module ), 单独管理每个模块的state,getter,mutation,action, 甚至是字模块module

const store = new Vuex.Store({
  state: {msg: '根store'},
  getters: {
    resolveMsg(state) {
      return '这是' + state.msg
    }
  },
  mutations: {
    update(state, payload) {
      console.log('根store');
      state.msg = '更新根store'
    }
  },
  actions: {
    doAction(context) {
      console.log('根store', context);
    }
  },
  modules: {
    moduleA: {
      state: {msg: 'A模块'},
      getters: {
        resolveMsg(state) {
          // getter接收的state是当前模块的局部状态
          return '这是' + state.msg
        }
      },
      mutations: {
        // mutation接收的state也是当前模块的局部状态
        update(state, payload) {
          console.log('模块A');
          state.msg = '更新A模块'
        }
      },
      actions: {
        // context也是局部的, 在后面介绍
        doAction(context) {
          console.log('模块A', context);
        }
      }
    },
    moduleB: {
      state: {/* 状态 */},
      getters: {/* 计算属性 */},
      mutations: {/* 同步方法 */},
      actions: {/* 异步方法 */},
      modules: {/* 字模块 */}
    }
  }
});

分割完模块, 在App.js中打印this.$store.state

模块化state

此时state已经分割成模块, 读取模块A的状态通过vm.$store.state.moduleA.msg

然后, 我们再打印this.$store.getters

模块化getters

此时, 还有根store实例的resolveMsg, 并且 警告resolveMsg重复

而且, 调用this.$store.commit('update')this.$store.dispatch('doAction'), 将会同时调用根store以及模块A的mutationaction

同时触发mutation和action

这是因为在 默认情况下, 模块内部getter,mutationaction是注册在 全局命名空间 的, 这样是为了 使多个模块对同一个mutationaction做出响应, 如果你想模块具有更高的 复用性复用性, 需要通过配置namespaced: true让其成为带命名空间的模块:

moduleA: {
  namespaced: true,
  // ...其他配置选项
}

配置完后, 打印结果

namespaced命名空间

此时, getter已经区分了, 读取通过this.$store.getters['moduleA/resolveMsg']

而调用this.$store.commit('update')this.$store.dispatch('doAction')只会触发根store实例的方法

那么怎么触发模块的mutationaction呢? 再打印一下this.$store

_mutation和_action

_mutation_action中, 我们能发现模块A的mutationaction已经分块了, 也是只能通过commitdispatch触发, 即:

  1. this.$store.commit('moduleA/update')
  2. this.$store.dispatch('moduleA/doAction')

编写到这, 你可能会觉得代码繁琐, 想起了 辅助函数

computed: {
  ...mapState(['moduleA']),
  ...mapGetters(['moduleA/resolveMsg'])
},
methods: {
  ...mapMutations(['moduleA/update']),
  ...mapActions(['moduleA/doAction']),
}

这样做也确实扩散到当前组件实例中了

模块使用辅助函数

在组件中<script>中使用是完全没有问题的

mounted() {
  // 组件挂载完就打印模块A中的状态以及调用mutation和action
  // 由于 '/' 在JS中是运算符, 因此只能通过[]中括号形式获取
  console.log(this.moduleA.msg);
  console.log(this['moduleA/resolveMsg']);
  this['moduleA/update']();
  this['moduleA/doAction']();
}

然而, 在<template>中, 需要手动绑定this去使用, 这也容易出现语法错误

<template>
  <div>
    {{this['moduleA/resolveMsg']}}
    <p :title="this['moduleA/resolveMsg']">段落</p>
    <button @click="this['moduleA/update']">按钮</button>
  </div>
</template>

解决方案: 通过对象的形式使用(重命名), 后面的例子只修改getter, 其他2项改法一样

computed: {
  ...mapState({
    // state也可以通过这方法, 模板中直接使用 a_msg
    a_msg: state => state.moduleA.msg
  }),
  ...mapGetters({
    // 在模板直接使用 a_resolveMsg
    // 相当于 state => state['moduleA/resolveMsg']
    a_resolveMsg: 'moduleA/resolveMsg'
  })
}

但是, 随着模块中state的增多, 甚至涉及到到多层 嵌套模块 时, 编写会越来麻烦, 如

computed: {
  // a模块中嵌套sub模块, 提取sub模块的`msg`和`num`
  ...mapState({
    sub_msg: state => state.moduleA.sub.msg,
    sub_msg: state => state.moduleA.sub.num,
  })
}

模块层级结构, 即state.moduleA.sub会重复多次, 此时你可以将命名空间名称字符串作为第一个参数传到辅助函数中

computed: {
  ...mapState('moduleA/sub', {
    sub_msg: state => state.msg,
    sub_num: state => state.num,
  })
}

此时, 如果没有重名的state, (getter,mutation,action也一样), 可以改回数组形式

computed: {
  ...mapState('moduleA/sub', ['msg', 'num'])
}

最后, Vuex 还提供了了一个帮助函数createNamespacedHelpers, 用于创建基于某个命名空间的辅助函数, 它返回一个对象, 该对象有对应命名空间的辅助函数

// 引入
import {createNamespacedHelpers} from "vuex";
// 解构, 如需创建多个模块的的辅助函数, 可以通过解构重命名解决
// 如 const {mapState: subState, mapGetters: subGetters, mapMutations: subMutations, mapActions: subActions} = new createNamespacedHelpers('moduleA/sub');
const {mapState, mapGetters, mapMutations, mapActions} = new createNamespacedHelpers('moduleA/sub');

// 使用
export default {
  computed: {
    ...mapState(['msg', 'num']),
    ...mapGetters([/* 映射sub模块的getter */])
  },
  methods: {
    ...mapMutations([/* 映射sub模块的mutation */]),
    ...mapActions([/* 映射sub模块的action */])
  }
}

Action 部分, 我们提及过action中接收的context参数 并不是 store 实例, 而是 当前 action 的上下文环境

const store = new Vuex.Store({
  state: {msg: '根store'},
  actions: {
    doAction({state}) {
      console.log(state.msg);
    }
  },
  modules: {
    moduleA: {
      namespaced: true,
      state: {msg: 'A模块'},
      actions: {
        doAction({state}) {
          // 分发模块A的`doAction` 打印的是 'A模块'
          // 同样context的getters, commit, dispatch也是局部的
          console.log(state.msg);
        }
      }
    }
  }
});

然而, 带命名空间的模块有时也需要 访问全局内容, 因此在模块中, rootStaterootGetters会作为 第三和第四个参数传到getter, 并 通过context传进action

getters: {
  getRootState(state, getter, rootState, rootGetters) {
    // 获取全局state的msg
    return rootState.msg
  }
},
actions: {
  doAction({rootState, rootGetters}) {
    // 打印全局state的msg
    console.log(rootState.msg);
  }
}

在模块action中, 如果想分发 提交全局mutation或者 分发全局action, 可将{root: true}作为第三参数传入当前上下文环境contextcommitdispatch即可

actions: {
  doAction({commit, dispatch, rootState, rootGetters}) {
    // 打印更新前的全局msg '根store'
    console.log(rootState.msg);
    // 提交全局的 update, 更新全局msg
    commit('update', null, {root: true});
    // 分发全局的 doAction, 打印更新后的全局msg, '更新根store'
    dispatch('doAction', null, {root: true});
  }
}

命名空间模块的action还能 注册到全局

actions: {
  doAction: {
    // 设置 root: true 后, 当前命名空间模板将没有doAction
    // 组件分发需要 this.$store.dispatch('doAction')
    // 注意 action 类型重复
    root: true,
    handler(namespacedCtx, payload) {
      // context 还是当前action的上下文
      console.log(namespacedCtx.state.msg);
    }
  }
}

Vuex的表单处理

因为 Vuex 中改变状态的唯一方式是通过提交mutation, 所以, 当表单元素使用v-model绑定 Vuex 中状态时, v-model试图直接修改状态, 这明显违背了 Vuex 的规则

解决方法1:

<template>
  <!-- 通过v-bind:value绑定msg状态, 监听input事件 -->
  <input :value="msg" @input="update">
</template>
<script>
  // 省略import
  export default {
    // 省略其他配置项
    computed: {
      ...mapState(['msg'])
    },
    methods: {
      ...mapMutations(['updateMsg']),
      update(e){
        // input事件提交mutation, 将输入框value值传进payload
        this.updateMsg({newVal: e.target.value})
      }
    }
  }
</script>

解决方法2:

<template>
  <!-- 通过v-model绑定msg -->
  <input type="text" v-model="msg">
</template>
<script>
  export default {
    // 省略其他配置项
    computed: {
      // 通过getter, setter方式实现双向数据绑定
      msg: {
        // getter 读取 msg 状态
        get() {
          return this.$store.state.msg
        },
        // setter 提交 mutation
        set(newVal) {
          this.$store.commit('updateMsg', {newVal})
        }
      }
    }
  }
</script>

混入 + store 模式

在上一节中, 我们提及到 Vuex 可以帮助我们 管理共享状态, 但是如果我们开发的项目并不大, 使用 Vuex 的操作是相当繁琐的, 此时, 我们只需使用一个简单的 混入 + store 模式 就能满足我们的需求

混入

混入 其实就是一个 对象, 该对象包含 任意 Vue 组件的配置选项, 是一种灵活的方式, 分发 Vue 组件中的 可复用 功能

  1. 创建MyMixin.js

    const MyMixin = {
      data() {
        return {}
      },
      computed: {},
      methods: {},
      watch: {}
      // ... 其他配置项
    };
    
    export default MyMixin
    
  2. 混入MyMixin

    a) 全局混入

    // main.js中
    // 引入 Vue
    import Vue from "vue"
    // 引入 Mixin
    import MyMixin from "./components/MyMixin"
    // 全局混入
    Vue.mixin(MyMixin)
    

    b) 局部混入

    // 单文件组件中
    <script>
      import MyMixin from "./MyMixin";
    
      export default {
        name: "MyComp",
        mixins: [MyMixin]
      }
    </script>
    

混入 + store 模式

依然用 计步器 当做例子

  1. MyMixin.js

    // 共享状态的储存位置(全局)
    const data = {
      num: 0
    };
    
    const MyMixin = {
      data() {
        return data
      },
      // 管理状态的方法 action
      methods: {
        increment() {
          this.num++
        },
        decrement() {
          this.num--
        }
      }
    };
    
    export default MyMixin
    
  2. 各个组件引用并使用

    // ShowNum组件
    <template>
      <div>
        {{num}}
      </div>
    </template>
    
    <script>
      import MyMixin from "./MyMixin";
    
      export default {
        name: "ShowNum",
        mixins: [MyMixin]
      }
    </script>
    
    // IncreBtn组件
    <template>
      <button @click="increment">增加</button>
    </template>
    
    <script>
      import MyMixin from "./MyMixin";
    
      export default {
        name: "IncreBtn",
        mixins: [MyMixin]
      }
    </script>
    
    // DecreBtn组件省略
    

   转载规则


《Vue + Webpack》 Tenderness 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录