一、虚拟DOM

直接操作真实的DOM会引发严重的效率问题,Vue使用虚拟 DOM (vnode) 的方式来描述要渲染的内容,Vue 的渲染系统正是基于这个概念构建

1.1 概念

虚拟 DOM (Virtual DOM,简称 VDOM) 是一种编程概念,意为将目标所需的 UI 通过数据结构“虚拟”地表示出来,保存在内存中,然后将真实的 DOM 与之保持同步。这个概念是由 React 率先开拓,随后被许多不同的框架采用,当然也包括 Vue。

与其说虚拟 DOM 是一种具体的技术,不如说是一种模式,所以并没有一个标准的实现

vnode 是一个普通的JS对象,用于描述界面上应该有什么,比如:

1
2
3
4
5
6
var vnode = {
tag:"h1",
children:[
{ tag:undefined, text:"第一个vue应用:Hello World"}
]
}

上面的对象描述了:

1
有一个标签名为h1的节点,它有一个子节点,该子节点是一个文本,内容为「第一个vue应用:Hello World」

vue模板并不是真实的DOM,它会被编译为虚拟DOM

1
2
3
4
<div id="app">
<h1>第一个vue应用:{{tit1e}}</h1>
<p>作者:{{author}}</p>
</div>
1
2
3
4
5
6
7
8
9
const vnode = {
type: 'div',
props: {
id: 'hello'
},
children: [
/* 更多 vnode */
]
}

这里所说的 vnode 即一个纯 JavaScript 的对象 (一个“虚拟节点”),它代表着一个 <div> 元素。它包含我们创建实际元素所需的所有信息。它还包含更多的子节点,这使它成为虚拟 DOM 树的根节点。

一个运行时渲染器将会遍历整个虚拟 DOM 树,并据此构建真实的 DOM 树。这个过程被称为挂载 (mount)。

如果我们有两份虚拟 DOM 树,渲染器将会有比较地遍历它们,找出它们之间的区别,并应用这其中的变化到真实的 DOM 上。这个过程被称为更新 (patch),又被称为“比对”(diffing) 或“协调”(reconciliation)。

虚拟 DOM 带来的主要收益是它让开发者能够灵活、声明式地创建、检查和组合所需 UI 的结构,同时只需把具体的 DOM 操作留给渲染器去处理。

二、渲染管线

三、渲染函数Vs模板

Vue 模板会被预编译成虚拟 DOM 渲染函数。Vue 也提供了 API 使我们可以不使用模板编译,直接手写渲染函数。在处理高度动态的逻辑时,渲染函数相比于模板更加灵活,因为你可以完全地使用 JavaScript 来构造你想要的 vnode。

那么为什么 Vue 默认推荐使用模板呢?有以下几点原因:

  1. 模板更贴近实际的 HTML。这使得我们能够更方便地重用一些已有的 HTML 代码片段,能够带来更好的可访问性体验、能更方便地使用 CSS 应用样式,并且更容易使设计师理解和修改。
  2. 由于其确定的语法,更容易对模板做静态分析。这使得 Vue 的模板编译器能够应用许多编译时优化来提升虚拟 DOM 的性能表现

在实践中,模板对大多数的应用场景都是够用且高效的。渲染函数一般只会在需要处理高度动态渲染逻辑的可重用组件中使用。

四、带编译时信息的虚拟 DOM

虚拟 DOM 在 React 和大多数其他实现中都是纯运行时的:更新算法无法预知新的虚拟 DOM 树会是怎样,因此它总是需要遍历整棵树、比较每个 vnode 上 props 的区别来确保正确性。另外,即使一棵树的某个部分从未改变,还是会在每次重渲染时创建新的 vnode,带来了大量不必要的内存压力。这也是虚拟 DOM 最受诟病的地方之一:这种有点暴力的更新过程通过牺牲效率来换取声明式的写法和最终的正确性。

但实际上我们并不需要这样。在 Vue 中,框架同时控制着编译器和运行时。这使得我们可以为紧密耦合的模板渲染器应用许多编译时优化。编译器可以静态分析模板并在生成的代码中留下标记,使得运行时尽可能地走捷径。与此同时,我们仍旧保留了边界情况时用户想要使用底层渲染函数的能力。我们称这种混合解决方案为带编译时信息的虚拟 DOM

4.1 静态提升

1
2
3
4
5
<div>
<div>foo</div> <!-- 需提升 -->
<div>bar</div> <!-- 需提升 -->
<div>{{ dynamic }}</div>
</div>

foobar 这两个 div 是完全静态的,没有必要在重新渲染时再次创建和比对它们。Vue 编译器自动地会提升这部分 vnode 创建函数到这个模板的渲染函数之外,并在每次渲染时都使用这份相同的 vnode,渲染器知道新旧 vnode 在这部分是完全相同的,所以会完全跳过对它们的差异比对。

此外,当有足够多连续的静态元素时,它们还会再被压缩为一个“静态 vnode”,其中包含的是这些节点相应的纯 HTML 字符串。(示例)。这些静态节点会直接通过 innerHTML 来挂载。同时还会在初次挂载后缓存相应的 DOM 节点。如果这部分内容在应用中其他地方被重用,那么将会使用原生的 cloneNode() 方法来克隆新的 DOM 节点,这会非常高效。

4.2 更新类型标记

对于单个有动态绑定的元素来说,我们可以在编译时推断出大量信息:

1
2
3
4
5
6
7
8
<!-- 仅含 class 绑定 -->
<div :class="{ active }"></div>

<!-- 仅含 id 和 value 绑定 -->
<input :id="id" :value="value">

<!-- 仅含文本子节点 -->
<div>{{ dynamic }}</div>

在为这些元素生成渲染函数时,Vue 在 vnode 创建调用中直接编码了每个元素所需的更新类型:

1
2
3
createElementVNode("div", {
class: _normalizeClass({ active: _ctx.active })
}, null, 2 /* CLASS */)

最后这个参数 2 就是一个更新类型标记 (patch flag)。一个元素可以有多个更新类型标记,会被合并成一个数字。运行时渲染器也将会使用位运算来检查这些标记,确定相应的更新操作:

1
2
3
if (vnode.patchFlag & PatchFlags.CLASS /* 2 */) {
// 更新节点的 CSS class
}

位运算检查是非常快的。通过这样的更新类型标记,Vue 能够在更新带有动态绑定的元素时做最少的操作。

Vue 也为 vnode 的子节点标记了类型。举例来说,包含多个根节点的模板被表示为一个片段 (fragment),大多数情况下,我们可以确定其顺序是永远不变的,所以这部分信息就可以提供给运行时作为一个更新类型标记。

1
2
3
4
5
export function render() {
return (_openBlock(), _createElementBlock(_Fragment, null, [
/* children */
], 64 /* STABLE_FRAGMENT */))
}

4.3 树结构打平

这里我们引入一个概念“区块”,内部结构是稳定的一个部分可被称之为一个区块。在这个用例中,整个模板只有一个区块,因为这里没有用到任何结构性指令 (比如 v-if 或者 v-for)。

每一个块都会追踪其所有带更新类型标记的后代节点 (不只是直接子节点),举例来说:

1
2
3
4
5
6
7
<div> <!-- root block -->
<div>...</div> <!-- 不会追踪 -->
<div :id="id"></div> <!-- 要追踪 -->
<div> <!-- 不会追踪 -->
<div>{{ bar }}</div> <!-- 要追踪 -->
</div>
</div>

编译的结果会被打平为一个数组,仅包含所有动态的后代节点:

1
2
3
div (block root)
- div 带有 :id 绑定
- div 带有 {{ bar }} 绑定

当这个组件需要重渲染时,只需要遍历这个打平的树而非整棵树。这也就是我们所说的树结构打平,这大大减少了我们在虚拟 DOM 协调时需要遍历的节点数量。模板中任何的静态部分都会被高效地略过。

-ifv-for 指令会创建新的区块节点:

1
2
3
4
5
6
7
<div> <!-- 根区块 -->
<div>
<div v-if> <!-- if 区块 -->
...
<div>
</div>
</div>

一个子区块会在父区块的动态子节点数组中被追踪,这为他们的父区块保留了一个稳定的结构。

五、组件

组件的出现是为了实现以下两个目标:

  1. 降低整体复杂度,提升代码的可读性和可维护性
  2. 提升局部代码的可复用性

绝大部分情况下,一个组件就是页面中某个区域,组件包含该区域的:

  • 功能 (JS代码)

  • 内容(模板代码)

  • 样式 (CSS代码)

    要在组件中包含样式,需要构建工具的支撑

5.1 创建组件

组件是根据一个普通的配置对象创建的,所以要开发一个组件,只需要写一个配置对象即可

该配置对象和vue实例的配置是几乎一样的

1
2
3
4
5
6
7
8
9
//组件配置对象
var myComp =
data(){
return
//...
}
},
template: `....`
}

值得注意的是,组件配置对象和vue实例有以下几点差异:

  • el
  • data必须是一个函数,该函数返回的对象作为数据
  • 由于没有el配置,组件的虚拟DOM树必须定义在templaterender

5.2 注册组件

注册组件分为两种方式,一种是全局注册,一种是局部注册

5.2.1 全局注册

一旦全局注册了一个组件,整个应用中任何地方都可以使用该组件

全局注册的方式是:

1
2
3
4
//参数1:组件名称,将来在模板中使用组件时,会使用该名称
//参数2:组件配置对象
//该代码运行后,即可在模板中使用组件
Vue.component('my-comp',myComp)
1
2
3
<my-comp />
<!-或-->
<my-comp></my-comp>

但在一些工程化的大型项目中,很多组件都不需要全局使用。

比如一个登录组件,只有在登录的相关页面中使用,如果全局注册,将导致构建工具无法优化打包

因此,除非组件特别通用,否则不建议使用全局注册

5.2.2 局部注册

局部注册就是哪里要用到组件,就在哪里注册

局部注册的方式是,在要使用组件的组件或实例中加入一个配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//这是另一个要使用my-comp的组件
var otherComp = {
components: {
//属性名为组件名称,模板中将使用该名称
//属性值为组件配置对象
"my-comp":myComp
},
template:`
<div>
<!--该组件的其他内容 -->
<my-comp></my-comp>
</div>
`;
}

只能在注册的组件内使用创建.vue文件在使用的组件内导入并注册

1
2
3
4
5
6
7
8
9
10
// 使用
<组件对象 />
// 导入
import 组件对象 from '文件路径'
export default
{
// 注册
components: { 组件对象 }

}

5.3 应用组件

在模板中使用组件特别简单,把组件名当作HTML元素名使用即可。

但要注意以下几点:

1.组件必须有结束

组件可以自结束,也可以用结束标记结束,但必须要有结束

下面的组件使用是错误的:

1
<my-comp>

2.组件的命名

无论你使用哪种方式注册组件,组件的命名需要遵循规范。

组件可以使用kebab-case短横线命名法,也可以使用PascalCase大驼峰命名法

下面两种命名均是可以的

1
2
3
4
var otherComp = {
components:{
"my-comp":myComp, //方式1
MyComp:myComp//方式2

实际上,使用小驼峰命名法camelcase也是可以识别的,只不过不符合官方要求的规范

使用PascalCase方式命名还有一个额外的好处,即可以在模板中使用两种组件名

1
2
3
4
5
var otherComp = {
components:{
MyComp:myComp
}
}

应用特定样式和约定的基础组件 (也就是展示类的、无逻辑的或无状态的组件) 应该全部以一个特定的前缀开头,比如 BaseAppV。像button icon 这类的

5.4 组件化开发

一个页面可以拆分成一个个的组件,每个组件都有着自己独立的结构、样式、行为

好处:便于维护,利于复用 → 提升开发效率>

data

一个组件的data选项必须是一个函数 → 保证每个组件实例,维护独立的一份数据对象。

原理:每次创建新的组件实例,都会新执行一次 data 函数,得到一个新的对象

如果是对象的话,就做不到这点

5.5 组件树

一个组件创建好后,往往会在各种地方使用它。它可能多次出现在Vue实例中,也可能出现在其他组件中,于是就形成了一个组件树

注意:在组件中,属性是只读的,绝不可以更改,这叫做单向数据流

六、props

6.1.单向数据流传递:

假设我们有一个父组件Parent和一个子组件Child,Parent中有一个message的数据需要传递给Child组件。在Parent组件中定义如下:

1
2
3
4
5
<template>
<div>
<ChildComponent message="Hello, World!"/>
</div>
</template>

在Child组件中通过props接收传递过来的数据:

1
2
3
<template>
<div>{{ message }}</div>
</template>
1
2
3
4
5
6
7
<script>
export default {
props: {
message: String
}
}
</script>

这里我们将”Hello, World!”通过props传递给了Child组件,Child组件中通过的方式来显示该数据。

6.2.动态props传递:

假设我们的Parent组件中有一个数据项data,该数据项可能会发生变化。我们可以通过props将该数据项传递给Child组件,并在Parent组件中动态更新data数据,从而实现Child组件的动态更新。

Parent组件:

1
2
3
4
5
6
<template>
<div>
<ChildComponent :message="data"/>
<button @click="updateData">Update Data</button>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
export default {
data() {
return {
data: 'Hello, World!'
}
},
methods: {
updateData() {
this.data = 'Hello, Vue!'
}
}
}
</script>

Child组件:

1
2
3
<template>
<div>{{ message }}</div>
</template>
1
2
3
4
5
6
7
<script>
export default {
props: {
message: String
}
}
</script>

在Parent组件中,我们通过动态绑定将data数据传递给Child组件。当我们点击”Update Data”按钮时,会触发updateData方法,该方法会更新data数据,从而实现Child组件的动态更新。

6.3.通过Prop验证传递:

假设我们在Parent组件中定义了一个age的prop,要求该prop必须为数字且大于0。可以通过下面的方式添加验证规则:

Parent组件:

1
2
3
4
5
<template>
<div>
<ChildComponent :age="age"/>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
export default {
data() {
return {
age: 10
}
},
props: {
age: {
type: Number,
required: true,
validator(value) {
return value > 0
}
}
}
}
</script>

在上述代码中,我们通过props中的type、required和validator属性来添加了验证规则,确保传递给Child组件的age数据是一个大于0的数字。如果传递的数据不符合验证规则,会触发一个警告。

6.4.通过Prop传递函数:

在Parent组件中定义一个处理数据的函数,并将其作为prop传递给Child组件:

1
2
3
<template>
<div>{{ processData('Hello, World!') }}</div>
</template>
1
2
3
4
5
6
7
<script>
export default {
props: {
processData: Function
}
}
</script>

在Child组件中,我们通过props接收传递过来的processData函数,并可以直接调用该函数来处理数据。这种方式可以使得Parent组件中的处理数据逻辑可以在多个子组件中复用。

6.5.默认Prop值:

我们可以为props设置默认值,以确保即使没有传递该prop,子组件也可以使用一个默认值进行操作。例如,在Child组件中,我们可以设置一个默认的message值:

1
2
3
<template>
<div>{{ message }}</div>
</template>
1
2
3
4
5
6
7
8
9
10
<script>
export default {
props: {
message: {
type: String,
default: 'Hello, World!'
}
}
}
</script>

在上述代码中,我们通过props中的default属性为message设置了一个默认值”Hello, World!”。如果在父组件中没有传递message数据,Child组件会使用该默认值来进行操作。

这些例子涵盖了Vue中props的一些常见使用方式。通过props,我们可以在Vue中实现组件之间的数据传递和解耦,从而让我们的应用更加灵活和可维护。

七、ref

7.1 用法

ref 有三种用法:

  • ref 加在普通的元素上,用this.$refs.(ref值) 获取到的是dom元素

  • ref 加在子组件上,用this.$refs.(ref值) 获取到的是组件实例,可以使用组件的所有方法。在使用方法的时候直接this.$refs.(ref值).方法() 就可以使用了。

  • 利用 v-for 和 ref 获取一组数组或者dom 节点

7.2 注意点

1、如果通过v-for 遍历想加不同的ref时记得加 :号,即 :ref =某变量 ;
这点和其他属性一样,如果是固定值就不需要加 :号,如果是变量记得加 :号。(加冒号的,说明后面的是一个变量或者表达式;没加冒号的后面就是对应的字符串常量(String))

2、通过 :ref =某变量 添加ref(即加了:号) ,如果想获取该ref时需要加 [0],如this.$refs[refsArrayItem] [0];如果不是:ref =某变量的方式而是 ref =某字符串时则不需要加,如this.$refs[refsArrayItem]。

八、mixin:

一个 Mixin 对象可以包含任意组件选项。当组件使用 Mixin 对象时,所有 Mixin 对象的选项将被“混合”进入该组件本身的选项。

Mixin的特点可以复用逻辑,并且变量是独立的,在组件中是独立的不会相互影响。

Mixin很容易造成命名冲突,因此在使用的时候必须要确保不会有冲突的属性名称,这样就会造成额外的起名负担

8.1 基础

混入(mixin)提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项(如data、methods、mounted等等)。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

8.2 基本使用

1、定义一个混入(mixin)

1
2
3
4
5
6
7
8
9
10
11
12
let mixin = {
created() {
console.log("我是mixin里面的created!")
},
methods: {
hello() {
console.log("hello from mixin!")
}
}
}

export default mixin

2、在组件(Home.vue)中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div class="home">
<span>This is a Home page</span>
</div>
</template>

<script>
import myMixins from "../mixins"; //导入混入(mixin)
export default {
name: 'Home',
mixins: [myMixins] //使用混入(mixin)
}
</script>

8.3 选项合并

当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。

如下,我们在Home.vue中定义与混入对象中同名的选项

mixin.js

Home.vue

8.4 全局混入

混入也可以进行全局注册。使用时格外小心!一旦使用全局混入,它将影响每一个之后创建的 Vue 实例。使用恰当时,这可以用来为自定义选项注入处理逻辑。

在main.js中通过Vue.mixin()引入混入对象即可全局使用(作用于该Vue实例下的所有组件)

工程版本一致性

node版本一致

node版本切换如下

Vue的版本全局安装一致性

九、vue-cli

vue-cIi是一个脚手架工具,用于搭建vue工程

它内部使用了webpack,并预置了诸多插件(plugin)和加载器(loader),以达到开箱即用的效果
除了基本的插件和加载器外,vue-cli还预置了:

  • babel
  • webpack-dev-server
  • eslint
  • postcss
  • less-loader

创建工程时遇到问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
C:\Users\xzj\Desktop\学习\vue\再识Vue2\再识Vue2_项目>vue create my_site
D:\nvm\v12.17.0\node_modules\@vue\cli\node_modules\vue-codemod\node_modules\@vue\compiler-sfc\dist\compiler-sfc.cjs.js:361
this.signal.onabort?.(reason);
^

SyntaxError: Unexpected token '.'
at wrapSafe (internal/modules/cjs/loader.js:1054:16)
at Module._compile (internal/modules/cjs/loader.js:1102:27)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1158:10)
at Module.load (internal/modules/cjs/loader.js:986:32)
at Function.Module._load (internal/modules/cjs/loader.js:879:14)
at Module.require (internal/modules/cjs/loader.js:1026:19)
at require (internal/modules/cjs/helpers.js:72:18)
at Object.<anonymous> (D:\nvm\v12.17.0\node_modules\@vue\cli\node_modules\vue-codemod\dist\src\run-transformation.js:9:24)
at Module._compile (internal/modules/cjs/loader.js:1138:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1158:10)

要解决这个问题需要:

1.更新 Node.js: 确保你使用的是支持 ECMAScript 2020(ES11)或更新版本的 Node.js。你可以将 Node.js 更新到更高版本,最好使用 LTS(长期支持)版本。

2.更新或修改代码: 如果更新 Node.js 不可行,可以修改代码以使用更传统的方法,而不使用可选链。例如,将:

1
this.signal.onabort?.(reason);

替换为

1
2
3
if (this.signal.onabort) {
this.signal.onabort(reason);
}

十、SFC

单文件组件,Single File Component,即一个文件就包含了一个组件所需的全部代码

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<!--组件模板代码-->
</template>

<script>
export default {
//组件配置
}
</script>

<style>
/*组件样式*/
</style>

十一、Vue打包运行的过程

源代码 => 打包 => 运行

预编译

当vue-cli进行打包时,会直接把组件中的模板转换为render函数,这叫做模板预编译

这样做的好处在于:

  1. 运行时就不再需要编译模板了,提高了运行效率
  2. 打包结果中不再需要vue的编译代码,减少了打包体积

十二、计算属性

完整的计算属性书写:

1
2
3
4
5
6
7
8
9
10
computed:{
propName:{
get(){
//getter
},
set(val){
//setter
}
}
}

只包含getter的计算属性简写:

1
2
3
4
5
computed:{
propName(){
//getter
}
}

十三、面试题

计算属性和方法有什么区别?

1
2
3
4
5
6
7
8
9
计算属性本质上是包含getter和setter的方法
当获取计算属性时,实际上是在调用计算属性的getter方法。vue会收集计算属性的依赖,并缓存计算属性的返回结果。只
有当依赖变化后才会重新进行计算。
方法没有缓存,每次调用方法都会导致重新执行。
计算属性的getter和setter参数固定,getter没有参数,setter只有一个参数。而方法的参数不限。
由于有以上的这些区别,因此计算属性通常是根据已有数据得到其他数据,并在得到数据的过程中不建议使用异步、当前时
间、随机数等副作用操作。
实际上,他们最重要的区别是含义上的区别。计算属性含义上也是一个数据,可以读取也可以赋值;方法含义上是一个操
作,用于处理一些事情。

使用当前时间反例子:

scoped的原理:

1、当前组件内标签都被添加 data-v-hash 值的属性

2、css选择器都被添加 [data-v-hash]值的属性选择器

最终效果:

必须是当前组件的元素,才会有这个自定义属性,才会被这个样式作用到