Vue3模板语法及虚拟DOM和Diff算法(二)

774 阅读10分钟

模板语法及虚拟DOM和Diff算法

使用模板语法可以将DOM和底层组件实例的数据绑定在一起

在底层的实现中,Vue将模板编译成虚拟DOM渲染函数

Vue模板语法有2大类:

  • 插值语法:
    • 功能:用于解析标签体内容
    • 写法:{{xxx}},xxx是js表达式,且可以直接读取到data中的所有属性
  • 指令语法:
    • 功能:用于解析标签(包括:标签属性、标签体内容、绑定事件.....)
    • 举例:v-bind:href="xxx" 或 简写为 :href="xxx",xxx同样要写js表达式, 且可以直接读取到data中的所有属性
    • Vue中有很多的指令,且形式都是:v-????

1.插值语法

  • 如果我们希望把数据显示到模板(template)上,就需要Mustache”语法 (双大括号) 的文本插值
  • Mustache中不仅仅可以是data中的属性,也可以是一个JavaScript的表达式
  • 当data中的数据发生改变时,对应的内容也会发生更新
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
    </head>
    <body>

        <div id="app">
            <!-- 1.mustache的基本使用 -->
            <h2>{{message}} - {{message}}</h2>
            <!-- 2.是一个表达式 -->
            <h2>{{counter * 10}}</h2>
            <h2>{{ message.split(" ").reverse().join(" ") }}</h2>
            <!-- 3.也可以调用函数 -->
            <h2>{{getReverseMessage()}}</h2>
            <!-- 4.三元运算符 -->
            <h2>{{ isShow ? "哈哈哈": "" }}</h2>
            <button @click="toggle">切换</button>

            <!-- 错误用法 -->
            <!-- {{var name = "abc" }}  赋值语句 -->
            <!-- <h2>{{var name = "abc"}} </h2>
			<h2>{{ if(isShow) {  return "哈哈哈" } }} </h2> -->
        </div>

        <script src="https://unpkg.com/vue@next"></script>
        <script>
            const App = {
                data() {
                    return {
                        message: "Hello World",
                        counter: 100,
                        isShow: true
                    }
                },
                methods: {
                    getReverseMessage() {
                        return this.message.split(" ").reverse().join(" ");
                    },
                    toggle() {
                        this.isShow = !this.isShow;
                    }
                }
            }

            Vue.createApp(App).mount('#app');
        </script>
    </body>
</html>

2.指令

指令是带有 v- 前缀的特殊 attribute,Vue 提供了许多 内置指令

指令 attribute 期望为一个 JavaScript 表达式(v-for和v-on是例外)

v-once

v-once用于指定元素及其子元素或者组件只渲染一次

当数据发生变化时,元素或者组件以及其所有的子元素将视为静态内容并且跳过,可优化性能

image-20220128151056036

如果是子节点,也是只会渲染一次:

image-20220128151018056

v-text

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

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

image-20220128150926515

v-html

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

与插值语法的区别:

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

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

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

image-20220128151641661

条件渲染

在某些情况下,我们需要根据当前的条件决定某些元素或组件是否渲染,这个时候我们就需要进行条件判断了

Vue提供了下面的指令来进行条件判断:

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

v-if、v-else、v-else-if

v-if、v-else、v-else-if用于根据条件来渲染某一块的内容,条件接受为表达式

这些内容只有在表达式条件为true时,才会被渲染出来

并且v-if是惰性的

  • 当条件为true时,才会真正渲染条件块中的内容
  • 当条件为false时,其判断的内容完全不会被渲染或者会被销毁掉

image-20220128160048773

由于v-if是一个指令,所以必须将其添加到一个元素上

但是我们有时候需要切换多个不同分组的元素而不希望在外面的包裹一个不必要的元素进行渲染

这时候我们可以使用template元素

template元素可以当做不可见的包裹元素,在v-if上使用的时候,最终template不会被渲染出来

image-20220128160435808

v-show

v-show和v-if的用法看起来是一致的,也是根据一个条件决定是否显示元素或者组件

但是v-show是不支持template以及和v-else一起使用

本质的区别是:

  • v-show元素无论是否需要显示到浏览器上,它的DOM实际都是有渲染的,只是通过CSS的display属性来进行切换
  • v-if当条件为false时,其对应的元素压根不会被渲染到DOM中

所以如果我们的需要在显示和隐藏之间频繁的切换,那么使用v-show

如果不会频繁的发生切换,那么使用v-if

image-20220128160702409

v-pre

n v-pre用于跳过元素和它的子元素的编译过程,显示原始的Mustache标签

跳过不需要编译的节点,加快编译的速度

image-20220128151930989

v-cloak

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

用css配合v-cloak可以解决网速慢时页面展示出{{xxx}}的问题

image-20220128152848772

v-bind的绑定属性

前面的一系列指令主要是将值插入到模板内容中

但是除了内容需要动态来决定外,某些属性我们也希望动态来绑定

  • 比如动态绑定a元素的href属性
  • 比如动态绑定img元素的src属性
  • 动态绑定元素的类名和样式

v-bind可以使我们动态地绑定一个或多个 attribute,或一个组件 prop 到表达式

预期:any | Object

image-20220128161347273

绑定class

绑定class有三种方式:

  • 字符串
    • 适用于类名不确定,要动态获取
  • 对象语法
    • 适用于要绑定的样式个数确定、名字也确定,但要动态决定用不用
  • 数组语法
    • 适用于要绑定的样式个数不确定、名字也不确定
字符串写法

image-20220130203406477

对象语法

image-20220128162539991

数组语法

image-20220128162844216

绑定style

我们可以利用v-bind:style来绑定一些CSS内联样式

CSS属性名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用引号括起来) 来命名

绑定class有两种方式:

  • 对象语法
  • 数组语法
对象语法

image-20220128163633276

数组语法

:style 的数组语法可以将多个样式对象应用到同一个元素上

image-20220128164456434

动态绑定属性

在某些情况下,我们属性的名称可能也不是固定的

前端我们无论绑定src、href、class、style,属性名称都是固定的

如果属性名称不是固定的,我们可以使用 :[属性名]=“值” 的格式来定义

这种绑定的方式,我们称之为动态绑定属性

image-20220128164609922

绑定一个对象

如果我们希望将一个对象的所有属性,对应绑定到元素上的所有属性

我们可以直接使用 v-bind 绑定一个对象

image-20220128165058382

v-on绑定事件

在前端开发中,我们很多时候需要监听用户发生的事件,比如点击、拖拽、键盘事件等等与用户进行交互

这时候就可以使用v-on:xxx 或 @xxx 绑定事件,其中xxx是事件名

事件的回调需要配置在methods对象中,注意不要写箭头函数

我们可以使用v-on来监听点击的事件

image-20220128165458625

参数传递

当通过methods中定义方法,以供@click调用时,需要注意参数问题

@click="demo" 和 @click="demo($event)" 效果一致,但后者可以传参

image-20220128165817829

修饰符

v-on支持修饰符,修饰符相当于对事件进行了一些特殊的处理

  • .stop - 调用 event.stopPropagation(),阻止事件冒泡(常用)
  • .prevent - 调用 event.preventDefault(),阻止默认事件(常用)
  • .capture - 添加事件侦听器时使用 capture 捕获模式
  • .self - 只有event.target是当前操作的元素时才触发事件
  • .passive 事件的默认行为立即执行,无需等待事件回调执行完毕 @wheel.passive="handler"
  • .once - 只触发一次回调,事件只触发一次(常用)
  • .{keyAlias} - 仅当事件是从特定键触发时才触发回调
  • .left - 只当点击鼠标左键时触发
  • .right - 只当点击鼠标右键时触发
  • .middle - 只当点击鼠标中键时触发

image-20220128170158571

image-20220130201823560

v-for列表渲染

我们往往会从服务器拿到一组数据,并且需要对其进行渲染

这个时候我们可以使用v-for来完成

v-for的基本格式是 "item in 数组":

  • 数组通常是来自data或者prop,也可以是其他方式
  • item是我们给每项元素起的一个别名,这个别名可以自定来定义
  • 如果需要数组索引,可使用 "(item, index) in 数组";

当然v-for也支持遍历对象和数组

image-20220128170502071

类似于v-if,你可以使用 template 元素来包裹循环渲染一段包含多个元素的内容,不使用div

image-20220128170654769

3.数组更新检测和v-for的key

Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括

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

v-for中的key

在使用v-for进行列表渲染时,我们通常会给元素或者组件绑定一个key属性

key属性主要用在Vue的虚拟DOM算法,在新旧nodes对比时辨识VNodes

如果不使用key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法

而使用key时,它会基于key的变化重新排列元素顺序,并且会移除/销毁key不存在的元素

认识VNode

VNode的全称是Virtual Node,也就是虚拟节点

事实上,无论是组件还是元素,它们最终在Vue中表示出来的都是一个个VNode

VNode的本质是一个JavaScript的对象

image-20220128173513920

  • 用JS模拟DOM结构,计算出最小的变更后去操作DOM
  • 只比较同一层级,不跨级比较
  • tag 不相同,则直接删掉重建,不再深度比较
  • tag和key ,两者都相同,则认为是相同节点,不再深度比较
  • 优化时间复杂度到O(n)

68dB1x.png

如果我们不只是一个简单的div,而是有一大堆的元素,那么它们应该会形成一个VNode Tree

image-20220128173557008

插入F的案例

当我们在数组中间插入一个F的时候

image-20220128173741539

当我们点击button的时候需要更新我们的li列表

  • 在Vue中,对于相同父元素的子元素节点并不会重新渲染整个列 表
  • 因为对于列表中 a、b、c、d它们都是没有变化的
  • 在操作真实DOM的时候,我们只需要在中间插入一个f的li即可

而在Vue对列表真正操作的时候会针对key做不同的操作

  • 有key,那么就使用 patchKeyedChildren方法
  • 没有key,那么久使用 patchUnkeyedChildren方法

没有key的操作

Vue3源码:

image-20220128191711728

这时候diff效率不高,c和d来说它们事实上并不需要有任何的改动

但是因为我们的c被f所使用了,所有后续所有的内容都要进行一次改动,并且最后进行新增

image-20220128174442513

有key的操作

Vue3源码:

image-20220128192336020

第一步的操作是从头开始进行遍历、比较:

  • a和b是一致的会继续进行比较
  • c和f因为key不一致,所以就会break跳出循环

image-20220128194301690

第二步的操作是从尾部开始进行遍历、比较:

image-20220128194314162

第三步是如果旧节点遍历完毕,但是依然有新的节点,那么就新增节点:

image-20220128194403068

第四步是如果新的节点遍历完毕,但是依然有旧的节点,那么就移除旧节点:

image-20220128194426051

第五步是最特色的情况,中间还有很多未知的或者乱序的节点:

image-20220128194846522

所以Vue在进行diff算法的时候,会尽量利用我们的key来进行优化操作

当没有key的时候我们的效率是非常低效的,所以保持相同的key可以让diff算法更加的高效