分类 精读 下的文章

复合类型

顾名思义, 复合类型就是其他类型组合而成的, 最典型的就是结构体struct和枚举enum.

字符串

rust中的字符串和我们平时说了解到的编程语言不一样, 比如下面这一段代码是会编译错误的:

fn main() {
  let my_name = "Pascal";
  greet(my_name);
}

fn greet(name: String) {
  println!("Hello, {}!", name);
}

切片

切片在go中就已经很流行了, 它允许你引用集合部分内容, 而不是全部, 在字符串中, 我们可以这么写:

let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

如果是从0开始, 就可以省略0:

let hello = &s[..5];

同理, 如果你要截取到尾部, 你可以这样省略:

let hello = &s[1..];

但是如果我们操作中文, 就需要格外注意utf-8啦, 一个中文是3个字节, 所以我们在做切片的时候, 如果切到第二个, 就会编译器报错:

实际上, 如果返回切片, 其实是一个字符串引用, 所以我们可以写出这样一段代码:

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);

    s.clear(); // error!

    println!("the first word is: {}", word);
}
fn first_word(s: &String) -> &str {
    &s[..1]
}

不仅仅字符串可以进行切片, 数组也是可以的:

let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);

如果我们通过字符串字面量创建字符串的话, 是这样的:

let a = "hello";

此时a就是&str类型, 我们完全可以这样编写代码, 因为此时a指向了可执行文件中的某个点, 也因此字面量创建的字符串是不可变的, 因为&str就是不可变的.

let a: &str = "hello";

字符串是什么

rust中的字符是由unicode编码实现的, 每一个字符占据了4bytes, 但是字符串是用utf-8实现的, 占用的字节数是1-4变化的, 所以更节省内存.

&str和String的区别

在语言层面上来说, 只有str一种类型, 是硬编码到可执行文件中的, 无法被修改; 但是在标准库中我们却可以使用String来创建一个不定长度的字符串类型; String同样也是utf-8编码也具有所有权特性;

如何互相转换

str → string

let a = String::from("hello world");
let b = "hello world".to_string();

string → str

fn main() {
    let s = String::from("hello,world!");
    say_hello(&s);
    say_hello(&s[..]);
    say_hello(s.as_str());
}

fn say_hello(s: &str) {
    println!("{}",s);
}

字符串索引

在js中, 我们可以使用索引轻松的访问字符串, 但是在rust中这是不被允许的:

let s1 = String::from("hello");
let h = s1[0];

因为字符串的底层是使用[u8]字符数组实现的, 如果我们在字符串中使用中文, 一般一个中文是3个byte, 那么此时我们操作中文字符串时, 可能会得到预想不到的值; 所以在rust中, 字符串索引是一个容易造成误解的功能, 因此不支持;

如何正确操作utf-8字符串

如果你想要通过unicode方式操作字符串, 就可以使用.chars()函数

for c in "中国人".chars() {
    println!("{}", c);
}

如果你想要查看字符串在底层的字节数组, 就可以使用bytes()函数

for b in "中国人".bytes() {
    println!("{}", b);
}

228
184
173
229
155
189
228
186
186

ps: 如果要在rust中截取正确的子串, 就要使用一些库, 比如utf8_slice

深入理解字符串

为什么string可变而str不可变呢? 这很容易理解因为str是在我们编译期间就可以知道的内容, 即不可变, 直接编译到可执行文件中; 但是我们在开发中, 不可能都使用不可变的str, 通常我们的值都会在运行时经过逻辑处理得到的, 所以我们需要一个可变的string类型; 可变的string类型是要在堆上划分一块区域存储的, 等到需要被释放时才会归还给操作系统, 整个周期都是在运行时;

元组

元组是由多种类型组合一起形成的, 它的长度是固定的, 顺序也是固定的; 我们可以通过这个函数创建一个元组:

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

可以用模式匹配来解构元组

let (x, y, z) = tup;

也可以通过.索引的方式来获取元组元素

tup.1

在函数中, 如果要返回多个值, 除了使用下面即将介绍的结构体之外, 还可以使用元组

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)
}

结构体

在元组的介绍中, 返回的calculate_length函数是一个元组类型, 这对于程序来说是不好维护的, 因为不清楚返回参数的任何意义; 那么rust中有一个复合类型叫做结构体可以解决; 那么在其他语言中结构体可以当作object, record(typescript); 结构体是由多个类型组合而成; 它可以给每个类型设置一个名称(key), 所以结构体对比元组更加灵活

我们定义一个结构体, 非常简单:

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

我们需要使用这个结构体构造一个实例, 就更简单啦:

let user1 = User {
      email: String::from("someone@example.com"),
      username: String::from("someusername123"),
      active: true,
      sign_in_count: 1,
  };

和ts一样, 我们定义的结构体需要完全初始化, 即数据模型要和结构体完全匹配.

如果我们需要修改user1的某个字段, 必须把整个user1变为mut可变类型, 结构体不支持仅把某个元素变为可变:

user1.email = String::from("anotheremail@example.com");

我们在结构体中, 对于同名的key和value是可以做到省略简写的, 比如这样:

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

同样的, 我们在ts中经常使用es的扩张运算符来更新对象, 在rust中, 你完全可以使用类似的语法更新结构体 (必须写在尾部)

let user2 = User {
    email: String::from("another@example.com"),
    ..user1
};

关于更新语句, 所有权也需要关注, 在rust所有权中, 部分类型如果支持copy, 那么数据在这里就会被拷贝, 比如bool和u64都实现了copy, 那么在此时的更新结构体的语句中, 仅仅只是把值copy了一份而已; 但是username是string类型, 在此处是转移了所有权到user2中, 那么在user1中就不能操作username了.

结构体不仅仅有key, value这种形式, 还可以有其他形式

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

枚举

枚举是一个类型, 有多个枚举成员, 枚举值是其中某个枚举成员的实例

enum PokerSuit {
  Clubs,
  Spades,
  Diamonds,
  Hearts,
}

在一些特殊场景下, 你不仅仅可以指定成员类型, 还可以代替结构体简化代码: 在ts中枚举是运行时类型(经过编译器编译之后为对象), 但是在rust中就是实打实的类型, 不能在声时指定值;

如果使用传统的结构体, 是这样写的

enum PokerSuit {
    Clubs,
    Spades,
    Diamonds,
    Hearts,
}

struct PokerCard {
    suit: PokerSuit,
    value: u8
}

fn main() {
   let c1 = PokerCard {
       suit: PokerSuit::Clubs,
       value: 1,
   };
   let c2 = PokerCard {
       suit: PokerSuit::Diamonds,
       value: 12,
   };
}

我们通过枚举进行改造, 可以大大简化代码:

enum PokerCard {
    Clubs(u8),
    Spades(u8),
    Diamonds(u8),
    Hearts(u8),
}

fn main() {
   let c1 = PokerCard::Spades(5);
   let c2 = PokerCard::Diamonds(13);
}

指定成员类型

enum PokerCard {
    Clubs(u8),
    Spades(u8),
    Diamonds(char),
    Hearts(char),
}

更复杂的枚举成员类型 (结构体)

struct Ipv4Addr {
    // --snip--
}

struct Ipv6Addr {
    // --snip--
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}

相比较元组结构体, 从语法角度上来说枚举的方式更为简洁且高内聚

Option

在rust中避免了使用大多数语言经常用到的null概念, 改为option, 其类型本身就是一个枚举

enum Option<T> {
    Some(T),
    None,
}

some指的是有值(任意值), 类型为t, t是一个泛型参数; none则为空

值得注意的是, option并不需要显式的引入, 因为它本身就被包含在标准库中, 也不需要::调用some和none;

let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;

在rust语言设计中, 你如果想使用类似null的概念, 就必须告诉编译器这个null如果有值的话是什么类型, 即第三段代码; 反之如果使用了some, 就代表了值存在于some之中; 那rust为何这么做, 多此一举新增一个option枚举呢?

其实主要核心问题还是安全性, rust编译器要在编译器确保数据的安全, 不能因为null的滥用导致程序错误, 如果不使用option来标记可能为空的值, 那么在之后的代码中你可能就会忘记这个值的类型和其他类型做逻辑运算, 那么此时null就会被爆雷; 所以如果使用option标记值, 在编译期就可以让rust判断, 提醒开发者.

数组

数组很简单, 同样rust可以设置数组的类型

fn main() {
    let a = [1, 2, 3, 4, 5];
        // 这里的类型声明, ;之前是类型, ;之后是重复几次
        let b: [i32; 5] = [1, 2, 3, 4, 5];
        // 同样的也可以通过上述的语法, 让我们初始化数组更快速, 初始化值为3, 重复5次
        let c = [3; 5];
}

访问数组

a[0]

可以进行切片

let slice: &[i32] = &a[1..3];

在这篇文章之后,我会经常发布一些关于框架设计/架构的一些文章,因为这将作为我的读书笔记,我最近在看一些书比如《vue.js设计与实现》和《前端架构入门和微前端》;我简单介绍一下这两本书,希望对你们有所帮助,首先前端架构这本书一直是我的床头书但是目前对我的工作帮助并不大,因为它比较偏理论个人认为,如果你有耐心并且非常愿意入门前端架构,这本书是一个非常不错的入门书籍;其次就是vue.js设计这本书是最近前端圈的网红书,如果你已经使用过了vue3一段时间了,想精通/深入了解vue3,那么这本书将会带你从设计到实现理清楚vue的所有脉络!

前言

我最近在写我人生中的第一款框架,尽管没有任何含金量,而且这种低级的作品居然是出自一个有着3年开发经验的程序员之手,我还蛮不好意思的;在写这款框架我犯了很多错误和技术债,由于前期没有很好的规划功能以及模块,导致走了不少弯路,而且没有设计框架的经验,我经常会把一个功能放到编译时还是运行时而苦恼,同样我会时常考虑用户的习惯,去联想其他后端框架,导致在框架API设计上有点四不像的感觉。无论如何这款框架再丑也是自己生的,相信不久之后就会和大家见面了,所以我这篇文章将结合我设计的sword.js和vue.js给大家好好聊一聊框架中如何权衡某些事情。

什么是权衡

我们在讲比如vue.js这一类框架时,其中的每一个模块并非独立的,而是互相依赖和制约,框架作者需要有着全局的把控才能更好的扣细节做优化,拆分...那么想象一下当我们要设计一款前端视图层框架的时候,我们需要首先考虑范式,它是声明式的还是命令式的呢,再比如说如果在框架中做hmr底层实现,甚至是构建工具,webpack/rollup/esbuild?可见我们要遇到的选择都太多太多了,那么这就是“权衡”的艺术,框架中的每一个地方,或者说我们在平时写业务的时候,我们都需要去考虑更多东西,这就是权衡。

声明式和命令式

我们从原生js开始说起,如果我想要给一个dom绑定一个点击事件(我全部用伪代码写):

const e = document.querySelector("#app");
e.innerText = "foo";
e.addEventListenner('click', () => {
    alert("hello foo")
})

这就是典型的命令式代码,代码的执行方式是可预期的,因为都是由开发人员自己编写的每一步操作,但是这就遇到一个很难的问题了,当程序越来越大,我们有多个dom需要绑定点击事件,就要获取n次dom并且一一绑定,这无疑是一种痛苦。那么声明式呢,它可以解决命令式的一些什么问题呢?

<div @click="() => {}" id="app">
</div>

如果你使用过vue.js,那么你肯定写过n个这样的代码,我们只给click提供了一个函数,我们并不关心vue是如何获取dom并且绑定点击事件的,我们只需要关注结果就可以了,但是不可否定的是,在vue内部的实现中一定是命令式的,而暴露给用户却是声明式的。那么关乎性能它们谁更好,答案当然是可以预想到的,命令式的代码有着不可替代的性能:

e.innerText = 'update text';

在命令式代码中只需要写这一句就可以了,但是如果是声明式代码,我们需要找出新dom和旧dom之间的差异,然后再动态修改text(调用上面这个代码),所以由此得知,尽管声明式代码的性能不如命令式,但是为了更好的维护,我们需要做的就是权衡(既然性能有差距,我们就往可维护性上靠,并且尽可能的优化diff算法,让性能无限接近命令式代码)。

虚拟dom的性能

刚刚我们讨论了声明式和命令式的区别,那么虚拟dom如果你使用了vue.js就一定不陌生,而且它是每个面试官都喜欢问的(我也不知道为什么喜欢问,感觉没啥技术含量)。那么虚拟dom就是为了能够更好的给vue进行diff而出现的,我们要比对如下2行代码:

<div @click="() => {}" id="app">hello foo</div> // old
<div @click="() => {}" id="app">hello bar</div> // new

如何用最小的性能消耗找出它们的差异呢?就是虚拟dom,我们在之前说过声明式和命令式代码天然的差距(虚拟dom更新不会比js dom api性能更好),但是事实上99%场景都很难写出绝对优化的命令式代码,但是声明式代码我们可以很轻松的写出来相对还不错的代码。我们为了了解虚拟dom,需要知道我们上述提到的js dom api是什么,要么是createElemnt或者innerHTML,所以我们就用虚拟dom对比一下这两个api的差异。

innerHTML vs 虚拟dom

innerHTML是我写jquery/jsp时的噩梦,因为在新手时期为了构建一个html字符串,我每天半夜调试屎山项目的html字符串,这个过程非常痛苦:

const html = `
<div>
    <span>innerHTML</span>
</div>
`
dom.innerHTML = html;

js新手小白都知道,dom操作的效率和js层面的计算是不能比较的,差距非常大,为了页面的展示,需要把html字符串转成dom树,然后再执行innerHTML; 反观虚拟dom创建页面需要2步:

  1. 把我们的模板代码转换成js对象
  2. 无限递归对象创建真实dom

这么一看,好像innerHTML更直接,而且html字符串转成dom树是dom层一次性且“高效”的运算,所以说虚拟dom创建页面的性能是不如innerHTML的,但是更新页面,虚拟dom的优势就显示出来了,首先innerHTML不仅会对html字符串进行运算,还会把之前的旧dom销毁,然后创建一个新的dom(恨人啊);虚拟dom只需要创建一个新的js对象再与旧的虚拟dom进行比对,哪里有变化就变更哪里!虽然说虚拟dom多了一个diff的操作,但是终究是js层面的运算是很快速的;当页面越来越大,而innerhtml必定都是全量更新,性能也会随着内容变多,和虚拟dom差距越来越大。

粗略比较三个方式的创建&更新技术

  • 性能:原生JS > 虚拟dom > innerhtml
  • 综合可维护性和性能以及心智负担权衡之下,虚拟dom是一个不错的选择。

运行时和编译时

我们作为框架的作者,希望程序是如何运行的,我们还是用vue.js举例子,刚刚我们讲了虚拟dom,但是却不知道虚拟dom这个js对象是什么样子,我们可以通过这个部分把虚拟dom重新梳理一下:

const obj = {
    tag: "div",
    children: [{
        tag: "p",
        children: "hello bar"
    }]
}

这就是一个虚拟dom对象,描述了每个node的信息以及每个子node的信息,我们如果要实现render方法,就需要对虚拟dom对象进行递归,我们简单实现一下:

const obj = {
  tag: "div",
  children: [
    {
      tag: "p",
      children: "hello bar"
    }
  ]
};

const render = (obj, root) => {
  // 创建一个父节点
  const element = document.createElement(obj.tag);
  if (typeof obj.children === "string") {
    // text节点
    element.appendChild(document.createTextNode(obj.children));
  } else if (obj.children) {
    obj.children.forEach((e) => {
      // 如果有多个子节点,就递归创建
      render(e, element);
    });
  }
  root.appendChild(element);
};

render(obj, document.body);

这样我们就完成了一个在运行时环境可以完美运行的render,用户可以使用render对页面进行创建元素,但是没有用户愿意每天写这种破数据结构的,所以就肯定要用到编译的东西帮助我们把模板语法转换成数据结构,这个时候就是编译时+运行时,所以vue大多数情况也是这样做的,通过vite/vue-cli对单组件文件进行编译。那么同理既然可以有纯运行时,那么就有纯编译时的东西,可以把我们的模板语法编译成命令式的代码,比如这样:

<div @click="() => {}" id="app">hello foo</div> // old

转换成

const e = document.querySelector("#app");
e.innerText = "foo";
e.addEventListenner('click', () => {
    alert("hello foo")
})

没有虚拟dom,没有diff,only compile!! 这也是svelte.js在做的很酷的事情。所以作为框架设计者关于运行时和编译时我们需要有自己的权衡,虽然vue.js是运行时+编译时,但是在编译时会提取内容,看看哪些内容是永远不可变哪些又是可变的,然后这部分会在运行时再次做优化。所以关于运行时和编译时,没有绝对的好也没有绝对的坏,还是看框架定位和作者自己的权衡了(佛系不引战)。

关于sword.js所做的权衡

如果还不清楚sword.js是做什么的,你可以看看以前的文章,简单的就是说一个nodejs框架,框架中自然就是拥有运行时和编译时,一个framework-core,一个cli。在sword.js中有一个蛮好玩的功能就是,ts运行时检测,这个技术的大概的原理就是,分析ts的类型生成一份schema,然后会有一个函数去比对对象和schema是否吻合,如果匹配成功,那么就算校验通过,这个技术用到参数校验特别好,比如这样:

export interface ReqParams{
    title: string;
    name: "小红" | "小蓝"
}

const obj = {
    title: "test',
    name: "小红"
}
validate(obj, schema); // 这里的schema就是interface转的json对象

那么我在实现这个功能的时候,分2步走,第一个就是生成schema,第二个就是校验;我把生成这部分放到了cli的编译层这里,程序会自动读取每一个API下的类型,然后转成一个proto.json,在这个json中,运行时可以去校验这部分的对象是否符合要求。权衡好了运行时该做什么,编译时该做什么,就可以把2个工具的大小大大压缩。

再比如说日志模块,在开发nodejs应用的时候,我们需要core的日志,也需要cli的日志,那么如何在终端表现也是需要权衡的。

结语

今年实在是很少时间写文章,就趁着看书和写框架做一个随心记录,希望你们能看得懂(内容偏水,应该都有看得懂)

往期回顾

2020年终总结

前言

每年都会写年终总结,目的就是为了3 5年后从博客中找出每一年的年终总结,可以一目了然看到成长,这种感觉是非常幸福的。今年真的收获巨大,因为完成了我职业生涯中很多第一次;或许前几年初入圈子有些许迷茫吧,虽然目前我对我今后几年的发展抱有很大期望,但是如果要达到我的最终目标,付出的时间和精力将会成倍上增。这个目标是什么,后面会有聊到。往年我写年终总结的时候主要是3个核心概念:疫情,心境成长,技术成长,今年我打算多增加几个板块,而且还会传一些图片上来,也算是一份宝贵经历。

先听首歌吧,边听边看

疫情

今年已经临近尾声,没想到西安疫情爆发,仅次于当年的武汉,不知道过年还能不能回家。我现在在家办公中,在家办公很舒服,但是有一点非常不好,我的生物钟全部被打乱了,每天11点睡,自然醒已经早上8点了;如果按照往常工作日,我应该是6点或者不到6点就起床了,但是这样也问题不大,省去了大量通勤时间,尽管我睡眠时间长,但是仍然有学习时间。西安疫情的事件上了很多次热搜,ZF的种种蜜汁操作,还有一码通小程序平均每周崩一次,应急预案也没有这次丢人真的丢大发了。

封城之后,时隔一周看到了政府的救济菜:白萝卜,大白菜,土豆,洋葱,但是不知道下一次送菜是什么时候...

疫情中不得不提的就是,我和几个小伙伴搞了一个核酸地图的小程序,可以清楚的看到自己身边有多少个核酸检测点,上线之后流量暴增,上了纸媒,也上了热搜,接受了采访,这一段经历真的是非常难忘。

IMG_0349.PNG

IMG_0350.PNG

小组开会的随手截图

WechatIMG3527.jpeg

随后当小区居家隔离,西安的临时检测点也就没了,这个程序在一段时间帮助了很多人,虽然现在它没有用处,但是它也曾出现过...

在家办公很爽,尤其是和姐姐们一起住,自己在屋里写代码,饭不用担心,只洗碗就够了哈哈,总之疫情居家生活还是蛮舒服的,舒服是相对的,相信大家也听说过西安有些小伙伴都饿晕了,也有出门买馒头被抓的,也有医院门口因为核酸流产的,也有老父亲因为送医不及时心脏病故,这样对比起来,我真的算幸运的了。


回顾老剧

跑男排面:

IMG_0090.JPG

薛仁贵传奇,真老剧了:

i45iqmbggfuik29.jpeg

怪侠一枝梅:

8c0f65f348ff435f91150b2a760608ee.png

庆余年:

v2-e3c1be7492a0a5b9b2eafc7504467820_1440w.jpeg

越狱1-5:

143892174951732900_a700x398.jpeg


推荐一些最近看的资源

不光有b站视频,还有最近一年写的比较好的文章,还有看的一些书

书籍

视频

文章


未来几年的打算

熟悉我的朋友们都知道,我毕业之后的工资很低很低,曾经只有1.2k只够养活自己,2年过去了,我的薪资差不多是翻了10倍多,听起来感觉很不错,但其实远远达不到我心中的高度,2022年将是我的第三年,在写这篇文章的前一天晚上,和朋友小马哥聊天,改变了一些原先计划:

WX20220105-152052.png

我决定还是2022年安稳过一年吧,因为2022年太多计划了,比如无止境的考试还有驾照,我希望安稳度过,而不是逞一时之快去拿所谓的15 16k的高薪offer。我希望2022年产出极高,水平提升极快,能够奠定相对扎实的基础去实现后面的事情。

我个人非常希望纸贵这个公司是我在中国近几年最后一家公司,这里的小伙伴很nice,等待疫情结束(几年后),如果我的本科以及雅思学习旅途顺利,我大概率将会申请国外的在职研究生,这也算圆了我的学习梦,也算是变相脱离内卷,也希望以后的职业发展将会在其他国家。

2022年非技术目标:

  • 一切考试顺利,英语学习顺利,驾照考试顺利

关于技术

今年说实话没什么开源的作品,大部分都是下半年开始做的,vite的流行意味着前端构建已经变天了,webpack纵然还有用武之地,但是中小新项目基本都会选择vite作为构建工具。vite的出现进一步的带动了esbuild等新兴工具的热度,使用go,rust等语言,借助语言特性可以使编译,检查等工作变得更快更靠谱。

在下半年,来到新团队,就开始搞模板搞新技术栈,开源了一套模板和项目管理工具

WX20220105-180205.png

也写了相关文章介绍了几个模板 基于vite的模板

同样的,写了一个较为完整的命令行工具

1641377080467.jpg

在来新团队时候,也做了一个试验性的东西就是低代码雏形,解决了模板代码生成的问题

WX20220105-180602.png

尽管这个项目不维护了哈哈: https://github.com/seho-code-life/code-template-generation-web

同样的,今年还给antdv以及esbuild-node-tsc(基于esbuild的tsc编译工具)贡献了一部分代码,而且还出了一款教程关于tsrpc(typescript的rpc框架)。

临近年底,也写了一个关于状态管理持久化的库,它是一个支持多平台的持久化插件,马上就发布了

WX20220105-181006.png

https://github.com/1018715564/store-persistedstate-killer

而且在公司内部做了2次分享,第一次是关于前端基建,第二次是关于全栈开发,第二次的经历录制成视频了换一个方式构建全栈应用

顺便说一句,slidev是真好用,用熟悉的语法构建ppt,真的省心不少,建议开发者以后使用slidev写ppt

今年技术这块没有取的突破进展,只是对rust和ts有了新的领悟,以后会在文章体现。明年团队有flutter的工作,到时候我也会分享一些flutter的小文。

2022年技术相关目标:

  • 学习rust和wasm
  • 学习flutter
  • vue3全家桶源码分析
  • 算法
  • ts水平提高
  • js基础和http基础

拜拜咯,2022年终总结见~