用Node.js手写一个DNS服务器

分布式的话就要划分什么域名让什么服务器来处理,把请求的压力分散开。

很容易想到的是顶级域、二级域、三级域分别放到不同的服务器来解析。

所有的顶级域服务器也有个目录,叫做根域名服务器。

这样查询某个域名的 IP 时就先向根域名服务器查一下顶级域的地址,然后有二级域的话再查下对应服务器的地址,一层层查,直到查到最终的 IP。

当然,之前的 hosts 的方式也没有完全废弃,还是会先查一下 hosts,如果查不到的话再去请求域名服务器。

也就是这样的:

比如查 www.baidu.com 这个域名的 IP,就先查本地 hosts,没有查到的话就向根域名服务器查 com 域的通用顶级域名服务器的地址,之后再向这个顶级域名服务器查询 baidu.com 二级域名服务器的地址,这样一层层查,直到查到最终的 IP。

这样就通过分布式的方式来分散了服务器的压力。

但是这样设计还是有问题的,每一级域一个服务器,如果域名的层次过多,那么就要往返查询好多次,效率也不高。

所以 DNS(Domain Name System)只分了三级域名服务器:

根域名服务器:记录着所有顶级域名服务器的地址,是域名解析的入口顶级域名服务器:记录着各个二级域名对应的服务器的地址权威域名服务器:该域下二级、三级甚至更多级的域名都在这里解析

其实就是把二、三、四、五甚至更多级的域名都合并在一个服务器解析了,叫做权威域名服务器(Authoritative Domain Name Server)。

这样既通过分布式减轻了服务器的压力,又避免了层数过多导致的解析慢。

当然,每次查询还是比较耗时的,查询完之后要把结果缓存下来,并且设置一个过期时间,域名解析记录在 DNS 服务器上的缓存时间叫做 TTL(Time-To-Live)。

但现在只是在某一台机器上缓存了这个解析结果,可能某个区域的其他机器在访问的时候还是需要解析的。

所以 DNS 设计了一层本地域名服务器,由它来负责完成域名的解析,并且把结果缓存下来。

这样某台具体的机器只要向这个本地域名服务器发请求就可以了,而且解析结果其他机器也可以直接用。

这样的本地域名服务器是移动、联通等 ISP(因特网服务提供商)提供的,一般在每个城市都有一个。某台机器访问了某个域名,解析之后会把结果缓存下来,其他机器访问这个域名就不用再次解析了。

这个本地域名服务器的地址是可以修改的,在 mac 里可以打开系统偏好设置 --> 网络 --> 高级 --> DNS来查看和修改本地域名服务器的地址。

这就是 DNS 的原理。

不知道大家看到本地域名服务器的配置可以修改的时候,是否有自己实现一个 DNS 服务器的冲动。

确实,这个 DNS 服务器完全可以自己实现,接下来我们就用 Node.js 实现一下。

我们先来分析下思路:

DNS 服务器实现思路分析

DNS 是应用层的协议,协议内容的传输还是要通过传输层的 UDP 或者 TCP。

我们知道,TCP 会先三次握手建立连接,之后再发送数据包,并且丢失了会重传,确保数据按顺序送达。

它适合一些需要进行多次请求、响应的通信,因为这种通信需要保证处理顺序,典型的就是 HTTP。

但这样的可靠性保障也牺牲了一定的性能,效率比较低。

而 UDP 是不建立连接,直接发送数据报给对方,效率比较高。适合一些不需要保证顺序的场景。

显然,DNS 的每次查询请求都是独立的,没有啥顺序的要求,比较适合 UDP。

所以我们需要用 Node.js 起一个 UDP 的服务来接收客户端的 DNS 数据报,自己实现域名的解析,或者转发给其他域名服务器来处理。之后发送解析的结果给客户端。

创建 UDP 服务和发送数据使用 Node.js 的 dgram 这个包。

类似这样:

const dgram = require(dgram);const server = dgram.createSocket(udp4)server.on(message, (msg, rinfo) => {    // 处理 DNS 协议的消息})server.on(error, (err) => {    // 处理错误})  server.on(listening, () => {    // 当接收方地址确定时});server.bind(53);

具体代码后面再细讲,这里知道接收 DNS 协议数据需要启 UDP 服务就行。

DNS 服务器上存储着域名和 IP 对应关系的记录,这些记录有 4 种类型:

A:域名对应的 IPCNAME:域名对应的别名MX:邮件名后缀对应的域名或者 IPNS:域名需要去另一个 DNS 服务器解析PTR:IP 对应的域名

其实还是很容易理解的:

类型 A 就是查询到了域名对应的 IP,可以直接告诉客户端。

类型 NS 是需要去另一台 DNS 服务器做解析,比如顶级域名服务器需要进一步去权威域名服务器解析。

CNAME 是给当前域名起个别名,两个域名会解析到同样的 IP。

PTR 是由 IP 查询域名用的,DNS 是支持反向解析的

而 MX 是邮箱对应的域名或者 IP,用于类似 @xxx.com 的邮件地址的解析。

当 DNS 服务器接收到 DNS 协议数据就会去这个记录表里查找对应的记录,然后通过 DNS 协议的格式返回。

那 DNS 协议格式是怎么样的呢?

大概是这样:

内容还是挺多的,我们挑几个重点来看一下:

Transction ID 是关联请求和响应用的。

Flags 是一些标志位:

比如 QR 是标识是请求还是响应。OPCODE 是标识是正向查询,也就是域名到 IP,还是反向查询,也就是 IP 到域名。

再后面分别是问题的数量、回答的数量、授权的数量、附加信息的数量。

之后是问题、回答等的具体内容。

问题部分的格式是这样的:

首先是查询的名字,比如 baidu.com,然后是查询的类型,就是上面说的那些 A、NS、CNAME、PTR 等类型。最后一个查询类一般都是 1,表示 internet 数据。

回答的格式是这样的:

Name 也是查询的域名,Type 是 A、NS、CNAME、PTR 等,Class 也是和问题部分一样,都是 1。

然后还要指定 Time to live,也就是这条解析记录要缓存多长时间。DNS 就是通过这个来控制客户端、本地 DNS 服务器的缓存过期时间的。

最后就是数据的长度和内容了。

这就是 DNS 协议的格式。

我们知道了如何启 UDP 的服务,知道了接收到的 DNS 协议数据是什么格式的,那么就可以动手实现 DNS 服务器了。解析出问题部分的域名,然后自己实现解析,并返回对应的响应数据。

大概理清了原理,我们来写下代码:

手写 DNS 服务器

首先,我们创建 UDP 的服务,监听 53 号端口,这是 DNS 协议的默认端口。

const dgram = require(dgram)const server = dgram.createSocket(udp4)server.on(message, (msg, rinfo) => {    console.log(msg)});server.on(error, (err) => {    console.log(`server error:\n${err.stack}`)    server.close()})  server.on(listening, () => {    const address = server.address()    console.log(`server listening ${address.address}:${address.port}`)})  server.bind(53)

通过 dgram 模块创建 UDP 服务,启动在 53 端口,处理开始监听的事件,打印服务器地址和端口,处理错误的事件,打印错误堆栈。收到消息时直接打印。

修改系统偏好设置的本地 DNS 服务器地址指向本机:

这样再访问网页的时候,我们的服务控制台就会打印收到的消息了:

一堆 Buffer 数据,这就是 DNS 协议的消息。

我们从中把查询的域名解析出来打印下,也就是这部分:

问题前面的部分有 12 个字节,所以我们截取一下再 parse:

server.on(message, (msg, rinfo) => {  const host = parseHost(msg.subarray(12))  console.log(`query: ${host}`)})

msg 是 Buffer 类型,是 Uint8Array 的子类型,也就是无符号整型。(整型存储的时候可以带符号也可以不带符号,不带符号的话可以存储的数字会大一倍。)

调用它的 subarray 方法,截取掉前面 12 个字节。

然后解析问题部分:

问题的最开始就是域名,我们只要把域名解析出来就行。

我们表示域名是通过 . 来区分,但是存储的时候不是,是通过

当前域长度 + 当前域内容 + 当前域长度 + 当前域内容 + 当前域长度 + 当前域内容 + 0

这样的格式,以 0 作为域名的结束。

所以解析逻辑是这样的:

function parseHost(msg) {  let num = msg.readUInt8(0);  let offset = 1;  let host = "";  while (num !== 0) {    host += msg.subarray(offset, offset + num).toString();    offset += num;    num = msg.readUInt8(offset);    offset += 1;    if (num !== 0) {      host += .    }  }  return host}

通过 Buffer 的 readUInt8 方法来读取一个无符号整数,通过 Buffer 的 subarray 方法来截取某一段内容。

这两个方法都要指定 offet,也就是从哪里开始。

我们先读取一个数字,也就是当前域的长度,然后读这段长度的内容,然后继续读下一段,直到读到 0,代表域名结束。

把中间的这些域通过 . 连接起来。比如 3 www 5 baidu 3 com 处理之后就是 www.baidu.com。

之后我们重启下服务器测试下效果:

我们成功的从 DNS 协议数据中把 query 的域名解析了出来!

解析 query 部分只是第一步,接下来还要返回对应的响应。

这里我们只自己处理一部分域名,其余的域名还是交给别的本地 DNS 服务器处理:

server.on(message, (msg, rinfo) => {    const host = parseHost(msg.subarray(12))    console.log(`query: ${host}`);    if (/guangguangguang/.test(host)) {        resolve(msg, rinfo)    } else {        forward(msg, rinfo)    }});

解析出的域名如果包含 guangguangguang,那就自己处理,构造对应的 DNS 协议消息返回。

否则就转发到别的本地 DNS 服务器处理,把结果返回给客户端。

先实现 forward 部分:

转发到别的 DNS 服务器,那就是创建一个 UDP 的客户端,把收到的消息传给它,收到消息后再转给客户端。

也就是这样的:

function forward(msg, rinfo) {    const client = dgram.createSocket(udp4);    client.on(error, (err) => {      console.log(`client error:\n${err.stack}`);      client.close();    });    client.on(message, (fbMsg, fbRinfo) => {      server.send(fbMsg, rinfo.port, rinfo.address, (err) => {        err && console.log(err)      })      client.close();    });    client.send(msg, 53, 192.168.199.1, (err) => {      if (err) {        console.log(err)        client.close()      }    });}

通过 dgram.createSocket 创建一个 UDP 客户端,参数的 udp4 代表是 IPv4 的地址。

处理错误、监听消息,把 msg 转发给目标 DNS 服务器(这里的 DNS 服务器地址大家可以换成别的)。

收到返回的消息之后传递给客户端。

客户端的 ip 和端口是通过参数传进来的。

这样就实现了 DNS 协议的中转,我们先测试下现在的效果。

使用 nslookup 命令来查询某个域名的地址:

可以看到,查询 baidu.com 是能拿到对应的 IP 地址的,在浏览器里也就可以访问。

而 guangguangguang.ddd.com 没有查找到对应的 IP。

接下来实现 resolve 方法,自己构造一个 DNS 协议的消息返回 。

还是这样的格式:

大概这样构造:

会话 ID 从传过来的 msg 取,flags 也设置下,问题数回答数都是 1,授权数、附加数都是 0。

问题区域和回答区域按照对应的格式来设置:

需要用 Buffer.alloc 创建一个 buffer 对象。

过程中还会用到 buffer.writeUInt16BE 来写一些无符号的双字节整数。

这里的 BE 是 Big Endian,大端序,也就是高位放在右边的、低位放在左边,

比如 是大端序的双字节无符号整数 1。而小端序的 1 则是,也就是高位放在左边。

拼装 DNS 协议的消息还是挺麻烦的,大家简单看一下就行:

function copyBuffer(src, offset, dst) {    for (let i = 0; i < src.length; ++i) {      dst.writeUInt8(src.readUInt8(i), offset + i)    }  }function resolve(msg, rinfo) {    const queryInfo = msg.subarray(12)    const response = Buffer.alloc(28 + queryInfo.length)    let offset = 0    // Transaction ID    const id  = msg.subarray(0, 2)    copyBuffer(id, 0, response)      offset += id.length        // Flags    response.writeUInt16BE(0x8180, offset)      offset += 2    // Questions    response.writeUInt16BE(1, offset)      offset += 2    // Answer RRs    response.writeUInt16BE(1, offset)      offset += 2    // Authority RRs & Additional RRs    response.writeUInt32BE(0, offset)      offset += 4    copyBuffer(queryInfo, offset, response)    offset += queryInfo.length     // offset to domain name    response.writeUInt16BE(0xC00C, offset)     offset += 2    const typeAndClass = msg.subarray(msg.length - 4)    copyBuffer(typeAndClass, offset, response)    offset += typeAndClass.length    // TTL, in seconds    response.writeUInt32BE(600, offset)      offset += 4    // Length of IP    response.writeUInt16BE(4, offset)      offset += 2    11.22.33.44.split(.).forEach(value => {      response.writeUInt8(parseInt(value), offset)      offset += 1    })    server.send(response, rinfo.port, rinfo.address, (err) => {      if (err) {        console.log(err)        server.close()      }    })}

最后把拼接好的 DNS 协议的消息发送给对方。

这样,就实现了 guangguangguang 的域名的解析。

上面代码里我把它解析到了 11.22.33.44 的 IP。

我们用 nslookup 测试下:

可以看到,对应的域名解析成功了!

这样我们就通过 Node.js 实现了 DNS 服务器。

贴一份完整代码,大家可以自己跑起来,然后把电脑的本地 DNS 服务器指向它试试:

const dgram = require(dgram)const server = dgram.createSocket(udp4)function parseHost(msg) {    let num = msg.readUInt8(0);    let offset = 1;    let host = "";    while (num !== 0) {      host += msg.subarray(offset, offset + num).toString();      offset += num;        num = msg.readUInt8(offset);      offset += 1;        if (num !== 0) {        host += .      }    }    return host}function copyBuffer(src, offset, dst) {    for (let i = 0; i < src.length; ++i) {      dst.writeUInt8(src.readUInt8(i), offset + i)    }  }function resolve(msg, rinfo) {    const queryInfo = msg.subarray(12)    const response = Buffer.alloc(28 + queryInfo.length)    let offset = 0    // Transaction ID    const id  = msg.subarray(0, 2)    copyBuffer(id, 0, response)      offset += id.length        // Flags    response.writeUInt16BE(0x8180, offset)      offset += 2    // Questions    response.writeUInt16BE(1, offset)      offset += 2    // Answer RRs    response.writeUInt16BE(1, offset)      offset += 2    // Authority RRs & Additional RRs    response.writeUInt32BE(0, offset)      offset += 4    copyBuffer(queryInfo, offset, response)    offset += queryInfo.length     // offset to domain name    response.writeUInt16BE(0xC00C, offset)     offset += 2    const typeAndClass = msg.subarray(msg.length - 4)    copyBuffer(typeAndClass, offset, response)    offset += typeAndClass.length    // TTL, in seconds    response.writeUInt32BE(600, offset)      offset += 4    // Length of IP    response.writeUInt16BE(4, offset)      offset += 2    11.22.33.44.split(.).forEach(value => {      response.writeUInt8(parseInt(value), offset)      offset += 1    })    server.send(response, rinfo.port, rinfo.address, (err) => {      if (err) {        console.log(err)        server.close()      }    })}function forward(msg, rinfo) {    const client = dgram.createSocket(udp4)    client.on(error, (err) => {      console.log(`client error:\n${err.stack}`)      client.close()    })    client.on(message, (fbMsg, fbRinfo) => {      server.send(fbMsg, rinfo.port, rinfo.address, (err) => {        err && console.log(err)      })      client.close()    })    client.send(msg, 53, 192.168.199.1, (err) => {      if (err) {        console.log(err)        client.close()      }    })}server.on(message, (msg, rinfo) => {    const host = parseHost(msg.subarray(12))    console.log(`query: ${host}`);    if (/guangguangguang/.test(host)) {        resolve(msg, rinfo)    } else {        forward(msg, rinfo)    }});  server.on(error, (err) => {    console.log(`server error:\n${err.stack}`)    server.close()})  server.on(listening, () => {    const address = server.address()    console.log(`server listening ${address.address}:${address.port}`)})  server.bind(53)

总结

本文我们学习了 DNS 的原理,并且用 Node.js 自己实现了一个本地 DNS 服务器。

域名解析的时候会先查询 hosts 文件,如果没查到就会请求本地域名服务器,这个是 ISP 提供的,一般每个城市都有一个。

本地域名服务器负责去解析域名对应的 IP,它会依次请求根域名服务器、顶级域名服务器、权威域名服务器,来拿到最终的 IP 返回给客户端。

电脑可以设置本地域名服务器的地址,我们把它指向了用 Node.js 实现的本地域名服务器。

DNS 协议是基于 UDP 传输的,所以我们通过 dgram 模块启动了 UDP 服务在 53 端口。

然后根据 DNS 协议的格式,解析出域名,对目标域名自己做处理,构造出 DNS 协议的消息返回。其他域名则是转发给另一台本地 DNS 服务器做解析,把它返回的消息传给客户端。

这样,我们就用 Node.js 实现了本地 DNS 服务器

elementui级联选择器的回显

今天在做项目的时候遇到了一个棘手的问题,使用elementui的级联选择器做省市区层级回显时老是有问题,后来百度了一大圈才找到解决方案,记录一下:代码结构如下:

<el-cascaderv-model="gdjzdSelectedOptions"style="width: 100%"placeholder="请选择":props="defaultProps":options="provincescitiesoptions"filterable@change="handleChange"></el-cascader>

data中的数据如下:

provincescitiesoptions: [],defaultProps: {label: "codeName",value: "code",children: "children",},// 固定居住地选择器绑定的数据gdjzdSelectedOptions: [],

其中provincescitiesoptions是一个数组,保存的是选中地区对应的id值,思路是监听级联选择器的change事件,在该事件中,把保存选中的值的数组通过事件发送给父组件,在负组件中监听所发射的事件,拿到值以后对数据进行简单处理,处理成请求接口所需要的格式,我这里的格式是这样的:

handleSelectChange(val) {this.addForm.provinceCode = val[0];//省份对应的idthis.addForm.cityCode = val[1];//城市对应的idthis.addForm.areaCode = val[2];//区对应的idthis.addForm.townCode = val[3];//乡镇对应的idthis.levelArr = val;},

在编辑的时候再把从接口拿到的数据处理成数组的格式给到组件,如下所示:

handleEdit(row) {getlawdevicedetail(row.deviceId).then((res) => {if (res.code != 200) return;this.levelArr = [res.data.provinceCode,res.data.cityCode,res.data.areaCode,];Object.keys(res.data).forEach((key) => {this.addForm[key] = row[key];});});this.addDialogVisible = true;this.addForm.deviceId = row.deviceId;},

Vue3 生命周期

选项式 API 组合式 API

beforeCreate 不需要(直接写到 setup 函数中)

created 不需要(直接写到 setup 函数中)

beforeMount onBeforeMount

mounted onMounted

beforeUpdate onBeforeUpdate

updated onUpdated

beforeDestroy Vue 3:beforeUnmount onBeforeUnmount

destroyed Vue 3:unmounted onUnmounted

errorCaptured onErrorCaptured

activated onActivated

deactivated onDeactivated

composition VS options

composition API 优点:

更好的代码组织

更好的逻辑复用

更好的类型推导

如何选择:

不建议共用,会引起混乱

小型项目,业务逻辑简单,用 Options API

中大型项目,逻辑复杂,用 Composition API

选项式 API(Options API)

所有方法都写在 methods 中,如果 data 中数据越来越多,找数据会非常困难

<template><h1 @click="changeCount">{{ count }}</h1></template><script>export default {name: App,data() {return {count: 0,}},methods: {changeCount() {this.count++},},}</script>

组合式 API(Composition API)

逻辑会清晰,可以让功能的代码集中抽取到一个函数中进行逻辑复用

<template><h1 @click="changeNum">{{ num }}</h1></template><script>import { ref } from vuefunction useNum() {const num = ref(0)function changeNum() {num.value++}return { changeNum, num }}export default {name: App,setup() {const { changeNum, num } = useNum()return {changeNum,num,}},}</script>

如何理解 ref toRef 和 toRefs

ref

生成值类型的响应式数据

可用于模板和 reactive

通过 .value 修改值

<template><p>值类型响应式:{{ ageRef }} {{ state.name }}</p><p ref="elemRef">templateRef</p></template><script>import { ref, reactive, onMounted } from vueexport default {name: Ref,setup() {const ageRef = ref(20)const nameRef = ref(cat)const elemRef = ref(elemRef)const state = reactive({name: nameRef,})setTimeout(() => {ageRef.value = 30 // .value 修改值nameRef.value = dog}, 1500)onMounted(() => {console.log(elemRef.value)})return {ageRef,state,elemRef,}},}</script>

toRef

针对一个响应式对象(reactive 封装)的 prop

创建一个 ref,具有响应式

两者保持引用关系

<template><p>{{ ageRef }} {{ state.age }}</p></template><script>import { toRef, reactive } from vueexport default {name: ToRef,setup() {const state = reactive({age: 20,name: cat,})const ageRef = toRef(state, age)setTimeout(() => {state.age = 25}, 1000)setTimeout(() => {ageRef.value = 30}, 3000)return { state, ageRef }},}</script>

toRef 如果用于普通对象(非响应式对象),产出的结果不具备响应式

const state = {age: 20,name: cat,}

toRefs

将响应式对象(reactive 封装)转换为普通对象

对象的每个 prop 都是对应的 ref

两者保持引用关系

注意:直接解构 reactive 返回的 state ,页面能显示内容,但内容不是响应式的

<template><p>{{ age }} {{ name }}</p></template><script>import { toRefs, reactive } from vueexport default {name: ToRef,setup() {const state = reactive({age: 20,name: cat,})// 将响应式对象,变为普通对象const stateAsRefs = toRefs(state)setTimeout(() => {state.age = 25}, 1000)// const { age: ageRef, name: nameRef } = stateAsRefsreturn { ...stateAsRefs }},}</script>

ref toRef 和 toRefs的最佳使用方式

用 reactive 做对象的响应式,用 ref 做值类型响应式

setup 中返回 toRefs(state) 或者 toRef(state, xxx)

ref 的变量命名都用 xxxRef

合成函数返回响应式对象,使用 toRefs

function useFeatureX() {const state = reactive({x: 1,y: 2})return toRefs(state)}export default {name: WhyRef,setup() {const { x, y } = useFeatureX()return {x,y}}}

为何需要 ref

返回值类型,会丢失响应式

在 setup、computed、合成函数,都有可能返回值类型

Vue 如不定义 ref,用户将自造 ref,反而混乱

<template><p>{{ age }}</p><p>{{ age1 }}</p></template><script>import { reactive, computed } from vueexport default {name: WhyRef,setup() {const state = reactive({age: 20,name: dog,})// computed返回的是一个类似于ref的对象,也有.valueconst age1 = computed(() => {return state.age + 1})setTimeout(() => {state.age = 25}, 1000)return {...state, // 这样不是响应式的age1,}},}</script>

为何需要 .value

ref 是一个对象(不丢失响应式),value 存储值

通过 .value 属性的 get 和 set 实现响应式

用于模板、reactive 时,不需要 .value,其他情况都需要

简单理解 computed 运算逻辑

// 错误function computed(getter) {let valuewatchEffect(() => { // 可以改为setTimeout进行模拟测试value = getter()})return value}// 正确function computed(getter) {const ref = {value: null,}watchEffect(() => {ref.value = getter()})return ref}

为何需要 toRef 和 toRefs

初衷:不丢失响应式的情况下,把对象数据 分解/扩展

前提:针对的是响应式对象(reactive 封装)非普通对象

注意:不创造 响应式,而是 延续 响应式

createApp// vue2.xconst app = new Vue({ /* ... */ })Vue.use(/* ... */)Vue.mixin(/* ... */)Vue.component(/* ... */)Vue.directive(/* ... */)// vue3const app = Vue.createApp({ /* ... */ })app.use(/* ... */)app.mixin(/* ... */)app.component(/* ... */)app.directive(/* ... */)emits 属性

父组件

<template><son :msg="msg" @onSayHello="sayHello" /></template><script>import Son from ./views/Son.vueexport default {components: { Son },data() {return {msg: hello vue3,}},methods: {sayHello(info) {console.log(hello, info)},},}</script>

子组件

<template><p>{{ msg }}</p></template><script>export default {props: {msg: String,},emits: [onSayHello],setup(props, { emit }) {emit(onSayHello, vue3)},}</script>

多事件处理

<button @click="one($event), two($event)">Submit</button>

Fragment

<!-- vue2.x组件模板 --><template><div><p>{{ msg }}</p><p>{{ content }}</p></div></template><!-- vue3组件模板 --><template><p>{{ msg }}</p><p>{{ content }}</p></template>移除 .sync<!-- vue2.x --><MyComponent :title.sync="title" /><!-- vue3.x --><MyComponent v-model:title="title" />

异步组件

// vue2.xnew Vue({components: {my-component: () => import(./async-com.vue),},})// vue3.ximport { createApp, defineAsyncComponent } from vuecreateApp({components: {AsyncComponent: defineAsyncComponent(() => import(./async-com.vue)),},})移除 filter<!-- vue2.x --><div>{{ message | capitalize }}</div><div :id="rawId | formatId">/div>

Teleport

可以把一些组件放到外面去,Vue2 只能操作 DOM 来实现,Vue3 可以通过 Teleport 来实现

<button @click="modalOpen = true">Open full screen modal!</button><teleport to="body"><div v-if="modalOpen" class="modal"><div>telePort 弹窗(父元素是 body)<button @click="modalOpen = false">Close</button></div></div></teleport>

Suspense

场景:比如一个列表,一刷新就需要加载数据,在数据未加载之前会显示 loading,加载完之后在显示列表数据

Vue2 这种场景一般会写一个 data 控制显示隐藏,Element UI 库对此进行封装,Vue3 自己做了一个封装,封装成 Suspense

<Suspense><template><!-- 是一个异步组件 --><Test1 /></template><!-- #fallback 就是一个具名插槽。即Suspense组件内部,有两个slot,其中一个具名为fallback --><template #fallback> Loading.. </template></Suspense>

Composition API

reactive

ref 相关

readonly

watch 和 watchEffect

setup

生命周期钩子函数

Composition API 实现逻辑复用

抽离逻辑代码到一个函数

函数命名约定为 useXxx 格式(React Hooks 也是)

在 setup 中引用 useXxx 函数

<template><p>mouse position {{ x }} {{ y }}</p></template><script>import useMousePosition from ./useMousePositionexport default {name: MousePosition,setup() {const { x, y } = useMousePosition()return {x,y,}},}</script>useMousePosition.jsimport { ref, onMounted, onUnmounted } from vuefunction useMousePosition() {const x = ref(0)const y = ref(0)function update(e) {x.value = e.pageXy.value = e.pageY}onMounted(() => {window.addEventListener(mousemove, update)})onUnmounted(() => {window.removeEventListener(mousemove, update)})return { x, y }}export default useMousePosition

如果不使用 ref 使用 reactive,需要将整个 reactive 暴露出去。在父组件接收的时候不能直接解构,否则会失去响应式

Vue3 如何实现响应式Object.defineProperty 的缺点:深度监听需要一次性递归无法监听新增属性/删除属性(Vue.set、Vue.delete)无法原生监听数组,需要特殊处理Object.defineProperty(target, key, {get() {return value},set(newValue) {if (newValue !== value) {observer(newValue) // 值修改后进行监听// value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值value = newValueupdateView() // 触发更新视图}},})Proxy 实现响应式优点深度监听,性能更好可监听新增/删除属性可监听数组变化性能是如何提升的Proxy 实现不是一次性监听的,这里深度监听是在 get 中处理的,什么时候用到什么时候处理(惰性)。而 Object.defineProperty 实现是在一开始就进行处理,一次性全部处理完成// 测试数据const data = {name: bird,age: 20,info: {city: beijing,},}function reactive(target = {}) {if (typeof target !== object || target == null) {// 不是对象或数组,则返回return target}// 代理配置const proxyConf = {get(target, key, receiver) {// 只处本身(非原型)属性const ownKeys = Reflect.ownKeys(target)if (ownKeys.includes(key)) {console.log(get, key) // 监听}const result = Reflect.get(target, key, receiver)// 惰性深度监听。什么时候用什么时候监听return reactive(result) // 返回结果},set(target, key, val, receiver) {// 重复的数据,不处理if (val === target[key]) {return true}const ownKeys = Reflect.ownKeys(target)if (ownKeys.includes(key)) {console.log(已有的key, key)} else {console.log(新增的key, key)}const result = Reflect.set(target, key, val, receiver) console.log(set, key, val)return result // 是否设置成功},deleteProperty(target, key) {const result = Reflect.deleteProperty(target, key)console.log(delete property, key)return result // 是否删除成功},}// 生成代理对象const observed = new Proxy(target, proxyConf)return observed}const proxyData = reactive(data)Reflect 的作用:和 Proxy 能力一一对应规范化、标准化、函数式a in obj -> Reflect.has(obj, a)delete obj.b -> Reflect.deleteProperty(obj, b)替代 Object 上的工具函数Object.getOwnPropertyNames(obj) -> Reflect.ownKeys(obj)总结Proxy 能规避 Object.defineProperty 的问题Proxy 无法兼容所有浏览器,无法 polyfillv-model 参数用法Vue2 的 .sync 修饰符<text-document v-bind:title.sync="doc.title" /><!-- 语法糖 --><text-documentv-bind:title="doc.title"v-on:update:title="doc.title = $event"/>Vue3 的 v-model<ChildComponent :title.sync="pageTitle" /><!-- 替换为 --><ChildComponent v-model:title="pageTitle" />v-model 相当于传递了 modelValue prop 并接受抛出的 update:modelValue 事件<template><p>{{ name }} {{ age }}</p><user-info v-model:name="name" v-model:age="age" /></template><script>import { reactive, toRefs } from vueimport UserInfo from ./UserInfo.vueexport default {components: { UserInfo },setup() {const state = reactive({name: bird,age: 20,})return toRefs(state)},}</script>UserInfo.vue<template><input :value="name" @input="$emit(update:name, $event.target.value)" /><input :value="age" @input="$emit(update:age, $event.target.value)" /></template><script>export default {props: {name: String,age: String,},}</script>watch 和 watchEffect 的区别两者都可监听 data 属性变化watch 需要明确监听哪个属性默认是惰性执行,监听源可以是一个具有返回值的 getter 函数,也可以直接是一个 refwatchEffect 会根据其中的属性,自动监听其变化<template><p>{{ numberRef }}</p><p>{{ name }} {{ age }}</p></template><script>import { reactive, toRefs, ref, watch, watchEffect } from vueexport default {setup() {const numberRef = ref(10)const state = reactive({name: bird,age: 20,})watchEffect(() => {// 初始化时,一定会执行一次(收集需要监听的数据)console.log(watchEffect, state.age)})watchEffect(() => {console.log(watchEffect, numberRef)})// watch监听ref属性watch(numberRef, (newNum, oldNum) => {console.log(ref watch, newNum, oldNum)})// watch监听state属性watch(// 1.确定监听哪个属性() => state.age,// 2.回调函数(newState, oldState) => {console.log(state watch, newState, oldState)},// 3.配置项      { immediate: true, // 初始化之前就监听// deep: true // 深度监听})setTimeout(() => {numberRef.value = 100}, 1000)setTimeout(() => {state.age = 25}, 1500)return { numberRef, ...toRefs(state) }},}</script>setup 中如何获取组件实例在 setup 和其他 Composition API 中没有 this可通过 getCurrentInstance 获取当前实例若用 Options API 可照常使用 this<template><p>getInstance</p></template><script>import { getCurrentInstance, onMounted } from vueexport default {data() {return {x: 1,y: 2,}},setup() { // created beforeCreate 组件还没有正式初始化console.log(setup this, this) // onMounted(() => {console.log(onMounted this, this) // console.log(x, instance.data.x) // 1})const instance = getCurrentInstance()console.log(instance, instance) // 组件实例console.log(x, instance.data.x) //   }, mounted() {console.log(mounted this, this) // Proxy 实例console.log(y, this.y) // 2},}</script>

Vue3 为何比 Vue2 快

proxy 响应式

PatchFlag

hoistStatic

cacheHandler

SSR 优化

tree-shaking

PatchFlag(标记)

编译模板时,动态节点做标记

标记,分为不同的类型,如:TEXT、PROPS

diff 算法时,可以区分静态节点,以及不同类型的动态节点

Vue2 没有区分静态节点和动态节点

Vue3 新增静态标记 patchFlag 与上次虚拟节点比较时,只比较有 patchFlag 的节点

<div><div>1</div><div>2</div><div>{{ name }}</div></div><script>export function render() {return (_openBlock(),_createBlock(div, null, [_createVNode(div, null, 1),_createVNode(div, null, 2),_createVNode(div, null, _toDisplayString(_ctx.name), 1 /* TEXT */),]))}</script>

hoistStatic(静态提升)

将静态节点的定义,提升到父作用域,缓存起来

多个相邻静态节点,会被合并起来

典型的拿空间换时间的优化策略

Vue2 无论元素是否参与更新,每次都会重新创建然后再渲染。Vue3 对于不参与更新的元素,做静态提升,只会被创建一次,在渲染时直接复用即可

<div><div>1</div><div>2</div><div>{{ name }}</div></div><script>const _hoisted_1 = /*#__PURE__*/ _createVNode(div, null, 1, -1 /* HOISTED */)const _hoisted_2 = /*#__PURE__*/ _createVNode(div, null, 2, -1 /* HOISTED */)export function render() {return (_openBlock(),_createBlock(div, null, [_hoisted_1,_hoisted_2,_createVNode(div, null, _toDisplayString(_ctx.name), 1 /* TEXT */),]))}</script>