点亮你的 Vue 技术栈(一):Vue 骨灰级入门

528 阅读10分钟

「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」。

1. Vue 入门简介

随着前端技术的不断发展,前端开发能够处理的业务也越来越多,网页也变得越来越强大与动态化,这些进步始终离不开 JavaScript 代码,它连接着各式各样的 HTML 与 CSS 文件,但是缺乏正规的组织形式。所以衍生出了许多 JavaScript 框架: Vue、React、Angular

为什么是 Vue:

  • 易学易用:只需学会 HTML、CSS、JavaScript 三件套即可快速上手
  • 高效:运行速度快,轻量级框架
  • 响应式与组件化开发:实时监听数据;组件复用

官方简介Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架

MVVM

🌤在你真正明白 MVVM 后,就该转变思想来学习 Vue 了,不要在想着怎么操作 DOM(那是 JS 干的事),而是应该想着如何操作数据!!!

在理解 MVVM 之前,着重提一下 vue 的特性:

  • 数据驱动视图:vue 会监听数据的变化,从而自动重新渲染页面结构的单项数据绑定
  • 双向数据绑定:在填写表单时,双向数据绑定可以辅助开发者在不操作 DOM 的前提下自动把用户填写的内容同步到数据源中(即下文的 v-model 的实现效果)。

MVVM 是 vue 实现数据驱动视图双向数据绑定的核心原理。MVVM 指的是 Model、View、ViewModel,它把每个 HTML 页面都拆分成了这三个部分,如图所示:

MVVM 示意图

  • Model: 当前页面渲染时所以来的数据源
  • View: 当前页面所渲染的 DOM 结构
  • ViewModel: 表示 vue 的示例,它是 MVVM 的核心,把当前页面的数据源(Model)和页面结构(View)连接在了一起

数据源 data 发生变化时,会被 ViewModel 监听到,VM 会根据最新的数据源自动更新页面结构。

表单元素值发生变化时,也会被 VM 监听到,VM 会把变化过后最新的值自动同步到 Model 数据源中。

现在看不懂没关系,等你慢慢往下学习,某一天就会恍然大悟。

2. 快速起步

2.1 引入 vue.js

<!-- 开发环境版本,包含了有帮助的命令行警告 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

<!-- 生产环境版本,优化了尺寸和速度 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>

2.2 HelloWorld 初体验

<!-- 使用el控制如下div,将数据填充到div内部 -->
<div id='app'>{{ Hello }}</div>

<!-- 1.导入 vue 的文件库,在 window 全局就有了 Vue 这个构造函数 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

<!-- 2.创建 Vue 实例对象! -->
<script>
    var vm = new Vue({
        // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
        el: '#app',
        // data 对象就是要渲染到页面上的数据
        data: {
            hello: 'Hello,World!'
        }
    })
</script>

运行结果:

如果网速较慢,会遇到插值闪烁的效果:

我们已经成功创建了第一个 Vue 应用!看起来这跟渲染一个字符串模板非常类似,但是 Vue 在背后做了大量工作。

3. Vue 常用指令

指令的 vue 为开发者提供的模板语法,用于辅助开发者渲染页面的基本结构。

vue 中的指令可以按照不同用途分为以下 6 大类:

  • 内容渲染指令: v-textv-html{{}}
  • 属性绑定指令: v-bind
  • 事件绑定指令: v-on
  • 条件渲染指令: v-ifv-else-ifv-elsev-show
  • 列表渲染指令: v-for
  • 双向绑定指令: v-model

你可能会觉得指令很复杂,其实不然,指令是 vue 开发中最基础、最简单、最常用的知识点了。接下来一一详细介绍这些指令。

3.1 v-text

指令 v-text 只能将 data 中的数据以文本的形式渲染到视图上,这就是区别于 v-html 的点 (下文讲解)。

注:如果标签内有内容,仍会被 v-text 渲染的文本所覆盖。

<div id="app">
    <div v-text='msg'></div>
    <div v-text='msg'>Hello Vue!</div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
    var vm = new Vue({
    	el: "#app",
        data: {
            msg: '半截的詩'
        }
    })
</script>

运行结果:

3.2 {{ }}

vue 提供的 {{ }} 语法,专门用于解决 v-text 覆盖默认文本内容的问题。同时,插值表达式支持 JS 表达式。

专业术语:插值表达式 (Mustache)

相对 v-text 指令而言,插值表达式在开发中更常用一些

<div id="app">
    <li>这是第 {{ index }} 个 box</li>
    <li>这是第 {{ index + 1 }} 个 box</li>

    <div>反转前:{{ msg }}</div>
    <div>反转后:{{ msg.split('').reverse().join('') }}</div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
    var vm = new Vue({
        el: "#app",
        data: {
            index: 1,
            msg: 'Hello Vue!'
        }
    })
</script>

运行结果:

3.3 v-html

v-text 指令与插值表达式只能渲染纯文本内容。如果要把包含 HTML 标签的字符串渲染为页面的 HTML 元素,则需要使用 v-html 指令:

<div id="app">
    <div v-html='msg'></div>
    <div v-html='info'></div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
    var vm = new Vue({
        el: "#app",
        data: {
            msg: `<div style="color:red; font-weight:bold">I'm Chinese!</div>`,
            info: 'v-html渲染'
        }
    })
</script>

运行结果:

3.4 v-bind

如果需要为元素的属性动态绑定属性值,则可以用 v-bind 属性绑定指令。

基础用法

简单演示下为 <a> 链接动态绑定 href 属性,若 GitHub 主页地址发生变化,修改 address 数据即可。

<div id="app">
    <a v-bind:href='address'>GitHub主页</a>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
    var vm = new Vue({
        el: '#app',
        data: {
            address: 'http://github.com/Wu-yikun'
        }
    })
</script>

运行结果:

v-bind 缩写语法

<!-- 完整语法 -->
<a v-bind:href="url">...</a>

<!-- 缩写 -->
<a :href="url">...</a>

绑定对象

Class 与 Style 的绑定

操作元素的 class 列表与内联样式 style 是常见的两种需求。我们可以通过 v-bindclassstyle 进行操作。

Vue.js 为此进行了增强,v-bind 绑定的表达式除了字符串以外,还可以是对象数组

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue</title>
  <style>
    .active {
      color: tomato;
    }

    .boldSty {
      font-weight: bold;
    }
  </style>
</head>

<body>
  <div id="app">
    <div :class="{ active: isActive, boldSty: 'true' }">我爱我的国,我爱我的家</div>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  <script>
    var vm = new Vue({
      el: '#app',
      data: {
        isActive: true
      }
    })
  </script>
</body>

渲染结果:

<div id="app">
    <div class="active boldSty"></div>
</div>

运行结果:


⭐绑定的数据对象不必内联在模板里:

<div id="app">
    <div v-bind:class="classObject"></div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
    var vm = new Vue({
        el: '#app',
        data: {
          classObject: {
            active: true,
            'text-danger': false
          }
        }
    })
</script>

style 内联样式的绑定方式也同理,无非就是将 true/false 变成具体的样式对应的属性值!

<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data: {
  activeColor: 'red',
  fontSize: 30
}

绑定数组

Class 与 Style 的绑定

如上说了,v-bind 可以绑定对象或数组,这里演示绑定数组:

<div id="app">
    <div :class="[activeClass, boldClass]">我爱我的国,我爱我的家</div>
    <!-- 加引号''表示直接赋值 -->
    <div :class="['active', 'boldSty']">我爱我的国,我爱我的家</div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
    var vm = new Vue({
        el: '#app',
        data: {
            activeClass: 'active',
            boldClass: 'boldSty'
        }
    })
</script>

style 同理!

3.5 v-on

vue 提供的事件绑定指令 v-on,用于为 DOM 元素绑定事件监听。

通过 v-on 绑定的事件处理函数,需要在 methods 节点中进行声明。

原生 DOM 对象有 onclickoninputonkeyup 等,替换为对应的 v-on:clickv-on:inpuptv-on:keyup

基础用法

<div id="app">
    <button v-on:click='reduce'>-1</button>
    {{ num }}
    <button v-on:click='add'>+1</button>

    <button @click="print('半截的詩')">打印我的名字</button>
    {{name}}
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
    var vm = new Vue({
        el: '#app',
        data: {
            num: 1,
            name: ''
        },
        methods: {
            add() {
                this.num++
            },
            reduce() {
                this.num--
            },
            print(username) {
                this.name = username
            }
        },
    })
</script>

运行结果:

v-on 缩写语法

<!-- 完整语法 -->
<button v-on:click='add'>+1</button>

<!-- 缩写 -->
<button @click='add'>+1</button>

$event

<div id="app">
    <p>number: {{ num }}</p>
    <!-- 显式传入 -->
    <button @click='sub($event)'>-1</button>
    <!-- 隐式传入 -->
    <button @click='add'>+1</button>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
    var vm = new Vue({
        el: "#app",
        data: {
            num: 1
        },
        methods: {
            // 调用该函数时没传参,则该实参为 event 对象
            add(event) {
                console.log(event);
                this.num++
                // 根据奇偶改变 button 的颜色(button 为事件源)
                if (this.num % 2 === 0) {
                    event.target.style.backgroundColor = 'tomato'
                } else {
                    event.target.style.backgroundColor = 'lightBlue'
                }
            },
            sub(e) {
                console.log(e);
                this.num--
            }
        }
    })
</script>

运行结果:

事件修饰符

在事件处理程序中,event.preventDefault()event.stopPropagation() 是非常常见的需求。

因此 Vue.js 为 v-on 提供了事件修饰符:

  • .stop:阻止冒泡,调用 event.stopPropagation()
  • .prevent:阻止默认行为,调用 event.preventDefault()
  • .capture:添加事件侦听器时使用事件捕获模式
  • .self:只当事件在该元素本身(比如不是子元素)触发时,才会触发事件
  • .once:事件只触发一次

修饰符在下一篇文章中详细介绍。

3.6 v-if

v-if 是条件渲染指令中的一种,至于它的用意显而易见。

<div id="app">
    <span v-if='flag'>我出现了!</span>
    <button @click='changeStatus'>{{ msg }}</button>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
    var vm = new Vue({
        el: '#app',
        data: {
            flag: false,
            msg: '显示'
        },
        methods: {
            changeStatus() {
                if (!this.flag) {
                    this.flag = true
                    this.msg = '隐藏'
                } else {
                    this.flag = false
                    this.msg = '显示'
                }
            }
        },
    })
</script>

运行结果:

注:v-if 还可以搭配 v-elsev-else-if 一起使用。

<div id="app">
    <div v-if="type === 'A'">优秀</div>
    <div v-else-if="type === 'B'">良好</div>
    <div v-else-if="type === 'C'">及格</div>
    <div v-else>不及格</div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
    var vm = new Vue({
        el: "#app",
        data: {
            type: 'A'
        }
    })
</script>

运行结果:

3.7 v-show

v-show 则是条件渲染指令中的另一种,它与 v-if 的细微区别在于:

  • 实现原理不同
    • v-if 指令会动态创建或移除 DOM 元素
    • v-show 指令会动态为元素添加或移除 style="display: none;" 样式
  • 性能消耗不同:v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销(不过在如今计算机的加持下,这两者在性能上几乎没有差别)
<div id="app">
    <span v-if='flag1'>v-if 出现!</span>
    <button @click='changeStatus1'>v-if</button>
    <br>
    <span v-show='flag2'>v-show 出现!</span>
    <button @click='changeStatus2'>v-show</button>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
    var vm = new Vue({
        el: '#app',
        data: {
            flag1: true,
            flag2: true
        },
        methods: {
            changeStatus1() {
                if (!this.flag1) {
                    this.flag1 = true
                } else {
                    this.flag1 = false
                }
            },
            changeStatus2() {
                if (!this.flag2) {
                    this.flag2 = true
                } else {
                    this.flag2 = false
                }
            }
        },
    })
</script>

运行结果:

3.8 v-for

v-for 指令可用于遍历数组,数组内可以是普通元素或对象。

<div id="app">
    <ul>
        <li v-for='(item, index) in list' :key='item.id'>序号{{ index }} : {{ item.name }} --- {{ item.age }}</li>
    </ul>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
    var vm = new Vue({
        el: '#app',
        data: {
            list: [
                { id: 1, name: '张三', age: 18 },
                { id: 2, name: '李四', age: 19 },
                { id: 3, name: '半截的詩', age: 21 }
            ]
        }
    })
</script>

运行结果:

⭐关于 v-for 指令有几个注意点:

  • 官方建议一旦使用 v-for 指令,那么就要绑定 :key 属性,且 key 的值不能重复 (否则报错: Duplicate keys detected),因为 key 的作用是为每一个节点做唯一标识
  • (item, index) 的顺序不能错位
  • 不要用 index 作为 key

接下来展开说说~

v-for 为什么必须用 key,且不能使用 index 作为 key?

因为 vue 组件高度复用,增加 key 可以标识组件的唯一性,key 的作用主要是为了高效地更新虚拟 DOM

举个栗子:

const list = [
    { id: 1, name: 'test1' },
    { id: 2, name: 'test2' }
]

如果要在该列表的末尾插入一条数据,那么使用 index 作为 key 完全没有问题,因为此时前三条数据会直接复用之前的数据,然后再新渲染插入的最后一条数据:

const list = [
    { id: 1, name: 'test1' },
    { id: 2, name: 'test2' },
    { id: 3, name: '新数据' }
]

但是如果在列表的中间插入呢?

const list = [
    { id: 1, name: 'test1' },
    { id: 3, name: '新数据' },
    { id: 2, name: 'test2' }
]

此时更新渲染数据后,通过 index 定义的 key 去对比前后数据,发现:

之前的数据                         之后的数据

key: 0  index: 0  name: test1     key: 0  index: 0  name: test1
key: 1  index: 1  name: test2     key: 1  index: 1  name: 新数据
								  key: 2  index: 2  name: test2

可以发现除了第一个数据以外,另外的两条数据都要重新渲染,起不到复用的效果。

这就会达到只插入一条数据,却要重新渲染两条数据的荒谬结果。

最好的办法就是每条数据都有唯有的 id 来标识该数据的唯一性,使用 id 作为 key 值,此时渲染的结果如下:

之前的数据                         之后的数据

key: 0  index: 0  name: test1     key: 1 id: 1 index: 0  name: test1
key: 1  index: 1  name: test2     key: 3 id: 3 index: 1  name: 新数据
								  key: 2 id: 2 index: 2  name: test2

现在对比发现只有一条数据发生了改变,只要渲染新插入的数据即可,其余数据皆复用。

⭐其实底层原理就是虚拟 DOM 的 Diff 算法

插入节点

没有 key 时,diff 算法的执行过程:

只是把 C 更新成 F,D 更新成 C,E 更新成 D,最后再插入 E,是不是很没有效率?

所有我们需要使用 key 来给每一个节点做唯一标识,diff 算法就可以正确识别该节点,找到正确的位置区域插入新节点。

无 key VS 有 key

对于 diff 算法:在插入节点时,有/无 key 就好比链式存储结构/顺序存储结构对插入节点执行的操作。

更多关于 diff 算法的描述:juejin.cn/post/684490…

⭐小结

所以经过以上解释,我们明确了几个点。

  • 用组件唯一的 id 作为它的 key
  • 如果列表顺序会发生改变,别用 index 作为 key,否则会导致 Vue 复用错误的旧子节点,从而多做许多额外的工作
  • 千万别用随机数作为 key,不然会导致旧节点被全部删除,新节点也重新创建

3.9 v-model

我们可以使用 v-model 指令在表单 <input><select><textarea> 元素上创建双向数据绑定。

基础用法

<div id="app">
    <input type="text" v-model='message'>
    <div> {{ message }} </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
    var vm = new Vue({
        el: '#app',
        data: {
            message: '双向绑定数据'
        }
    })
</script>

运行结果:

修饰符

  • .lazy:改变输入框的值时,绑定的 lazyValue 不会改变,当失去焦点时 v-model 绑定的 lazyValue 才会发生改变
  • .number:将值转化为数字
  • .trim:去除首尾空格

修饰符在下一篇文章中详细介绍。

Vue 双向绑定的极简实现【Vue 响应式原理

既然都讲到 v-model 了,我们来看看简单感受下底层是如何实现其双向绑定的效果的。

🎨原理:Object.defineProperty 中的 setget 方法,,通过 gettersetter 访问器指定 读取或设置 对象属性值的时候,执行相应操作。

// defineProperty 语法介绍
var obj = {}

Object.defineProperty(obj, 'msg', {
    set: function (newVal) {
        console.log('set: ' + newVal);
    },
    get: function () {
        console.log('旧的不去,新的不来..');
    }
})

obj.msg = '1'
obj.msg

👑接下来就是极简双向绑定的实现:

<!-- 示例 -->
<input type="text" id="txt"><br/>
<span id="sp"></span>

<script>
    var txt = document.getElementById('txt')
    var sp = document.getElementById('sp')
    obj = {}

    // obj.msg => input、span
    Object.defineProperty(obj, 'msg', {
        // 当 obj.msg 变化时调用set方法
        set: function (newVal) {
            // 当 obj.msg 被赋值时, 同时设置给span与input
            txt.value = newVal
            sp.innerText = newVal
        }
    })

    // 监听文本框的改变, 当文本框输入内容时改变 obj.msg
    txt.addEventListener('keyup', function (event) {
        obj.msg = event.target.value
    })
</script>

看看效果:

🥣动态添加数据的注意点:

  • Vue 中只有 data 中的数据才是响应式的,动态添加进来的数据默认为非响应式
  • 可以通过以下方法实现动态添加响应式数据
    • Vue.set(object, key, value):适用于添加单个属性
    • Object.assign():适用于添加多个属性
var vm = new Vue({
    el: '#app',
    data: {
        info: {
            name: '半截的詩',
            age: 21
        }
    }
})

/* Vue.set */
Vue.set(vm.info, 'gender', 'male')

/* Object.assign: 将参数中所有对象属性和值合并到第一个参数, 并返回合并后的对象 */
Object.assign({}, vm.info, { gender: 'female', height: '170+' })

关于 Vue 双向绑定原理推荐几篇不错的博客

❤️/ END / 如果本文对你有帮助,点个「赞」支持下吧。