seho 发布的文章

久别重逢,最近换了新工作,三个月很忙碌,以后博客会定期更新;前几天Vue发布了3.3,我今年没写过Vue,但是也关注Vue的进展,总的来说Vue的发展方向在version 3这个版本中基本稳定了,反而DX的设计上会越来越好,如果你不使用Typescript,那么Vue3的许多新特性你可能并不适用,今天我们简单看一下Vue3.3的内容。

概览 (GPT总结)

  • defineOptions:可以在 setup 中使用它来声明组件名称等组件属性,避免了需要再写一个script标签,使得新手和 Vue2 -> Vue3 的用户更容易理解。
  • setup 中的 TypeScript 类型改进:支持从其他类型文件中导入类型,不再需要在 内使用 defineProps 等宏定义类型,但是目前还不支持复杂的类型操作。
  • defineModel:简化了父组件和子组件双向通信(v-model)的代码,使得双向绑定的逻辑更加明了。
  • defineSlots:可以自定义组件的 slot 类型,适用于在复杂场景下 Volar 不能准确推导组件类型的情况。
  • 泛型组件:对于带有 slot 的复杂组件来说,可以自动推断传入的类型。
  • setup 中的提升变量:支持将基础数据类型(除了 Symbol)的变量提升到顶部,解决了之前在 defineProps 时只能使用字面量的问题。

defineOptions

在之前的setup语法糖里面,如果要声明组件名称等组件属性,需要再写一个script标签,这会给新手和Vue2 -> Vue3的用户带来困惑,并且会给Eslint/Volar带来一定的难度,现在可以使用defineOptions宏来指定这样的信息,但是defineOptions不允许指定props/emits,因为这两者可以使用其他宏指定。

<script setup lang="ts">
defineOptions({
  name: 'HelloWorld',
})
</script>

setup中的Typescript类型改进

这是一个很旧的问题,在使用defineProps时,是不能从其他地方引入类型使用的,可以具体查看RFC。在这个版本将支持从其他类型文件中导入,比如这样:

<script setup lang="ts">
import type { Props } from './foo'

defineProps<Props & { extraProp?: string }>()
</script>

只不过目前并不支持复杂的类型操作,在之前我们使用defineProps做类型声明时,如果传入了类型,编译器将会将类型转换为运行时代码;而现在如果要支持外部的类型要么就调用Typescript的龟速编译器,要么就自己实现一个基础的编译器,让其识别到导入的“是什么类型”。目前Vue采用了第二种方案,并且支持一个复杂类型的例子(如上代码所示)。

defineModel

简化了父组件和子组件双向通信(v-model)的代码,比如在以前我们编写v-model相关逻辑,需要写这么多代码:

<script setup lang="ts">
const props = defineProps<{
  modelValue: number
}>()

const emit = defineEmits<{
  (evt: 'update:modelValue', value: number): void
}>()

emit('update:modelValue', props.modelValue + 1)
</script>

现在使用defineModel:

<script setup>
const modelValue = defineModel<number>()
modelValue.value++
</script>

defineSlots

在复杂场景下,有时候Volar并不能准确推导组件类型,或者有时你想自定义组件的slot类型,可以使用defineSlots:

defineSlots<{
  default(props: { item: T }): any
}>()
<HelloWorld :data="['foo', 'bar']">
  <template #default="{ item }">{{ item }}</template>
</HelloWorld>

泛型组件

这对于普通组件来说意义不大,但是对于带有slot的复杂组件来说很有用,意味着可以自动推断传入的类型,在组件中使用,比如以下的场景:

<script setup lang="ts" generic="T extends string | number, U extends Item">
import type { Item } from './types'
defineProps<{
  id: T
  list: U[]
}>()
</script>

T和U都可以被用作emit/ts代码逻辑中,它和defineSlot在一起用最好

scripts中的提升变量

Vue3在之前在编译sfc的template时,会将静态内容提升,这样在大型项目中重复的静态内容会用这样的优化手段会得到提升,可以看一下之前的Vue文章。但是现在在setup中,也支持这样的优化了,如果在script中编写了基础数据类型(除了Symbol)的变量,都会被提升到顶部。这个改进主要解决了之前的问题,比如defineProps时,只能使用字面量:

<script setup>
const hello = 'world'
defineOptions({
  hello,
})
</script>

这样的代码在之前是会有编译错误的,现在不会了,原因是defineProps宏编译之后,会变成下面的代码:

const __sfc__ = {
  props: [propName],
  setup(__props) {
    
  },
}

显然这是错误的,无法访问到propName,经过提升优化之后,将会解决这个问题:

const __sfc__ = {
  propName: "hello",
  props: [this.propName],
  setup(__props) {
    
  },
}

(伪代码)

更符合人体工学 的 defineEmits

同样的,这次改进主要是少写一些代码:

const emit = defineEmits<{
  (e: 'foo', id: number): void
  (e: 'bar', name: string, ...rest: any[]): void
}>()

现在可以这么写:

const emit = defineEmits<{
  foo: [id: number]
  bar: [name: string, ...rest: any[]]
}>()

解构Props不失去其响应式(实验性)

如下代码所示,这更好的提供props的默认值:

const { msg = 'hello' } = defineProps(['msg'])

此时,msg仍然还是一个响应式变量,并且如下代码可能也不会再需要了:

export interface Props {
  msg?: string
  labels?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  msg: 'hello',
  labels: () => ['one', 'two']
})

在此前经常使用withDefaults宏去提供props默认值,现在有更好的趋近于es6的写法,所以建议大家之后可以使用这个功能,但是这个功能是实验性质的,需要自行斟酌。

今天我们简单聊一下Typescript5带来了什么,因为最近开源工作的需要,对于其装饰器的实现好奇,今天就去着重学习了一下。但是这篇文章其实并没有什么深度和内容,时间紧迫,我最近在适应新的工作环境以及Sword.js 2.0开发,这段时间的文章可能间隔时间会很长,话不多说,言归正传。

装饰器

在ts最新的版本中对(还在ecma stage-3阶段)装饰器最新的提案进行了实现,在我们之前使用ts时也偶尔使用装饰器,但是之前ts实现的装饰器版本是比标准tc39的更早(实验性质),所以我们不得不在tsconfig.json去打开实验性装饰器的开关(experimentalDecorators):

img_jsconfig-json.png

否则我们的代码会报错,在未来,这个配置仍然会继续存在,假如没有这个配置,那么现在装饰器的语法将是有效的,不会报错。但是无论是类型检查和tsc,新旧装饰器会有很大不同,尽管我们可以编写装饰器去兼容新旧装饰器,但是如果是老版本的代码,这样做不是一个明智的选择。

新的装饰器不支持装饰参数,希望未来ecma标准会支持这一行为:

class Person {
    @required
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    greet() {
        console.log(`Hello, my name is ${this.name}.`);
    }
}

下面我们可以编写一个装饰器函数,比如在一个函数中添加输出前和输出后的语句:

function loggedMethod(originalMethod: any, _context: any) {

    function replacementMethod(this: any, ...args: any[]) {
        console.log("LOG: Entering method.")
        const result = originalMethod.call(this, ...args);
        console.log("LOG: Exiting method.")
        return result;
    }

    return replacementMethod;
}

为了便于理解,2个参数都暂时使用了any,可以看到第一个参数为originalMethod代表了原method,第二个参数context代表了上下文。在装饰器函数中我们返回了一个新的函数去代替,像下面一样使用装饰器,就可以得到一个输出前和输出后的功能:

@loggedMethod
greet() {
    console.log(`Hello, my name is ${this.name}.`);
}

有时候,我们也可能这样调用一个方法:

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  greet() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

const greet = new Person("Ray").greet;

greet();

这段代码将会报错,原因是作为单独调用,没有重新绑定this;按照以往,我们可以在构造时重新绑定this:

this.greet = this.greet.bind(this);

我们尝试编写一个装饰器来替代这种写法:

function bound(originalMethod: any, context: ClassMethodDecoratorContext) {
    const methodName = context.name;
    if (context.private) {
        throw new Error(`'bound' cannot decorate private properties like ${methodName as string}.`);
    }
    context.addInitializer(function () {
        this[methodName] = this[methodName].bind(this);
    });
}

在context中我们可以给其中的addInitializer函数创建一个回调,(addInitializer函数可以允许我们将对应的逻辑添加在构造函数之前),将绑定this的操作更新上去,此时我们可以将bound函数作为装饰器给greet方法:

@bound
@loggedMethod
greet() {
    console.log(`Hello, my name is ${this.name}.`);
}

它们的调用顺序是相反的,即先装饰原有方法并修饰结果。也可以将装饰器放在同一行:

@bound @loggedMethod greet() {
    console.log(`Hello, my name is ${this.name}.`);
}

和以前一样,也可以给装饰器函数传递参数,在其内部返回的函数将构建成为一个闭包,比如:

function loggedMethod(headMessage = "LOG:") {
    return function actualDecorator(originalMethod: any, context: ClassMethodDecoratorContext) {
        const methodName = String(context.name);
        function replacementMethod(this: any, ...args: any[]) {
            console.log(`${headMessage} Entering method '${methodName}'.`)
            const result = originalMethod.call(this, ...args);
            console.log(`${headMessage} Exiting method '${methodName}'.`)
            return result;
        }
        return replacementMethod;
    }
}
@loggedMethod("seho")

官方建议我们,在编写装饰器时,应该根据自己的需求来编写带类型的装饰器,即尽量保持简单,因为在编写装饰器时如果非要死磕类型的话,那么也会损失可读性,下面是一个通过泛型实现的类型版本装饰器:

function loggedMethod<This, Args extends any[], Return>(
    target: (this: This, ...args: Args) => Return,
    context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
    const methodName = String(context.name);
    function replacementMethod(this: This, ...args: Args): Return {
        console.log(`LOG: Entering method '${methodName}'.`)
        const result = target.call(this, ...args);
        console.log(`LOG: Exiting method '${methodName}'.`)
        return result;
    }
    return replacementMethod;
}

const

ts5中也对const做出了调整,在以前推断类型时,通常会选择一个更通用的类型,比方说我们需要取record的key,那么会被默认推断为string;在之前的开发过程中也时常会想获取到具体的字面类型,通常会在推断之后加入const,比如我们回忆一下const断言的经典使用场景:

const x = 'x';
let y = 'x'; // string
let y = 'x' as const; // string

默默吐槽一句,官方说有时候会忘记as const断言,然后提供了一个我个人认为更复杂的写法:

type HasNames = { names: readonly string[] };
function getNamesExactly<const T extends HasNames>(arg: T): T["names"] {
//                       ^^^^^
    return arg.names;
}

// Inferred type: readonly ["Alice", "Bob", "Eve"]
// Note: Didn't need to write 'as const' here
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] });

给类型前面挂一个const,这样推断出来的对象key值才是具体的(不做任何讲解,我觉得我一辈子也用不到这个东西hh)

支持配置文件多个继承

{
    "compilerOptions": {
        "extends": ["a", "b", "c"]
    }
}

改进枚举

在以前,枚举有2种类型,一种是数字枚举,一种是文本类型枚举,它们是不能混用的。但是在ts5中将它们进行了融合,换句话说此时枚举其实就是它的成员类型的联合,并且初始化枚举时,可以是常量也可以是一个表达式:

enum E {
    A = 10 * 10,  // Numeric literal enum member
    B = "foo",    // String literal enum member
    C = bar(42)   // Opaque computed enum member
}

那么此时E的类型就是E.A | E.B | E.C;值得注意的是我们应用的常量必须事先声明,否则也会报错,比如:

enum E {
    a = b,
}

const b = 1

新的模块查找规则

简单的说对于现代捆绑器而言,我们都已习惯了在相对导入下不用编写文件后缀,但是在ts之前没有支持,ts5新增了一个模块查找规则,可以帮助我们使用现代捆绑器时,直接编写如下的代码:

import * as utils from "./utils";
{
    "compilerOptions": {
        "target": "esnext",
        "moduleResolution": "bundler"
    }
}

同时,与之相关,还有一批和此功能类似的配置项更新:https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-beta/#resolution-customization-flags

支持export type *

export type * from "mod"
export type * as ns from "mod"

TSC更新

这块我不咋用,因为我捆绑ts程序好像好多年都不用tsc了,这个更新大家就主要过个眼熟吧:

  • declaration
  • emitDeclarationOnly
  • declarationMap
  • soureMap
  • inlineSourceMap

源码改进

大概就是,算法,数据结构,包体积有了显著的升级,不管是安装速度还是使用速度上,相较于4.x版本有个幅度较高的升级。我简单整理了一下ts的优化方案:

  • namespace -> module (终于舍弃了namespace,得益于module,ts可以用现代的工具链来优化整个打包体积)
  • 缓存了信息到字符串这一过程,在一些操作中可以重用
  • 减少了内存占用,主要是内部对象类型增加统一性,对某些对象类型进行瘦身,减少了多态使用次数

关于版本号的争议

有网友说对此次的更新不满意,认为这没有breakchange,版本号变更到5.0是不妥的,我个人感觉不管是新的模块查找规则,还有枚举,面向更现代的es等等更新都可以算的上是5.0;等过段时间稳定之后,我已经迫不及待的想使用5.0啦。

参考资料

概念

HTTP是超文本传输协议, 一个词里面有3个关键信息

  • 超文本
  • 传输
  • 协议

那么超文本的意思就是字面含义, 就是不止是文本, 也有可能是图片,音乐, 视频等等, 而传输协议指的就是在计算机层面之上的一种交流约定方式, 那么我们可以理解为这个HTTP可以允许我们在计算机上实现双向的传输交流的规范

Methods

每个http都有自己的method, 每个method对应的操作虽然是开发人员自定义实现的, 但是我们在编写操作逻辑时, 也要尽量遵守http method的语义标准

  • GET和POST的区别

首先根据RFC的定义, GET是从服务端获取一个资源; 而POST是根据报文内容对资源去做对应的处理

  • GET和POST是安全的么, 它们都是幂等的吗?

首先根据HTTP定义的安全标准, 指的就是是否在服务端操作了数据, 一旦操作了数据, 其实这个请求就是不安全的; 幂等指的就是多次提交和一次提交的影响都是一样的, 显然GET只是获取服务器资源, 它是安全且幂等的; 反之, POST多次提交不一定和一次提交的影响是一样的, 所以POST是不安全且不幂等的

缓存

在HTTP中缓存分为2种

  • 协商缓存
  • 强制缓存

当浏览器向服务端第一次请求资源时, 浏览器通常会缓存数据, 那么在下一次请求时如果资源还没过期, 将会使用缓存中的数据, 那么这就是强制缓存, 通常会在HTTP中显示(from disk cache), 强制缓存主要取决于客户端

而取决于服务端的HTTP就是协商缓存, 在协商缓存中的服务端会返回给客户端不同的响应头, 并且状态码是304, 即告知浏览器可以使用缓存, 大名鼎鼎的Vite在预构建依赖时, 就用了这种缓存方法优化了HTTP性能.

我们在学习协商缓存的时候, 也需要知道最后修改时间实现缓存的, 还是基于ETAG实现缓存的; 我们在业务使用中, 会选择ETAG实现 (并且浏览器机制下ETAG优先级更高), 因为ETAG解决了时间实现的几个缺陷:

  • 时间只能精确到秒, 有时候修改文件是在秒之内, 所以最后修改时间存在误差
  • 服务器可能获取不到最新的时间
  • 我们关注的是文件内容本身, 虽然本身内容没有被修改, 但是最后修改时间可能会改变

ETAG: 唯一资源标识符: 当第一次请求服务端会返回一个唯一标识, 客户端请求携带, 由服务端告知客户端是返回200 (被变更, 返回最新资源), 还是304(没有变更)

协商缓存和强制缓存需要搭配使用, 当强制缓存未命中时, 再使用协商缓存

HTTP版本演变

我们在谈论HTTP版本时, 通常会谈论HTTP1.1和2.0以及3.0

首先, HTTP1.1相比HTTP1.0有哪些重要改进呢?

  • 使用长连接改进了1.0时的短连接
  • 支持pipeline网络传输, 不需要等待前一个请求返回再发出第二个

但是尽管改进了很多, 但是HTTP1.1还是有很多不足

  • header不压缩, 只压缩了body部分
  • 队头阻塞, 虽然可以发出请求可以不用等待之前的返回, 但是HTTP1.1是按照请求的顺序返回的, 如果前一个请求响应慢, 会造成之后的请求返回阻塞
  • 发送了冗长的首部, 造成浪费
  • 只能客户端主动发起请求

为了解决这些不足以及安全性问题, HTTP2.0诞生了, 解决了如下的痛点问题

  • 发送二进制格式
  • 头部压缩
  • 并发传输, 解决了响应阻塞问题
  • 服务器主动推送

头部压缩

头部压缩, 如果你同时发出了多个请求, 那么它们的头部都是一样的或者是相似的, 那么HTTP内部会使用HPACK算法对其进行处理, 大概的原理就是客户端和服务端共同维护一个表, 在表中存储各种的头信息, 每次没有必要发送冗长的头部, 只需要发送索引号即可.

二进制传输

二进制传输格式也大大提高了传输效率, 它们被称之为帧(Frame), 分为头部帧和数据帧, 在原来的HTTP版本中, 是明文传输的, 但是在HTTP2.0中直接使用了二进制, 无需将明文转换为二进制

并发原理

那么HTTP2.0是如何实现并发传输的呢? 原理也很简单, HTTP2.0中引入了Stream的概念, 在一个TCP连接中可以有多个Stream, 每个Stream有不同的id, 在其中也会有不同的request和response的请求和响应, 那么再里面就是我们熟悉的, 由于Stream之间都是独立的, 所以可以乱序发送, 然后在客户端会根据id拼接HTTP消息, 这就是HTTP2.0并发传输的原理.

服务端推送

服务端推送也和Stream理念不可分割, 因为在1.0和1.1都是请求应答传统模式, 即客户端请求, 服务端应答, 在HTTP2中对这个模式进行了优化. 在客户端和服务端分别建立Stream, 它们的id略有不同, 客户端是奇数, 而服务端是偶数. 那么服务端推送有什么好处呢? 最大的好处就是减少网络次数往返, 比如客户端接受一个html文件, 那么在html中又引入了css,js等文件, 那么我们就可以用服务端推送html中的css和js文件, 减少了消息传递次数.

HTTP3

看似HTTP2很完美了, 该有的都有了, 但是我们仔细想一下, HTTP2解决的队头阻塞看样子是通过Stream完美解决了, 但是HTTP2是基于TCP的, TCP是字节流协议, 它最大的特点就是要保证数据是连续并且完整的, 它有一个缓冲区的概念, 当前一个请求缺少字节的时候, 只能将这部分数据保存到缓冲区中, 等字节达到之后, 才可以被HTTP这个应用层获取到数据. 也就是说, 我们虽然解决了HTTP队头阻塞, 但是并没有解决TCP的队头阻塞, 所以要想完美的解决这个问题, 我们只能使用UDP

那么, TCP是可靠传输, UDP是不可靠传输, HTTP3是如何底层使用UDP的呢, 原因就是使用了UDP为底层的QUIC协议, 它完美解决了队头阻塞, 即就算某个Stream丢包, 也不会影响其他Stream 而且在QUIC协议中, 对建立连接做了很多优化, 比如说在HTTP2之前与TLS握手, 都要建立三次; 但是在QUIC中, 因为其包含了TLS, 并且使用TLS的更高版本, 可以在HTTP的三次握手中进行TLS信息的携带, 所以可以说把TLS的握手过程隐藏了.

那么为什么HTTP2不把TLS进行囊括合并呢? 是因为TCP是内核实现的传输层, TLS是openssl实现的表示层, 无法合并

HTTPS

HTTPS就是在TLS/SSL基础上的一个应用, 我们在HTTP中发送的数据都是明文的, 这并不安全会带来很多隐患; 通过SSL校验服务端身份, 并且在通信过程中间加密; 那么它们的建立连接的过程可以分为以下几步

  • 发起HTTPS请求
  • 服务端返回SSL证书的public key
  • 客户端自己生成一个对称加密的public key
  • 拿SSL的public key加密刚刚生成的public key, 并且发送
  • 之后客户端发送的消息都会拿刚刚加密过后的public key对数据进行加密

区别

  • SSL证书要钱, 但是大部分是免费的
  • 多一个TSL握手的过程
  • 一个明文, 一个是密文
  • HTTP默认端口是80, HTTPS是443

OSI七层模型

OSI将网络体系划分为7层, 7层互不干扰, 每一层互相独立协议, 并且独立完成和相邻接口通信功能

应用层

我们熟悉的DNS, HTTP, SMTP都是应用层

表示层

主要作用就是让数据能够解释和交换

会话层

建立, 管理会话

传输层

TCP/UDP

网络层

IP

数据链路层

令牌环网

物理层

以太网

TCP/IP4层模型

TCP模型比OSI模型更为抽象, 它将应用, 会话, 表示都称之为应用层, 将数据链路层和物理层称之为网络接口层

DNS解析

简单说DNS就是一个翻译官, 把我们的域名翻译为IP地址, DNS查询的过程可以简述为

  • 查浏览器
  • 查操作系统
  • 查本地域名服务器
  • 查上游服务器
  • 给本地域名服务器缓存ip信息
  • 给操作系统缓存ip信息
  • 给浏览器缓存ip信息

TCP的三握四挥

三次握手的作用就是, 其实就是小明小红打电话

  • 小明: 小红听到了么 (客户端发送正常)
  • 小红: 我能听到的 (服务端发送和接收正常, 但是此时小红不确定小明的接收是不是正常的)
  • 小明: 那我开始说了 (此时2个端的发送和接收都是正常的)

四次挥手也是一样的, 也就是挂电话的逻辑

  • 小明: 我说完了, 你还有啥要说的么
  • 小红: 好吧, 我这里也没了
  • 小明: 那我挂了
  • 小红: 行, 你挂吧, 我也挂了

经典八股文: 浏览器地址输入到显示经历了哪些步骤

哪个前端面试没背过的, 都不配称之为前端.哈哈哈哈哈

我们简单过一下这一块, 虽然这里烂大街了

  • 首先就是对输入的内容进行解析

浏览器要判断你输入的是一个网址, 还是一个关键词

  • 如果是网址, 那就经过DNS解析拿到IP地址

DNS查询过程请看上方

  • 建立TCP连接

详见小明打电话的例子

  • 发送HTTP请求
  • 响应HTTP请求

对资源进行解析, 主要看content-type, 还有gzip等等, 需要对返回内容作处理

  • 渲染页面

解析和构造dom树 → 解析和构造css树 → 合并生成render树 → 布局layout树 → 绘制像素树 → 通知gpu进行绘制显示在屏幕上

这篇文章是一个小记, 因为涉及到的东西很简单, 所以只是做一个总结, 想学新东西的读者可以关掉网页了, 如果你还是一个vue3小白, 那么这篇简短的小记可能会帮助你.

渲染器

我们都知道vue中的sfc单文件模板是会被编译为一个渲染函数的, 这个渲染函数最终返回的就是VDOM, VDOM是虚拟dom的自定义实现, 因为虚拟dom不是一个规范, 而vue3对比以往版本也在VDOM上做了很多优化; 我们从宏观上来看vue3的渲染pipeline可以由下面的步骤表示

预编译/运行时编译出渲染函数 -> 进行渲染创建真实dom -> diff比对高效替换dom

具体的优化主要体现在预编译上, 因为vue支持我们手写渲染函数, 但是官方推荐我们使用模板语法, 最主要的原因就在于其更容易静态分析, 也就更容易做编译时的优化, 就和vue的treeshaking优化一样(es规范是可被静态分析的, 从而实现死代码消除技术), 所以我们在后面的讨论都围绕模板语法来讲.

试想一下, 在vue2中的渲染原理, 都是静态编译我们的模板, 但是有一个致命的缺点: 静态内容也会被重新diff

什么意思呢?

如果我们在模板中写一段代码

<p>hello world</p>

事实上这段代码根本不可能有变更, 没有绑定, 也没有读取任何响应式变量, 所以在vue3中采用了PacthFlag去给这段vdom子节点标注类型, 让其在diff期间主动跳过. 关于PacthFlag它在vue源码中是一个枚举, 定义了很多标识, 它们都有一个共同点就是都是位运算, 在diff运行时其实表现的性能消耗非常小

export const enum PatchFlags {
  TEXT = 1,// 动态的文本节点
  CLASS = 1 << 1,  // 2 动态的 class
  STYLE = 1 << 2,  // 4 动态的 style
  PROPS = 1 << 3,  // 8 动态属性,不包括类名和样式
  FULL_PROPS = 1 << 4,  // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较
  HYDRATE_EVENTS = 1 << 5,  // 32 表示带有事件监听器的节点
  STABLE_FRAGMENT = 1 << 6,   // 64 一个不会改变子节点顺序的 Fragment
  KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment
  UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment
  NEED_PATCH = 1 << 9,   // 512
  DYNAMIC_SLOTS = 1 << 10,  // 动态 solt
  HOISTED = -1,  // 特殊标志是负整数表示永远不会用作 diff
  BAIL = -2 // 一个特殊的标志,指代差异算法
}

除了PatchFlag, vue编译在其渲染过程中还提供了其他优化, 比如静态内容提升以及树结构打平,监听函数事件缓存, 下面我来一一介绍以下的渲染优化方案

我们在预编译之前已经知道了静态内容, 我们不仅可以让它跳过diff的判断, 也可以让它在生成vdom树时共享引用, 比如下面的伪代码就很好的阐述了vue2和vue3的渲染优化区别

// vue2
const output = () => {
    const template = {text: "hello world"}
    return {
        children: [{template}]
    }
}
// vue3
const template = {text: "hello world"}
const output = () => {
    return {
        children: [{template}]
    }
}

这样的提升对于大型vue项目来说减少了很多内存占用.

而树结构打平也很容易理解, 和我们之前讲的PatchFlag有所关系, 我们刚知道静态内容都会被跳过, 那么如何跳过的呢? 就是通过打平, 我们在模板中编写的html代码可以称之为一个block, 在一个block中会存在很多很多节点, 那么vue编译器将会把所有后代节点的动态节点全部挑出来并且返回一个没有层级关系的vdom, 这样又大大提高了diff效率以及update效率.

而事件监听也很简单, vue在之前的版本会把下面的代码点击事件视作一个动态内容, 以至于每次更新都需要追踪它的变化

<div>
  <button @click = 'onClick'>点我</button>
</div>

而现在不会了, 在内部提供了一个事件缓存, 解决了这个问题

响应式

这个部分是vue3的重中之重, 在vue2中其阉割版的响应式导致了很多恶心的API, 在vue2中vue都会将data中的属性进行深层次的递归, 以便每个对象都绑定了getter和setter方法; 并且在数组的表现上差强人意, 原因都是因为defineProperty, 这个API我相信已经存在了vue人的脑子里好几年了, 我就不展开讲了, 我们着重讲一下vue3的响应式.

首先vue3的响应式是基于proxy的, 而且这个API是不能被polyfill的, 但是由于我们的浏览器越来越先进, 所以使用先进的API提升开发体验也是值得的, 况且IE已死; proxy为对象提供了很强大的拦截功能, 包括数组的delete, push等等, 而且vue也在响应式核心中做了很多魔法, 比如在for循环时的读取和绑定新值...

vue3和vue2一样, 都提供了类观察者模式的方案, 但是我更愿意称之为发布订阅模式, 因为观察者模式是一对一有强关联, 但是发布订阅通常会有一个调度中心; 而在vue中, 一个又一个的副作用函数都订阅了响应式变量, 只要在代码中使用过响应式变量, vue都会追踪到, 并且将副作用追加到一个WeakMap队列中, 当变量变更就会以此执行所属的所有副作用.

但是我们没有办法知道哪里读取了响应式变量, 这就要靠proxy了, proxy可以拦截属性的读, 那么同理我们使用ref函数创建一个基本类型

const foo = ref(1)

它也会被包装为一个对象, 如果给ref传递了一个对象, 那么其实和reactive这个函数无异, 它们的底层都是一样的.

另外在vue3中提供了很多编译器宏, 使得我们可以便捷的操作响应式变量, 比如我们想要操作ref的变量

const foo = $ref(1)

foo = 2

因为编译器宏会自动帮助我们加入.value, 还有我们对响应式变量解包时, 会让其失去响应式, 我们同样可以使用编译器宏$()来简化toRef的操作

const { x, y } = $(useXXX())

Composition API

一些面试官也特别喜欢问组合API和Option API的区别, 又会有人问React Hook和Composition API的优劣势, 第一个问题很容易解答, 第二个问题需要我们结合各种周边环境回答 (哈哈哈哈哈)

首先是option和composition的区别

composition主要解决了一个代码复用和可维护性的问题, 尤其是代码复用, 我们曾经在一个组件中要想复用逻辑, 通常会选择mixin, 抱歉我真的没有用过mixin, 但是我见过别人用, 觉得很不理解...

首先mixin它可以编写多个, 并且可以同时混入到一个组件中, 说句难听的话, 这就是把你眼睛蒙上把粑粑拉你头上; 你并不知道程序错误是哪个mixin导致的, 而且也没有命名空间, 会导致逻辑冲突;

而composition就像一个函数一样, 正常导入完全不受限制.

当我们的vue文件变得越来越大时, 我们的关注度并不是methods, computed, 而是业务, 我们需要快速的找到业务代码, 如果在vue2中, 你会发现你的业务代码分散到不同地方, 你无法聚焦你的业务; 但是在vue3中, 我们没有options api的限制, 可以自由的将不同业务进行聚合, 并且实现快速定位.

那么和react hook区别是什么呢? 这个vue官方都说了compistion的设计是借鉴了react hook, 但是其内部实现和react大大不同. 原因还是我之前在知乎说的, react的历史包袱太重, 它本来就不是面向普通应用的, 一切设计都要以内部为先, 而且react hook变量是闭包实现的, 并且由于其核心机制导致react组件渲染一次就会跑一边所有的hook, 所以你可能经常会使用useMemouseCallback来优化调用次数;

vue3不存在react的问题, 所有composition api都会仅执行一次, 并且你很容易捕捉到它最新值, 相比而言react你需要关心它的依赖值和副作用, 心智负担较大; 这里并不是说react不好, 因为react的种种缺点都是react大神承认的, 对于js开发者而言, vue确实负担更轻.

但是我们在面试中, 要揣测面试官是不是react吹, 如果是的话, 请尽可能的吹react

常见问题

  • watch和watchEffect的应用场景和区别

我觉得应该不会问这么傻的问题, watch是懒执行, 依赖明确; watchEffect依赖不明确但是是同步执行, 我们需要在初始化时执行逻辑并且变更也执行逻辑, 就可以考虑watchEffect

  • 为什么会用proxy设计响应式

同上, 文章前面已经解答过了

  • treeshaking咋实现的

基于esmodule静态分析, vue2因为是单例, 无法按需加载

  • vue3对比vue2整体的提升有哪些

全方位的, 我们刚刚讲的响应式和渲染器都是补了vue2的坑, composition也补了option api的坑, 并且源码使用ts构建, 虽然vue3的sfc类型提示很牵强, 但是相比vue2的类型提示要熬掉鱿鱼丝几根头发写类型体操好得多. 而且源码是采用monorepo构建, 其实在阅读源码更方便了, 每个模块都是独立的, 尤其是响应式模块, 我们如果想要在其他平台单独使用响应式特性, 直接npm安装即可. 而且vue的核心模块也独立了, 支持很多平台, 甚至可以使用vue编写终端应用, 对渲染出口不作限制

前言

这是一篇非常草率的2022总结文章, 我会更多的介绍关于技术方向, 以便同行们在明年有更多方向和想法. 如何形容2022年呢? 总的来说就是“进步的太慢, 或者说根本没有实质性的进步”; 导致这个问题的原因有很多, 简单的说就是被一些垃圾工作缠绕住了.

工作

2022年我仍然在西安, 2023年将是我工作的第四年, 作为一个软件从业者, 我可能在西安混的还算看得下去, 但是只有我自己知道, 我其实什么也不是. 因为西安这个城市造就了互联网行业注定没有氛围, 周围的同行技术不错的太少了. 我以前想这可能和我的圈子或者西安(地域)有关系. 事实上我们只要下班之后多学一点多看一会, 就可以超越大部分人了. 学习对于程序员来说是最简单的事情, 为什么大部分程序员都没有这样的习惯, 是因为大家都被结婚, 孩子, 父母, 傻逼领导等各种事情困住了, 我想等我年龄大一点可能也会变得不爱学习了.

我今年接触了很多一线城市的开发, 发现整个中国软件行业都太浮躁了, 换句话说优秀的团队太少了. 企业喜欢赚快钱, 大多数企业不会关心技术, 他们注重结果混KPI, 有时候甚至不需要展示demo演示程序, 一个ppt足以征服领导. 这种公司通常有以下几个特征:

  1. 设置了日报和周报
  2. 喜欢开会
  3. 没有加班费
  4. 每1,2周发一次版本, 美其名曰敏捷开发
  5. 每个公司都会有一个卷王, 半夜三更提交代码, 也不知道公司给他了多少钱
  6. 测试, 前端, 后端互相甩锅
  7. 裙带关系
  8. 地域歧视

如果你的公司碰巧都有以上症状, 请你立即下班开始学习, 你要努力提升自己, 摆脱这种工作环境. 我今年划水机会蛮多的, 在朋友圈和各个地方吐槽了很多岗位, 比如产品经理和前端, 这两个岗位是最浮躁的, 我甚至觉得这两个岗位让2个高中生过来都能完成, 如果我是老板, 我就会雇佣高中生来替代他们 (事实上我也是前端)

产品经理的jd我会写:

  1. 熟练使用微信 (例如转发消息)
  2. 熟练使用腾讯会议
  3. 熟悉互联网黑话 (能抽时间做一下么 | 我们能不能临时出一个给老板看的版本)

前端的jd我会写:

好吧, 我承认前端的门槛比产品高多了, 至少产品可以不用出高保真原型和文档, 只需要转发消息就可以把产品搞定, 我觉得是一个中国人都会;

我今年在工作中收获很多, 得到了很多人的帮助, 他们带我成长, 比如说在上一家公司的康凯哥, joe, 枨, 聪, 测试双煞和其他技术部同僚, 他们真诚, 负责, 为我解决和分担了不少麻烦; 其中凯哥和joe在技术上帮助我了很多, 到目前凯哥在我心目中仍然是榜样的存在, 因为他才华横溢而且不厌其烦的为我解答问题; 同样的还有joe, 和他一起共事, 是一段非常难忘的经历. 在新公司中, 我和我的同窗好友(基友)在一起工作的体验是我从业以来最爽的, 因为我们在一起会有当时一起学程序的美妙感觉, 以至于我和他在一起配合, 比去大厂还要更好. 所以我跳槽时, 我没有准备任何面试, 毅然决然和他一起战斗.

技术

在技术这一篇中, 我会聊的更多一些, 我们先从公司项目说起吧, 我今年接触了万恶的flutter来构建一个史一样的app, 尽管我们没有任何架构可言, 写出了和我便秘20天拉出的史一样的又臭又长的代码, 但是打包出来, 我们惊讶的发现, app居然如此流畅, 这让我认识到了四件事:

  1. flutter真吊
  2. 对js又爱又恨
  3. reactnative要完蛋
  4. 原生开发要被抢占一大部分份额

但是flutter的开发体验太糟糕了, dart的标准库设计对标es6, 简直是天大的差距; 而且最头疼的生态问题仍然很难解决, 比如地图, 图表仍然没有很好的支持. 而且对于前端而言上手难度颇大, 因为html和flutter ui抽象机制完全是思维的转变, 而且flutter/dart的编译器简直是太糟糕了, debug难度非常高, 如果离开了google和ai, 我很怀疑我能不能用flutter开发第二个app.

希望dart标准库能够多多迭代, 参照es6的设计好好改进, flutter是一个优秀的跨平台app开发框架, dart的唯一用武之地就是服务这个框架. 这相对于js不同, js的核心标准不变, 但是需要对不同设备和环境扩展api, 比如浏览器前端开发app时, 他会学习一些新的东西来挑战和质疑已经存在的知识, 这一定程度上会造成知识混乱. 但是开发app不同, 运行环境相对单一, 目的明确, 我们只需要知道开发网页需要使用js, 开发app需要使用flutter, 这就够了, 这就是最优解. 希望2023年flutter能够干掉reactnative一统跨平台app天下.

如果有人问我跨平台app推荐什么技术, 我会毫不犹豫的说flutter, 尽管它有太多缺点.

《Flutter实战·第二版》
猫哥flutter课程


我们再来说说前端框架吧, 今年我是react黑粉, 同样的也是vue3黑粉, 我使用vue3已经快2年多了, 2020年我就写了vue3教程, 甚至在去年我写了一部分vue3的电子书. 但是后来放弃了, 因为我对浏览器的东西渐渐失去了兴趣, 在上一家公司做了不少网页的东西, 让我意识到这对职业发展很不利. react难么, react-hook我学了一个小时就能写一个完整的线上应用, vue3难么, 我学了几个小时就能写教程. 不是我有多厉害, 是因为框架的团队太强了, 它们把“开发者养懒了, 它们是技术罪犯

当一个技术变得特别简单的时候, 就开始卷了, 它们试图让开发者写更少的代码能完成更多需求, 如果要找到一份心仪的工作, 你不得不去卷源码, 就和java一样, 你得在面试的时候准确说出jvm源码中的函数名称, 有时候函数名称有几十个单词组成, 你能用标准的英语读出来, 就能震慑傻逼面试官. 前端已经快要和java一样卷了, 现代前端才存在多少年, java已经存在几十年, 可见前端的发展速度令人震惊, 我们如果要想在这个圈子有更多机会, 我们需要逃避, 要去更换赛道.

每一个框架都有或多或少的缺点, react hook无处不在的编程陷阱, 我今年开发在线ide, 就被react各种陷阱逼疯了, 同样的功能使用vue3能节约60%的代码; 反观vue3, 它的ts支持也是非常糟糕的, 如果没有vite, volar等优秀的插件生态, 我的第一框架首选也不会是vue3. 在今年有很多框架值得我们去关注, next, nuxt, astro, qwik, svelte, solidjs, preact.

  1. 如果你喜欢react的api设计和虚拟dom框架, 我强烈推荐你使用preact.
  2. 如果你喜欢极简干净短小的js代码运行在你的网站上, 推荐svelte
  3. 如果你喜欢island架构, 推荐你使用astro
  4. 想玩好全栈框架, 推荐nuxt3和nextjs, nextjs有swc, nuxt3有我的代码和强大的api引擎

vite是今年前端宠儿, 也是我的心头爱, 鱿鱼丝是一个才华横溢的程序员, 他是一个天才, 有着独到的想法, 而且vite的团队技术都很顶尖, vite已经4.0了, 我有空一定要拜读一下vite的源码, vite在2023年的优化空间有限, 因为vite本质上的原理没有特别复杂, 它进一步的简化了开发模式和优化程序体验, 我们摆脱了webpack噩梦. 得益于插件系统, 理论上vite可以基于nodejs做出很多前端领域的扩展, 甚至很多nodejs框架也在集成vite.

在今年稍晚的时候, turbo团队发布了它们的构建工具turbopack, 我对于turbopack持乐观状态, 但是我不认为它能打败vite, vite的生态很庞大, 在前端界中生态就是王道, 就像npm一样, 再如何吐槽npm/nodemodules设计, 它仍然是最网红的依赖管理工具. turbopack我们应该关注它的核心技术turbo, turbo团队还有一个作品是turborepo, 也是解决monorepo的杀手锏框架, 它们很擅长利用缓存来优化一个流水线, 并且在turborepo中甚至可以将缓存共享到ci/cd中 (我记得是, 看到过相关功能), 并且它们推出了云服务, 对于企业用户而言, 在构建一些微应用/大型代码库和框架, 是非常适合使用turbo团队的作品的.

顺便再说一句, 不要学习webpack了, 我们不得不承认, webpack是一个优秀的构建系统, 理论上它能完成任意复杂度的产物构建工作, 但是它太重了, 而且大多数团队缺乏webpack技术沉淀, 而且大多数业务不需要庞大构建系统支持, 我们甚至只需要vite的默认配置进行打包, 性能可能会更好.


在今年我写了一个nodejs框架, 也就是sword.js, 我很喜欢nodejs, 我很喜欢用nodejs来构建后端应用, 简单易用, 但是swordjs也是我最后一个用nodejs写的后端开源项目, 我不得不抛弃nodejs. 是因为nodejs和我之后的发展方向存在一定冲突, 但是我仍然在前端领域频繁用到nodejs, 如果你是一个前端, 请务必学习nodejs, 它太简单了, 使用nodejs你可以做任何很酷的事情. 但是请允许我介绍一下deno, 和我为什么要使用deno?

deno是一个js运行时, 它原生支持js和ts和wasm, 请记住wasm这个技术. deno是使用rust编写的, 它也是构建在v8之上的. 我之后的发展方向是区块链, 大量的要和rust打交道, 那么deno和rust中的联系就由wasm构建, 我可以使用我熟悉的语言(typescript/rescript)开发api, 并且我可以直接通过deno运行rust的合约代码, 因为rust/ink编译出的合约代码就是wasm. 而wasm是一种类汇编语言, 是一个低级语言, 它是作为c++, rust等低级语言的编译目标而出现的.

wasm的性能非常优越, 它已经在前端音视频领域取得了巨大成功, 也在浏览器中作为js的补充出现, 会比同等功能的js代码更加高效, 更接近本地速度运行, 所以我认为wasm是未来, 也是区块链的未来. 你可能会说nodejs也可以写区块链应用呀? 使用deno仅仅是因为它能直接运行wasm吗?

答案不是, 如果是半年前, 我不会使用deno, 因为我说过生态是最重要的, 但是deno在下半年兼容了nodejs绝大部分api, nodejs作者也是deno的作者, 他曾经说不会兼容nodejs的api, 但是现在为了生态做了妥协. 此时此刻, deno不仅可以兼容nodejs代码, 还可以直接运行ts和wasm, 还具有无与伦比的安全性和速度. 我铁定站它!


我们继续聊一下typescript, 我曾经是ts的狂热粉丝, 但是今年我把类型挑战的中等题刷完之后, 我改变看法了, 我不需要类型体操, 它在业务中使用的地方很少, 尽管它为重构和构建应用带来了很大的好处, 但是我们不得不承认, 要深入ts不是那么容易的, 而且在工作中我们只需要学会低阶的ts语法就可以给我们带来很大的开发益处: “给开发者良好的类型提示, 消除隐藏错误”

但是我们有没有想过, 就算写出花一样的ts代码, 对应用性能和速度有任何益处吗? 答案是没有的, 编译出来的js仍然可能存在性能和类型错误, 因为超级多的人喜欢any, 和关闭类型错误提示; 我们需要一个语言在特殊时候代替typescript, 我认为是rescript, 它可以编译性能更好的js代码, 具体的好处我就不介绍了. deno + rescript + rust/ink + flutter将是我进入区块链的理想前后端技术栈


关于开源, 我今年大部分时间都在写我的swordjs框架, 目前可以跨nodejs server和unicloud nodejs端, 有着优秀的ts运行时校验等等功能, 结合vscode插件和编译器, 无论在开发还是生产环境, 都有畅快的开发体验, 实际上它拉高了nodejs后端api应用开发速度.

结束

就写这么多, 希望大家2023年顺利, 下面是我2023年的发展方向, 希望给大家一些启发:

  1. 进入区块链行业 (重要)
  2. 重新审视所有技术栈, 对技术栈进行换血 (重要)
  3. 学会rust和deno以及rescript相关技术栈 (重要)
  4. 学英语
  5. 对程序语言理论和编程基本功进行针对训练, 比如看一些相关书籍, 编译器知识, 程序语言, 算法等 (重要)
  6. 有空学习街舞
  7. 渐渐放弃前端, 尤其是常见的业务, 比如浏览器, 小程序方向
  8. 加强后端语言学习, 比如go, 我对go比较青睐, 它写api真的挺不错的
  9. 看情况适当了解docker, k8s
  10. 对设计和产品领域持续输出新的idea, 适当做一些练习和创业项目