Giter Club home page Giter Club logo

blog's People

Contributors

sedationh avatar

Watchers

 avatar

blog's Issues

0.0.0.0 🆚 127.0.0.1

127.0.0.1 是本地环回地址,专供自己访问自己,速度快(不用经过整个协议栈),永远都不能出现在主机外部的网络中,所以只适合用在开发环境。

localhost 只是 127.0.0.1 的别名

0.0.0.0 监听每一个可用的网络接口,127.0.0.1 也被监听了,在服务器可公网访问的场景,这意味着接受通过公网 ip 的访问

测试

要求

  1. 有个公网可访问的服务器
  2. 开放需要的端口
const express = require("express")
const app = express()
const port = 3002

app.get("/", (req, res) => {
  res.send("Hello World!")
})

app.listen(port, "127.0.0.1", () => {
  console.log(`Example app listening on port ${port}`)
})

无法访问到

const express = require("express")
const app = express()
const port = 3002

app.get("/", (req, res) => {
  res.send("Hello World!")
})

app.listen(port, "0.0.0.0", () => {
  console.log(`Example app listening on port ${port}`)
})

可以访问到

Nest 系列 - 2:装饰器和类型解析

上一节,我们留了个问题:

但我们知道 类型源于 TS,而真正运行的时候被转换成 JS 是不带有类型的,那框架层「factory」又是如何做类型匹配的呢?

在探讨这个问题之前先来看看什么是装饰器

装饰器

建议结合官网一起看
https://www.typescriptlang.org/docs/handbook/decorators.html

装饰器首先是一个函数,其次他的用法不同于一般的函数调用而是用

function xxx() {}

@xxx
class A {}

其次这个函数的使用场景有限,仅在用类的场景,并且操作对象是下面其中的内容

  • Class
  • Method
  • Accessor
  • Property
  • Parameter

若几个场景同时存在,执行顺序如下

There is a well defined order to how decorators applied to various declarations inside of a class are applied:

  1. Parameter Decorators, followed by MethodAccessor, or Property Decorators are applied for each instance member.
  2. Parameter Decorators, followed by MethodAccessor, or Property Decorators are applied for each static member.
  3. Parameter Decorators are applied for the constructor.
  4. Class Decorators are applied for the class.
@classDecorator
class Point {
  [x: string]: any

  @functionDecorator()
  print() {}

  @functionDecorator()
  static staticPrint() {}

  // 因为我们的 functionDecorator2 ,本身便是装饰器
  @functionDecorator2
  print2() {}
}

function classDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
  console.log("run classDecorator", constructor)
  return class extends constructor {
    newProperty = "new property"
  }
}

// 生成装饰器的工厂函数
function functionDecorator() {
  console.log("run functionDecorator")
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // target 实例方法是实例, 静态方法是类的构造函数
    console.log("run functionDecorator", target, propertyKey, descriptor)
  }
}

function functionDecorator2(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  // target 实例方法是实例, 静态方法是类的构造函数
  console.log("run functionDecorator2", target, propertyKey, descriptor)
}
run functionDecorator
run functionDecorator {} print {
  value: [Function: print],
  writable: true,
  enumerable: false,
  configurable: true
}
run functionDecorator2 {} print2 {
  value: [Function: print2],
  writable: true,
  enumerable: false,
  configurable: true
}
run functionDecorator
run functionDecorator [class Point] staticPrint {
  value: [Function: staticPrint],
  writable: true,
  enumerable: false,
  configurable: true
}
run classDecorator [class Point]

⚠️注意:这些行为是在 声明 class 的时候就进行的

Metadata

https://www.typescriptlang.org/docs/handbook/decorators.html#metadata

TS在编译过程中会去掉原始数据类型相关的信息,将TS文件转换为传统的JS文件以供JS引擎执行。但是,一旦我们引入reflect-metadata并使用装饰器语法对一个类或其上的方法、属性、访问器或方法参数进行了装饰,那么TS在编译后就会自动为我们所装饰的对象增加一些类型相关的元数据

  • 类型元数据使用元数据键"design:type"
  • 参数类型元数据使用元数据键"design:paramtypes"
  • 返回值类型元数据使用元数据键"design:returntype"
import "reflect-metadata"

function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log(Reflect.getMetadata("design:type", target, propertyKey))
  console.log(Reflect.getMetadata("design:paramtypes", target, propertyKey))
  console.log(Reflect.getMetadata("design:returntype", target, propertyKey))
}

class Point {
  constructor() {}

  @validate
  setX(x: number, y: string): number {
    return "1" as any
  }
}

const p = new Point()
[Function: Function]
[ [Function: Number], [Function: String] ]
[Function: Number]

可以看到,我们在运行时,拿到了TS环境中的类型,这样就解决了文章开头所提的如何在运行时拿到类型的问题

如果我们想自己设置一些值在 matadata 中可以

// 还未成为标准,因此想使用reflect-metadata中的方法就需要手动引入该库,引入后相关方法会自动挂在Reflect全局对象上
import 'reflect-metadata'

class Example {
  text: string
}
// 定义一个exp接收Example实例,: Example/: string提供给TS编译器进行静态类型检查,不过这些类型信息会在编译后消失
const exp: Example = new Example()

// 注意:手动添加元数据仅为展示reflect-metadata的使用方式,实际上大部分情况下应该由编译器在编译时自动添加相关代码
// 为了在运行时也能获取exp的类型,我们手动调用defineMetadata方法为exp添加了一个key为type,value为Example的元数据
Reflect.defineMetadata('type', 'Example', exp)
// 为了在运行时也能获取text属性的类型,我们手动调用defineMetadata方法为exp的属性text添加了一个key为type,value为Example的元数据
Reflect.defineMetadata('type', 'String', exp, 'text')

// 运行时调用getMetadata方法,传入希望获取的元数据key以及目标就可以得到相关信息(这里得到了exp以及text的类型信息)
// 输出'Example' 'String'
console.log(Reflect.getMetadata('type', exp))
console.log(Reflect.getMetadata('type', exp, 'text'))

编译结果探究

利用 tsc 编译上面的文件

"use strict"
var __decorate =
  (this && this.__decorate) ||
  function (decorators, target, key, desc) {
    var c = arguments.length,
      r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc,
      d
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
      r = Reflect.decorate(decorators, target, key, desc)
    else
      for (var i = decorators.length - 1; i >= 0; i--)
        if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r
    return c > 3 && r && Object.defineProperty(target, key, r), r
  }
exports.__esModule = true
require("reflect-metadata")
function validate(target, propertyKey, descriptor) {
  console.log(Reflect.getMetadata("design:type", target, propertyKey))
  console.log(Reflect.getMetadata("design:paramtypes", target, propertyKey))
  console.log(Reflect.getMetadata("design:returntype", target, propertyKey))
}
var Point = /** @class */ (function () {
  function Point() {}
  Point.prototype.setX = function (x, y) {
    return "1"
  }
  __decorate([validate], Point.prototype, "setX")
  return Point
})()
var p = new Point()

看到这里 我们可以理解官网中

As such, the following steps are performed when evaluating multiple decorators on a single declaration in TypeScript:

  1. The expressions for each decorator are evaluated top-to-bottom.
  2. The results are then called as functions from bottom-to-top.

的含义了
1的部分就是 when composing functions f and g, the resulting composite (f ∘ g)(x) is equivalent to f(g(x)).
2的部分就是

for (var i = decorators.length - 1; i >= 0; i--)

  __decorate([validate], Point.prototype, "setX")

这一块更详细的部分可以参考 Nest.js入门 —— TS装饰器与元数据(二) 的第四节

🔗参考链接

Nest.js入门 —— TS装饰器与元数据(二)
Decorators

用一个项目快速上手新的编程语言

  • 用一个项目快速上手新的编程语言
    • https://www.bilibili.com/video/BV1bp4y1G7MM/
    • 语言熟练度要素
      • 基本类型
      • 基本控制语句
      • 类/接口/包管理(抽象能力)项目文件管理
      • 构建工具
      • 内置类库:日志、基本多线程等
    • 熟悉新语言的流程
      • 找到官方文档,快速查看基本用法、基本语句、基本类型
      • 找到一个项目边查边开始实现
        • 如 实现一个 C/S 架构的加密版本的 Socks5 代理
    • 用好 ChatGPT

React 组件库 CSS 样式方案

背景

最近考虑构建一个自己的组件库,需要考虑 CSS 样式方案

分析

当我们构建组件库时,考虑问题的角度和普通项目可能会不太一样,不但需要考虑开发体验,同时也要照顾到使用者的感受。

CSS 与 JS 的关联关系

CSS 的方案分为以下三种类型

1. 样式和逻辑分离。

组件的 CSS 和 JS 在代码层面分离,JS 里不引入样式文件,在组件库打包时分别生成独立的逻辑和样式文件。对于组件库的使用者来说,添加一个组件,需要分别引入组件代码和 CSS 文件。
假设做一个 Foo 的组件
index.tsx

import React, { type FC } from 'react';

const Foo: FC<{ title: string }> = (props) => <h4 className='foo'>{props.title}</h4>;

export default Foo;

index.less

.foo {
  color: red;
}

如果要使用这个组件的话
要同时引入 index.tsx 和 index.less

使用案例的话
element-plus

// main.ts
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

antd@4

import React, { useState } from 'react';
import { render } from 'react-dom';
import { ConfigProvider, DatePicker, message } from 'antd';
// 由于 antd 组件的默认文案是英文,所以需要修改为中文
import zhCN from 'antd/es/locale/zh_CN';
import moment from 'moment';
import 'moment/locale/zh-cn';
import 'antd/dist/antd.css';
import './index.css';

此外 还可以借助打包工具进行按需样式自动引入,如

module.exports = {
  plugins: [
    ["import", { libraryName: "antd", style: true}] // `style: true` 会加载 less 文件
  ]
};

2. 样式和逻辑结合

将组件的 JS 和 CSS 打包在一起,最终只输出 JS 文件。使用时只需要引入组件就可以直接使用。

import 样式文件

import React, { type FC } from 'react';

+ import './style.less'

const Foo: FC<{ title: string }> = (props) => <h4 className='foo'>{props.title}</h4>;

export default Foo;

生成的产物是

import React from 'react';
import "./style.less";
var Foo = function Foo(props) {
  return /*#__PURE__*/React.createElement("h4", {
    className: "foo"
  }, props.title);
};
export default Foo;
.foo {
  color: red;
}

上面这种写法对我们使用组件的项目的打包工具是有要求的,通过打包工具将 CSS 打进 JS 里。例如使用 webpack 配合 style-loader 或 rollup 配合 rollup-plugin-styles。

举 rollup-plugin-styles 的例子

rollup.config.js

import styles from "rollup-plugin-styles"

export default {
  input: "src/main.js",
  output: {
    file: "dist/index.js",
    format: "cjs",
  },
  plugins: [styles({
    mode: "inject",
    modules: true,
  })],
}

main.js

import './index.css'
import style from './index.module.css'

console.log(style)

index.css

body {
  height: 100vh;
  background-color: pink;
}

@import url('./style.css');

index.module.css

.foo {
  color: blue;
}

@import url(./style.css);

style.css

.bar {
  color: red;
}

image.png

CSS in JS

在这个方案下,不存在 CSS 文件,一切都是 JS,对打包工具是没要求了,但是有运行时的性能消耗,作为组件库而且不好覆盖 「但也有解,见下一节」

import styled from 'styled-components'

const Title = styled.h1`
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;

  .foo {
    color: red;

    &:hover {
      background: blue;
    }
  }
`;

function Demo1() {
  return (
    <div>Demo1
      <Title >I am Happy

        <div className="foo">
          foo
        </div>
      </Title>
    </div>
  )
}

image.png

再谈 CSS in JS

CSS in JS 最大的优势便是灵活,注意到 antd 5 和 mui 都选用了 CSS in JS 的方案

https://mui.com/material-ui/customization/how-to-customize/


image.png

antd5做的很棒,在样式方面借助 :where()
结合 cssinjs 的 hash 选择器,我们可以让组件的样式始终处于在 hash 的范围之下,这可以保证组件样式不会对全局样式造成任何污染,又方便覆盖
https://ant-design.github.io/antd-style/guide/components-usage

选择

最终选择用 antd-style 来做基于 antd 的二次开发,很舒服

image.png

下面是写的一个小 demo
image.png

Foo/index.tsx

import React from 'react';
import { createStyles } from '../utils';

const compCls = `foo`;

const useStyles = createStyles(({ token, css, prefixCls, cx }) => {
  const prefix = [prefixCls, compCls].join('-');

  return {
    container: cx(
      prefix,
      css`
        &.${prefix} {
          background-color: ${token.colorPrimaryBg};
        }
      `,
    ),
    card: cx(
      `${prefix}-card`,
      css`
        &.${prefix}-card {
          color: ${token.colorPrimary};
        }
      `,
    ),
  };
});

const Foo = (props: { title: string }) => {
  const { styles } = useStyles();
  return (
    <div className={styles.container}>
      <div className={styles.card}>{props.title}</div>
    </div>
  );
};

export default Foo;

utils.ts

import { createInstance } from "antd-style";

export const { createStyles, ThemeProvider } = createInstance({
  hashPriority: "low",
  prefixCls: "sedationh",
});

参考

https://juejin.cn/post/7097100515535765534
https://blog.pig1024.me/posts/62fa1f4e8631e51c9414da21

重新思考何为幸福的人生

值得反复读的一篇文章 ~
原文: https://www.owenyoung.com/blog/money/

正文

我在上篇文章中畅想了一种有钱后做慈善的方式,通过提供基本收入给员工,让员工专心发展自己,而实际上我才是那个最想被发送无条件基本收入的员工。

幸运的是,我目前处于一个比较不错的阶段,没有房子,没有负债,没有特别热衷(并且需要花不少钱)的爱好,不准备生孩子,有一定的储蓄(在维持我目前的生活状态下(低物质欲,学会省钱),至少可以失业 10 年以上。

我目前的人生可以有 2 种选择:

  1. 需要一份体面的工作来维持一种:有房有车,能在节假日享受休闲的自由(比如和家人出国旅行)的,符合绝大多数人的期待和想象的中产阶级生活。
  2. 转变思路,认为幸福的人生是有自由去探索自己想创造的事情,不再为了钱而工作,而是为了自己想创造的事情而终身工作。

我能幸运的有第二种选择,是因为:

  1. 我没有负债
  2. 我不准备生孩子

如果我有以上任何一种,我想应该没有任何选择,只能选择 1。

这篇文章是想提醒也有同样困惑的同学:

  • 如果你只是为了钱而工作,本身并不享受你的工作
  • 你目前已经有了一定的储蓄,但是你把不少的钱花在了维持主流的中产阶级生活品质上。

也许你可以重新考虑下自己的处境,是否要转变心态,重新定义幸福的人生。

在一个一线城市的周边小城市里的生活成本真的很低,也许你现在已经拥有了这样的选择权。

同时推荐这篇文章:《钱是身份的传声筒》给有同样困惑的人 ,分析的很到位(基于美国),就是有点长,主要观点是:

每个人都处于下图金钱谱系中的某个位置:

image.png

从下到上依次是:

  1. 生存阶段
    1. 温饱线以下
    2. 缺乏住所或无家可归
    3. 解决了衣食住行的问题
    4. 有一定储蓄
  2. 自由阶段
    1. 有休闲的自由
    2. 工作的自由
    3. 注意力自由
  3. 有权力和影响力的阶段

但是,这个谱系并不是钱越多,就越上层:

image.png

比如:

一个很好的例子是一个年薪 3 万美元的人,但她从事的是她喜欢的工作。她的生活方式是每年只花 2 万美元。她有从事自己喜欢的工作的自由,她是一个幸福的人。

相反,你可以有一大笔钱,但却处于生存的频谱。比如一个年薪 30 万美元的人,但他在做他讨厌的工作。此外,他的生活方式背负很多贷款,比如房子的抵押贷款,车子的抵押贷款。这个人的自由就有限,他只能勉强维持生计。他是一个不幸的人。

如果不追求权力和影响力的话,那么工作的自由和注意力自由阶段就是最棒的阶段,但是有不少人在物质上已经达到了这个阶段,但是心态上却停留在储蓄阶段或者更下面。

所以重点是你的选择是什么?与其通过你有多少钱来看待这个金钱光谱,不如通过情感范围来看待它:清晰和恐惧。

image.png

你在生存阶段中的频谱越低,你就越害怕金钱和它对你生活的影响。然而,当你上升到储蓄阶段并接近自由时,围绕金钱的恐惧逐渐消失,事情进入清晰的阶段。当你处于自由阶段时,事情更加清晰,但休闲的自由仍然会有一些恐惧和困惑。

当你开始对你的生活有更多的控制,比如进入工作自由和注意力自由的阶段时,这就是最甜蜜的阶段。

如果你通过匮乏和生存的视角来看待世界,金钱只会放大这种不足的感觉。但是,如果自由是你的定义,那么无论你拥有多少钱,都会感到充实。

文章里权力的那一段(由于我完全没有经验),所以我 Get 不到他的点。

最后,不管怎么样,我会鼓励你重新考虑下应该如何定义幸福的人生。

元素的宽高

这个回答已经很全面

scrollHeight = Entire content 「包含内容总长度,不仅是被看到的部分,还包含可滚动的部分」 + padding

The Element.scrollHeight read-only property is a measurement of the height of an element's content, including content not visible on the screen due to overflow. 「注意需要 display 不是 inline」

clientHeight = Visible content +padding
offsetHeight = Visible content + padding + border +scrollbar

image.png

image.png
image.png

举个具体的例子:
https://codepen.io/sedationh/pen/RwQEbWQ

注意用的是 border-box
clientHeight = 140 = 180 - border * 2
offsetHeight = 180「设置的 height」
scrollHeight = 460

getBoundingClientRect() 获取的是更精确的 offsetHeight

The width and height properties of the DOMRect object returned by the method include the padding and border-width, not only the content width/height. In the standard box model, this would be equal to the width or height property of the element + padding + border-width. But if box-sizing: border-box is set for the element this would be directly equal to its width or height.

不过 getBoundingClientRect 和 offsetHeight 也是有区别的

Most of the time these are the same as width and height of getBoundingClientRect(), when there aren't any transforms applied to the element. In case of transforms, the offsetWidth and offsetHeight returns the element's layout width and height, while getBoundingClientRect() returns the rendering width and height. As an example, if the element has width: 100px; and transform: scale(0.5); the getBoundingClientRect() will return 50 as the width, while offsetWidth will return 100.

从机制上说,经过 transform scale 后盒子模型是没变的,offsetHeight 返回的是盒子模型的数值,getBoundingClientRect 返回的是展示出来的值。

Puppeteer 分享

是什么

image

取自 官网
Puppeteer is a Node.js library which provides a high-level API to control Chrome/Chromium over the DevTools Protocol. Puppeteer runs in headless mode by default, but can be configured to run in full (non-headless) Chrome/Chromium.

Most things that you can do manually in the browser can be done using Puppeteer! Here are a few examples to get you started:

  • Generate screenshots and PDFs of pages.
  • Crawl a SPA (Single-Page Application) and generate pre-rendered content (i.e. "SSR" (Server-Side Rendering)).
  • Automate form submission, UI testing, keyboard input, etc.
  • Create an automated testing environment using the latest JavaScript and browser features.
  • Capture a timeline trace of your site to help diagnose performance issues.
  • Test Chrome Extensions.

可以通过 puppeteer 提供的 API 去操作浏览器,重复性的去完成一些操作。

Case

case 1:打开 MDN 截个图

  • 了解基本结构
    • puppeteer
    • browser
    • page
import puppeteer from "puppeteer"
;(async () => {
  const browser = await puppeteer.launch({
    headless: false,
  })
  const page = await browser.newPage()

  await page.goto("https://developer.mozilla.org/")

  await page.screenshot({ path: "MDN.png", fullPage: true })

  await browser.close()
})()

case 2: 拿到飞书登陆态的cookie

  • 等待行为
  • 信息读取

流程梳理

  1. 打开某个需要飞书登陆态的网页 如 审批管理后台列表页 「A」
  2. 因为没有登陆态,所以进不去 A,会被重定向去扫码登陆页面「B」
  3. 完成扫码后会跳回 「A」
  4. 此时可以在 「A」页面读取登陆态的 cookie 信息

import puppeteer from "puppeteer"
;(async () => {
  const browser = await puppeteer.launch({
    headless: false,
  })
  const page = await browser.newPage()

  await page.goto("https://www.feishu.cn/approval/admin/approvalList")

  // 带有这个类的元素在登陆页面不存在,在有登陆态的列表页存在
  await page.waitForSelector(".approval-header")

  // 去拿 cookie
  const cookies = await page.cookies()
  // 可以通过在当前 page 环境去执行 JavaScript 来拿到 cookie,但是有些 cookie 可能是 HttpOnly 就拿不到
  const cookieStr = await page.evaluate(() => {
    return document.cookie
  })
  // 保存下来,下次就可以直接用 cookie,不用再重新登陆了
  page.setCookie(...cookies)
  await browser.close()
})()

如何在 VSCode 中 debug node

  • Toggle Auto Attach
    • smart
  • new terminal
  • node xxx.js

有效学习规划与阅读:泛读精读深读

介绍

本篇内容是对字节内分享的总结

应对信息爆炸与目标迷失

你学习这件事,你是医生也是患者这大家体会一下,你是医生也是患者,如果你把自己当成你自己的病人,你去干这个事情,你把你自己你的学习欲望当成自己的病人,你把你自己的这个这个学习目标当成自己病人

image.png

二、学习的目的和导向

image.png

  • 客观
    • 市场需要什么
    • 环境需要什么
  • 主观
    • 我能做什么
    • 我想做什么

主观客观共同决定

image.png

image.png

拖延拖延是会给人的那个痛苦是最大的,因为拖延带来的情绪是羞耻,羞耻是人这个所有的负面情绪里最严重的一种,就是负面性最强的一种情绪。

image.png

读书方法论

非虚构泛读法

  1. 细读绪论/导论/前言,了解写作目的、基本结构,再读一下目录
    1. 用好目录、减少精力浪费
  2. 先看下感兴趣的章节
    1. 看的时候划线 + 补充
    2. 把书读厚「创建和自己已有认识的连接」
  3. 看完一章节,回顾下划线,提三个问题,用本章的内容去回答
    1. 必要消化,进一步进行已有内容与输入内容的连接
  4. 全书读完、通看划线,简要读后感「<140」
  5. 整本书读薄

细读前言和抽章节时候问自己的问题

  1. 这是一本关于什么的书
  2. 我认可这本书的内容吗
  3. 这本书和我有什么关系

看完一章节,回顾下划线,提三个问题,用本章的内容去回答

  1. 这个章节讲了什么问题
  2. 给自己提一个封闭性问题
  3. 假设一个开放性待解决的问题

先想好自己的领域再决定精度

image.png

image.png

image.png

如何让自己持续执行呢?

  1. 定目标:定时间的目标
  2. 定时:25 分钟不可打断
  3. 延迟满足感:把要做的耗费精力的、让自己舒服的事情在 25 分钟后完成
  4. 奖励自己:完成后再做让自己舒服的事情

推荐应用:OffScreen 苹果安卓均可使用

Corepack 安利

简介

https://github.com/nodejs/corepack

Zero-runtime-dependency package acting as bridge between Node projects and their package managers

包管理器的管理器

In practical terms, Corepack lets you use Yarn, npm, and pnpm without having to install them.

Corepack is distributed by default with all recent Node.js versions.
我这边看新的 14、16 都是有的

使用方式

  1. 启用
    corepack enable
  2. 加 packageManager
    在项目的 package.json 文件中加
{
  "packageManager": "[email protected]+sha224.953c8233f7a92884eee2de69a1b92d1f2ec1655e66d08071ba9a02fa"
}

Here, yarn is the name of the package manager, specified at version 3.2.3, along with the SHA-224 hash of this version for validation. [email protected] is required. The hash is optional but strongly recommended as a security practice. Permitted values for the package manager are yarn, npm, and pnpm.

[email protected] 是必须的,后面的 hash 是可选的

使用效果

新建个项目

{
  "name": "corepack",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

在 package.json 所在路径执行pnpm --version

➜ pnpm --version
8.6.11

添加 "packageManager": "[email protected]",

{
  "name": "corepack",
  "version": "1.0.0",
  "description": "",
  "packageManager": "[email protected]",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
workspace/repositories/corepack is 📦 v1.0.0 via ⬢ v16.20.0
➜ pnpm --version
7.1.1

注意到我这里已经自动换成了项目规定的 packageManager

如果尝试使用 yarn 呢?

➜ yarn
Usage Error: This project is configured to use pnpm

$ yarn ...

但是 npm 还没拦,可以用

React 中的闭包问题

const useStateWithRef = initialVal => {
  const [state, setState] = useState(initialVal);
  const ref = useRef(initialVal);
  const setStateCopy = newVal => {
    ref.current = newVal;
    setState(newVal);
  };
  const getState = () => ref.current;
  return [state, setStateCopy, getState];
};

https://ahooks.js.org/hooks/use-get-state

function useGetState<S>(initialState?: S) {
  const [state, setState] = useState(initialState);
  const stateRef = useRef(state);
  stateRef.current = state;

  const getState = useCallback(() => stateRef.current, []);

  return [state, setState, getState];
}

export default useGetState;

React 18更新相关的类型改变

这是个探索性的分享,听完你可以了解到

  • React18 更新中涉及类型相关的部分内容
  • 关于 ReactFragment 的一些内容

背景

image.png

运行效果

17
image.png

18
image.png

变更比对

image.png

17

    type ReactText = string | number;
    type ReactChild = ReactElement | ReactText;

    /**
     * @deprecated Use either `ReactNode[]` if you need an array or `Iterable<ReactNode>` if its passed to a host component.
     */
    interface ReactNodeArray extends ReadonlyArray<ReactNode> {}
    type ReactFragment = {} | Iterable<ReactNode>;
    type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;

18

    /**
     * @deprecated - This type is not relevant when using React. Inline the type instead to make the intent clear.
     */
    type ReactText = string | number;
    /**
     * @deprecated - This type is not relevant when using React. Inline the type instead to make the intent clear.
     */
    type ReactChild = ReactElement | string | number;

    /**
     * @deprecated Use either `ReactNode[]` if you need an array or `Iterable<ReactNode>` if its passed to a host component.
     */
    interface ReactNodeArray extends ReadonlyArray<ReactNode> {}
    type ReactFragment = Iterable<ReactNode>;
    type ReactNode = ReactElement | string | number | ReactFragment | ReactPortal | boolean | null | undefined;

ReactFragment 是个什么?

image.png

https://reactjs.org/docs/fragments.html

image.png

搜索了一些内容还是找不到他的具体实现逻辑。
但回顾类型的时候,有了些体会。

    type ReactFragment = Iterable<ReactNode>;

从React提供的类型和React利用 ReactFragment 的视角来看,这就是一个可以迭代的对象而已,并且支持 key 的属性

在 DOM 上应该没啥映射关系,在 React 虚拟 DOM 中用得上,比对啥的(猜测)

mini-react 实现

从 0 开始实现一个最简单版本的 React

  • 支持 Function Component
  • 支持 useState useReducer

从 入口 开始

import App from "./App.tsx"

ReactDOM.createRoot(document.getElementById("root")!).render(<App />)
function App() {
  return <>This is App</>;
}

export default App;

先来实现一个 react-dom.js

function ReactDOMRoot(internalRoot) {
  this._internalRoot = internalRoot
}

ReactDOMRoot.prototype.render = function (children) {
  const root = this._internalRoot
  console.log("sedationh render", root, children)
}

function createRoot(container) {
  const root = { containerInfo: container }

  return new ReactDOMRoot(root)
}

export default { createRoot }

render 函数可以拿到

image.png

<App /> 会 形成一个对象「Virtual DOM」,大概是下面的样子,更详细的内容 可看 或者 这里

这一层是由编译来做的,被转为一个函数调用,返回对象

{
  type: 'marquee',
  props: {
    bgcolor: '#ffa7c4',
    children: 'hi',
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element'),
}

支持 HostComponent、仅仅创建流程,最简 React

大白话就是只渲染 原生 DOM

ReactDOM.createRoot(document.getElementById("root")!).render(
  <div>
    <h1>App</h1>
    <a href="https://baidu.com">baidu</a>
    This is App
  </div>
)

在 render 后 去 updateContainer

function updateContainer(element, container) {
  const { containerInfo } = container
  const fiber = createFiber(element, {
    type: containerInfo.nodeName.toLocaleLowerCase(),
    stateNode: containerInfo,
  })
  // 组件初次渲染
  scheduleUpdateOnFiber(fiber)
}

ReactDOMRoot.prototype.render = function (children) {
  const root = this._internalRoot
  console.log("sedationh render", root, children)
  + updateContainer(children, root)
}

containerInfo 就是 document.getElementById("root")

  const fiber = createFiber(element, {
    type: containerInfo.nodeName.toLocaleLowerCase(),
    stateNode: containerInfo,
  })

这里用 containerInfo 直接创建了一个 root node 的 fiber

export function createFiber(vnode, returnFiber) {

createFiber 会利用 Virtual DOM 和 returnFiber 构建 fiber 结构

returnFiber 会给 return 构建 fiber 关系

    // 第一个子fiber
    child: null,
    // 下一个兄弟节点
    sibling: null,
    // 父亲节点
    return: returnFiber,
export function createFiber(vnode, returnFiber) {
  console.log("sedationh createFiber", vnode, returnFiber)
  const fiber = {
    // 类型
    type: vnode.type,
    key: vnode.key,
    // 属性
    props: vnode.props,
    // 不同类型的组件, stateNode也不同
    // 原生标签 dom节点
    // class 实例
    stateNode: null,

    // 第一个子fiber
    child: null,
    // 下一个兄弟节点
    sibling: null,
    // 父亲节点
    return: returnFiber,

    flags: Placement,

    // 记录节点在当前层级下的位置
    index: null,
  }

  if (isString(vnode.type)) {
    fiber.tag = HostComponent
  }

  return fiber
}

形成的 fiber 会喂给 scheduleUpdateOnFiber

let wip = null // work in progress 当前正在工作中的
let wipRoot = null

export function scheduleUpdateOnFiber(fiber) {
  wip = fiber
  wipRoot = fiber
}

到这里,render 的调用栈就结束了,requestIdleCallback 登场

/**
 - @param {IdleDeadline} idleDeadline
 */
function workLoop(idleDeadline) {
  while (wip && idleDeadline.timeRemaining() > 0) {
    performUnitOfWork()
  }

  if (!wip && wipRoot) {
    commitRoot()
  }
}

requestIdleCallback(workLoop)

这个可以理解为死循环一直执行 workLoop 函数,但只会在浏览器的渲染进程空闲的时候进行

RequestIdleCallback 简单的说,判断一帧有空闲时间,则去执行某个任务。 目的是为了解决当任务需要长时间占用主进程,导致更高优先级任务(如动画或事件任务),无法及时响应,而带来的页面丢帧(卡死)情况。 故 RequestIdleCallback 定位处理的是: 不重要且不紧急的任务

workLoop 干两件事情

  • performUnitOfWork
    • 找不同,看看要干哪些活,准备活「工作单元(fiber)」
  • commitRoot
    • 干活啦,进行 DOM 更改

以下为 GPT 的说法
在 React Fiber 架构中,workLoop  函数是一个循环,用于驱动 React 应用程序的工作进程。它负责执行两个主要任务:performUnitOfWork  和  commitRoot

  1. performUnitOfWork

    • performUnitOfWork  是  workLoop  的第一个任务。
    • 它的作用是执行当前工作单元(fiber)的工作并返回下一个工作单元。
    • 一个工作单元代表了 React 中的一个组件或元素,需要进行处理、更新或渲染。
    • 在执行工作单元期间,会根据不同的工作类型和组件类型执行相应的操作,如调用函数组件、类组件的生命周期方法,处理更新队列,创建子工作单元等。
    • 当一个工作单元的工作完成后,performUnitOfWork  会返回下一个要执行的工作单元,以便继续进行下一轮的工作。
  2. commitRoot

    • commitRoot  是  workLoop  的第二个任务。
    • 它在所有工作单元都被处理完毕后被调用,用于将更新结果提交到实际的 DOM 中。
    • 在  commitRoot  中,React 会遍历整个 Fiber 树,将需要更新的 DOM 节点进行插入、更新或删除操作,以反映应用程序的最新状态。
    • 这个过程通常涉及到底层的 DOM 操作,如创建新的 DOM 节点、更新属性、添加事件监听器等。
    • 一旦  commitRoot  执行完成,React 应用程序的界面就会得到更新,并呈现给用户。

通过  performUnitOfWork  和  commitRoot  的交替执行,React 能够以递增的方式处理组件的更新,同时保持对用户界面的响应和流畅度。这种增量更新的方式也是 React Fiber 架构的核心**之一。

需要注意的是,workLoop  函数还可能执行其他任务,如处理错误、调度优先级等,但  performUnitOfWork  和  commitRoot  是其最重要的两个任务,负责驱动 React 应用程序的工作。
GPT 说法结束

function performUnitOfWork() {
  const { tag } = wip

  switch (tag) {
    case HostComponent:
      updateHostComponent(wip)
      break

    default:
      break
  }

  // dfs
  if (wip.child) {
    wip = wip.child
    return
  }

  let next = wip
  while (next) {
    if (next.sibling) {
      wip = next.sibling
      return
    }

    next = next.return
  }

  wip = null
}

performUnitOfWork 以 dfs 的方式走 fiber 链

遍历过程中,根据 fiber 结构中的 tag 进行区分

ReactWorkTags.js

export const FunctionComponent = 0
export const ClassComponent = 1
export const IndeterminateComponent = 2 // Before we know whether it is function or class
export const HostRoot = 3 // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4 // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5
export const HostText = 6
export const Fragment = 7
export const Mode = 8
export const ContextConsumer = 9
export const ContextProvider = 10
export const ForwardRef = 11
export const Profiler = 12
export const SuspenseComponent = 13
export const MemoComponent = 14
export const SimpleMemoComponent = 15
export const LazyComponent = 16
export const IncompleteClassComponent = 17
export const DehydratedFragment = 18
export const SuspenseListComponent = 19
export const ScopeComponent = 21
export const OffscreenComponent = 22
export const LegacyHiddenComponent = 23
export const CacheComponent = 24
export const TracingMarkerComponent = 25

updateHostComponent 的工作是两块

  • updateNode -> 创建 DOM ,存在 stateNode 里,更新 DOM 属性、处理文本内容
  • reconcileChildren 根据 Arrau<Vritual DOM> children 接着完善 fiber 链
export function updateHostComponent(wip) {
  if (!wip.stateNode) {
    wip.stateNode = document.createElement(wip.type)
    updateNode(wip.stateNode, wip.props)
  }

  reconcileChildren(wip, wip.props.children)
}

export function updateNode(node, nextVal) {
  Object.keys(nextVal).forEach((key) => {
    if (key === "children") {
      if (isStringOrNumber(nextVal[key])) {
        node.textContent = nextVal[key]
      }
    } else {
      node[key] = nextVal[key]
    }
  })
}

function reconcileChildren(wip, children) {
  if (isStringOrNumber(children)) {
    return
  }

  const newChildren = isArray(children) ? children : [children]
  let previousNewFiber = null
  for (let i = 0; i < newChildren.length; i++) {
    const newChild = newChildren[i]
    if (newChild == null) {
      continue
    }
    const newFiber = createFiber(newChild, wip)

    if (previousNewFiber === null) {
      // head node
      wip.child = newFiber
    } else {
      previousNewFiber.sibling = newFiber
    }

    previousNewFiber = newFiber
  }
}

接下来看 commitRoot 的工作。以 dfs 的方式进行 DOM 改动,改动的流程就是找父亲节点,然后吧在 performUnitOfWork 中准备的 DOM 节点塞进去

function commitRoot() {
  commitWorker(wipRoot)
  wipRoot = null
}

function commitWorker(wip) {
  if (!wip) {
    return
  }

  // 1. 提交自己
  const parentNode = getParentNode(wip.return)
  const { flags, stateNode } = wip
  if (flags & Placement && stateNode) {
    parentNode.appendChild(stateNode)
  }
  // 2. 提交子节点
  commitWorker(wip.child)
  // 3. 提交兄弟
  commitWorker(wip.sibling)
}

function getParentNode(wip) {
  let p = wip
  while (p) {
    if (p.stateNode) {
      return p.stateNode
    }

    p = p.return
  }
}

至此,完成 「支持 HostComponent、仅仅创建流程,最简 React」

看效果 👇

ReactDOM.createRoot(document.getElementById("root")!).render(
  <div>
    <h1>App</h1>
    <a href="https://baidu.com">baidu</a>
    This is App
  </div>
)

image.png

支持 FunctionComponent && ClassComponent && HostText

createFiber 的时候,增加 fiber.tag = FunctionComponent

export function createFiber(vnode, returnFiber) {
  console.log("sedationh createFiber", vnode, returnFiber)
  const fiber = {
    // 类型
    type: vnode.type,
    key: vnode.key,
    // 属性
    props: vnode.props,
    // 不同类型的组件, stateNode也不同
    // 原生标签 dom节点
    // class 实例
    stateNode: null,

    // 第一个子fiber
    child: null,
    // 下一个兄弟节点
    sibling: null,
    return: returnFiber,

    flags: Placement,

    // 记录节点在当前层级下的位置
    index: null,
  }

  if (isString(vnode.type)) {
    fiber.tag = HostComponent
  }
  if (isFunction(vnode.type)) {
    fiber.tag = FunctionComponent
  }

  return fiber
}

然后在 performUnitOfWork 处理 FunctionComponent,调用函数,返回值作为 children 接着构建 fiber 链

function performUnitOfWork() {
  const { tag } = wip

  switch (tag) {
    case HostComponent:
      updateHostComponent(wip)
      break
    case FunctionComponent:
      updateFunctionComponent(wip)
      break

export function updateFunctionComponent(wip) {
  const { type, props } = wip
  const children = type(props)
  reconcileChildren(wip, children)
}

接下来 ClassComponent 和 HostText

function performUnitOfWork() {
  const { tag } = wip

  switch (tag) {
    case HostComponent:
      updateHostComponent(wip)
      break
    case FunctionComponent:
      updateFunctionComponent(wip)
      break
    case ClassComponent:
      updateClassComponent(wip)
      break
    case HostText:
      updateHostComponent(wip)
      break

export function updateClassComponent(wip) {
  const { type, props } = wip
  const instance = new type(props)
  const children = instance.render()
  reconcileChildren(wip, children)
}

export function updateHostTextComponent(wip) {
  wip.stateNode = document.createTextNode(wip.props.children)
}

解释下 HostText
「就是 同级 下有至少两个的节点、且有文本节点的情况」,如下面的 「有其他同级元素的文本」,「App」 不算

    <div>
      <h1>App</h1>
      <a href="https://baidu.com">baidu</a>
      有其他同级元素的文本
      {/* @ts-ignore */}
      <ClassComp />
    </div>

「App」 的这种情况在 updateNode 的时候进行了处理

export function updateNode(node, nextVal) {
  Object.keys(nextVal).forEach((key) => {
    if (key === "children") {
      if (isStringOrNumber(nextVal[key])) {
        // STUDY: seda 文本节点处理
        node.textContent = nextVal[key]
      }
    } else {
      node[key] = nextVal[key]
    }
  })
}

并且会在 reconcileChildren 的时候进行返回

function reconcileChildren(wip, children) {
  if (isStringOrNumber(children)) {
    return
  }

Fragment 的处理比较简单,略

引入最小堆和更新队列

前面提到requestIdleCallback工作只有 20FPS,一般对用户来感觉来说,需要到 60FPS 才是流畅的, 即一帧时间为 16.7 ms,所以这也是react团队自己实现requestIdleCallback的原因。实现大致思路是在requestAnimationFrame获取一桢的开始时间,触发一个postMessage,在空闲的时候调用idleTick来完成异步任务。
-- https://juejin.cn/post/6844904196345430023#heading-11

React Scheduler 为什么使用 MessageChannel 实现

先来实现一个最小堆吧
https://github.com/sedationh/code-playground/blob/871f11bdda338ecdbe8921d6a086d5086a3d33d9/ts/src/Heap/index.ts

image.png

基本思路可参考 https://github.com/kodecocodes/swift-algorithm-club/blob/master/Heap/README.markdown

先去除之前用 requestIdleCallback 的调用方式

function workLoop() {
  while (wip) {
    performUnitOfWork()
  }

  if (!wip && wipRoot) {
    commitRoot()
  }
}

// requestIdleCallback(workLoop)

workLoop 的触发时机是在

export function scheduleUpdateOnFiber(fiber) {
  wip = fiber
  wipRoot = fiber

  scheduleCallback(workLoop)
}

scheduleCallback 负责调度我们的 workLoop

实现如下

import { Heap } from "./heap"

let taskIdCounter = 0

const taskHeap = new Heap((parent, child) => {
  if (parent.expirationTime === child.expirationTime) {
    return parent.id < child.id
  }
  return parent.expirationTime < child.expirationTime
})

export function scheduleCallback(callback) {
  const currentTime = getCurrentTime()
  const timeout = -1

  const expirationTime = currentTime - timeout

  const newTask = {
    id: taskIdCounter,
    callback,
    expirationTime,
  }
  taskIdCounter += 1

  taskHeap.add(newTask)

  requestHostCallback()
}

const channel = new MessageChannel()
function requestHostCallback() {
  channel.port1.postMessage(null)
}
channel.port2.onmessage = function () {
  workLoop()
}

function workLoop() {
  let currentTask = taskHeap.pop()

  while (currentTask) {
    const callback = currentTask.callback
    callback()
    currentTask = taskHeap.pop()
  }
}

export function getCurrentTime() {
  return performance.now()
}

我们传入的 workLoop 作为 scheduleCallbackcallback 进入,被组织为 task 加入 taskHeap

添加 hooks 能力

useReducer

下面是我们测试自己写的 useReducer 的例子

function FunctionComp() {
  const [cnt, dispatchCnt] = useReducer((state, action) => {
    console.log("sedationh action", action, n++)
    return state + 1
  }, 0)

  const [cnt2, dispatchCnt2] = useReducer((state, action) => {
    console.log("sedationh action", action, n++)
    return state + 2
  }, 0)

  return (
    <div>
      <button
        onClick={() => {
          dispatchCnt("action")
        }}
      >
        dispatchCnt cnt1
      </button>
      {cnt}
      <hr />
      <button
        onClick={() => {
          dispatchCnt2("action")
        }}
      >
        dispatchCnt cnt2
      </button>
      {cnt2}
      <h1>FunctionComp</h1>
    </div>
  )
}

先简单处理下事件行为 「onClick」

export function updateNode(node, nextVal) {
  Object.keys(nextVal).forEach((key) => {
    if (key === "children") {
      if (isStringOrNumber(nextVal[key])) {
        node.textContent = nextVal[key]
      }
    } else if (key.slice(0, 2) === "on") {
      const eventName = key.slice(2).toLowerCase()
      node.addEventListener(eventName, nextVal[key])
    } else {
      node[key] = nextVal[key]
    }
  })
}

hook 的结构如下

    hook = {
      memoriedState: null, // 状态
      next: null, // 下一个 hook
    }

关键逻辑

  • 找到当前 hook
  • 找到当前 hook 所在的 fiber
  • 在 hook 所在的 fiber 上标记 老 fiber「alternate」
  • 利用 reducer 去更新新的 hook.memoriedState
  • 利用 scheduleUpdateOnFiber 以当前更新 fiber 节点为 rootFiber 进行更新处理
export const useReducer = (reducer, initialState) => {
  const hook = updateWorkInProgressHook()

  if (!currentlyRenderingFiber.alternate) {
    // 初次渲染
    hook.memoriedState = initialState
  }

  const dispatch = (action) => {
    hook.memoriedState = reducer(hook.memoriedState, action)
    currentlyRenderingFiber.alternate = { ...currentlyRenderingFiber }
    scheduleUpdateOnFiber(currentlyRenderingFiber)
  }

  return [hook.memoriedState, dispatch]
}

为了能满足上面的代码,需要在 fiber 上增加一些信息

export function createFiber(vnode, returnFiber) {
  const fiber = {

...

    // old fiber
    alternate: null,

    // function component, hook0
    memoriedState: null,
  }

...
let currentlyRenderingFiber = null
let workInProgressHook = null

function updateWorkInProgressHook() {
  let hook

  const current = currentlyRenderingFiber.alternate
  if (!current) {
    // 初次渲染
    hook = {
      memoriedState: null,
      next: null,
    }

    if (!workInProgressHook) {
      // hook0
      workInProgressHook = currentlyRenderingFiber.memoriedState = hook
    } else {
      // hook1, hook2, hook3 ...
      workInProgressHook = workInProgressHook.next = hook
    }
  } else {
    // 更新
    currentlyRenderingFiber.memoriedState = current.memoriedState
    if (!workInProgressHook) {
      // hook0
      hook = workInProgressHook = currentlyRenderingFiber.memoriedState
    } else {
      // hook1, hook2, hook3 ...
      hook = workInProgressHook = workInProgressHook.next
    }
  }

  return hook
}

currentlyRenderingFiber 怎么拿到呢?

export const renderWithHooks = (wip) => {
  currentlyRenderingFiber = wip
  currentlyRenderingFiber.memoriedState = null
  workInProgressHook = null
}
export function updateFunctionComponent(wip) {
  renderWithHooks(wip)

  const { type, props } = wip
  const children = type(props)
  reconcileChildren(wip, children)
}

接下来在 reconcileChildren 中进行 diff,进行更新标记

// 协调(diff)
// 创建新 fiber
function reconcileChildren(wip, children) {
  if (isStringOrNumber(children)) {
    return
  }

  const newChildren = isArray(children) ? children : [children]
  let oldFiber = wip.alternate?.child
  let previousNewFiber = null
  for (let i = 0; i < newChildren.length; i++) {
    const newChild = newChildren[i]
    if (newChild == null) {
      continue
    }
    const newFiber = createFiber(newChild, wip)
    const same = isSame(oldFiber, newFiber)
    if (same) {
      Object.assign(newFiber, {
        alternate: oldFiber,
        stateNode: oldFiber.stateNode,
        flags: Update,
      })
    }

    if (oldFiber) {
      oldFiber = oldFiber.sibling
    }

    if (previousNewFiber === null) {
      // head node
      wip.child = newFiber
    } else {
      previousNewFiber.sibling = newFiber
    }

    previousNewFiber = newFiber
  }
}

接下来完善 commit 环节

function commitWorker(wip) {
  if (!wip) {
    return
  }

  // 1. 提交自己
  const parentNode = getParentNode(wip.return)
  const { flags, stateNode } = wip
  if (flags & Placement && stateNode) {
    parentNode.appendChild(stateNode)
  }
  if (flags & Update && stateNode) {
    updateNode(stateNode, wip.alternate.props, wip.props)
  }
  // 2. 提交子节点
  commitWorker(wip.child)
  // 3. 提交兄弟
  commitWorker(wip.sibling)
}

因为需要比较,所有原来的 updateNode 也要改下

export function updateNode(node, prev, nextVal) {
  Object.keys(prev).forEach((key) => {
    if (key === "children") {
      if (isStringOrNumber(nextVal[key])) {
        node.textContent = ""
      }
    } else if (key.slice(0, 2) === "on") {
      const eventName = key.slice(2).toLowerCase()
      node.removeEventListener(eventName, prev[key])
    } else {
      node[key] = ""
    }
  })

  Object.keys(nextVal).forEach((key) => {
    if (key === "children") {
      if (isStringOrNumber(nextVal[key])) {
        // STUDY: seda 文本节点处理
        node.textContent = nextVal[key]
      }
    } else if (key.slice(0, 2) === "on") {
      const eventName = key.slice(2).toLowerCase()
      node.addEventListener(eventName, nextVal[key])
    } else {
      node[key] = nextVal[key]
    }
  })
}

现在来整体看下流程

  • 初次渲染 Function 组件
    • render
    • updateContainer
    • scheduleUpdateOnFiber
    • workLoop
    • performUnitOfWork
    • updateFunctionComponent
    • renderWithHooks
    • 调用 Function 组件
    • useReducer
    • reconcileChildren
    • commitWorker
  • 更新
    • dispatch
    • scheduleUpdateOnFiber

useState

思路是差不多的

export const useState = (initialState) => {
  const hook = updateWorkInProgressHook()

  if (!currentlyRenderingFiber.alternate) {
    // 初次渲染
    hook.memoriedState = initialState
  }

  const fiber = currentlyRenderingFiber

  const dispatch = (newState) => {
    hook.memoriedState = typeof newState === "function" ? newState(hook.memoriedState) : newState
    fiber.alternate = { ...fiber }
    fiber.sibling = null
    scheduleUpdateOnFiber(fiber)
  }

  return [hook.memoriedState, dispatch]
}

完善更新能力

目前的更新能力都是通过位置来比较的,具体来说如

new abcd
old cd

a !== c
b !== d

这俩都会被删除

export function createFiber(vnode, returnFiber) {
  const fiber = {

...

	// 处理删除
    deletoins: null,
  }
function reconcileChildren(returnFiber, children) {
  if (isStringOrNumber(children)) {
    return
  }

  const newChildren = isArray(children) ? children : [children]
  let oldFiber = returnFiber.alternate?.child
  let previousNewFiber = null
  let newIndex
  for (newIndex = 0; newIndex < newChildren.length; newIndex++) {
    const newChild = newChildren[newIndex]
    if (newChild == null) {
      continue
    }
    const newFiber = createFiber(newChild, returnFiber)
    const same = isSame(oldFiber, newFiber)
    if (same) {
      Object.assign(newFiber, {
        alternate: oldFiber,
        stateNode: oldFiber.stateNode,
        flags: Update,
      })
    }
    if (!same && oldFiber) {
      // 删除节点
      deleteChild(returnFiber, oldFiber)
    }

    if (oldFiber) {
      oldFiber = oldFiber.sibling
    }

    if (previousNewFiber === null) {
      // head node
      returnFiber.child = newFiber
    } else {
      previousNewFiber.sibling = newFiber
    }

    previousNewFiber = newFiber
  }

  /**
   - new ab
   - old abcdef
   *
   - 通过遍历 ab 是走不到 cdef 的
   */
  if (newIndex === newChildren.length) {
    deleteRemainingChildren(returnFiber, oldFiber)
    return
  }
}

function deleteRemainingChildren(returnFiber, currentFirstChild) {
  let childToDelete = currentFirstChild
  while (childToDelete) {
    deleteChild(returnFiber, childToDelete)
    childToDelete = childToDelete.sibling
  }
}

2022-2023

为啥要写这个呢?

  1. 给一年的经历留个文字记录,给以后的自己看,真实、保持客观
  2. 回顾,是为了想清楚想要什么,想做什么

时间线

  • 1 - 6
    • 在郑州、居家
    • 毕业论文、毕业答辩,大学毕业
    • 参加了一期电鸭的英语练习俱乐部
      • 了解外企、远程工作的一些内容
      • 取了个英文名 Holden、整了份英文版本的简历
    • 字节服务端训练营、学习 Go
    • 看完了一本英文书 Atomic habits
    • 开发浏览器插件 awesome-toc 、上线谷歌上线
    • 买了一台台式Win整机,用来专门打游戏,5000+,完成了一个小梦想「有一台好的台式机,快乐打游戏」
  • 6 - 12
    • 杭州、开始工作
      • 实习一个月 + 正式入职
      • 作为 PaC 主要前端负责任人,承接开发任务
    • 看了很久的惊悚乐园、后面还断断续续的看,但是频率下降,是一本很不错的书
    • 组装小桌子,在杭州有了个较为舒服的居住环境
    • 和同事们一起去漂流
    • 和婷去黄山玩
      • 坐了一次空中飞机,脚下是一叠一叠的山,空气顺着山形成向上的风,热天凉意
      • 黄山很美,山长的花样很多,天很蓝,晚霞很美
      • 这么想着,有着不少美好的回忆
    • 国庆节回了趟家,给爸爸和自己一起换了新手机
    • 和婷在浙大紫荆港附近的游览路上转了转,感觉很惬意
    • 和同事们爬西湖的山
    • 放开,开始阳,烧了几天,和婷相互照顾「刚好一起阳」
    • 开始学 Java

aha

  • 公有知识 公共知识
    • https://www.youtube.com/watch?v=b7NZfkqFc6k
    • 有趣的蓝眼红眼问题
    • 把自己经过深思熟虑觉得正确的东西和别人分享,在检验自己的同时,也让正确的观点变成公共知识
    • 沟通、交流的重要价值
  • 996 机会成本
    • https://www.youtube.com/watch?v=U4kpHYIuV6c
    • 避免追求收入最大化,掉入自我剥削的陷阱,而要去寻求编辑效应和边际成本的交叉点,也就是生活和工作的平衡点。另一方面,提高知识、技能、人脉,增加生产可能曲线,才能真正提高生活品质
    • image.png
  • **、专制
  • 工具
    • 笔记
      • Obsidian
      • Logseq
      • 坚果云
      • 体验很好
    • 浏览器
      • Arc
    • 编程工具
      • copilot

flag

  • 向 fullstack 发展
    • 持续精进前端水平,构建系统化的知识
    • 利用Java生态,了解后端领域
  • 去 >= 2 个地方旅游
  • 十本书,任何类型皆可
  • 准备雅思

其他

在写本文的时候,记忆不够用,回想不起来
是去看了微信,抖音,电商平台进行拼拼凑凑出来的
针对会写在这里的内容,对笔记中的内容形成一套标机体系

#time 时间线
#aha 想法、观念 / flomo

其实还有很多 aha 时刻,预期也是在这里出现的内容是最多的,接下来一年注意积累。
图片也一个季度整理一次

今年就要 24 岁了

使用 RxJS 实现可靠的异步搜索框

本文源于张乐聪同学的分享,在原有代码基础上进行了一些修改

前言

异步搜索框是一个业务中非常常见的诉求,但是想实现一个可靠的异步搜索框却不是一个简单的任务,为了使其可靠(性能好 + Bug 少 + 体验好 + 易维护),实现者需要考虑非常多的方面.

异步搜索框的难点

  1. 针对于搜索做 debounce 操作,在用户的输入过程中不立即搜索(性能好,节省网络资源)
  2. 对于输入为空的时候不进行 debounce(体验好,从有搜索内容到无搜索内容立即响应)
  3. 对于 debounce 后的输入去重,不发送重复请求,例如从 a -> ab(debounce 掉,不发送)-> a,可能对 a 发送两次搜索请求(性能好,节省网络资源)
  4. 正确处理时序,不要被早发送的请求响应覆盖晚发送的请求响应(体验好)
  5. 正确处理异常(体验好)
  6. 正确处理 loading,只要还有请求没有返回就维持 loading(体验好)
  7. 在正确实现之前所有需求的前提下维持实现的可维护性(易维护 + 不容易出 Bug)

常见实现的问题

function SearchBox() {
  const [result, setResult] = useState("")
  const handleInput = (e) => {
    const value = e.target.value
    request(value).then((response) => {
      setResult(response.data)
    })
  }
  return (
    <>
      <input onChange={handleInput} />
      {result}
    </>
  )
}

这种实现最典型的问题是时序问题不能被正确的处理,没有个先来后到的讲究,谁来谁覆盖。
因此要进行处理的话要么维持发送时间,要么记下来发送的内容,来确保响应可以和请求匹配。

// 产生 4000 2000 1000 4000 ...
const random2 = (function () {
  let i = 1

  const array = [4000, 2000, 1000]

  return () => {
    return array[i++ % array.length]
  }
})()

function request(value) {
  return new Promise<{
    data: string
  }>((resolve) => {
    setTimeout(() => {
      resolve({
        data: value,
      })
    }, random2())
  })
}

function SearchBox() {
  const [result, setResult] = useState("")
  const latestRequestTimeRef = useRef(0)
  const handleInput = (e) => {
    const value = e.target.value
    const requestTime = Date.now() // 记录时间
    latestRequestTimeRef.current = requestTime
    request(value).then((response) => {
      if (requestTime >= latestRequestTimeRef.current) {
        // 对比时间
        setResult(response.data)
      }
    })
  }
  return (
    <>
      <input onChange={handleInput} />
      {result}
    </>
  )
}

如果涉及 debounce,通常我们都会直接使用工具函数比如 lodash 的 debounce,它无法实现条件 debounce,因此我们需要自己专门实现。

即使过了这关,在后续的 error、loading 处理中,你会发现,所有的代码都挤在 handleInput 中,状态相互纠缠。不光可靠性难以保证、持续维护的难度也会越来越大。

可靠实现的难度在哪?

如果你有一些编写异步操作的经验,会发现每增加一个 feature 都需要维护一些状态、并且由于逻辑关联,会和原有的逻辑搅在一起,就像一个线团一样。在没有高层次抽象的情况下,很难将不同的异步 feature 进行隔离。随着功能的增多,这个线团越来越大、越来越乱,直到艰难维护、崩溃、重写或者消亡。

所以解决问题的一个思路就是:将不同的 feature 以解耦、内聚的形式实现,相互独立,各自维护,再统一串联。

1. 引入 RxJS 做一个输入的流

通过制造一个流,在输入值改变的时候向流发送数据,并监听这个流,可以将输入内容实时的同步在页面上。

function SearchBox() {
  const [result, setResult] = useState("")
  // 下面的 BehaviorSubject 和 Subject 一模一样,除了有一个初始值会在订阅时立刻发出
  const input$ = useMemo(() => new BehaviorSubject(""), [])
  // 输入内容时向流发送值
  const handleInput = (e) => {
    input$.next(e.target.value)
  }
  useEffect(() => {
    // 订阅这个流
    const subscription = input$.subscribe((v) => {
      setResult(v)
    })
    return () => {
      // 组件卸载时取消订阅
      subscription.unsubscribe()
    }
  }, [])

  return (
    <>
      <input onChange={handleInput} />
      {result}
    </>
  )
}

2. debounce

第一步我们先进行 debounce 的实现,在搜索值为空的时候立即响应,其他情况下 debounce:
我们利用 debounce 操作符,在输入值为空字符串的时候立马发送值,在输入不为空的时候等待 500ms 再发送值。

function SearchBox() {
  const [result, setResult] = useState("")
  // 下面的 BehaviorSubject 和 Subject 一模一样,除了有一个初始值会在订阅时立刻发出
  const input$ = useMemo(() => new BehaviorSubject(""), [])
  // 输入内容时向流发送值
  const handleInput = (e) => {
    input$.next(e.target.value)
  }
  useEffect(() => {
    const subscription = input$
      .pipe(
        // 防抖的实现 -----------------------------
        debounce((input) => {
          if (input.length === 0) {
            return of(null) // 立即响应
          } else {
            return timer(500) // 等待 500ms
          }
        })
      )
      .subscribe((v) => {
        setResult(v)
      })
    return () => {
      subscription.unsubscribe()
    }
  }, [])

  return (
    <>
      <input onChange={handleInput} />
      {result}
    </>
  )
}

3. 去重

function SearchBox() {
  const [result, setResult] = useState("")
  // 下面的 BehaviorSubject 和 Subject 一模一样,除了有一个初始值会在订阅时立刻发出
  const input$ = useMemo(() => new BehaviorSubject(""), [])
  // 输入内容时向流发送值
  const handleInput = (e) => {
    input$.next(e.target.value)
  }
  useEffect(() => {
    const subscription = input$
      .pipe(
        // 防抖的实现 -----------------------------
        debounce((input) => {
          if (input.length === 0) {
            return of(null) // 立即响应
          } else {
            return timer(500) // 等待 500ms
          }
        }),
        distinctUntilChanged()
      )
      .subscribe((v) => {
        setResult(v)
      })
    return () => {
      subscription.unsubscribe()
    }
  }, [])

  return (
    <>
      <input onChange={handleInput} />
      {result}
    </>
  )
}

4. 网络请求 + 时序处理

Rxjs 提供了 switchMap 操作符来完成 Promise 到值的解包过程和异步时序控制能力。switchMap 可以将一个流映射为新的流,我们可以将一个文本流通过 Promise 映射为一个文本流到 Promise resolve 结果的流,同时 switchMap 还有一个特殊的能力就是会丢弃掉比最新输入发起时间晚到的值:

// 产生 4000 2000 1000 4000 ...
const random2 = (function () {
  let i = 1

  const array = [4000, 2000, 1000]

  return () => {
    return array[i++ % array.length]
  }
})()

function request(value) {
  return new Promise<{
    data: string
  }>((resolve) => {
    setTimeout(() => {
      resolve({
        data: value,
      })
    }, random2())
  })
}

function SearchBox() {
  const [result, setResult] = useState("")
  // 下面的 BehaviorSubject 和 Subject 一模一样,除了有一个初始值会在订阅时立刻发出
  const input$ = useMemo(() => new BehaviorSubject(""), [])
  // 输入内容时向流发送值
  const handleInput = (e) => {
    input$.next(e.target.value)
  }
  useEffect(() => {
    const subscription = input$
      .pipe(
        // 防抖的实现 -----------------------------
        debounce((input) => {
          if (input.length === 0) {
            return of(null) // 立即响应
          } else {
            return timer(500) // 等待 500ms
          }
        }),
        distinctUntilChanged(),
        // 网络请求的实现 -----------------------------
        switchMap((input) => {
          return request(input) // 取最新开发发起的结果
        })
      )
      .subscribe((v) => {
        setResult(v.data)
      })
    return () => {
      subscription.unsubscribe()
    }
  }, [])

  return (
    <>
      <input onChange={handleInput} />
      {result}
    </>
  )
}

5. Loading + 异常处理

function fetcher(input: string): Promise<{
  value: string
  error: boolean
}> {
  return new Promise<string>((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() > 0.5) reject()
      resolve("api " + input)
    }, Math.random() * 1000)
  })
    .then((res) => {
      return {
        value: res,
        error: false,
      }
    })
    .catch(() => {
      return {
        value: "",
        error: true,
      }
    })
}

function SearchBox() {
  const [result, setResult] = useState("")
  // 下面的 BehaviorSubject 和 Subject 一模一样,除了有一个初始值会在订阅时立刻发出
  const input$ = useMemo(() => new BehaviorSubject(""), [])
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState(false)

  const errorRef = useRef<boolean>(false)
  errorRef.current = error

  // 输入内容时向流发送值
  const handleInput = (e) => {
    input$.next(e.target.value)
  }
  useEffect(() => {
    const subscription = input$
      .pipe(
        // 防抖的实现 -----------------------------
        debounce((input) => {
          // 补充 error 处理
          if (input.length === 0 || errorRef.current) {
            return of(null) // 立即响应
          } else {
            return timer(500) // 等待 500ms
          }
        }),
        distinctUntilChanged(),
        // 网络请求的实现 -----------------------------
        switchMap((input) => {
          if (input.length === 0) {
            setLoading(false)
            setError(false)
            return of({
              value: "default",
              error: false,
            })
          }
          setError(false)
          setLoading(true)
          return fetcher(input)
        })
      )
      .subscribe({
        next: ({ error, value }) => {
          if (error) {
            setError(true)
            setLoading(false)
          } else {
            setError(false)
            setLoading(false)
            setResult(value)
          }
        },
      })
    return () => {
      subscription.unsubscribe()
    }
  }, [])

  return (
    <>
      <div>
        <input onChange={handleInput} />
      </div>
      {error ? "error" : loading ? "loading" : result}
    </>
  )
}

最后

代码可见
https://github.com/sedationh/search-box

Go 之 接口型函数

效果:利用 GetterFunc 把函数转成接口,来满足 Getter 的要求
优点:可传函数可传实现接口的结构体

// Interface-type functions
func Test1(t *testing.T) {
	A := func(getter func() string) string {
		return getter()
	}

	if A(func() string {
		return "A"
	}) != "A" {
		t.Fatal("failed")
	}
}

type Getter interface {
	Get() string
}

// A GetterFunc implements Getter with a function.
type GetterFunc func() string

// Get implements Getter interface function
func (f GetterFunc) Get() string {
	return f()
}

type B struct {
	Name string
}

func (b B) Get() string {
	return b.Name
}

func Test2(t *testing.T) {

	A := func(getter Getter) string {
		return getter.Get()
	}

	if A(GetterFunc(func() string {
		return "A"
	})) != "A" {
		t.Fatal("failed")
	}

	b := B{
		Name: "B",
	}

	if A(b) != "B" {
		t.Fatal("failed")
	}
}

出自 动手写分布式缓存 - GeeCache 第二天 单机并发缓存

// A Getter loads data for a key.
type Getter interface {
	Get(key string) ([]byte, error)
}

// A GetterFunc implements Getter with a function.
type GetterFunc func(key string) ([]byte, error)

// Get implements Getter interface function
func (f GetterFunc) Get(key string) ([]byte, error) {
	return f(key)
}

文章参考 https://geektutu.com/post/7days-golang-q1.html

Overflow 组件

背景

此文是对 react-component/overflow 的一些总结

开始是想整 overflow 组件到自己的组件库,看了源代码发现处理的问题和边界很多,遇到类似的场景还是直接用这个库吧

内容

下面是整理的一些流程和思考过程
在线可以取这里编辑

image.png

image.png

文章参考

认知觉醒

收获

  1. 本能、情绪、理智。让他们相互补充,来完成目标
  2. 刻意练习,在舒适区边缘扩展自己
  3. 做规划、将思考和行动都清晰化,减少模糊
  4. 以结果为导向

Highlights

第一章 大脑——一切问题的起源

描述

可见我们人类与这个世界上的其他动物已经迥然不同,在我们的大脑里,由内到外至少有三重大脑:年代久远的本能脑、相对古老的情绪脑和非常年轻的理智脑。

一起工作时必然会出现很多兼容问题

理智脑虽然高级,但比起本能脑和情绪脑,它的力量实在是太弱小了

·避难趋易——只做简单和舒适的事,喜欢在核心区域周边打转,待在舒适区内逃避真正的困难;·急于求成——凡事希望立即看到结果,对不能马上看到结果的事往往缺乏耐心,非常容易放弃。

大脑和肌肉一样,遵循用进废退的原则。如果我们习惯感情用事、不假思索,那感性思维就会占据主导;而若是习惯经常思考、时常反思,那理性思维便会占据上风。

理智脑不是直接干活的,干活是本能脑和情绪脑的事情,因为它们的“力气”大;上天赋予理智脑智慧,是让它驱动本能和情绪,而不是直接取代它们。

为了缓解焦虑,我开始不自觉地求多、求快,结果又陷入只关注阅读量的低水平勤奋——每本书都读得很快,回头却发现什么都没记住。

为了今后不再承受这种情绪起伏,我必须正视它,彻底解决这个困扰。一天下午,我拿出笔和纸,把心中的烦恼、担忧、顾虑和欲望全部列了出来,大到人生愿景,小到 10 分钟后要做的事,慢慢地,我勾勒出焦虑的几种形式。

归结起来,焦虑的原因就两条:想同时做很多事,又想立即看到效果。王小波说:人的一切痛苦,本质上都是对自己无能的愤怒。焦虑的本质也契合这一观点:自己的欲望大于能力,又极度缺乏耐心。焦虑就是因为欲望与能力之间差距过大。

耐心可以说是人类最珍贵的品质之一了,它直指我们急于求成、避难趋易的天性,可谓得耐心者得天下,所以我们不妨从耐心这个关键词开始谈起。

认知规律,耐心的倍增器

描述

当然,前提是选择正确的方向,并在积累的过程中遵循刻意练习的原则,在舒适区边缘一点一点地扩展自己的能力范围。

描述

描述

原因仍然是我们的天性在作祟。因为单纯保持学习输入是简单的,而思考、行动和改变则相对困难。在缺乏觉知的情况下,我们会本能地避难趋易,不自觉地沉浸在表层的学习量中。

读书时不求记住书中的全部知识,只要有一两个观点促使自己发生了切实的改变就足够了,其收获与意义比读很多书但仅停留在知道的层面要大得多。时常以这样的标准指导自己学习,我们的收获就会越来越多,焦虑就会越来越少,耐心自然也就越来越强了。

描述

从这个角度看,耐心不是毅力带来的结果,而是具有长远目光的结果。这也侧面回答了为什么我们需要终身学习。因为当我们知道的规律越多,就越能定位自己所处的阶段和位置、预估未来的结果,进而增强自己持续行动的耐心。毫无疑问,对外部世界的规律的认知能使我们耐心倍增。

这种“后娱乐”的好处是,将享乐的快感建立在完成重要任务后的成就感之上,很放松、踏实,就像一种奖赏;而“先娱乐”虽然刚开始很快活,但精力会无限发散,拖延重要的工作,随着时间的流逝,人会空虚、焦虑。

除了知晓前面提到的各种规律,还有一个重要的原因是他们更擅长探索原理,会主动改变认知视角,来找到行动的意义和好处。

所以,想办法让本能脑和情绪脑感受到困难事物的乐趣并上瘾,才是理智脑的最高级的策略。学会释放本能脑和情绪脑的强大力量,我们就会无往不胜!

第二章 潜意识——生命留给我们的彩蛋

为了更好地生存,进化之手巧妙地采用了意识分层的手段,它让潜意识负责生理系统,让意识负责社会系统,如此分工,意识便得到了解放,可以全力投入高级的社会活动。

这就是进化的力量。然而进化是一把双刃剑,意识分层在给人类带来巨大好处的同时也带来了副作用——模糊。因为处理各种信息的速度不对等,意识很难介入潜意识,而潜意识却能轻易左右意识,所以人们总是做着自己不理解的事,比如明明想去学习,结果转身就拿起了手机;明明知道有些担忧毫无意义,却总是忍不住陷入焦虑,就像身后有个影子,它能影响你,但你不知道它是什么,回头看去一片模糊。这种模糊让人心生迷茫和恐惧,而迷茫和恐惧又使我们的认知、情绪和行动遭遇各种困扰,继而影响人生的走向。

种种现象都在告诉我们一个事实:提升思考能力的方法正是不断明确核心困难和心得感悟,并专注于此。

优秀的人更倾向于做高耗能的事,比如“学霸”的秘诀往往在他们的错题本上——他们更愿意花时间明确错误,并集中精力攻克。而学习成绩一般的同学更喜欢勤奋地重复已经掌握的部分,对真正的困难选择睁一只眼闭一只眼,希望能够搪塞过去,结果模糊点越积越多,以致无力应付。

写代码也是如此,沉浸在已经熟练的操作上提升是有限的,要想如何设计、如何优化。

认知模糊来自内部,而情绪模糊来自外界。人们每天都会面临各种烦恼,但多数人习惯被动承受,少有人乐于主动面对。德国心理治疗师伯特·海灵格曾这样描述人们对烦恼的态度:受苦比解决问题来得容易,承受不幸比享受幸福来得简单。这极符合人类不愿动脑的天性。因为解决问题需要动脑,享受幸福也需要动脑平衡各种微妙的关系,而承受痛苦则只需陷在那里不动。

多数人为了逃避真正的思考,愿意做任何事情。

然而回避痛苦并不会使痛苦消失,反而会使其转入潜意识,变成模糊的感觉。而具体事件一旦变模糊,其边界就会无限扩大,原本并不困难的小事,也会在模糊的潜意识里变得难以解决。这感觉就像在听池塘中“无数只青蛙”的叫声,让人心烦透顶,等到实在忍不住了、跑去一看究竟时,却发现其实只有几只青蛙。

自己用 logseq 来把工作中做的事情进行记录也是让复杂、繁多的工作进行清晰化

心理催眠师在治疗时使用的一切手段其实都只为做成一件事:唤醒潜意识里的痛苦事件,让患者重新面对它、看清它、从而将其彻底化解。

一层层挖下去,直到挖不动为止。坦然地承认、接纳那些难以启齿的想法,让情绪极度透明。虽然直面情绪不会让痛苦马上消失,甚至短时间内还会加剧痛苦,但这会让你主导形势,至少不会被情绪无端恐吓。

认知清晰,情绪平和,最终还要行动坚定。很多人把行动力不足的原因归结为环境干扰或是意志力弱,其实,行动力不足的真正原因是选择模糊。

因此,在现代生活中,要想让自己更胜一筹,就必须学会花费更多的脑力和心力去思考如何拥有足够清晰的目标。我们要把目标和过程细化、具体化,在诸多可能性中建立一条单行通道,让自己始终处于“没得选”的状态。

描述

这也是我自己的读书方法——只取一个全书最触动自己的点,然后尽可能去实践、改变。这样读书不仅收获更大,而且也不会焦虑。

很多人为了找到自己的人生目标,费尽心思地分析什么事情最值得做,最后得到的答案往往是“变得很有钱”或“被别人崇拜”。这样的目标不能说有错,但往往不能长久,也无法给人真正的动力,因为这是理性思维权衡利弊和考量得失之后的结果,其动机往往来自“自我索取和外在评价”,时间一长,很容易使人迷失方向,使动力枯竭。

我们需要用心去感受什么事情让自己最触动,而不是用脑去思考什么事情最有利。理智的分析和计算无法解出内心的真正需求,唯有感性的觉知和洞察才能让答案浮出水面。而且正确的答案往往都是利他的,因为真正长久的人生意义和幸福只能从他人的反馈中获得。

归纳起来,我们可以发现,理性思维虽然很高级,但在判断与选择方面可能并不具有优势,它那蹩脚的性能实在无法与灵敏快速的感性媲美。所以,先用感性选择,再用理性思考,或许是一个更好的策略,尤其是在做那些重大选择时。诚如洪兰教授的建议:小事听从你的脑,大事听从你的心。这话不无道理。

第三章 元认知——人类的终极能能力

描述

第四,提高元认知能力的方法有很多,但最让人意想不到是下面这条——冥想。是的,冥想就是那种只要静坐在某处,然后放松身体,把注意力完全集中到呼吸和感受上的活动。

活动本质上都在做同一件事:监控自己的注意力,然后将其集中到自己需要关注的地方。

所以从实用角度讲,元认知能力可以被重新定义为:自我审视、主动控制,防止被潜意识左右的能力。

描述

那么,在“元时间”内我们要做什么呢?很简单,就做一件事:想清楚。

比如我们希望成为一个会说话的人,那么遵守一个原则:想两遍再说。

元认知能力强的一个突出表现是:对模糊零容忍。

模糊,不仅需要在这些小事上消除,在选择人生目标等大事上也是如此。现实生活中,我们总是想都不想就一头扎进具体事情里,对什么事情更重要、什么事情最重要、做这件事对自己到底意味着什么等长远意义却极不清楚。

做规划就是去想清楚这些东西

·针对当下的时间,保持觉知,审视第一反应,产生明确的主张;·针对全天的日程,保持清醒,时刻明确下一步要做的事情;·针对长远的目标,保持思考,想清楚长远意义和内在动机。元认知能力强的人就是这样:无论是当下的注意力、当天的日程安排,还是长期的人生目标,他们都力求想清楚意义、进行自我审视和主动控制,而不是随波逐流。

第四章 专注力——情绪和智慧的交叉地带

描述

描述

第五章 学习力——学习不是一味地努力

由于想快速看到改变,她制订了远超自身水平的学习、训练计划,结果因体验太痛苦而中途放弃,这非常像我们常见的激励模式。很多缺少经历的年轻人都是这样的,总想同时实现太多、太大的目标,还希望在很短的时间内实现,于是不自觉地把自己推到了困难区内。他们总是兴冲冲地开始,热火朝天地做上几天,然后很快就没劲了——做事情半途而废就是这个原因。

在逼迫我们走出肌肉的舒适区到拉伸区。

千万不要认为没有管束的生活很美好,一旦进入完全自由的时间,虽然开始会很舒服,但很快,我们就会迷失在众多选项中——做这个也行,做那个也行。做选择是一件极为耗能的事情,如果没有与之匹配的清醒和定力,绝大多数人最终都会被强大的天性支配,去选择娱乐消遣。在有约束的环境下我们反而效率更高,生活更充实。

理想的状态是持续获取与自己当前能力相匹配的财富或自由。这一点,做父母的应该有所启示:我们要关注孩子当前对自由、财富的掌控程度,在适当的时候适当放权或鼓励,这样的父母才是真正明智的。那些溺爱孩子的父母,往往在孩子很小的时候就给他们很大的决策权,让他们自己决定吃什么、玩什么、做什么,但孩子根本没有相应的掌控能力,最后变成了自以为是、自私自利的人,造成这些后果的原因正是我们缺少对匹配这个概念的认识。

刻意练习的四要素看上去各自独立,实际上环环相扣、互连互通,而且它们最终都指向匹配。

可惜这只是一种错觉。科技和信息虽然在我们这一代发生了巨大的发展,但人类的学习机制并未随之快速变化,我们大脑的运作模式几乎和几百年前一样。更坏的消息是,丰富的信息和多元的方式带来便捷的同时,也深深地损耗着人们深度学习的能力,并且这种倾向越来越明显。

更深一层的是,读完书能去实践书中的道理,哪怕有那么一两点内容让生活发生了改变,也是很了不起的,因为从这一刻开始,书本中的知识得到了转化。

作者上面记了一个他的方法,一本书不用做冗长详细的笔记,只要能把书中一个点进行实践应用,便是有价值的。

请注意,遇到这种困难才是深度学习真正的开始!因为你必须动用已有的知识去解释新知识,当你能够把新学的知识解释清楚时,就意味着把它纳入了自己的知识体系,同时达到了可以教授他人的水平,并可能创造新的知识。

这也是自己写文章,笔记的重要原因

如果让我推荐一个不可或缺的习惯,我必推每日反思。

我立即感慨道,这就是“未来视角”啊,国王用未来视角回望现在,然后做出了理智的决定,克制了自己的感情,放唐僧西行

真巧,我高中的时候也产生过这样的视角看当下

这既是有效阅读的三个步骤,也是深度学习的三个层次:·知道信息点·关联信息点·行动和改变

描述

鉴于此,我时常也鼓励人们写作。因为单纯阅读时,人容易满足于获取新知识,而一旦开始写作,就必须逼迫自己把所学的知识关联起来,所以写作就是一条深度学习的自然路径。

请注意,这不是在说,实用的知识才是知识,而是在说,只有当知识能够帮助你做实际决策的时候,它才是你的知识。

所以在个人成长领域,没有最优、最确定、最权威的认知体系,只有最适合我们当前状态的认知体系。换句话说,知识不一定能给我们带来认知能力,而认知能力必然包含有效的知识。这部分有效的知识是能帮助我们判断、选择、行动、改变和解决实际问题的,也是本节要重点阐述的。为了避免混淆,下面我会使用“认知体系”来指代“知识体系”。

所以,除非对方的认知体系刚好和自己的认知圈比较匹配,否则痴迷于全盘接受,学习效果有限,还很浪费时间(见图 5-5)。

这就是搭建个人认知体系的真相:打碎各家的认知体系,只取其中最触动自己的点或块,然后将其拼接成自己的认知网络。

三是在生活中能够经常练习或使用这些知识,因为实践是产生强关联的终极方法。学习不是为了知道,而是为了发生真实的改变。当你运用那些知识践行那些道理时,相关细节就会源源不断地显现在你的视野里。到那时,你不仅能成为认知上的强者,也会成为行动上的巨人。

这些做法虽然有些极端,也只是少数人的行为,但大多数人在意志力薄弱的情况下,都会为了完成打卡任务而不自觉地降低标准,此时做多做少、做好做坏已然不是最重要的,最重要的是完成打卡任务。人们坚持的动机,就这样不知不觉地从学习本身转移到了完成任务上,由内在需求转移到了外在形式上。

将这一概念扩展到行为上也是一样的:一件事若迟迟没有完成,心里就总是记挂,期盼着早点结束;此事一旦完成,做这件事的动机就会立即趋向于零。

所有处于类似困境中的学习者,无论是在校的还是在职的,无不认为只要自己努力地输入,不停地学,就一定能学有所成,然而现实总是令他们失望。他们似乎从来没有考虑过要尽快产出点什么,以换取反馈,通过另一种方式来激励自己。也许是因为在人造的学习体制内待久了,有些人很难相信“跳过原理,直接实操”的方式是有效的,他们认为这种方法不过是奇技淫巧,强大的毅力和认知才是学习的正道。

主动的回想测试是最好的学习方法之一,比坐在那儿被动地重读材料要好得多。

只是很多同学对错题本不以为意,要么不去写,要么写了不去看,要么去看时因为碰到痛苦而回避,转头回到舒适区里转悠。在没有让自己的情绪脑体会到学习的快感之前,我们总得先逼自己一把,对吧?

越是接近一天的尾声,我们就越要注意自己的精力和情绪水平,毕竟我们还要抵制一些诱惑,防止自己不小心滑入深渊呢。

第六章 行动力——没有行动世界只是个概念

或许你并没有意识到,每天早上醒来,我们都会收到一份礼物——纯净的注意力。不管你昨天经历了什么,经过一晚的睡眠,你的精力总会得以“重启”。然而很多人并不把这当回事,在一天开始的时候,一头扎进手机信息或是自己觉得有趣的事情中,然后迷失其中。这好比把这份珍贵的礼物直接摔在了地上,长此以往,自然就得不到命运的眷顾了。

描述

所以,仅仅知道要事第一是不够的,我们还需要拥有另外一种能力:清晰力,也就是把目标细化、具体化的能力——行动力只有在清晰力的支撑下才能得到重构。

聪明的思考者都知道“想清楚”才是一切的关键,在“想清楚”这件事上,他们比任何人都愿意花时间,而普通人似乎正好相反,喜欢一头扎进生活的细节洪流中,随波逐流,因为这样似乎毫不费力。于是在普通人眼里是“知易行难”,而在聪明人眼里是“知难行易”,这一点值得我们反思。

我知道这一切是怎么回事,因为自己有过这种经历。在那个时候,我心里始终萦绕着两个念头:一是凡事必须在看到明确的结果后才行动,如果前景不确定、不明朗,即使别人说得再有道理,我也不愿意投入;二是如果一个道理或方法不能让自己快速发生变化,就不是最优的,所以要不断寻找,这样才有希望找到最好的方法。

你觉得学英语没用,是因为你看不到生活中有需要英语的地方。只有英语学好了,和英语有关的机会才会慢慢地出现在你的周围。你觉得学历没用,是因为你根本不知道学习对你的生活轨迹能带来多少改变,你只是基于当时的场景,认为自己手里只是额外多了一张纸。你觉得锻炼身体没有用,正是因为你不去运动,所以感受不到它的价值……

事实上,你只要做上一次就会发现:做成一件事真得很不容易。这揭示了又一个悖论:当自己从来没有主动做成过一件事情的时候,总会以为做成一件事很容易,于是生出很多不切实际的欲望和想法,而欲望越多,就越做不成事(见图 6-7)。

描述

行动力强,是因为自己赞同行动背后的原理、依据和意义,而不是别人说做这个好,自己不深入了解就跟风去做,那才是真的傻。

这也引出了人们不愿意行动的另一个原因:欲望太多。

而打破执念最好的办法就是着眼于现实改变,毕竟现实结果是最好的“评判师”,如果学习不能让自己发生真正的改变,那学再多又有什么用呢?

当“改变”成了读书学习的最高标尺后,我们的学习量还有可能下降

对成长来讲,道理都是“空头支票”,改变才是“真金白银”。当你凡事都以改变为标准时,你的成长路径会更加清晰。

第七章 情绪力——情绪是多角度看问题的智慧

研究结果显示:在一定的前提下,贫穷确实会使人变笨,这不是因为贫穷让人能力不足,而是因为贫穷造成的稀缺俘获了人的注意力,进而降低了人的心智带宽。

所谓“情人眼里出西施”是对这种现象的另一种表述,但本质上就是稀缺心态导致判断力下降。

这道理其实是一样的:当一个人同时面临很多任务的时候,他的心智带宽就会降低,反而没有了行动力和自控力。有生活经验的人都会尽量克制自己的欲望,在做重要之事的同时主动安排娱乐活动,尽量保持日程的闲余——这种方法是科学的、智慧的。

有生活经验的人都会尽量克制自己的欲望,在做重要之事的同时主动安排娱乐活动,尽量保持日程的闲余——这种方法是科学的、智慧的。

很多人认为我是个善于利用时间的高手,问我如何才能同时做更多的事情。事实上,我做事的诀窍恰恰和大家想的相反,就是少做事,甚至不做事。我时常站在一生的高度去审视自己真正要做的是什么,然后打破思维定式,拒绝所有那些即使不去做天也不会塌下来的事情。”

真正的行动力高手不是有能耐在同一时间做很多事的人,而是会想办法避免同时做很多事的人。这样的人自然不会把自己的日程安排得太满,无论做学习计划,还是做工作安排,他们都会给自己留足够的闲余,让自己从容地面对每一刻。

如果你的人生有如此好运,一切都很富足,不妨想办法给自己设限,适当制造稀缺,以成就自己。

所以自古就有“日久见人心”的处世箴言,因为时间久了,我们就可以在各种场景下多维度地观察一个人,观察他生气的时候、高兴的时候、遇挫的时候、愤怒的时候的状态,看他对待弱势群体、富豪权贵的态度,看他娱乐消遣、学习自律时的状态……那些习惯从单一角度识人的人,往往比较单纯,也更容易受伤,本质上是因为他们缺乏多角度认知事物的意识。

换句话说,一个人的性格和脾气好不好,也取决于他多角度看问题的能力:视角单一的人容易固执、急躁和钻牛角尖,而视角多元的人则表现得更为智慧、平和与包容。

比如有些人成年后和自己的父母越来越疏远,因为看不惯老一辈人的言论、习惯,接受不了他们对自己的关爱(干涉);很多儿媳跟婆婆不和睦,在带孩子的问题上矛盾不断;很多亲密的夫妻或情侣,也常常因为对同一件事存在分歧而相互怄气。如果我们知道出现这种情况仅仅是因为他们的“相机”和自己的不同,就很容易明白他们其实并非存心与我们作对,甚至他们已经尽了自己最大的努力。

如果你确定自己的相机比他们的更高级,那就应该有“向下兼容”的意识——要么对其一笑而过,要么拿出自己的高清照片,耐心地向他们讲解什么是更好的,而不是一味地指责对方拍出来的东西很糟糕。毕竟低层的事物不会也不能向上兼容,但我们通过引导,让它们不断升级倒是有可能的。如果自己也曾有一台“落后的相机”,那就更应该体会和包容对方的立场。在“相机”这件事情上,我们一定要保持觉知,要清醒地意识到自己的视角偏误,时刻做好向上升级、向下兼容的准备。拥有这种心态,不仅我们自己能越来越完善,还能与其他人都合得来。

一是勤移动。顾名思义,就是多移动你的“相机”机位,尝试用不同的视角看问题。

他人、客观、未来

吴军的想法更明智,他说:“我对任何人,一般都先假设他是正直、善良和诚信的。”以开放引导开放,是为高级。这也是我推崇的交流方式。

这种经历我也经常有,因为我有每日反思的习惯。所以每当自己心情郁闷、无法排解的时候,我就会打开电脑,把心中的烦恼全部倒出来进行复盘,梳理的过程中,自己往往会拨云见日,真的很神奇。

这个过程和自己那个阶段写日记是一个路子

特别是“自主需求”,它是自我决定理论的关键与核心。

第八章 早冥读写跑,人生五件套——成本最低的成长之道

闭眼静坐,专注于自己的呼吸,每天持续 15 分钟以上……你会感受到它的效果。当然,把它看成一种健脑操(事实上它就是),就像我们通过举哑铃锻炼自己的手臂肌肉一样,你就能更好地理解了。只是这种锻炼并不像肌肉锻炼那样直观,所以很多人并不相信,也不愿意去做。但了解了这部分内容后,你现在还需要更多理由吗?仅仅知道冥想能让人变得更聪明,你就可以试试看了。

阅读量<思考量<行动量<改变量

国际化配置方案

问题

在文案配置的时候有以下场景

  1. 需要国际化
  2. 文案中参杂着链接和文字,并且相对位置可能改变

假设场景为

function App() {
  return (
    <div>
      <button>Hi</button>
      <div>
        我是产品想配置的文案,<a href="https://www.google.com.hk/">https://www.google.com.hk</a>
        中间可能夹杂链接,并且位置不知道在哪
      </div>
    </div>
  )
}

export default App

方案

import Markdown from "markdown-to-jsx"

function App() {
  return (
    <div>
      <button>Hi</button>
      <div>
        我是产品想配置的文案,<a href="https://www.google.com.hk/">https://www.google.com.hk</a>
        中间可能夹杂链接,并且位置不知道在哪
      </div>

      <hr />
      <h2>markdown-to-jsx 方案</h2>
      <Markdown>
        我是产品想配置的文案,[https://www.google.com.hk/](https://www.google.com.hk/)
        中间可能夹杂链接,并且位置不知道在哪
      </Markdown>
    </div>
  )
}

export default App

效果图

Pasted image 20231117114545

其他

这个问题本质上是想用字符串来描述一些动态行为,这里是利用了 Markdown 语法作为协议,若有更多样的诉求,可以再进一步制定协议内容,对能力进行支持。

整体目标是做到只改文案配置,不动代码 ~

仓库可见 https://github.com/sedationh/demo-i18n-text

Go 结构体方法接受者用值还是指针

func (s *MyStruct) pointerMethod() { } // method on pointer
func (s MyStruct)  valueMethod()   { } // method on value

接受者的表现和 Go 中传递参数一样,是值传递

考量点 「官方 FAQ

  • 值传递的空间耗费
  • 是否需要修改原值
  • 一致性
    • Next is consistency. If some of the methods of the type must have pointer receivers, the rest should too, so the method set is consistent regardless of how the type is used. See the section on method sets for details.

对于官网中提到的一致性,实际测试下来是都能相互调用的,值调用指针方法,指针调用值方法,Go 会自己帮忙做转化

Note

https://stackoverflow.com/questions/27775376/value-receiver-vs-pointer-receiver/27775558#27775558

The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers

Which is not true, as commented by Sart Simha

type String struct {
	Value string
}

func (r String) ValueChange(newValue string) {
	r.Value = newValue
}

func (r *String) PointerChange(newValue string) {
	r.Value = newValue
}

func TestString(t *testing.T) {
	s1 := String{Value: "123"}
	s2 := String{"123"}

	s1.ValueChange("456")
	if s1.Value != "123" {
		t.Fatal("failed")
	}

	s2.PointerChange("456")
	if s2.Value != "456" {
		t.Fatal("failed")
	}

	s3 := &String{"123"}
	s3.ValueChange("456")
	if s3.Value == "456" {
		t.Fatal("failed")
	}
	s3.PointerChange("456")
	if s3.Value != "456" {
		t.Fatal("failed")
	}
}

Arc Fix Bug Week

  • Arc 在 230831 有个更新
    • image.png
    • 有专门用于 fix bug 的一整个 week,不用管新 feature 的更新
    • 我觉得这一点做的很好,尤其是在构造比较复杂的系统的时候

Zustand 核心能力与代码梳理

https://github.com/sedationh/debug-zustand

用法分析

import { create } from 'zustand'

const useStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))
function BearCounter() {
  const bears = useStore((state) => state.bears)
  return <h1>{bears} around here...</h1>
}

function Controls() {
  const increasePopulation = useStore((state) => state.increasePopulation)
  return <button onClick={increasePopulation}>one up</button>
}

create 传入函数,传入的函数「A」可以拿到 set 的方法用于更改状态,A 返回一个 hooks 「B」,B 可以传入 selector 来按需引用,减少渲染,B 返回 所选的状态

入口

import { create } from 'zustand'

const useBearStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))

跳转代码
src/react.ts

export const create = (<T>(
  createState: StateCreator<T, [], []> | undefined
) => {
  // STUDY: seda 入口函数
  return createState ? createImpl(createState) : createImpl
}) as Create

进入 createImpl

import { createStore } from './vanilla.ts'

const createImpl = <T>(createState: StateCreator<T, [], []>) => {
  if (
    import.meta.env?.MODE !== 'production' &&
    typeof createState !== 'function'
  ) {
    console.warn(
      "[DEPRECATED] Passing a vanilla store will be unsupported in a future version. Instead use `import { useStore } from 'zustand'`."
    )
  }
  const api =
    typeof createState === 'function' ? createStore(createState) : createState
...

进入 createStore
src/vanilla.ts

export const createStore = ((createState) =>
  createState ? createStoreImpl(createState) : createStoreImpl) as CreateStore


const createStoreImpl: CreateStoreImpl = (createState) => {
  type TState = ReturnType<typeof createState>
  type Listener = (state: TState, prevState: TState) => void
  let state: TState
  const listeners: Set<Listener> = new Set()

  const setState: StoreApi<TState>['setState'] = (partial, replace) => {

可见核心 能力是由 vanilla.ts 完成的,对 react 的场景在 vanilla 上包一层
另外,源代码中半数以上的代码都是在写类型,如下图

image.png

考虑到这里处理的 TS 场景还比较复杂,本节不予考虑,后面单开一节写

vanilla

https://www.unpkg.com/browse/[email protected]/esm/vanilla.mjs
从官方的 mjs 版本去除一些非核心分支和判断可得下述代码

const createStoreImpl = (createState) => {
  let state
  const listeners = new Set()

  const setState = (partial, replace) => {
    const nextState = typeof partial === 'function' ? partial(state) : partial

    if (!Object.is(nextState, state)) {
      const previousState = state
      // https://docs.pmnd.rs/zustand/guides/immutable-state-and-merging#replace-flag
      // However, as this is a common pattern, set actually merges state, and we can skip the ...state part:
      state =
        replace ?? typeof nextState !== 'object'
          ? nextState
          : Object.assign({}, state, nextState)
      listeners.forEach((listener) => listener(state, previousState))
    }
  }

  const getState = () => state

  const subscribe = (listener) => {
    listeners.add(listener)
    return () => listeners.delete(listener)
  }

  const destroy = () => listeners.clear()

  const api = { setState, getState, subscribe, destroy }

  state = createState(setState, getState, api)

  return api
}
const createStore = (createState) =>
  createState ? createStoreImpl(createState) : createStoreImpl

export { createStore }

实现了一个监听和通知
用法如下,可见我们需要去手动的注册监听、取消监听、维持状态并触发更新

const vStore = createStore((set) => ({
  count: 0,
  increase: () => set((state) => ({ count: state.count + 1 })),
}))

function VanillaPage() {
  const [v, setV] = useState(() => vStore.getState())
  useEffect(() => {
    return vStore.subscribe((state) => {
      setV(state)
    })
  }, [])

  return (
    <div>
      VanillaPage
      <button
        onClick={() => {
          v.increase()
        }}>
        increase
      </button>
      {v.count}
    </div>
  )
}

react 提供一个用于订阅的 hooks -- useSyncExternalStore,上面的代码可被改写为

const vStore = createStore((set) => ({
  count: 0,
  increase: () => set((state) => ({ count: state.count + 1 })),
}))

function SyncExternalStore() {
  const v = useSyncExternalStore(vStore.subscribe, vStore.getState)
  return (
    <div>
      SyncExternalStore
      <button
        onClick={() => {
          v.increase()
        }}>
        increase
      </button>
      {v.count}
    </div>
  )
}

react

结合上面的 react useSyncExternalStore 使用场景,可以理解下面的 react 实现了

import { useDebugValue } from 'react'
import useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector.js'
import { createStore } from './vanilla.js'

const { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports

function useStore(api, selector = api.getState, equalityFn) {
  const slice = useSyncExternalStoreWithSelector(
    api.subscribe,
    api.getState,
    api.getServerState || api.getState,
    selector,
    equalityFn
  )
  useDebugValue(slice)
  return slice
}

const createImpl = (createState) => {
  const api = createStore(createState)

  const useBoundStore = (selector, equalityFn) =>
    useStore(api, selector, equalityFn)

  return useBoundStore
}

const create = (createState) =>
  createState ? createImpl(createState) : createImpl

export { create }

useSyncExternalStoreWithSelector 的前三个入参和 useSyncExternalStore 一样
官方的相关讨论 reactwg/react-18#86
zustand 的的修改 https://github.com/pmndrs/zustand/pull/550/files?short_path=7ae45ad#diff-ca56e63fa839455c920562a44ebc44594f47957bbd3e9873c8a9e64104af2c41L103

之前做 强制渲染的写法是

const [, forceUpdate] = useReducer((c) => c + 1, 0)

useSyncExternalStoreWithSelector 也是在 useSyncExternalStore 上进行的 memo selector 封装

image.png

selector 的能力并不是 zustand 实现的,而是交给了 react 去比对 selection 「selector 产生的 state」是否变化来决定是让组件进行 forceUpdate

middleware

如何添加一个 middleware 以 immer 为例子

import { immer } from '../mini-js/immer.js'
import { create } from '../mini-js/react.js'

export const useTodoStore = create(
  immer((set) => ({
    todos: {
      '82471c5f-4207-4b1d-abcb-b98547e01a3e': {
        id: '82471c5f-4207-4b1d-abcb-b98547e01a3e',
        title: 'Learn Zustand',
        done: false,
      },
      '354ee16c-bfdd-44d3-afa9-e93679bda367': {
        id: '354ee16c-bfdd-44d3-afa9-e93679bda367',
        title: 'Learn Jotai',
        done: false,
      },
      '771c85c5-46ea-4a11-8fed-36cc2c7be344': {
        id: '771c85c5-46ea-4a11-8fed-36cc2c7be344',
        title: 'Learn Valtio',
        done: false,
      },
      '363a4bac-083f-47f7-a0a2-aeeee153a99c': {
        id: '363a4bac-083f-47f7-a0a2-aeeee153a99c',
        title: 'Learn Signals',
        done: false,
      },
    },
    toggleTodo: (todoId) =>
      set((state) => {
        state.todos[todoId].done = !state.todos[todoId].done
      }),
  }))
)

export default () => {
  const todo = useTodoStore()
  return (
    <div>
      ImmerPage
      <h1>{JSON.stringify(todo.todos)}</h1>
      <button
        onClick={() => todo.toggleTodo('82471c5f-4207-4b1d-abcb-b98547e01a3e')}>
        toggleTodo
      </button>
    </div>
  )
}
const immer = (initializer) => (set, get, store) => {
  store.setState = (updater, replace, ...a) => {
    const nextState = typeof updater === 'function' ? produce(updater) : updater
    return set(nextState, replace, ...a)
  }
  return initializer(store.setState, get, store)
}

export { immer }

把传入的 set 方法包了一层

Nest 系列 - 1:控制反转和依赖注入

Nest 可以看作是 Node.js 版本的 Spring 框架,解决了项目开发中一个重要的问题 -- 如何管理好项目的模块依赖。
下面来通过一个场景来理解依赖问题与解决思路。

螺丝生产

// 人
class People {
  work() {
    console.log("我是 People,我在工作 ... 螺丝出来了")
  }
}

// 螺丝生产车间
class ScrewWorkshop {
  private worker: People = new People()

  produce() {
    this.worker.work()
  }
}

// 工厂
class Factory {
  start() {
    const screwWorkshop = new ScrewWorkshop()
    screwWorkshop.produce()
  }
}

const factory = new Factory()
// 工厂开工啦!!!
factory.start()
我是 People,我在工作 ... 螺丝出来了

科技进步,人干的活「拧螺丝」机器能干的更好了

// 人
class People {
  work() {
    console.log("我是 People,我在工作 ... 螺丝出来了")
  }
}

// 机器
class Machine {
  work() {
    console.log("我是 Machine,我在工作 ... 螺丝出来了")
  }
}

// 螺丝生产车间
class ScrewWorkshop {
  // private worker: People = new People()
  private machine: Machine = new Machine()

  produce() {
    this.machine.work()
  }
}

// 工厂
class Factory {
  start() {
    const screwWorkshop = new ScrewWorkshop()
    screwWorkshop.produce()
  }
}

const factory = new Factory()
// 工厂开工啦!!!
factory.start()
我是 Machine,我在工作 ... 螺丝出来了

后续科技还在进步, Machine 还在不断更换
我们的 Demo 代码很少,但业务的场景这样的硬编码替换可能会有很高的工作量
这是因为我们的模块之间较为耦合,需要的降低耦合

生产改造

interface Workable {
  work(): void
}

// 人
class People implements Workable {
  work() {
    console.log("我是 People,我在工作 ... 螺丝出来了")
  }
}

// 机器
class Machine implements Workable {
  work() {
    console.log("我是 Machine,我在工作 ... 螺丝出来了")
  }
}

// 螺丝生产车间
class ScrewWorkshop {
  private worker: Workable

  constructor(private _worker: Workable) {
    this.worker = _worker
  }

  produce() {
    this.worker.work()
  }
}

// 工厂
class Factory {
  start() {
    const worker = new People()
    const screwWorkshop = new ScrewWorkshop(worker)
    screwWorkshop.produce()
    const worker2 = new Machine()
    const screwWorkshop2 = new ScrewWorkshop(worker2)
    screwWorkshop2.produce()
  }
}

const factory = new Factory()
// 工厂开工啦!!!
factory.start()

我们主要做了两件事情

  1. 定义了 Workable 接口,让ScrewWorkshop、People 和 Machine 都依赖接口,不相互依赖
  2. 不在 ScrewWorkshop 中直接创建 work 的实例,而是通过外界传入

我们用张图来感受下两者的差异

改造前
image.png

改造后
image.png

概念引入

再回看我们做的两件事情

  1. 定义了 Workable 接口,让ScrewWorkshop、People 和 Machine 都依赖接口,不相互依赖

设计原则
依赖倒置原则
高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。

  1. 不在 ScrewWorkshop 中直接创建 work 的实例,而是通过外界传入

控制反转
外界「factoy」 拿到了原有 ScrewWorkshop 对 People 和 Machine 的控制权「实例化」
依赖注入
就是只通过构造传入 ScrewWorkshop worker
依赖注入是实现控制反转的其中一种方式

Nest Custom providers 有这样一段话

Dependency injection is an inversion of control (IoC) technique wherein you delegate instantiation of dependencies to the IoC container (in our case, the NestJS runtime system), instead of doing it in your own code imperatively. Let's examine what's happening in this example from the Providers chapter.

在我们的 Demo 里 Factory 起到了类似 IoC container 的作用

Spring 和 Nest 的主要作用都是提供一个 IoC 的 container 来统一管理实例,降低模块耦合

🌰 例子 & 下一节

假设 IoC 中已经帮我们管理了一个 type 为 Machine 的实例,我们现在有个新的类叫做

@Injectable()
class Machine {
  work() {
    console.log("我是 Machine,我在工作 ... 螺丝出来了")
  }
}

class Store {
  constructor(private machine: Machine) {}

  sell() {
    this.machine.work()
  }
}

在使用 Store 的时候,machine 会直接传入进来,这一层是在框架层来处理的,也就是 NestJS 和 Spring 这一层,无需我们开发者去做实例化。
那这是怎么匹配过去的呢?
有很多种方式,在这个例子中,就是类型 Machine,这也是用的比较多的一种。

但我们知道 类型源于 TS,而真正运行的时候被转换成 JS 是不带有类型的,那框架层「factory」又是如何做类型匹配的呢?
在下一节,Nest 系列 - 2: 装饰器和类型解析 中会有介绍

🔗 参考链接

Nest.js入门 —— 控制反转与依赖注入(一)
依赖倒置原则DIP
轻松学,浅析依赖倒置(DIP)、控制反转(IOC)和依赖注入(DI)

TS 应用案例分享 - 2

type UseEffectOnceOptions1 =
  | {
      ready?: boolean
      globalOnce?: false
    }
  | {
      ready?: boolean
      globalOnce: true
      globalStoreKey: string
    }

const a: UseEffectOnceOptions1 = {} as any;

if (a.globalOnce === true) {
  a.globalStoreKey.charAt
} else {
  a.
}

image.png

TS 应用案例分享 - 1

场景1

类型转换系统

目前你在开发一系列的表单页面,对于进入表单的数据,目前是以下格式

interface DataType {
  name: string
  age: number
}

interface DataType2 {
  name: string
  gender: number
}

interface DataType3 {
  name: string
  age: number
  address: string
}

// ...

在一波迭代后,发现目前规范的数据格式无法满足前端的展示需求,因此,后端更新了数据结构,目前进入前端的数据格式变成了这样

interface NewDataType {
  name: {
    label: string
    value: string
  }
  age: {
    label: string
    value: number
  }
}

// NewDataType2 NewDataType3 ...

目前要改的字段有很多,而且有些 DataType 的 key 特别多,改起来很机械,有没有办法让这个过程变得简单一些呢?

引入范型来帮助我们完成这个过程

export interface FormModelField<T> {
  label: string
  value: T
}

interface NewDataType {
  name: FormModelField<string>
  age: FormModelField<number>
}

已经好了一些了,但是有些 DataType 的 key 特别多,不想每个都去改,有没有更短一点的办法呢?

分析下需求:

  • 我们希望有个工具,输入已有的 DataType 类型,生成所需的 NewDataType 类型
    如何实现呢?
  • 拿到每个 DataType 的 key
  • 拿到每个 DataType 的 key 对应的 value
  • 利用 FormModelField 来组装 对应的 value
  • 形成新的 kv 结构

keyof https://www.typescriptlang.org/docs/handbook/2/keyof-types.html
Mapped Types https://www.typescriptlang.org/docs/handbook/2/mapped-types.html

type CreateNewDataType<T> = {
  [key in keyof T]: FormModelField<T[key]>
}

image.png

OK 非常完美

类型提取系统

解决了类型编写面临的重复性问题,接下来还要解决运行层的数据转换问题

// newData -> data 或者部分
const newData: NewDataType = {
  name: {
    label: 'name',
    value: 'SedationH',
  },
  age: {
    label: 'age-label',
    value: 123,
  },
}

const data: DataType = {
  name: 'SedationH',
  age: 123,
}

const data2: DataType = {
  age: 123,
}
function formValueExtractor(newData: NewDataType, fields: any[]) {
  const res = {}

  fields.forEach((field) => {
    if (field in newData)
      res[field] = newData[field].value
  })

  return res
}

const newData: NewDataType = {
  name: {
    label: 'name',
    value: 'SedationH',
  },
  age: {
    label: 'age-label',
    value: 123,
  },
}

// eslint-disable-next-line no-console
console.log(formValueExtractor(newData, ['name', 'age']),
  formValueExtractor(newData, ['age']),
  formValueExtractor(newData, []),
  // 希望有报错
  // @ts-expect-error
  formValueExtractor(newData, ['ccc']),
)

image.png

fields 和 newData 之间在类型系统上没啥关联,要这俩产生关联

interface DataType {
  name: string
  age: number
}

export interface FormModelField<K> {
  label: string
  value: K
}

type CreateNewDataType<T> = {
  [key in keyof T]: FormModelField<T[key]>
}

type NewDataType = CreateNewDataType<DataType>

type CreateDataTypeFromNewDataType<T extends CreateNewDataType<V>, V = object> = {
  [key in keyof T]: T[key]['value']
}

function formValueExtractor<T extends CreateNewDataType<V>, V>(newData: T, fields: (keyof T)[]) {
  // 理解 T 作为类型的入参,一切要围着他来
  const res = {} as CreateDataTypeFromNewDataType<T>

  fields.forEach((field) => {
    if (field in newData)
      res[field] = newData[field].value
  })

  return res
}

const newData: NewDataType = {
  name: {
    label: 'name',
    value: 'SedationH',
  },
  age: {
    label: 'age-label',
    value: 123,
  },
}

// eslint-disable-next-line no-console
console.log(formValueExtractor(newData, ['name', 'age']),
  formValueExtractor(newData, ['age']),
  formValueExtractor(newData, []),
  // 希望有报错
  // @ts-expect-error
  formValueExtractor(newData, ['ccc']),
)
formValueExtractor(newData, [""])


// https://github.com/SedationH/blog/issues/7

image.png

无痛胃镜检查记录

  • 在浙大一附做的
  • 时间线
    • 找消化科医生
      • 约麻醉科+无痛胃镜检查
    • 找麻醉科医生
      • 例行询问
    • 预约无痛胃镜检查时间
    • 无痛胃镜检查
      • 打点滴、氯化钠液体
      • 喝麻醉喉咙的小瓶液体
      • 进操作室
        • 嘴巴上套个塑料物品
        • 鼻子里插管,搞催眠气体
        • 醒来,身体不是很受控
  • 回顾
    • 全麻苏醒后感觉呆呆地,走路有点晃
    • 像这种检查提前去签到,检查是按签到时间而不是预约时间

React 基本渲染机制

swc link

const MyArrayField = ({ children }: {
  children: Function
}) => {
  return <div>
    {children("InnerComponent 2")}
  </div>
}

export default () => {
  return <div>
    <MyArrayField>
      {
        (text: string) => {
          return text
        }
      }
    </MyArrayField>
    <span>2</span>
  </div>
}
var MyArrayField = function (param) {
    var children = param.children;
    return React.createElement("div", null, children("InnerComponent 2"));
};
export default function () {
    return React.createElement("div", null,
        React.createElement(MyArrayField, null, function (text) {
            return text;
        }),
        React.createElement("span", null, "2")
    );
};

createElement
https://github.com/SedationH/react/pull/2/files

render
https://juejin.cn/post/6844904131124002829#heading-12
参考这个简化理解

我把错误的思考记录也保留下来了,用 「🙅思考」 代表

function render(node, mountNode) {
	// 🙅思考 原本想在这里加一个判断的,这样是错误的,思考错的了,原因看代码下面的图片和文字
    // if (typeof node === 'function') {
    //     return
    // }
    if (typeof node === 'string') {
        return mountNode.append(document.createTextNode(node))
    }
    let type = node.type;
    let props = node.props;
    if (type.isReactComponent) {
        let element = new type(props).render();
        props = element.props;
        type = element.type;
    } else if (typeof type === 'function') {
        let element = type(props);
        props = element.props;
        type = element.type;
    }
    let domElement = document.createElement(type);
 
    for (let propName in props) {
        if (propName === 'children') {
            let children = props[propName];
            children = Array.isArray(children) ? children : [children];
            children.forEach(child => render(child, domElement))
            // 🙅思考 child 就是传入的 function,没有被包成 { type: function }
        } else if (propName === 'style') {
            let styleObj = props[propName];
            for (let attr in styleObj) {
                domElement.style[attr] = styleObj[attr];
            }
        }
    }
    mountNode.appendChild(domElement);
}

export default {
    render
}

48G36EDdVQ.jpg

思考的时候容易陷入传入的 children 是一个 function 不符合 ReactNode 的要求,事实上,这个 函数不是直接作为 children 参与 render 的,而是被作为 props 中的一个值「函数也好,对象也好,数组也好,都可以」被 type(props) 的时候,内部使用

Collapse 效果实现

这个问题曾经在 开发 tree 组件的时候遇到过
今天系统整理下
纯CSS的解决方案有二

  1. max-height
  2. translateY
    这里的困境源于动画的渐变是需要数值到数值的,而 height: auto 这个不算数值
  3. 针对这点,可以上 JS 进行高度获取 height,其中对 scrollHeight 的利用注意下

The Element.scrollHeight read-only property is a measurement of the height of an element's content, including content not visible on the screen due to overflow.

针对上述三个解决方式,demo 可以参看这个集合

阳康后

从1125晚上喉咙开始有不适感,大致是有症状的起点,到今天0102已经基本恢复。整个过程中大致经历了七次发烧,前几天喉咙特别疼,在第五六天的时候得到缓解,剩余全身无力、偶尔咳嗽。
很早自己其实就知道这个防疫的政策维持不下去,知道总有一天会放开,但却没思考放开了会怎样,需要去做什么准备
多亏了朋友在开始送给我的退烧药,不然开始几天的高烧会比较难熬。
这种需要提前想几步的策略,是这次生病过程中让我收获的教训。

生病的这几天有婷一起度过,时间过的挺快 : )

Go从零实现 - 分布式缓存 - GeeCache

缓存淘汰策略、LRU 实现

存储是优先的,满了去掉谁

LFU(Least Frequently Used)

访问次数,去掉访问最少的

  • 内存占用大
  • 受已有模式影响多

LRU(Least Recently Used)

LRU 认为,如果数据最近被访问过,那么将来被访问的概率也会更高。

LRU 实现

Pasted image 20231105074400

整体逻辑并不复杂,包装 entry 是为了删除队尾的时候方便

delete(c.cache, kv.key)

package lru

import "container/list"

type Value interface {
	Len() int
}

type Cache struct {
	maxBytes int64
	nbytes   int64
	// 使用双向链表
	ll *list.List
	// 使用 map
	cache     map[string]*list.Element
	OnEvicted func(key string, value Value)
}

func New(maxBytes int64, onEvicted func(string2 string, value Value)) *Cache {
	return &Cache{
		maxBytes:  maxBytes,
		ll:        list.New(),
		cache:     make(map[string]*list.Element),
		OnEvicted: onEvicted,
	}
}

type entry struct {
	key   string
	value Value
}

func (c *Cache) Add(key string, value Value) {
	if ele, ok := c.cache[key]; ok {
		// LRU 调整顺序到头部 头部是最近使用的,尾部是最久未使用的
		c.ll.MoveToFront(ele)
		c.nbytes += int64(len(key)) + int64(value.Len())
		kv := ele.Value.(*entry)
		kv.value = value
	} else {
		// 新增
		ele := c.ll.PushFront(&entry{key, value})
		c.cache[key] = ele
		c.nbytes += int64(len(key)) + int64(value.Len())
	}
	for c.maxBytes != 0 && c.maxBytes < c.nbytes {
		c.RemoveOldest()
	}
}

func (c *Cache) RemoveOldest() {
	ele := c.ll.Back()

	if ele == nil {
		return
	}

	c.ll.Remove(ele)
	kv := ele.Value.(*entry)
	delete(c.cache, kv.key)
	c.nbytes -= int64(len(kv.key)) + int64(kv.value.Len())
	if c.OnEvicted != nil {
		c.OnEvicted(kv.key, kv.value)
	}
}

func (c *Cache) Get(key string) (value Value, ok bool) {
	if ele, ok := c.cache[key]; ok {
		c.ll.MoveToFront(ele)
		kv := ele.Value.(*entry)
		return kv.value, true
	}
	return
}

func (c *Cache) Len() int {
	return c.ll.Len()
}

单机并发缓存

为现有的 Cache 增加并发读取的能力

先看看使用层

func TestGet(t *testing.T) {
	loadCounts := make(map[string]int, len(db))
	gee := NewGroup("scores", 2<<10, GetterFunc(
		func(key string) ([]byte, error) {
			log.Println("[SlowDB] search key", key)
			if v, ok := db[key]; ok {
				if _, ok := loadCounts[key]; !ok {
					loadCounts[key] = 0
				}
				loadCounts[key] += 1
				return []byte(v), nil
			}
			return nil, fmt.Errorf("%s not exist", key)
		}))

	for k, v := range db {
		if view, err := gee.Get(k); err != nil || view.String() != v {
			t.Fatal("failed to get value of Tom")
		} // load from callback function
		if _, err := gee.Get(k); err != nil || loadCounts[k] > 1 {
			t.Fatalf("cache %s miss", k)
		} // cache hit
	}

	if view, err := gee.Get("unknown"); err == nil {
		t.Fatalf("the value of unknow should be empty, but %s got", view)
	}
}

NewGroup 创建返回 gee,也就是缓存实例,创建的时候第三个参数是缓存未命中时的获取逻辑,获取到了,拿到的结果会加入缓存

NewGroup 的 核心 Get 实现

// A Getter loads data for a key.
type Getter interface {
	Get(key string) ([]byte, error)
}

// A GetterFunc implements Getter with a function.
type GetterFunc func(key string) ([]byte, error)

// Get implements Getter interface function
func (f GetterFunc) Get(key string) ([]byte, error) {
	return f(key)
}

// A Group is a cache namespace and associated data loaded spread over
type Group struct {
	name      string
	getter    Getter
	mainCache cache
}

// Get value for a key from cache
func (g *Group) Get(key string) (ByteView, error) {
	if key == "" {
		return ByteView{}, fmt.Errorf("key is required")
	}

	if v, ok := g.mainCache.get(key); ok {
		log.Println("[GeeCache] hit")
		return v, nil
	}

	return g.load(key)
}

func (g *Group) load(key string) (value ByteView, err error) {
	return g.getLocally(key)
}

func (g *Group) getLocally(key string) (ByteView, error) {
	bytes, err := g.getter.Get(key)
	if err != nil {
		return ByteView{}, err

	}
	value := ByteView{b: cloneBytes(bytes)}
	g.populateCache(key, value)
	return value, nil
}

func (g *Group) populateCache(key string, value ByteView) {
	g.mainCache.add(key, value)
}

Getter 这里接口型函数的设计可看

cache 的实现

package geecache

import (
	"geecache/lru"
	"sync"
)

type cache struct {
	mu         sync.Mutex
	lru        *lru.Cache
	cacheBytes int64
}

func (c *cache) add(key string, value ByteView) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.lru == nil {
		c.lru = lru.New(c.cacheBytes, nil)
	}
	c.lru.Add(key, value)
}

func (c *cache) get(key string) (value ByteView, ok bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.lru == nil {
		return
	}

	if v, ok := c.lru.Get(key); ok {
		return v.(ByteView), ok
	}

	return
}

这里为 lru 的读取套上了一层锁的机制

Note

多个协程(goroutine)同时读写同一个变量,在并发度较高的情况下,会发生冲突。确保一次只有一个协程(goroutine)可以访问该变量以避免冲突,这称之为互斥,互斥锁可以解决这个问题。

sync.Mutex 是一个互斥锁,可以由不同的协程加锁和解锁。

sync.Mutex  是 Go 语言标准库提供的一个互斥锁,当一个协程(goroutine)获得了这个锁的拥有权后,其它请求锁的协程(goroutine) 就会阻塞在  Lock()  方法的调用上,直到调用  Unlock()  锁被释放。

byte 是很通用的存储结构,抽象一层这个结构,并满足我们 Value 的接口要求

// Value use Len to count how many bytes it takes
type Value interface {
	Len() int
}

package geecache

// A ByteView holds an immutable view of bytes.
type ByteView struct {
	b []byte
}

// Len returns the view's length
func (v ByteView) Len() int {
	return len(v.b)
}

// ByteSlice returns a copy of the data as a byte slice.
func (v ByteView) ByteSlice() []byte {
	return cloneBytes(v.b)
}

// String returns the data as a string, making a copy if necessary.
func (v ByteView) String() string {
	return string(v.b)
}

func cloneBytes(b []byte) []byte {
	c := make([]byte, len(b))
	copy(c, b)
	return c
}

至此整体结构

geecache/
    |--lru/
        |--lru.go  // lru 缓存淘汰策略
    |--byteview.go // 缓存值的抽象与封装
    |--cache.go    // 并发控制
    |--geecache.go // 负责与外部交互,控制缓存存储和获取的主流程

至此,这一章节的单机并发缓存就已经完成了。

HTTP 服务端

Note

分布式缓存需要实现节点间通信,建立基于 HTTP 的通信机制是比较常见和简单的做法。如果一个节点启动了 HTTP 服务,那么这个节点就可以被其他节点访问。今天我们就为单机节点搭建 HTTP Server。

目标是可以通过 HTTP 协议访问到 上面实现的 geecache

使用层为

func main() {
	geecache.NewGroup("scores", 2<<10, geecache.GetterFunc(
		func(key string) ([]byte, error) {
			log.Println("[SlowDB] search key", key)
			if v, ok := db[key]; ok {
				return []byte(v), nil
			}
			return nil, fmt.Errorf("%s not exist", key)
		}))

	addr := "localhost:9999"
	peers := geecache.NewHTTPPool(addr)
	log.Println("geecache is running at", addr)
	log.Fatal(http.ListenAndServe(addr, peers))
}

启动后可以通过 HTTP 进行访问

$ curl http://localhost:9999/_geecache/scores/Tom
630

$ curl http://localhost:9999/_geecache/scores/kkk
kkk not exist

URL 格式为 /<basepath>/<groupname>/<key>
通过 groupname 得到 group 实例,再使用  group.Get(key)  获取缓存数据。

peers := geecache.NewHTTPPool(addr)
http.ListenAndServe(addr, peers)

peers 需要满足 ListenAndServe 的接口要求

func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

因此造一个 HTTPPool 实现 ServeHTTP 接口就好

const defaultBasePath = "/_geecache/"

// HTTPPool implements PeerPicker for a pool of HTTP peers.
type HTTPPool struct {
	// this peer's base URL, e.g. "https://example.net:8000"
	self     string
	basePath string
}

// NewHTTPPool initializes an HTTP pool of peers.
func NewHTTPPool(self string) *HTTPPool {
	return &HTTPPool{
		self:     self,
		basePath: defaultBasePath,
	}
}

// Log info with server name
func (p *HTTPPool) Log(format string, v ...interface{}) {
	log.Printf("[Server %s] %s", p.self, fmt.Sprintf(format, v...))
}

// ServeHTTP handle all http requests
func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if !strings.HasPrefix(r.URL.Path, p.basePath) {
		panic("HTTPPool serving unexpected path: " + r.URL.Path)
	}
	p.Log("%s %s", r.Method, r.URL.Path)
	// /<basepath>/<groupname>/<key> required
	parts := strings.SplitN(r.URL.Path[len(p.basePath):], "/", 2)
	if len(parts) != 2 {
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}

	groupName := parts[0]
	key := parts[1]

	group := GetGroup(groupName)
	if group == nil {
		http.Error(w, "no such group: "+groupName, http.StatusNotFound)
		return
	}

	view, err := group.Get(key)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/octet-stream")
	w.Write(view.ByteSlice())
}

接下来,运行 main 函数,使用 curl 做一些简单测试:

$ curl http://localhost:9999/_geecache/scores/Tom
630
$ curl http://localhost:9999/_geecache/scores/kkk
kkk not exist

GeeCache 的日志输出如下:

2020/02/11 23:28:39 geecache is running at localhost:9999
2020/02/11 23:29:08 [Server localhost:9999] GET /_geecache/scores/Tom
2020/02/11 23:29:08 [SlowDB] search key Tom
2020/02/11 23:29:16 [Server localhost:9999] GET /_geecache/scores/kkk
2020/02/11 23:29:16 [SlowDB] search key kkk

一致性哈希(hash)

场景

当我们有多个 Cache Server 的时候,一个 key 的请求过来,去哪个 Cache Server 来请求呢
需要满足以下条件

  1. 缓存命中率高
  2. 容易拓展

Pasted image 20231119175124

方案

方案 1 如下图
Pasted image 20231119175235
命中率高,算法简单,但是一旦 Cache Server 数量变化,现有缓存都无用了

方案 2 一致性哈希
如上图
Pasted image 20231119175558

并且为了解决分布不均匀的问题,增加虚拟节点
Pasted image 20231119175635
Pasted image 20231119175713

实现

package consistenthash

import (
	"hash/crc32"
	"sort"
	"strconv"
)

// Hash maps bytes to uint32
type Hash func(data []byte) uint32

// Map constains all hashed keys
type Map struct {
	hash     Hash
	replicas int
	keys     []int // Sorted
	hashMap  map[int]string
}

// New creates a Map instance
func New(replicas int, fn Hash) *Map {
	m := &Map{
		replicas: replicas,
		hash:     fn,
		hashMap:  make(map[int]string),
	}
	if m.hash == nil {
		m.hash = crc32.ChecksumIEEE
	}
	return m
}

// Add adds some keys to the hash.
func (m *Map) Add(keys ...string) {
	for _, key := range keys {
		for i := 0; i < m.replicas; i++ {
			hash := int(m.hash([]byte(strconv.Itoa(i) + key)))
			m.keys = append(m.keys, hash)
			m.hashMap[hash] = key
		}
	}
	sort.Ints(m.keys)
}

// Get gets the closest item in the hash to the provided key.
func (m *Map) Get(key string) string {
	if len(m.keys) == 0 {
		return ""
	}

	hash := int(m.hash([]byte(key)))
	// Binary search for appropriate replica.
	idx := sort.Search(len(m.keys), func(i int) bool {
		return m.keys[i] >= hash
	})

	return m.hashMap[m.keys[idx%len(m.keys)]]
}

最后一行 m.keys[idx%len(m.keys)] 有些理解成本

Note

如果  idx == len(m.keys),说明应选择  m.keys[0],因为  m.keys  是一个环状结构,所以用取余数的方式来处理这种情况。

其他参考

小林 coding:什么是一致性哈希?

一致性哈希 - Consistent Hashing 是什么?为什么系统设计面试中经常会提到?10 分钟讲解一致性哈希 | 系统设计 System Design EP1

分布式节点

现有流程

现在的流程为

                            是
接收 key --> 检查是否被缓存 -----> 返回缓存值 ⑴
                |  否                         是
                |-----> 是否应当从远程节点获取 -----> 与远程节点交互 --> 返回缓存值 ⑵
                            |  否
                            |-----> 调用`回调函数`,获取值并添加到缓存 --> 返回缓存值 ⑶

1、3 已经在 单机并发缓存那里实现,本节主要来解决 2

使用一致性哈希选择节点        是                                    是
    |-----> 是否是远程节点 -----> HTTP 客户端访问远程节点 --> 成功?-----> 服务端返回返回值
                    |  否                                    ↓  否
                    |----------------------------> 回退到本地节点处理。

先看使用层

import (
	"flag"
	"fmt"
	"geecache"
	"log"
	"net/http"
	"strings"
)

var db = map[string]string{
	"Tom":  "630",
	"Jack": "589",
	"Sam":  "567",
}

func createGroup() *geecache.Group {
	return geecache.NewGroup("scores", 2<<10, geecache.GetterFunc(
		func(key string) ([]byte, error) {
			log.Println("[SlowDB] search key", key)
			if v, ok := db[key]; ok {
				return []byte(v), nil
			}
			return nil, fmt.Errorf("%s not exist", key)
		}))
}

func startCacheServer(addr string, addrs []string, gee *geecache.Group) {
	peers := geecache.NewHTTPPool(addr)
	peers.Set(addrs...)
	gee.RegisterPeers(peers)
	log.Println("geecache is running at", addr)
	log.Fatal(http.ListenAndServe(strings.TrimPrefix(addr, "http://"), peers))
}

func startAPIServer(apiAddr string, gee *geecache.Group) {
	http.Handle("/api", http.HandlerFunc(
		func(w http.ResponseWriter, r *http.Request) {
			key := r.URL.Query().Get("key")
			view, err := gee.Get(key)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			w.Header().Set("Content-Type", "application/octet-stream")
			w.Write(view.ByteSlice())

		}))
	log.Println("fontend server is running at", apiAddr)
	log.Fatal(http.ListenAndServe(strings.TrimPrefix(apiAddr, "http://"), nil))

}

func main() {
	var port int
	var api bool
	flag.IntVar(&port, "port", 8001, "Geecache server port")
	flag.BoolVar(&api, "api", false, "Start a api server?")
	flag.Parse()

	apiAddr := "http://0.0.0.0:9999"
	addrMap := map[int]string{
		8001: "http://0.0.0.0:8001",
		8002: "http://0.0.0.0:8002",
		8003: "http://0.0.0.0:8003",
	}

	var addrs []string
	for _, v := range addrMap {
		addrs = append(addrs, v)
	}

	gee := createGroup()
	if api {
		go startAPIServer(apiAddr, gee)
	}
	startCacheServer(addrMap[port], addrs, gee)
}
#!/bin/bash
trap "rm server;kill 0" EXIT

go build -o server
./server -port=8001 &
./server -port=8002 &
./server -port=8003 -api=1 &

sleep 2
echo ">>> start test"
curl "http://127.0.0.1:9999/api?key=Tom" &
curl "http://127.0.0.1:9999/api?key=Tom" &
curl "http://127.0.0.1:9999/api?key=Tom" &

wait

起了三个 Cache Server,端口分别在 8001 8002 8003
其中有个服务同时起了 Cache Server 和 对外暴露的 API 服务,这个 API 服务放在 9999 端口上

执行结果是

2023/11/22 09:29:09 geecache is running at http://0.0.0.0:8001
2023/11/22 09:29:09 geecache is running at http://0.0.0.0:8002
2023/11/22 09:29:09 fontend server is running at http://0.0.0.0:9999
2023/11/22 09:29:09 geecache is running at http://0.0.0.0:8003
>>> start test
2023/11/22 09:29:11 [Server http://0.0.0.0:8003] Pick peer http://0.0.0.0:8002
2023/11/22 09:29:11 [Server http://0.0.0.0:8003] Pick peer http://0.0.0.0:8002
2023/11/22 09:29:11 [Server http://0.0.0.0:8003] Pick peer http://0.0.0.0:8002
2023/11/22 09:29:11 [Server http://0.0.0.0:8002] GET /_geecache/scores/Tom
2023/11/22 09:29:11 [SlowDB] search key Tom
2023/11/22 09:29:11 [Server http://0.0.0.0:8002] GET /_geecache/scores/Tom
2023/11/22 09:29:11 [GeeCache] hit
2023/11/22 09:29:11 [Server http://0.0.0.0:8002] GET /_geecache/scores/Tom
2023/11/22 09:29:11 [GeeCache] hit
630630630

整个流程

如下图
Pasted image 20231123115425

Pasted image 20231121082054

Pasted image 20231121082201

上面这两个图可以结合着理解 一致性哈希在分布式节点的使用

其他

var _ PeerPicker = (*HTTPPool)(nil)  这行代码是用于验证  *HTTPPool  类型是否实现了  PeerPicker  接口的一种方式。

即利用强制类型转换,确保 struct HTTPPool 实现了接口 PeerPicker。这样 IDE 和编译期间就可以检查,而不是等到使用的时候。

参考这里

防止缓存击穿

背景

缓存雪崩:缓存在同一时刻全部失效,造成瞬时 DB 请求量大、压力骤增,引起雪崩。缓存雪崩通常因为缓存服务器宕机、缓存的 key 设置了相同的过期时间等引起。

缓存击穿:一个存在的 key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到 DB ,造成瞬时 DB 请求量大、压力骤增。

缓存穿透:查询一个不存在的数据,因为不存在则不会写到缓存中,所以每次都会去请求 DB,如果瞬间流量过大,穿透到 DB,导致宕机。

在分布式节点中有以下使用结果

#!/bin/bash
trap "rm server;kill 0" EXIT

go build -o server
./server -port=8001 &
./server -port=8002 &
./server -port=8003 -api=1 &

sleep 2
echo ">>> start test"
curl "http://127.0.0.1:9999/api?key=Tom" &
curl "http://127.0.0.1:9999/api?key=Tom" &
curl "http://127.0.0.1:9999/api?key=Tom" &

wait
2023/11/22 09:29:09 geecache is running at http://0.0.0.0:8001
2023/11/22 09:29:09 geecache is running at http://0.0.0.0:8002
2023/11/22 09:29:09 fontend server is running at http://0.0.0.0:9999
2023/11/22 09:29:09 geecache is running at http://0.0.0.0:8003
>>> start test
2023/11/22 09:29:11 [Server http://0.0.0.0:8003] Pick peer http://0.0.0.0:8002
2023/11/22 09:29:11 [Server http://0.0.0.0:8003] Pick peer http://0.0.0.0:8002
2023/11/22 09:29:11 [Server http://0.0.0.0:8003] Pick peer http://0.0.0.0:8002
2023/11/22 09:29:11 [Server http://0.0.0.0:8002] GET /_geecache/scores/Tom
2023/11/22 09:29:11 [SlowDB] search key Tom
2023/11/22 09:29:11 [Server http://0.0.0.0:8002] GET /_geecache/scores/Tom
2023/11/22 09:29:11 [GeeCache] hit
2023/11/22 09:29:11 [Server http://0.0.0.0:8002] GET /_geecache/scores/Tom
2023/11/22 09:29:11 [GeeCache] hit
630630630

对着 api 请求了 三次,8003 向 8002 发了三次请求

本次来实现一个叫作 singleflight 的包来解决这个问题,做到只对远端进行一次请求

方案

从实现上来看,就是对向远端请求的那个流程加锁

func (g *Group) load(key string) (value ByteView, err error) {
	// each key is only fetched once (either locally or remotely)
	// regardless of the number of concurrent callers.
	viewi, err := g.loader.Do(key, func() (interface{}, error) {
		if g.peers != nil {
			if peer, ok := g.peers.PickPeer(key); ok {
				if value, err = g.getFromPeer(peer, key); err == nil {
					return value, nil
				}
				log.Println("[GeeCache] Failed to get from peer", err)
			}
		}

		return g.getLocally(key)
	})

	if err == nil {
		return viewi.(ByteView), nil
	}
	return
}

实现如下

package singleflight

import "sync"

// call is an in-flight or completed Do call
type call struct {
	wg  sync.WaitGroup
	val interface{}
	err error
}

// Group represents a class of work and forms a namespace in which
// units of work can be executed with duplicate suppression.
type Group struct {
	mu sync.Mutex       // protects m
	m  map[string]*call // lazily initialized
}

// Do executes and returns the results of the given function, making
// sure that only one execution is in-flight for a given key at a
// time. If a duplicate comes in, the duplicate caller waits for the
// original to complete and receives the same results.
func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
	g.mu.Lock()
	if g.m == nil {
		g.m = make(map[string]*call)
	}
	if c, ok := g.m[key]; ok {
		g.mu.Unlock()
		c.wg.Wait()
		return c.val, c.err
	}
	c := new(call)
	c.wg.Add(1)
	g.m[key] = c
	g.mu.Unlock()

	c.val, c.err = fn()
	c.wg.Done()

	g.mu.Lock()
	delete(g.m, key)
	g.mu.Unlock()

	return c.val, c.err
}

两个锁

  • mu 来处理对 group m 的操作
  • wg 来实现对一个 key 只执行一次函数,其余的等待这个函数请求的结果

参考

原文

Vue3 实现

template

编译时

Pasted image 20231207204428

运行时

Pasted image 20231207204456

Pasted image 20231207204535

Pasted image 20231207205250

import vue from "rollup-plugin-vue";
import styles from "rollup-plugin-styles";

export default {
  plugins: [
    vue({
      preprocessStyles: true,
    }),
    styles(
      {
        mode: "extract",
        // ... or with relative to output dir/output file's basedir (but not outside of it)
        mode: ["extract", "awesome-bundle.css"],
      }
    ),
  ],
  input: "index.vue",
  output: {
    format: "esm",
    file: "dist/index.js",
  },
};

reactivity

it("Proxy & Reflect ", () => {
  const target = {
    message1: "hello",
    message2: "everyone",
    get message3() {
      return this.message1 + this.message2;
    },
    set xx(x) {
      this.message1 = x;
    },
  };

  const handler = {
    get(target, key, receiver) {
      console.log("read", key, target === receiver);
      // Reflect 处理调用对象的基本方法
      return Reflect.get(target, key, receiver);
    },
    set(target, property, value, receiver) {
      return Reflect.set(target, property, value, receiver);
    },
  };

  const proxy = new Proxy(target, handler);

  proxy.message3;
  proxy.message1 = "2";
  expect(target.message1).toBe("2");
});

https://vitest.dev/guide/debugging

{
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Current Test File",
      "autoAttachChildProcesses": true,
      "skipFiles": ["<node_internals>/**", "**/node_modules/**"],
      "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
      "args": ["run", "${relativeFile}"],
      "smartStep": true,
      "console": "integratedTerminal"
    }
  ]
}
it("should observe basic properties", () => {
  let dummy;
  // init start
  const counter = reactive({ num: 0 });
  effect(() => (dummy = counter.num));
  // init end

  expect(dummy).toBe(0);
  counter.num = 7;
  expect(dummy).toBe(7);
});

初始化

effect(() => (dummy = counter.num)); -> const _effect = new ReactiveEffect(fn); -> _effect.run(); -> activeEffect = this as any; // # 很重要!! -> const result = this.fn(); -> () => (dummy = counter.num) -> counter.num -> return function get(target, key, receiver) { -> track(target, "get", key); -> trackEffects(dep); -> dep.add(activeEffect); (activeEffect as any).deps.push(dep);

初始化的目的是形成
targetMap 全局的 WeekMap

Map<Target extends object, Map<string | symbol, Set<ReactiveEffect>>>

{
    [{num: 0}]: {
        "num": [[new ReactiveEffect(() => (dummy = counter.num))]]
    }
}
export function effect(fn, options = {}) {
  const _effect = new ReactiveEffect(fn);

  // 把用户传过来的值合并到 _effect 对象上去
  // 缺点就是不是显式的,看代码的时候并不知道有什么值
  extend(_effect, options);
  _effect.run();

  // 把 _effect.run 这个方法返回
  // 让用户可以自行选择调用的时机(调用 fn)
  const runner: any = _effect.run.bind(_effect);
  runner.effect = _effect;
  return runner;
}


export class ReactiveEffect {
  active = true;
  deps = [];
  public onStop?: () => void;
  constructor(public fn, public scheduler?) {
    console.log("创建 ReactiveEffect 对象");
  }

  run() {
    console.log("run");
    // 运行 run 的时候,可以控制 要不要执行后续收集依赖的一步
    // 目前来看的话,只要执行了 fn 那么就默认执行了收集依赖
    // 这里就需要控制了

    // 是不是收集依赖的变量

    // 执行 fn  但是不收集依赖
    if (!this.active) {
      return this.fn();
    }

    // 执行 fn  收集依赖
    // 可以开始收集依赖了
    shouldTrack = true;

    // 执行的时候给全局的 activeEffect 赋值
    // 利用全局属性来获取当前的 effect
    activeEffect = this as any; // # 很重要!!
    // 执行用户传入的 fn
    console.log("执行用户传入的 fn");
    const result = this.fn();

触发变化

counter.num = 7; -> return function set(target, key, value, receiver) { -> trigger(target, "set", key); -> triggerEffects(createDep(effects)); -> effect.run(); -> const result = this.fn();

前端报错与 crossorigin

问题背景

用户传来报问题但日志平台查询不到错误

定位和结论

前端的 JavaScript 代码会被上传到 CDN,假设为 xxx-cdn.com/hash.js
用户访问的是 xxx.com/path
xxx-cdn-hash 和 xxx.com 不同源 xxx-cdn.com/hash.js 这里的报错能力受限

报错能力具体来说,日志平台收集报错的方式有通过下面的方式来处理未捕获的全局错误

window.onerror = function (message, source, line, column, error) {
  // Handle the error here
  console.log("from handle-error |Error:", {
    message,
    source,
    line,
    column,
    error,
  })
}

而 跨域的 script 中抛出的 error,详细信息会被干掉「普通 throw 的 error」或者拿不到「Promise 的情况」

增加 crossorigin 可以解决这个问题

<script src="http://localhost:4000/index.js" crossorigin></script>

相关出处

  1. MDN

https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin

By default (that is, when the attribute is not specified), CORS is not used at all. The user agent will not ask for permission for full access to the resource and in the case of a cross-origin request, certain limitations will be applied based on the type of element concerned:

script: Access to error logging via window.onerror will be limited.

这里直接说了 onerror 的错误收集会受限制

  1. whatwg

https://html.spec.whatwg.org/multipage/scripting.html#attr-script-type

The crossorigin attribute is a CORS settings attribute. For classic scripts, it controls whether error information will be exposed, when the script is obtained from other origins. For module scripts, it controls the credentials mode used for cross-origin requests.

对于 classic scripts,crossorigin 控制了错误信息是否会被暴露

那什么是 classic scripts 呢?

The type attribute allows customization of the type of script represented:
Omitting the attribute, setting it to the empty string, or setting it to a JavaScript MIME type essence match, means that the script is a classic script, to be interpreted according to the JavaScript Script top-level production. Classic scripts are affected by the async and defer attributes, but only when the src attribute is set. Authors should omit the type attribute instead of redundantly setting it.

把 type 属性置为 空或者 为下列 则为 classic scripts

A JavaScript MIME type is any MIME type whose essence is one of the following:

  • application/ecmascript
  • application/javascript
  • application/x-ecmascript
  • application/x-javascript
  • text/ecmascript
  • text/javascript
  • text/javascript1.0
  • text/javascript1.1
  • text/javascript1.2
  • text/javascript1.3
  • text/javascript1.4
  • text/javascript1.5
  • text/jscript
  • text/livescript
  • text/x-ecmascript
  • text/x-javascript

最佳实践

上 CDN 的 script 都加一下 crossorigin

其他与参考

debug 实践的仓库可以看 这个

Git技巧:一个机器,搞不同仓库

关键能力

  1. 密钥对的区分
  2. 信息的区分「name、email」

密钥对的区分

利用 host 去找 对应的 IdentityFile

~/.ssh/config

# Work ByteDance
 Host code.byted.org
 IdentityFile ~/.ssh/bytedance

# Personal GitHub account
 Host *.github.com
 IdentityFile ~/.ssh/id_rsa

信息的区分「name、email」

~/.ssh/config

[user]
	name = SedationH
	email = [email protected]
[includeIf "gitdir:~/workspace/bytedance/"]
 	path = ~/workspace/bytedance/.gitconfig

~/workspace/bytedance/.gitconfig

[user]
	email = [email protected]
	name = xxx

黑客与画家

书本简介

作者是美国互联网界举足轻重、有“创业教父”之称的哈佛大学计算机博士保罗·格雷厄姆(Paul Graham)。本书是他的文集。

2004 年出版

阮一峰翻译

“书中的内容并不深奥,不仅仅是写给程序员和创业者的,更是写给普通读者的。作者最大的目的就是,通过这本书让普通读者理解我们所处的这个计算机时代。”

本书阐述了作者的一些有趣观点,虽已过十几年,但读起来仍觉犀利。

黑客这种叫法的来源

“为了把这个问题说清楚,有必要从源头上讲起。1946年,第一台电子计算机ENIAC在美国诞生,从此世界上一些最聪明、最有创造力的人开始进入这个行业,在他们身上逐渐地形成了一种独特的技术文化。在这种文化的发展过程中,涌现了很多“行话”(jargon)。20世纪60年代初,麻省理工学院有一个学生团体叫做“铁路模型技术俱乐部”(Tech Model Railroad Club,简称TMRC),他们把难题的解决方法称为hack。

“在这里,hack作为名词有两个意思,既可以指很巧妙或很便捷的解决方法,也可以指比较笨拙、不那么优雅的解决方法。两者都能称为hack,不同的是,前者是漂亮的解决方法(cool hack或neat hack),后者是丑陋的解决方法(ugly hack或quick hack)。hack的字典解释是砍(木头),在这些学生看来,解决一个计算机难题就好像砍倒一棵大树。那么相应地,完成这种hack的过程就被称为hacking,而从事hacking的人就是hacker,也就是黑客。”

关于财富

金钱与财富

创造有价值的东西就是创造财富。

财富与金钱并不是同义词,财富存在的时间与人类历史一样长久,甚至更长久,事实上蚂蚁也拥有财富。金钱是一种历史相对较短的发明。

财富是最基本的东西。我们需要的东西就是财富,食品、服装、住房、汽车、生活用品、外出旅行等都是财富。即使你没有钱,你也能拥有财富。如果有一台魔法机器,能够按照你的命令变出汽车,为你洗衣做饭,提供其他你想要的东西,那么你就不需要钱了。要是你身处南极洲内陆,再多的钱对你也是无用的,因为没有东西可买,你真正需要的是财富。

财富才是你的目标,金钱不是。但是,如果财富真的这么重要,为什么大家都把挣钱挂在嘴边呢?部分原因是,金钱是财富的一种简便的表达方式:金钱有点像流动的财富,两者往往可以互相转化。但是,它们确实不是同样的东西,除非你打算伪造货币,否则使用“挣钱”这个词会不利于理解如何才能挣钱。

金钱是专业化的副产品。在一个高度分工的社会,你需要的大部分产品无法自己制造。你需要土豆、铅笔、住房以及别的东西,你不得不让别人来提供。

金钱、又或者称之为交换媒介的优点是,它使得交易可以进行下去。缺点是,它往往模糊了交易的实质。人们觉得做生意就是为了挣钱,但是金钱其实只是一种中介,让大家可以更方便地获得自己想要的东西。大多数生意的目的是为了创造财富,做出人们真正需要的东西。

强调金钱与财富的差异是想说,如果你真的想实际创造、获得更多的财富,要真正考虑你计划的事情、做的事情是否是别人认为有价值的,需要的

财富是创造出来的

最可能明白财富能被创造出来的人就是那些善于制作东西的人,也就是手工艺人。他们做出来的东西直接放在商店里卖。但是,随着工业化时代的来临,手工艺人越来越少。目前还存在的最大的手工艺人群体就是程序员。

程序员坐在电脑前就能创造财富。优秀软件本身就是一件有价值的东西。你输入的文字符号就是一件完整的制成品。如果某人坐在电脑前,写出了一个不那么糟糕的浏览器(顺便说一句,这是一件很值得做的事),世界就会变得富有得多。

看到这就想到了 VSCode,同时被世界上许多程序员使用,提供的流畅和方便的使用体验是一个宝贵的财富。

公司就是许多人聚在一起创造财富的地方,能够制造更多人们需要的东西。当然,有些雇员(比如收发室和人事部的员工)并不直接参与制造过程,但是程序员不然。他们真正地面对产品,一行行地写代码把产品做出来。所以,在程序员看来,事情再明显不过,财富就是被做出来的,而不是某个想象出来的神秘人物分发的大饼。

另一件程序员看来显而易见的事情就是创造财富的速率存在巨大的差异。一个优秀程序员连续工作几个星期可能可以创造价值100万美元的财富。同样的时间内,一个平庸的程序员不仅无法创造财富,甚至还可能减少财富(比如引入了bug)。

这就是为什么如此之多的最优秀程序员都是自由主义者的原因。我们这个世界,你向下沉沦或者向上奋进都取决于你自己,不能把原因推给外界。许许多多不创造任何财富的人——比如本科生、记者和政客——一听到最富有的5%人口占有全社会一半以上的财富,往往会认定这是不公平的。一个有经验的程序员很可能也认为这是不公平的。因为最顶尖的5%的程序员写出了全世界99%的优秀软件

工作是什么

在工业化国家,一个人至少在二十多岁之前,总是从属于这样或那样的某个组织。经过这么多年,你已经习惯了自己属于这样一群人,早上全部起床,都来到同样几幢建筑物,开始做自己正常情况下没兴趣做的事情。这样的组织变成了你身份标志之一:姓名、年龄,头衔、组织名称。如果你要做自我介绍或者他人要描述你,结果无非就是,张三,10岁,某某小学的学生,或者,张三,20岁,某某大学的学生。

当张三从学校毕业后,他应该要找工作。找工作其实就是加入另一个组织。表面上,这个组织与大学很相像。你先挑选想去的公司,然后向它递交申请。如果它觉得你不错,你就能加入了。你早上起床,来到一个新的地点,也是几幢建筑物,开始做你正常情况下没兴趣做的事情。仅有的区别就是,上班的日子不如上学的日子有趣,但是有人付钱给你,而不是你付钱给学校。但是,上学和上班的相似之处要大于它们的不同。张三,20岁,某某大学的学生,现在变成了,张三,22岁,某某公司的程序员。

事实上,张三的生活比他意识到的发生了更大的变化。虽然公司和学校都是类似的社会组织,但是如果你深入观察现实,就会发现很大的区别。

公司一切行为的目的都是盈利,从而生存下去。创造财富是大多数公司盈利的手段。公司的业务高度专业化,掩盖了它们都是在创造财富的这种相似性。几乎所有情况下,公司的存在目的就是满足人们的某种需要。

当你为一家公司工作时,这也是你所做的事情。但是,公司内部的各种层级使得这一点有时不容易觉察到。你在公司内部所做的工作是与许多人一起合作完成的,你只是其中的一分子。你觉得自己是为公司的需要而工作,可能不会觉察到你其实是为了满足顾客的某种需求而工作。你的贡献也许不是直接性的,但是公司作为一个整体必须提供某种人们需要的东西,否则不可能盈利。

一个大学毕业生总是想“我需要一份工作”,别人也是这么对他说的,好像变成某个组织的成员是一件多么重要的事情。更直接的表达方式应该是“你需要去做一些人们需要的东西”。即使不加入公司,你也能做到。公司不过是一群人在一起工作,共同做出某种人们需要的东西。真正重要的是做出人们需要的东西,而不是加入某个公司

对于大多数人来说,最好的选择可能是为某个现存的公司打工。但是,理解这种行为的真正含义对你没有什么坏处。工作就是在一个组织中,与许多人共同合作,做出某种人们需要的东西。

你的产出对业务的帮助呢?数据呢?(狗头

大公司会使得每个员工的贡献平均化,这是一个问题。我觉得,大公司最大的困扰就是无法准确测量每个员工的贡献。大多数时候它只是在瞎猜。在大公司中,你只要一般性地努力工作,就能得到意料之中的薪水。你不能明显无能或懒惰,但是谁也没觉得你会把全部精力投入工作。

成立公司的目的不是奖励那些全部精力投入工作的员工。你不能对老板说,我打算十倍努力地工作,请你把我的薪水也增加十倍吧!因为公司已经假定你在全力工作了。但是,真正的问题实际上在于公司无法测量你的贡献。

可测量性和可放大性

要致富,你需要两样东西:可测量性和可放大性。你的职位产生的业绩,应该是可测量的,否则你做得再多,也不会得到更多的报酬。此外,你还必须有可放大性,也就是说你做出的决定能够产生巨大的效应。

单单具备可测量性是不够的。比如,血汗工厂的工人报酬是按照计件制计算的,这是一个只有可测量性、没有可放大性的例子。你的表现可以被测量,并且据此得到回报,但是你没有决策的权力。你能做的唯一决策就是以多快的速度完成工作。即使你做到最快,回报可能也只增加一到二倍。

乔布斯曾经说过,创业的成败取决于最早加入公司的那十个人。我基本同意这个观点,虽然我觉得真正决定成败的其实只是前五人。小团队的优势不在于它本身的小,而在于你可以选择成员。我们不需要小村庄的那种“小”,而需要全明星第一阵容的那种“小”。

团队越大,每个人的贡献就越接近于整体的平均值。所以,在不考虑其他因素的情况下,一个非常能干的人待在大公司里可能对他本人是一件很糟的事情,因为他的表现被其他不能干的人拖累了。当然,许多因素都会产生影响,比如这个人可能不太在乎回报,或者他更喜欢大公司的稳定。但是,一个非常能干而且在乎回报的人,通常在同类人组成的小团队中会有更出色的表现,自己也会感到更满意。

回顾历史,大多数因为创造财富而发财的人都是通过开发新技术而实现的。你不可能通过煎鸡蛋或剪头发而致富,因为使用你的服务的人是有限的。13世纪,佛罗伦萨人发明了精纺布,那是当时的高科技产品,这种新技术造就了佛罗伦萨的繁荣。17世纪,荷兰人掌握了造船术和航海知识,那也是当时的高科技,因此荷兰人主宰了欧洲前往远东的航线。小团队天生就适合解决技术难题。技术的发展是非常快的,今天很有价值的技术,几年后可能就会丧失价值。小团队在如今这个时代可谓如鱼得水,因为他们不受官僚主义和繁琐管理制度的拖累。而且,技术的突破往往来自非常规的方法,小团队就较少受到常规方法的约束。

React 多实例问题

问题背景

最近在优化 awesome-toc 的开发环境

image.png

原因

简单说下问题是咋产生的

原来的项目是

node_modules
src
	content
		index.ts

现在建了一个 demo 用于快速开发 /src/content 里的效果

demo
	node_modules
	src
		main.ts
		App.tsx
	vite.config.ts
node_modules
src
	content
		index.ts

/demo/src/App.tsx 会直接引用 /src/conent/index.ts

/src/conent/index.ts 相关的逻辑有「引用的文件里有」

import React from 'react'

这里的 react 来源是 /node_modules 「寻找最近一层的 node_modules」

/demo/src/App.tsx 也有

import React from 'react'

这里的 react 来源是 /demo/node_modules

因此 react 会有两个来源

方案

目标:不能有两个 react,需要做合并

demo 效果构建是由 /demo 下的 打包工具驱动的「vite.config.ts」
是由 vite 去寻找的 最近的 node_modules

我们固定一个就好了,不用自动寻找最近的

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      react: path.join(__dirname, 'node_modules/react'),
    },
  },
})

flex 实现表格方案

https://github.com/SedationH/flex-table/

背景描述

小程序平台无表格组件库、不支持使用table 要能够实现 HTML table 中 colspan 或者 rowspan 的类似展示效果

预期效果见 https://codepen.io/sedationh/pen/oNyMyjO

方案描述

基本思路是利用 flex 进行布局,利用特定数据结构完成 xxxspan 的效果,实际上没有 xxxspan 类似的填补能力,而是去分割当前单元格(用 .table 来拆)

table
  row
    col
      col-content
    table
      row
        col
          col-content

效果展示

image.png

koa-compose 代码分析

背景、为什么要了解这个

  1. 学习优雅的代码实现
  2. 这个库的实现代码量很少,并伴有完善的测试
  3. 为 koa 的理解做铺垫

是什么

Compose middleware specifically for koa.

提供了用于组织中间件函数的方法

为每个中间件加入了共用的 ctx 和 用于转移到下个中间件的 next 方法

const compose = require("./index")

const consoleWithTabsize = (tabsize, ...args) => {
  console.log("\t".repeat(tabsize), ...args)
}

const fn1 = (ctx, next) => {
  console.log({ ctx })
  consoleWithTabsize(0, ">>> fn1")
  next()
  consoleWithTabsize(0, "<<< fn1")
}
const fn2 = (ctx, next) => {
  consoleWithTabsize(1, ">>> fn2")
  next()
  consoleWithTabsize(1, "<<< fn2")
}

const fn3 = (ctx, next) => {
  consoleWithTabsize(2, ">>> fn3")
  next()
  consoleWithTabsize(2, "<<< fn3")
}

compose([fn1, fn2, fn3])({
  name: "this is ctx"
})
{ ctx: { name: 'this is ctx' } }
 >>> fn1
         >>> fn2
                 >>> fn3
                 <<< fn3
         <<< fn2
 <<< fn1

从上面可以看出,调用 next 实际上就是去调用下一个 中间件函数(也就是有些文章中所说的转移控制权)

下图摘自 koa 中文 官网
https://github.com/demopark/koa-docs-Zh-CN/blob/master/guide.md#%E7%BC%96%E5%86%99%E4%B8%AD%E9%97%B4%E4%BB%B6

img

代码

仓库在这里 https://github.com/koajs/compose
推荐本地拉下来仓库,进行手动断点理解
https://github.com/koajs/compose/blob/master/test/test.js

base case

下面是一个基本的测试 case

  it('should work', async () => {
    const arr = []
    const stack = []

    stack.push(async (context, next) => {
      arr.push(1)
      await wait(1)
      await next()
      await wait(1)
      arr.push(6)
    })

    stack.push(async (context, next) => {
      arr.push(2)
      await wait(1)
      await next()
      await wait(1)
      arr.push(5)
    })

    stack.push(async (context, next) => {
      arr.push(3)
      await wait(1)
      await next()
      await wait(1)
      arr.push(4)
    })

    await compose(stack)({})
    expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6]))
  })
function compose(middleware) {
  return function (context, next) {
    return dispatch(0)
    function dispatch(i) {
      const fn = middleware[i]
      if (!fn) return Promise.resolve()
      return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
    }
  }
}

image.png

这段代码是 koa-compose 的核心逻辑,我删除了原有代码中的分支代码

compose 的输入是一个函数组成的数组 middleware,输出是一个接受 context【一个对象】 和 next 【一个函数】的函数

context 对象会在 middleware 中的所有函数中的第一个参数拿到
next 则是对下一个中间件函数的调用函数,对应代码的 dispatch.bind(null, i + 1)

重点说一下为啥要 return
其实如果我们传入的中间件中不会用到 Promise 或着 async await 的写法
不 return 也是 OK 的,因为函数调用本身就在转移控制

  it.only("should work in not return condition", async () => {
    const arr = []
    const stack = []

    stack.push((context, next) => {
      arr.push(1)
      next()
      arr.push(6)
    })

    stack.push((context, next) => {
      arr.push(2)
      next()
      arr.push(5)
    })

    stack.push((context, next) => {
      arr.push(3)
      next()
      arr.push(4)
    })

    await compose(stack)({})
    expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6]))
  })
function compose(middleware) {
  return function (context, next) {
    return dispatch(0)
    function dispatch(i) {
      const fn = middleware[i]
      if (!fn) return
      fn(context, dispatch.bind(null, i + 1))
    }
  }
}

image.png

但我们 should work case 就跑不通了

究其原因是,我们需要等待 next() 这个函数调用完,如果其中存在 Promise 的执行流,也要能让外界知道他的执行情况

所以必须要 return,我们调用 next 的时候,通常也会进行 await next() ,next 的参数和返回值都不关注,它的作用只是用来控制程序流

从结果上来看,只考虑主分支,会形成如下的效果

const [fn1, fn2, fn3] = stack;
const fnMiddleware = function(context){
    return Promise.resolve(
      fn1(context, function next(){
        return Promise.resolve(
          fn2(context, function next(){
              return Promise.resolve(
                  fn3(context, function next(){
                    return Promise.resolve();
                  })
              )
          })
        )
    })
  );
};

至于 return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))Promise.resolve 包一层是否必要
如果我们只考虑 await next 的场景,不是必要的的,因为
参考

const a = await 1
// ->
let a
Promise.resolve(1).then(v => a = v)

image.png

只有这个 case 没通过

  it('should create next functions that return a Promise', function () {
    const stack = []
    const arr = []
    for (let i = 0; i < 5; i++) {
      stack.push((context, next) => {
        arr.push(next())
      })
    }

    compose(stack)({})

    for (const next of arr) {
      assert(isPromise(next), 'one of the functions next is not a Promise')
    }
  })

function isPromise (x) {
  return x && typeof x.then === 'function'
}

https://stackoverflow.com/questions/27746304/how-to-check-if-an-object-is-a-promise

If it has a .then function - that's the only standard promise libraries use.

边界情况

看注释吧

/**
 - Compose `middleware` returning
 - a fully valid middleware comprised
 - of all those which are passed.
 *
 - @param {Array} middleware
 - @return {Function}
 - @api public
 */

function compose(middleware) {
  // 入参类型限制
  if (!Array.isArray(middleware)) throw new TypeError("Middleware stack must be an array!")
  for (const fn of middleware) {
    if (typeof fn !== "function") throw new TypeError("Middleware must be composed of functions!")
  }

  /**
   - @param {Object} context
   - @return {Promise}
   - @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch(i) {
      // 不能重复调用next
      if (i <= index) return Promise.reject(new Error("next() called multiple times"))
      index = i
      let fn = middleware[i]
      // 支持 compose([fn1, fn2, fn3])({}, fn4) 的写法,fn4 会在 fn3 后执行
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {
        // 向外给错误
        return Promise.reject(err)
      }
    }
  }
}

对于 支持 compose([fn1, fn2, fn3])({}, fn4) 的写法,fn4 会在 fn3 后执行 这点
可以结合下面理解下

  it("should work 2", async () => {
    const arr = []
    const stack = []

    stack.push(async (context, next) => {
      arr.push(1)
      await wait(1)
      await next()
      await wait(1)
      arr.push(6)
    })

    stack.push(async (context, next) => {
      arr.push(2)
      await wait(1)
      await next()
      await wait(1)
      arr.push(5)
    })

    stack.push(async (context, next) => {
      arr.push(3)
      await wait(1)
      await next()
      await wait(1)
      arr.push(4)
    })

    await compose(stack)({}, async (_, next) => {
      arr.push(99.1)
      await next()
      arr.push(99.2)
    })
    expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 99.1, 99.2, 4, 5, 6]))
  })

参考

https://github.com/lxchuan12/koa-compose-analysis
https://bytedance.feishu.cn/wiki/wikcnnAWRea37N3fBa8cQOlkP4N

简化新建分支推送时候的命令

背景

在新建分支并推送的时候,会遇到下面的问题

awesome-toc on  fix/title-ellipsis-short [!] is 📦 v0.0.4 via ⬢ v14.20.0 
➜ git push
fatal: The current branch fix/title-ellipsis-short has no upstream branch.
To push the current branch and set the remote as upstream, use

git push --set-upstream origin fix/title-ellipsis-short

这个时候你还要再执行一次

git push --set-upstream origin fix/title-ellipsis-short

才能把分支推上去

问题

只想都用 git push,不用考虑新建要更换命令

新建的
git push -u origin xxx
不是新建的
git push

方案

参考这里的

As of git 2.37.0, this is now possible with git configuration.

Run to update your configuration:

git config --global --add --bool push.autoSetupRemote true

Then git push will automatically setup the remote branch.

Note: The --global flag means this will apply to all git commands on your machine (regardless of which repo it is), you can omit the flag to make it specific to a single repo on your machine.

注意下自己的 Git 版本

git --version

版本不够 2.37.0 用 brew 来升级一下

React 演进 及 18 新特性解析

本篇文章行文思路如下

  1. React 的产生背景、要解决的问题
  2. 梳理下 React 发展的历史,理解 React 的设计思路「从哪来、到哪去」
  3. 过一些 18 版本的新能力

构建用户界面

image.png

React 是一个用来构建用户界面的前端库,从执行的角度来说,在 Web 语境下,就是帮我们操作 DOM 的库。

在 JQuery 时代,我们用 JQuery 或 类似的库在满足兼容性的同时,命令式的操作 DOM 来完成用户界面的变化。

但在 React、Vue 等框架的帮助下,我们与 DOM 之间多了层代理,开发者只用告诉需要什么样的 DOM,预期 DOM 要变化成什么样子,框架则帮我们找出差异,处理变化,进行 DOM 操作。这是一种声明式的代码。

举个例子,假如我要实现下面的需求

01 - 获取 id 为 app 的 div 标签
02 - 它的文本内容为 hello world
03 - 为其绑定点击事件
04 - 当点击时弹出提示:ok

如果我们使用 JQuery 来做

const div = document.querySelector("#app") // 获取 div02
div.innerText = "hello world" // 设置文本内容
div.addEventListener("click", () => {
  alert("ok")
}) // 绑定点击事件

对于 声明式的框架呢? 以 React 为例子

<div onClick={() => alert("ok")}>Hi</div>

命令式更加关注过程,而声明式更加关注结果。命令式在理论上可以做到极致优化,但是用户要承受巨大的心智负担;而声明式能够有效减轻用户的心智负担,但是性能上有一定的牺牲。

在用户体验愈发重要、前端交互愈加复杂的趋势下,声明式的框架逐渐成了主流。

在设计一个这样的视图层框架时,能力加点有俩方向,如下图。

image.png

所谓编译时,可以理解为不依赖用户打开网页时 JavaScript 的执行构建渲染所需的数据和指令,而是在前端代码 构建「build」 的过程中提前进行代码分析生成渲染所需的数据和指令。

对于这块感兴趣的同学,安利 《Vue.js 设计与实现》 ,其中 第 1 章权衡的艺术 就很好的描述了这块内容,限于篇幅不进行展开了。
截取一段总结给大伙瞅瞅 image.png

相较于 Vue 和 Svelte,React 是一个没有太多 编译行为 的框架,表达和写法基本和 JavaScript 一致,灵活的写法也导致 React 很难在编译时提前做太多的事情,因此我们可以看到  React  几个大版本的的优化主要都在运行时。

运行时要考虑哪些问题

计算被集中放在了运行时,有影响、被占用的是浏览器的渲染进程

image.png

渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。

而且 JavaScript 是 单线程运行 的,而且从上面可知在浏览器环境麻烦事非常多,它要负责页面的 JS 解析和执行、绘制、事件处理、静态资源加载和处理


图片来自

它只是一个’JavaScript’,同时只能做一件事情,这个和  DOS  的单任务操作系统一样的,事情只能一件一件的干。要是前面有一个任务长期霸占 CPU,后面什么事情都干不了,浏览器会呈现卡死的状态,这样的用户体验就会非常差

image.png
JavaScript 就像单行道,这幅图很好的描绘了计算资源不够用的场景 😄

计算资源不够,卡顿,从技术上如何度量呢?

Most devices today refresh their screens 60 times a second. If there’s an animation or transition running, or the user is scrolling the pages, the browser needs to match the device’s refresh rate and put up 1 new picture, or frame, for each of those screen refreshes.
Each of those frames has a budget of just over 16ms (1 second / 60 = 16.66ms). In reality, however, the browser has maintenance work to do, so all of your work needs to be completed inside 10ms. When you fail to meet this budget the frame rate drops, and the content judders on screen. This is often referred to as jank, and it negatively impacts the user's experience.
-- 上面内容取自 https://web.dev/rendering-performance/

image.png

主流浏览器的刷新频率一般是  60Hz,也就是每秒刷新  60  次,大概  16.6ms  浏览器刷新一次。由于  GUI  渲染线程和  JS  线程是互斥的,所以  JS  脚本执行和浏览器布局、绘制不能同时执行。

在这  16.6ms  的时间里,浏览器既需要完成  JS  的执行,也需要完成样式的重排和重绘,如果  JS  执行的时间过长,超出了  16.6ms,这次刷新就没有时间执行样式布局和样式绘制了,于是在页面上就会表现为卡顿。

如何更好的协调好 JavaScript 执行、渲染、用户行为响应等 合理的利用宝贵的 CPU 资源,是 React 这个运行时框架要去解决的问题

React 的时间线

下文的主要参考是 React 已有的所有 blogEvolution of React on a Timeline,挑一些我觉得关键的节点和内容

2013 年之前

React 这样的库的需求诞生于 Facebook 的广告组织,随着 Facebook 的规模越来越大,一个开始简单的代码库也在增长,功能的数量增加了,代码的复杂度增加的更多,变的不好维护。

三个背景进行理解

  1. Jordan Walke 搞了个  FaxJS 来降低新功能加入导致的复杂度不可控。
  2. 此时,Facebook 在使用 XHP,它是一种用于 PHP 的 HTML 组件框架。XHP 在 Facebook Lite 中充当了 UI 渲染层,并且在创建自定义和可重用的 HTML 元素方面表现良好。
  3. Facebook 新收购 Instagram 对 Facebook 的广告组织 的已有方案有兴趣,并推动与现有业务解耦和进行开源

搞的 Server Component 也算是不忘初心了 (😄

2013

在 2013 5 月 29 日至 31 日举行的 JS ConfUS 期间,Jordan Walke 向全世界介绍了 React。

https://www.youtube.com/watch?v=GW0rj4sNH2w

image.png

image.png

当时已经有很多框架了,如 Angular。React 选用 diff 的方案 来实现最小的 DOM 更新。

  • Declarative Components
  • No observable data binding
  • Embedded XML Syntax 「JSX」

Why did we build React?

https://legacy.reactjs.org/blog/2013/06/05/why-react.html

  1. 鼓励创建可重用的 UI 组件、而不是使用模板
  2. 提供 JSX
  3. reconciliation 找出 应用于 DOM 的最小更改
  4. it’s a lightweight description of what the DOM should look like 「虚拟 DOM」

2014

2015

  • v0.13 - v0.14
  • The most notable new feature is support for ES6 classes
    • 就是我们现在用的 class 组件,这个版本之前用的都是 React.createClass
  • v0.13
    • 在上周的 EmberConf 和 ng-conf 上,我们很高兴地看到 Ember 和 Angular 一直在努力提高速度,现在它们的性能都可以与 React 相媲美。我们一直认为性能不是选择 React 的最重要原因,但我们仍在计划更多的优化以使 React 更快。

2016

整体设计

存在的问题

不完美的批处理

同一事件回调函数上下文中的多次 setState 只会触发一次更新。

class Example extends React.Component {
  constructor() {
    super()
    this.state = {
      val: 0,
    }
  }

  componentDidMount() {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

    setTimeout(() => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val)
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val)
    }, 0)
  }

  render() {
    return null
  }
}

15 版本上面代码的打印顺序是  0、0、2、3

递归同步更新,无法中途停止,很容易在复杂场景出现长任务导致渲染卡顿

React 15  本身的架构是递归同步更新的,如果节点非常多,即使只有一次  state  变更,React  也需要进行复杂的递归更新,更新一旦开始,中途就无法中断,直到遍历完整颗树,才能释放主线程。

image.png

A Cartoon Intro to Fiber - React Conf 2017 给了这种 reconciler 名字,叫作 stack reconciler

2017

  • v16.0.0 - v16.2.0
  • Fiber
  • 提供 ErrorBoundary
  • New render return types: fragments and strings
  • 传送门
  • Fragment 支持
  • 改进仓库结构

Fiber

对于 16 这个版本,最重要的 改动是 引入了 Fiber 这一结构来解决 stack reconciler 中的问题,这也是后续 React 其他 feature 的基石。

image.png

  • 从运行机制上来解释,fiber是一种流程让出机制,它能让react中的同步渲染进行中断,并将渲染的控制权让回浏览器,从而达到不阻塞浏览器渲染的目的。
  • 从数据角度来解释,fiber能细化成一种数据结构,或者一个执行单元。存着与其他 Fiber 的关系。

传统递归,一条路走到黑

image.png

react fiber,灵活让出控制权保证渲染与浏览器响应

image.png

结合上文提到的 # 运行时要考虑哪些问题 进行理解,Diff 工作可以分解成更小的单元,拆分在多个 Frame 中执行,这样的处理不会卡 Frame

2018

2019

  • v16.8
  • 开始支持 hooks

2020

2022

  • v18
  • 全面开启 Concurrent Mode
  • 细节看下文吧

React 18

Concurrent

这里推荐读下 原文 ## What is Concurrent React? 的内容,因为大部分 18 版本的 新 Feature 能力,都是在这个 Concurrent Mode 的基础上才能 work

https://sedationh.notion.site/Concurrent-40c33ff7e7c643cb98965606200e3906?pvs=4

这里总结下
Concurrent 可以让 React 在同一时刻准备多个版本的 UI

特点

  1. 渲染行为可中断,可恢复
  2. 处理任务有优先级(如 离屏渲染< 当前渲染 < 用户行为)
  3. Concurrent 是 实现很多 feature 的基础能力(如 Suspense)

createRoot

https://sedationh.notion.site/React-18-createRoot-62cd00627e5340a2a8f585397af34a0a?pvs=4
https://github.com/facebook/react/blob/main/CHANGELOG.md#react-dom-client

  1. 支持并发模式
  2. Use it instead of ReactDOM.render. New features in React 18 don’t work without it.
  3. 复用根节点
// old
const rootElement = document.getElementById("root")
ReactDOM.render(<App />, rootElement)

// new
const rootElement = document.getElementById("root")
const root = ReactDOM.createRoot(rootElement)
root.render(<App />)
root.render(<App2 />)

Automatic Batching

https://sedationh.notion.site/React-18-Automatic-Batching-462af57a7ff74b3798a6a3a43918c84f?pvs=4

// Before: only React events were batched.
setTimeout(() => {
  setCount((c) => c + 1)
  setFlag((f) => !f)
  // React will render twice, once for each state update (no batching)
}, 1000)

// After: updates inside of timeouts, promises,
// native event handlers or any other event are batched.
setTimeout(() => {
  setCount((c) => c + 1)
  setFlag((f) => !f)
  // React will only re-render once at the end (that's batching!)
}, 1000)

Batching 是指 React 将多个状态更新分组到单个重新渲染中以获得更好的性能。18 之前,只在 React 事件处理程序中 Batching,promises、setTimeout、本机事件处理程序或任何其他事件中的更新不会在 React 中 Batching。

看 Demo
https://codesandbox.io/s/automatic-batching-g8jg63?file=/src/index.js

可以利用 flushSync 进行强制更新

import { flushSync } from "react-dom" // Note: react-dom, not react

function handleClick() {
  flushSync(() => {
    setCounter((c) => c + 1)
  })
  // React has updated the DOM by now
  flushSync(() => {
    setFlag((f) => !f)
  })
  // React has updated the DOM by now
}

useTransition

useTransition is a React Hook that lets you update the state without blocking the UI.
https://sedationh.notion.site/Transition-7b2702649a9a4a43930a951fff0f03ba?pvs=4

function TabContainer() {
  const [isPending, startTransition] = useTransition()
  const [tab, setTab] = useState("about")

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab)
    })
  }
  // ...
}

体验 Demo
The pythagoras tree is a fractal. A deeply nested data structure that brings any rendering framework to its knees。「复杂的 DOM 渲染场景」
https://react-fractals-git-react-18-swizec.vercel.app

再体会下切换 Tab 的场景
https://react.dev/reference/react/useTransition

useSyncExternalStore

useSyncExternalStore is a React Hook that lets you subscribe to an external store.
给状态库用的

比较这俩,看着的使用场景

sedationh/debug-zustand@ce8c6df#r124976497
sedationh/debug-zustand@ce8c6df#r124976321

useDeferredValue

Call useDeferredValue at the top level of your component to get a deferred version of that value.
https://sedationh.notion.site/useDeferredValue-cde018f201e34be3a0af0745ce556ded?pvs=4

useDeferredValue 可以让一个 state 延迟生效,只有当前没有紧急更新时,该值才会变为最新值。useDeferredValue 和 startTransition 一样,都是标记了一次非紧急更新。

其他

还有一些特性并没有介绍、尤其是和服务端渲染相关的(业务还没有用的场景),感兴趣的同学可去官网进行进一步的了解。

如何升级呢?

https://react.dev/blog/2022/03/08/react-18-upgrade-guide

整体回顾

  1. 复杂的业务催生「框架」的诞生
  2. React 经历了从 Facebook Ads 团队解决问题产生,到开源给社区用,再到被广泛应用的过程
  3. JSX、Vitrual DOM、Hooks、Fiber、Concurrent Mode ... React 专注于运行时的能力,变的越来越像一个操作系统,React OS,处理不同资源的调度、优先级 ...
  4. 随着 React 越来越复杂、被用的越来越多,React 的发布和更新也变的更加缓慢和谨慎。

flow this 类型提示

问题背景

Appromal Admin 用的mobx进行状态管理

Mobx 官网推荐在异步场景下使用 flow 进行包装,进行状态更改

https://mobx.js.org/actions.html#using-flow-instead-of-async--await-

flow, like action, can be used to wrap functions directly. The above example could also have been written as follows:

import { flow } from "mobx"

class Store {
    githubProjects = []
    state = "pending"

    fetchProjects = flow(function* (this: Store) {
        this.githubProjects = []
        this.state = "pending"
        try {
            // yield instead of await.
            const projects = yield fetchGithubProjectsSomehow()
            const filteredProjects = somePreprocessing(projects)
            this.state = "done"
            this.githubProjects = filteredProjects
        } catch (error) {
            this.state = "error"
        }
    })
}

const store = new Store()
const projects = await store.fetchProjects()

The upside is that we don't need flowResult anymore, the downside is that this needs to be typed to make sure its type is inferred correctly.

问题

当时官网似乎还没有代码,因此我的写法是下面这样的

image.png

当使用上图的写法来写的时候,会丢失类型提示

解决方案

mobxjs/mobx#1769 (comment)

image.png

https://www.logicbig.com/tutorials/misc/typescript/function-this-parameter.html

从这个来看,是个TS的行为,不过没能寻找到出处

问题记录:Vite 处理 UMD 包生产环境和开发环境不一致

问题描述

条件

import * as x from "build"
  1. build 包为 umd 规范
  2. build 包 只有一份 default 导出
  3. 使用上面的方式进行引入

表现

console.log("Mode", import.meta.env.MODE)
console.log("x", x)

pnpm run dev 后 控制台可见

Mode development
x ƒ index() {
        console.log("hi");
      }

pnpm run build && pnpm run preview 后 控制台可见

Mode production
x
Module {Symbol(Symbol.toStringTag): 'Module', default: ƒ}

复现项目结构描述

项目可见

注意在构建环境的时候 build 用了 file 协议
通过 rollup 来进行构建生成的

Pasted image 20231117162334

注意若把 export.ts 加个导出

export const sum = () => {
  console.log("sum")
}

export default function () {
  console.log("hi")
}

dev 环境打印就变成了

Mode development
x {__esModule: true, default: ƒ, sum: ƒ}

调试 dev 环境要注意
Pasted image 20231117162614

内网穿透服务

一直以来,自己折腾云服务器的主要诉求是给自己用。有一个公网 IP,其他设备也可以访问到服务。

但使用云服务器有一些缺点

  1. 性能
  2. 操作不方便

以上需求的关键是要有一个公网 IP

内网穿透的服务给了一个公网域名,也差不多是等价的

体验了几个做内网穿透的,从易用性和价格选择了 DDNSTO

以自己的 Mac 作为服务器,用 Docker 很容易就走完了搭建流程

体验不错

Pasted image 20231029113526

https://www.ddnsto.com/app/#/devices

Mac 被当作一个服务器来用,需要 when lock display close keep runing

Pasted image 20231030083010

可以使用来解决
Amphetamine
https://iffy.freshdesk.com/support/home

Go从零实现 - Web 框架 - Gee

本文出自 https://geektutu.com/post/gee.html 对部分代码进行了修改并加上一些理解和调试图片

Note

大部分时候,我们需要实现一个 Web 应用,第一反应是应该使用哪个框架。不同的框架设计理念和提供的功能有很大的差别。比如 Python 语言的  djangoflask,前者大而全,后者小而美。Go 语言/golang 也是如此,新框架层出不穷,比如BeegoGinIris等。那为什么不直接使用标准库,而必须使用框架呢?在设计一个框架之前,我们需要回答框架核心为我们解决了什么问题。只有理解了这一点,才能想明白我们需要在框架中实现什么功能。

net/http提供了基础的 Web 功能,即监听端口,映射静态路由,解析 HTTP 报文。一些 Web 开发中简单的需求并不支持,需要手工实现。

  • 动态路由:例如hello/:namehello/*这类的规则。
  • 鉴权:没有分组/统一鉴权的能力,需要在每个路由映射的 handler 中实现。
  • 模板:没有统一简化的 HTML 机制。

当我们离开框架,使用基础库时,需要频繁手工处理的地方,就是框架的价值所在。

标准库

Gee 本质上是对标准库的封装,先来看看标准库的用法

package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", indexHandler)
	http.HandleFunc("/hello", helloHandler)
	log.Fatal(http.ListenAndServe(":9999", nil))
}

// handler echoes r.URL.Path
func indexHandler(w http.ResponseWriter, req *http.Request) {
	fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
}

// handler echoes r.URL.Header
func helloHandler(w http.ResponseWriter, req *http.Request) {
	for k, v := range req.Header {
		fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
	}
}

重点是这里

log.Fatal(http.ListenAndServe(":9999", nil))

点去实现

func ListenAndServe(addr string, handler Handler) error {

再点 Handler 跳转实现

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

通过查看net/http的源码可以发现,Handler是一个接口,需要实现方法  ServeHTTP ,也就是说,只要传入任何实现了  ServerHTTP  接口的实例,所有的 HTTP 请求,就都交给了该实例处理了。

因此搞一个有 ServeHTTP 方法的 Engine 类型就好了

// Engine is the uni handler for all requests
type Engine struct{}

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	switch req.URL.Path {
	case "/":
		fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
	case "/hello":
		for k, v := range req.Header {
			fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
		}
	default:
		fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
	}
}

func main() {
	engine := new(Engine)
	log.Fatal(http.ListenAndServe(":9999", engine))
}

Note

在实现Engine之前,我们调用  http.HandleFunc  实现了路由和 Handler 的映射,也就是只能针对具体的路由写处理逻辑。比如/hello。但是在实现Engine之后,我们拦截了所有的 HTTP 请求,拥有了统一的控制入口。在这里我们可以自由定义路由映射的规则,也可以统一添加一些处理逻辑,例如日志、异常处理等。

雏形

使用层

package main

import (
	"fmt"
	"net/http"

	"gee"
)

func main() {
	r := gee.New()
	r.GET("/", func(w http.ResponseWriter, req *http.Request) {
		fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
	})

	r.GET("/hello", func(w http.ResponseWriter, req *http.Request) {
		for k, v := range req.Header {
			fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
		}
	})

	r.Run(":9999")
}
package gee

import (
	"fmt"
	"net/http"
)

// HandlerFunc defines the request handler used by gee
type HandlerFunc func(http.ResponseWriter, *http.Request)

// Engine implement the interface of ServeHTTP
type Engine struct {
	router map[string]HandlerFunc
}

// New is the constructor of gee.Engine
func New() *Engine {
	return &Engine{router: make(map[string]HandlerFunc)}
}

func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
	key := method + "-" + pattern
	engine.router[key] = handler
}

// GET defines the method to add GET request
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
	engine.addRoute("GET", pattern, handler)
}

// POST defines the method to add POST request
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
	engine.addRoute("POST", pattern, handler)
}

// Run defines the method to start a http server
func (engine *Engine) Run(addr string) (err error) {
	return http.ListenAndServe(addr, engine)
}

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	key := req.Method + "-" + req.URL.Path
	if handler, ok := engine.router[key]; ok {
		handler(w, req)
	} else {
		fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
	}
}

Note

  • 首先定义了类型HandlerFunc,这是提供给框架用户的,用来定义路由映射的处理方法。我们在Engine中,添加了一张路由映射表router,key 由请求方法和静态路由地址构成,例如GET-/GET-/helloPOST-/hello,这样针对相同的路由,如果请求方法不同,可以映射不同的处理方法(Handler),value 是用户映射的处理方法。
  • 当用户调用(*Engine).GET()方法时,会将路由和处理方法注册到映射表  router  中,(*Engine).Run()方法,是  ListenAndServe  的包装。
  • Engine实现的  ServeHTTP  方法的作用就是,解析请求的路径,查找路由映射表,如果查到,就执行注册的处理方法。如果查不到,就返回  404 NOT FOUND 。

Context

先看使用层

func main() {
	r := gee.New()
	r.GET("/", func(c *gee.Context) {
		c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
	})
	r.GET("/hello", func(c *gee.Context) {
		// expect /hello?name=geektutu
		c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
	})

	r.POST("/login", func(c *gee.Context) {
		c.JSON(http.StatusOK, gee.H{
			"username": c.PostForm("username"),
			"password": c.PostForm("password"),
		})
	})

	r.Run(":9999")
}

为啥要设计 Context

  1. 简化操作
  2. 考虑额外功能

Note

框架需要支持中间件,那中间件产生的信息放在哪呢?Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。因此,设计 Context 结构,扩展性和复杂性留在了内部,而对外简化了接口。路由的处理函数,以及将要实现的中间件,参数都统一使用 Context 实例, Context 就像一次会话的百宝箱,可以找到任何东西。

给 handle 前先生成 context

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := newContext(w, req)
	engine.router.handle(c)
}

在 Context 对一些常用的能力进行封装

func (c *Context) SetHeader(key string, value string) {
	c.Writer.Header().Set(key, value)
}

func (c *Context) String(code int, format string, values ...interface{}) {
	c.SetHeader("Content-Type", "text/plain")
	c.Status(code)
	c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
}

func (c *Context) JSON(code int, obj interface{}) {
	c.SetHeader("Content-Type", "application/json")
	c.Status(code)
	encoder := json.NewEncoder(c.Writer)
	if err := encoder.Encode(obj); err != nil {
		http.Error(c.Writer, err.Error(), 500)
	}
}

前缀树路由 Router

介绍

实现动态路由最常用的数据结构,被称为前缀树(Trie 树)。

  • /:lang/doc
  • /:lang/tutorial
  • /:lang/intro
  • /about
  • /p/blog
  • /p/related

Pasted image 20231030163703

tire.go

type node struct {
    pattern  string  // 是否一个完整的url,不是则为空字符串
    part     string  // URL块值,用/分割的部分,比如/abc/123,abc和123就是2个part
    children []*node // 该节点下的子节点
    isWild   bool    // 是否模糊匹配,比如:filename或*filename这样的node就为true
}

路由的相关流程有 2

  1. 构建过程
  2. 匹配过程
func TestNew1(t *testing.T) {
	r := newRouter()
	r.addRoute("GET", "/", nil)
	r.addRoute("GET", "/hello/:name", nil)

	n, params := r.getRoute("GET", "/hello/geektutu")
	fmt.Printf("1: %v %v\n", n, params)
	fmt.Printf("2: %v\n", r.getRoutes("GET"))
}

输出如下

1: node{pattern=/hello/:name, part=:name, isWild=true} map[name:geektutu]
2: [node{pattern=/, part=, isWild=false} node{pattern=/hello/:name, part=:name, isWild=true}]

构建流程

GET-/ 的构建

Pasted image 20231030164528

{
  "pattern": "/",
  "part": "",
  "children": [],
  "isWild": false
}

GET-/hello/:name 的构建

Pasted image 20231030165501

{
  "pattern": "/",
  "part": "",
  "children": [{ "pattern": "", "part": "hello", "children": [], "isWild": false }],
  "isWild": false
}

Pasted image 20231030170054

{
  "pattern": "/",
  "part": "",
  "children": [
    {
      "pattern": "",
      "part": "hello",
      "children": [
        {
          "pattern": "/hello/:name",
          "part": ":name",
          "isWild": true
        }
      ],
      "isWild": false
    }
  ],
  "isWild": false
}

在下一层递归调用 insert 的时候加上的 pattern

func (n *node) insert(pattern string, parts []string, height int) {
	if len(parts) == height {
		// 如果已经匹配完了,那么将pattern赋值给该node,表示它是一个完整的url
		// 这是递归的终止条件
		n.pattern = pattern
		return
	}

核心代码可见

匹配流程

n, params := r.getRoute("GET", "/hello/geektutu")

Pasted image 20231030170716

利用 node 中存储的 part 和 匹配传入的 parts 进行一层又一层的寻找

核心代码可见

Param

根据 pattern 和 searchParts 去做匹配的

func (r *router) getRoute(method string, path string) (*node, map[string]string) {
	searchParts := parsePattern(path)
	params := make(map[string]string)
	root, ok := r.roots[method]

	if !ok {
		return nil, nil
	}

	n := root.search(searchParts, 0)

	if n != nil {
		parts := parsePattern(n.pattern)
		for index, part := range parts {
			if part[0] == ':' {
				params[part[1:]] = searchParts[index]
			}
			if part[0] == '*' && len(part) > 1 {
				params[part[1:]] = strings.Join(searchParts[index:], "/")
				break
			}
		}
		return n, params
	}

	return nil, nil
}

分组控制

Note

分组控制(Group Control)是 Web 框架应提供的基础功能之一。所谓分组,是指路由的分组。如果没有路由分组,我们需要针对每一个路由进行控制。但是真实的业务场景中,往往某一组路由需要相似的处理。例如:

  • /post开头的路由匿名可访问。
  • /admin开头的路由需要鉴权。
  • /api开头的路由是 RESTful 接口,可以对接第三方平台,需要三方平台鉴权。

大部分情况下的路由分组,是以相同的前缀来区分的。因此,我们今天实现的分组控制也是以前缀来区分,并且支持分组的嵌套。例如/post是一个分组,/post/a/post/b可以是该分组下的子分组。作用在/post分组上的中间件(middleware),也都会作用在子分组,子分组还可以应用自己特有的中间件。

中间件可以给框架提供无限的扩展能力,应用在分组上,可以使得分组控制的收益更为明显,而不是共享相同的路由前缀这么简单。例如/admin的分组,可以应用鉴权中间件;/分组应用日志中间件,/是默认的最顶层的分组,也就意味着给所有的路由,即整个框架增加了记录日志的能力。

使用层为

func main() {
	r := gee.New()
	r.GET("/index", func(c *gee.Context) {
		c.HTML(http.StatusOK, "<h1>Index Page</h1>")
	})
	v1 := r.Group("/v1")
	{
		v1.GET("/", func(c *gee.Context) {
			c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
		})

		v1.GET("/hello", func(c *gee.Context) {
			// expect /hello?name=geektutu
			c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
		})
	}
	v2 := r.Group("/v2")
	{
		v2.GET("/hello/:name", func(c *gee.Context) {
			// expect /hello/geektutu
			c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
		})
		v2.POST("/login", func(c *gee.Context) {
			c.JSON(http.StatusOK, gee.H{
				"username": c.PostForm("username"),
				"password": c.PostForm("password"),
			})
		})

	}

	r.Run(":9999")
}

结构

type (
	RouterGroup struct {
		prefix      string
		middlewares []HandlerFunc // support middleware
		parent      *RouterGroup  // support nesting
		engine      *Engine       // all groups share a Engine instance
	}

	Engine struct {
		*RouterGroup
		router *router
		groups []*RouterGroup // store all groups
	}
)
	Engine struct {
		*RouterGroup

这种写法类似 Go 中的继承,可参考理解

// New is the constructor of gee.Engine
func New() *Engine {
	engine := &Engine{router: newRouter()}
	engine.RouterGroup = &RouterGroup{engine: engine}
	engine.groups = []*RouterGroup{engine.RouterGroup}
	return engine
}

// Group is defined to create a new RouterGroup
// remember all groups share the same Engine instance
func (group *RouterGroup) Group(prefix string) *RouterGroup {
	engine := group.engine
	newGroup := &RouterGroup{
		prefix: group.prefix + prefix,
		parent: group,
		engine: engine,
	}
	engine.groups = append(engine.groups, newGroup)
	return newGroup
}

func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) {
	pattern := group.prefix + comp
	log.Printf("Route %4s - %s", method, pattern)
	group.engine.router.addRoute(method, pattern, handler)
}

// GET defines the method to add GET request
func (group *RouterGroup) GET(pattern string, handler HandlerFunc) {
	group.addRoute("GET", pattern, handler)
}

// POST defines the method to add POST request
func (group *RouterGroup) POST(pattern string, handler HandlerFunc) {
	group.addRoute("POST", pattern, handler)
}

中间件

RouterGroup struct {
	prefix      string
	middlewares []HandlerFunc // support middleware

刚刚定义的 Group 能力为中间件的存放留了存储位置

重点是 在 router handle 的时候来使用中间件的逻辑

func (r *router) handle(c *Context) {
	n, params := r.getRoute(c.Method, c.Path)

	if n != nil {
		key := c.Method + "-" + n.pattern
		c.Params = params
		c.handlers = append(c.handlers, r.handlers[key])
	} else {
		c.handlers = append(c.handlers, func(c *Context) {
			c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
		})
	}
	c.Next()
}

Context 中对 中间件进行支持

type Context struct {
	// origin objects
	Writer http.ResponseWriter
	Req    *http.Request
	// request info
	Path   string
	Method string
	Params map[string]string
	// response info
	StatusCode int
	handlers []HandlerFunc // 这里存放的不仅有 middleware,还有最终的处理函数
	index    int // 当前访问到哪个 hander 了
}

func newContext(w http.ResponseWriter, req *http.Request) *Context {
	return &Context{
		Path:   req.URL.Path,
		Method: req.Method,
		Req:    req,
		Writer: w,
		index:  -1,
	}
}

func (c *Context) Next() {
	c.index++
	s := len(c.handlers)
	for ; c.index < s; c.index++ {
		c.handlers[c.index](c)
	}
}

Context 中的中间件是在 ServeHTTP 中对已有的 groups 进行扫描加入的

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	var middlewares []HandlerFunc
	for _, group := range engine.groups {
		if strings.HasPrefix(req.URL.Path, group.prefix) {
			middlewares = append(middlewares, group.middlewares...)
		}
	}
	c := newContext(w, req)
	c.handlers = middlewares
	engine.router.handle(c)
}

以 / 路由的访问为例子,调用栈如下

Pasted image 20231031105713

Template

利用 http.FileServer 来处理 assets 各种资源的获取

r.Static("/assets", "./static")

func (group *RouterGroup) Static(relativePath string, root string) {
	handler := group.createStaticHandler(relativePath, http.Dir(root))
	urlPattern := path.Join(relativePath, "/*filepath")
	// Register GET handlers
	group.GET(urlPattern, handler)
}

Note

/assets/*filepath,可以匹配/assets/开头的所有的地址。例如/assets/js/geektutu.js,匹配后,参数filepath就赋值为js/geektutu.js

模板渲染这里也是 html/template 套一层官方的能力

结合代码理解并不复杂,略

错误恢复

// hello.go
func test_recover() {
	defer func() {
		fmt.Println("defer func")
		if err := recover(); err != nil {
			fmt.Println("recover success")
		}
	}()

	arr := []int{1, 2, 3}
	fmt.Println(arr[4])
	fmt.Println("after panic")
}

func main() {
	test_recover()
	fmt.Println("after recover")
}
$ go run hello.go
defer func
recover success
after recover

利用 中间件的机制来完成错误恢复

Pasted image 20231031171249

// Default use Logger() & Recovery middlewares
func Default() *Engine {
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}

React 中对于 asynchronous operation 的处理

引言

最近和@Bolt ᶘ ᵒᴥᵒᶅ 聊到一些React项目中异步数据的处理优化,结合着搜了一些资料,有些收获,予以记录。

为什么说 asynchronous operation ?
https://react.dev/blog/2022/03/29/react-v18#suspense-in-data-frameworks

As in previous versions of React, you can also use Suspense for code splitting on the client with React.lazy. But our vision for Suspense has always been about much more than loading code — the goal is to extend support for Suspense so that eventually, the same declarative Suspense fallback can handle any asynchronous operation (loading code, data, images, etc).

自然是保持 React 官方对这个行为的描述

问题

  1. 对于接口数据,即使使用 类似 swr 的工具,我们也有很多的状态要写在逻辑层进行处理,这种工作非常无趣
import useSWR from "swr";
function App() {
  const fetcher = (...args) => fetch(...args).then((res) => res.json());

  // fetch data
  const { data, error } = useSWR(
    "https://jsonplaceholder.typicode.com/todos/1",
    fetcher
  );

  if (error) return <div>failed to load</div>;
  if (!data) return <div>loading...</div>;

  // render data
  return (
    <div className="App">
      <h2>{data.title}</h2>
      <p>UserId: {data.userId}</p>
      <p>Id: {data.id}</p>
      {data.completed? <p>Completed</p> : <p>Not Completed</p>}
    </div>
  );
}

export default App;
  1. 有些接口数据之间存在依赖关系,有些并没有,但业务中我们常常是集中处理接口情况,会造成不必要的等待,无法实现完美的按需 loading 提示。
    「懒得写例子了」
useEffect(() => {
	Promise.all([a, b, c])
	...
})

只消费 a

消费 b、c

方案

新React hook: use

依赖注意

  "dependencies": {
    "react": "experimental",
    "react-dom": "experimental"
  },
import { Suspense, useMemo, use } from "react"

function Comp({ data: prosData }) {
  const data = use(prosData)
  return <div>{JSON.stringify(data)}</div>
}

const mockApi = () =>
  new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        name: "John",
      })
    }, 3000)
  })

function App() {
  const promiseData = useMemo(() => mockApi(), [])

  return (
    <Suspense fallback={<div>loading ++</div>}>
      <Comp data={promiseData} />
    </Suspense>
  )
}

export default App

行为描述:
use(prosData) 会告知 最近的 Suspense「A」 数据情况,若处于 pending 状态,则走 A 的 fallback 行为
无视常规 hook 要求的规则,如不能 condition

  • 缺点
    • 处于 experimental 阶段
  • 优点
    • React 官方支持,用起来非常爽

本质上,就是在 pending 状态下 throw promise,下文有进一步的代码描述

Router Await

限定下问题:穿入数据为 Promise,自行会 loading
我们不难写出如下通用组件来完成

export function Await(props: AwaitProps) {
  const { children, resolve } = props
  const [resolved, setResolved] = useState(false)
  const [res, setRes] = useState(null)
  useEffect(() => {
    if (resolve instanceof Promise) {
      resolve.then((res) => {
        setResolved(true)
        setRes(res)
      })
    } else {
      setResolved(true)
    }
  }, [resolve])

  if (!resolved) {
    return <div>loading</div>
  }
  return children(res)
}


// 用法
          <Await resolve={comp1Data}>
            {(res) => {
              return <Comp data={res} />
            }}
          </Await>

大致是这样的思路,React Router 已经干的很好了

import { Await, useLoaderData } from "react-router-dom";

function Book() {
  const { book, reviews } = useLoaderData();
  return (
    <div>
      <h1>{book.title}</h1>
      <p>{book.description}</p>
      <React.Suspense fallback={<ReviewsSkeleton />}>
        <Await
          resolve={reviews}
          errorElement={
            <div>Could not load reviews 😬</div>
          }
          children={(resolvedReviews) => (
            <Reviews items={resolvedReviews} />
          )}
        />
      </React.Suspense>
    </div>
  );
}

declare function Await(
  props: AwaitProps
): React.ReactElement;

interface AwaitProps {
  children: React.ReactNode | AwaitResolveRenderFunction;
  errorElement?: React.ReactNode;
  resolve: TrackedPromise | any;
}

interface AwaitResolveRenderFunction {
  (data: Awaited<any>): React.ReactElement;
}

Note: <Await> expects to be rendered inside of a <React.Suspense> or <React.SuspenseList> parent to enable the fallback UI.

和自己实现的弱鸡版本相比,React Router 的 Await 可以利用 React 的 Suspense

你可能会好奇,咋做到通知 Suspense 的呢?

在官网的 Suspense 中 点 fock 可以看到示例代码
https://codesandbox.io/s/s9zlw3?file=/Albums.js&utm_medium=sandpack

export default function Albums({ artistId }) {
  const albums = use(fetchData(`/${artistId}/albums`));
  return (
    <ul>
      {albums.map(album => (
        <li key={album.id}>
          {album.title} ({album.year})
        </li>
      ))}
    </ul>
  );
}

// This is a workaround for a bug to get the demo running.
// TODO: replace with real implementation when the bug is fixed.
function use(promise) {
  if (promise.status === 'fulfilled') {
    return promise.value;
  } else if (promise.status === 'rejected') {
    throw promise.reason;
  } else if (promise.status === 'pending') {
    throw promise;
  } else {
    promise.status = 'pending';
    promise.then(
      result => {
        promise.status = 'fulfilled';
        promise.value = result;
      },
      reason => {
        promise.status = 'rejected';
        promise.reason = reason;
      },      
    );
    throw promise;
  }
}
  • 优点
    • 现在就能用上
  • 缺点
    • 还是没有 use 用起来优雅

总结

业务使用中,可以把业务数据请求结果抽到合理的上层,向下传递 Promise
合理放置 Suspense,减少不必要的 rerender

其他

2021年12月9日 React 18 Keynote 就提过本文讲的内容
因为 use 这个在官方文档还找不到,推荐去 rfcs 里看下出处

Mac 下命令行启动的应用,授予权限无法生效

最近的业务开发需要用到命令行来启动飞书,但这样启动的飞书总会报你没有xxx的权限,如屏幕分享的场景,按着指引开了相关权限也不生效。

原以为是飞书自己的问题,没处理好系统的权限授予,现了解到是和你用命令行启动飞书的软件有关,你需要也给它开相关的权限。

如我是用 iTerm2 来启动飞书的,那给 iTerm2 也加上相关的权限就好了

Sparse Arrays

中文 Sparse Arrays 叫作稀疏数组

控制台打印会展示

(8) [empty × 8]

一些老的 API 执行会跳过这些 empty 的数组,新的会把这些 empty 作为 undefined 来执行

以下内容摘自 MDN

Array methods and empty slots

Empty slots in sparse arrays behave inconsistently between array methods. Generally, the older methods will skip empty slots, while newer ones treat them as undefined.

Among methods that iterate through multiple elements, the following do an in check before accessing the index and do not conflate empty slots with undefined:

For exactly how they treat empty slots, see the page for each method.

These methods treat empty slots as if they are undefined:

刷LeetCode的性价比在降低,如何正确高效利用LeetCode

  • https://www.bilibili.com/video/BV1kN411e7f6
  • 为什么会出现 LeetCode 算法面试
    • 筛选 聪明 / 勤奋 的人
    • 上升期,培养成本能够接收
    • 大公司的技术封闭性
    • 简化流程,先有的普遍有算法面试,才有的 LeetCode
  • 缺点
    • 套路化
    • 挤压其他能力的时间投入
  • 趋势
    • 权重降低、出也是高频部分
    • 注重领域和经验,匹配度;降低培养成本
  • 如何利用 LeetCode 更高效?
    • 只刷高频题,Top100
    • 作为学习新语言的工具

异步请求后进行 window.open 被拦截

问题如题,去找了一些参考,为什么要被拦截

https://javascript.info/popup-windows

In the past, evil sites abused popups a lot. A bad page could open tons of popup windows with ads. So now most browsers try to block popups and protect the user.

Most browsers block popups if they are called outside of user-triggered event handlers like onclick.

For example:

// popup blocked
window.open('https://javascript.info');

// popup allowed
button.onclick = () => {
  window.open('https://javascript.info');
};

This way users are somewhat protected from unwanted popups, but the functionality is not disabled totally.

出于安全和用户友好考虑,限制了 window.open 的调用

如何解决呢?

const mockApi = () => new Promise((resolve) => {
  setTimeout(() => {
    resolve('hello')
  }, 4000)

})

function Demo1() {
  useEffect(() => {
    mockApi().then((res) => {
      // window.open("https://baidu.com")
    })
  }, [])

  return (
    <div onClick={async () => {
      const newWindow = window.open() as any
      await mockApi()
      if (!newWindow?.location) {
        return
      }
      newWindow.location.href = 'https://baidu.com'
    }}>Demo1</div>
  )
}

如何定制化 dotted 的距离

原来是使用

.xxx {
	border-bottom: 1px dotted rgb(100, 106, 115);
}

来搞的,UI反馈太密了,要求按设计稿走

方案如下

.border-pop-underline-container {
  background-image: linear-gradient(to right, black 33%, rgba(255, 255, 255, 0) 33% 100%);
  background-position: bottom;
  background-size: 3px 1px;
  background-repeat: repeat-x;
}

background-image 中使用 linear-gradient
to right, black 33%, rgba(255, 255, 255, 0) 33% 100%
含义为方向是朝右,33%之前是黑色,33% - 100% 之间是白色

3px 是每个单位的宽度

MDN 上可以找到依据

body {
  background: linear-gradient(
    to right,
    red 20%,
    orange 20% 40%,
    yellow 40% 60%,
    green 60% 80%,
    blue 80%
  );
}

image.png

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.