katex
Latex/MathJax/Katex 数学公式手册
| 字母 | Katex | 字母 | Katex | 字母 | Katex | 字母 | Katex |
|---|---|---|---|---|---|---|---|
| \alpha | \beta | \gamma | \delta | ||||
| \epsilon | \zeta | \eta | \theta | ||||
| \iota | \kappa | \lambda | \mu | ||||
| \nu | \xi | \omicron | \pi | ||||
| \rho | \sigma | \tau | \upsilon | ||||
| \phi | \chi | \psi | \omega |
Latex/MathJax/Katex 数学公式手册
| 字母 | Katex | 字母 | Katex | 字母 | Katex | 字母 | Katex |
|---|---|---|---|---|---|---|---|
| \alpha | \beta | \gamma | \delta | ||||
| \epsilon | \zeta | \eta | \theta | ||||
| \iota | \kappa | \lambda | \mu | ||||
| \nu | \xi | \omicron | \pi | ||||
| \rho | \sigma | \tau | \upsilon | ||||
| \phi | \chi | \psi | \omega |
二次贝塞尔曲线的公式为:
我们先来回归一下高中数学,二次函数的标准形式为:
其中, 为二次项系数, 为一次项系数, 为常数项。
二次函数的图像为抛物线,抛物线的顶点坐标为:
我们可以把二次贝塞尔曲线的公式转换为二次函数的标准形式:
拿二阶过程描述如下:
高阶的不过是需要多重复几次直到剩一条线段,然后再取 t 值计算出点。
此过程可以用向量表示
程序设计
这个公式是贝塞尔曲线的定义。它描述了如何根据一组控制点 P_i 和一个参数 t 计算贝塞尔曲线上的点 B(t)。
function bezier(points, t) {
const n = points.length - 1;
let x = 0;
let y = 0;
for (let i = 0; i <= n; i++) {
const b = binomial(n, i);
const a = Math.pow(1 - t, n - i);
const c = Math.pow(t, i);
x += b * a * c * points[i].x;
y += b * a * c * points[i].y;
}
return { x, y };
}
function binomial(n, i) {
return factorial(n) / (factorial(i) * factorial(n - i));
}
function factorial(n) {
let result = 1;
for (let i = 1; i <= n; i++) {
result *= i;
}
return result;
}
binomial 这里查看 二项式定理
function bezier(points, t) {
const n = points.length - 1;
if (n === 0) {
return points[0];
}
const left = bezier(points.slice(0, n), t);
const right = bezier(points.slice(1, n + 1), t);
return {
x: (1 - t) * left.x + t * right.x,
y: (1 - t) * left.y + t * right.y,
};
}
公式模板
在使用Rollup构建企业代码库的时候,对于其使用方式,尤其是依赖管理存在一些疑虑,加以分析说明。
在搜索框里敲「react」:r、re、rea、reac、react——如果每次敲键都发一次请求,一次输入就白白浪费 4 次网络请求,而且前 4 次的结果你根本来不及看,它们已经在回来的路上了。再往下想:滚动页面时 onScroll 一秒钟触发几十次,resize 拖拽时更是每帧一次。
这类问题有一个共同的名字:事件风暴——短时间内触发太多次,而真正需要执行的只有少数几次。
事件风暴的本质是:触发频率远高于执行需求。那么「到底该执行哪几次、什么时候执行」,其实只取决于你对这件事的期望。把期望问清楚,答案自己就出来了。
输入搜索、窗口 resize、表单项校验……这些场景里,过程不重要,结果才重要。你敲了一长串,真正需要的只是「停下来之后」那一次的搜索结果;窗口拖了一路,真正需要的只是「停住之后」那一次的布局重算。
防抖的定义用一句话说:持续触发时一律不执行,直到停止触发后过一段时间,才执行最后一次。
一个贴切的比喻是电梯关门:有人进出,电梯就重新计时关门,等人流彻底安静了,才关门上楼。你不在乎期间谁进进出出,只在乎「最后走的那一刻」。
滚动加载更多、滚动进度条、拖拽坐标跟随、游戏里的连点……这些场景里,过程是有价值的——滚到一半就该触发加载,进度条该跟着走。但你不能让每一次 scroll 都触发,那太浪费了。
节流的定义用一句话说:保证一定时间间隔内最多执行一次,但会持续执行,直到结束。
贴切的比喻是班车时刻表:每 10 分钟一班,不管站台多少人、什么时候来人,到点就发。你保证的是「规律性」——不会因为人挤就狂发车,也不会因为没人就永远不发。
心智模型:
| 防抖(debounce) | 节流(throttle) | |
|---|---|---|
| 模型 | 电梯等人,人静才关门 | 班车到点发车 |
| 关心的事 | 最后一次的结果 | 过程中的规律性 |
| 触发密集时 | 全部取消,只等最后 | 固定节奏放行 |
防抖的核心:新来的调用,作废旧的计时。 翻译成代码:
function debounce(fn, wait = 300) {
let timer = null;
return function (...args) {
clearTimeout(timer); // ① 取消上一个还没生效的定时器
timer = setTimeout(() => fn(...args), wait); // ② 重新计时
};
}
逐行看为什么必须这么写:
timer? 因为每次触发都要「看到」同一个 timer 才能取消它。timer 藏在闭包里,新函数每次调用共享这一个变量。clearTimeout 再 setTimeout? 这一句就是防抖的全部秘密。时间线:t=0 调用 → 定 300ms → t=100ms 又调用 → 取消 t=0 那次、重排到 t=400ms → 只要你一直在敲,它就永远在重排、永远不执行 → 你一停,300ms 后执行最后一次。这个「重置」动作,就是前面说的电梯重新计时。
如果直接用 () => fn(...args),this 就丢了。当防抖函数被用在事件监听里(el.addEventListener('click', debounced)),监听器内部的 this 是那个元素;但 setTimeout 回调里的 this 不是它。所以要先记住:
function debounce(fn, wait = 300) {
let timer = null;
return function (...args) {
const context = this; // 记住调用时的 this
clearTimeout(timer);
timer = setTimeout(() => fn.apply(context, args), wait);
};
}
到这里,一个「停下来才执行」的防抖就好了——这是**后缘(trailing)**版本,也是输入搜索最常用的形态。
有个反例:防连点。用户快速双击「提交」按钮,你希望第一次点击就立即执行,后面的点击才被防抖掉。如果只用上面的 trailing 版本,第一次点击也会被延迟 300ms,体验很差。
于是防抖又有了 前缘(leading):一个周期开始先立即执行一次,之后进入等待。
function debounce(fn, wait = 300, { leading = false } = {}) {
let timer = null;
return function (...args) {
const context = this;
const isIdle = timer === null; // 当前没有在等待 = 一个周期的开始
clearTimeout(timer);
timer = setTimeout(() => {
timer = null; // 等待结束,回到空闲
if (!leading) fn.apply(context, args); // trailing 模式在结束时补执行
}, wait);
if (leading && isIdle) fn.apply(context, args); // leading 模式:空闲时立即执行
};
}
注意这里的取舍:leading 模式第一次立即执行、之后防抖;trailing 模式全部延迟、只在安静后执行最后一次。两者通常二选一。
节流的核心:记上次执行的时间,间隔不够就拦下。
function throttle(fn, wait = 200) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= wait) { // 距上次执行够久了 → 放行
lastTime = now;
fn.apply(this, args);
}
};
}
这就是时间戳闸门:第一次调用时 lastTime=0、now 是很大的毫秒时间戳,now - 0 远超 wait,直接放行并记录;之后 wait 毫秒内的调用全被 if 拦下;时间一到,第一次撞上来的调用放行。
它有个优点:首次立即执行,绝不空等。但也有个隐藏的坑:被拦在闸门内的最后一次调用,被永久丢弃了。
场景很真实:滚动到底部触发「加载更多」,如果最后一次 scroll 恰好滚到了底,却撞在闸门内被丢掉,加载就永远不触发。所以你还需要一个兜底——闸门放行的同时,给「卡在门里」的调用排一个定时器,等间隔到了补执行最后一次。
function throttle(fn, wait = 200, { leading = true, trailing = true } = {}) {
let lastTime = 0;
let timer = null;
let lastArgs, lastCtx;
const invoke = () => {
lastTime = Date.now();
timer = null;
fn.apply(lastCtx, lastArgs);
};
return function (...args) {
lastCtx = this;
lastArgs = args;
const now = Date.now();
const remaining = wait - (now - lastTime);
if (remaining <= 0) { // 间隔已到 → 立即放行(leading)
clearTimeout(timer);
invoke();
} else if (!timer) { // 还没到 → 排定时器,兜住最后一次(trailing)
timer = setTimeout(invoke, remaining);
}
};
}
于是节流从「时间戳闸门」生长成了「闸门 + 定时器兜底」:闸门保证节奏(leading),定时器保证不丢最后一次(trailing)。这已经是生产级的形态了。
看到这里你会发现,防抖和节流的完整版都有 leading / trailing 两个开关——它们其实是同一对「前后缘」概念:
| leading(前缘) | trailing(后缘) | |
|---|---|---|
| 防抖 | 周期开始立即执行一次,之后防抖 | 周期结束(安静后)执行最后一次 |
| 节流 | 间隔到点立即执行 | 结束时补执行被拦住的最后一次 |
| 一句话 | 「先做再说」 | 「做完收尾」 |
配上场景,选择就顺理成章了:
| 场景 | 选型 | 原因 |
|---|---|---|
| 输入框实时搜索 | 防抖(trailing) | 只关心停下来的最终结果 |
| 按钮防连点 | 防抖(leading) | 第一次要立刻响应 |
| 滚动加载更多 / 滚动进度 | 节流(leading + trailing) | 过程中要触发,且不能丢最后 |
| resize 重算布局 | 防抖 | 等 resize 停下来再做 |
| mousemove 坐标跟随 | 节流 | 跟住但不每帧执行 |
一个补充:如果你要的是「每帧执行一次」的视觉类工作(动画、拖拽预览),那其实还有第三个更专门的工具——
requestAnimationFrame。它本质上是「以浏览器帧率为间隔的节流」,浏览器会帮你合并到帧回调里,视觉场景下优先用它。
在 React 里,上面的闭包逻辑要套上 hook 的「生命周期」才能用对——尤其注意两点:回调要始终拿到最新闭包(用 ref 兜底),卸载时要清理定时器(避免 setState 在卸载后执行)。
最常用的形态是「防抖一个值」——输入内容进 state,经过防抖的值才去发请求:
import { useEffect, useState } from 'react';
function useDebounce<T>(value: T, delay = 300): T {
const [debounced, setDebounced] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer); // 依赖变化 → 取消上一次计时 = 电梯重新关门
}, [value, delay]);
return debounced;
}
用法——搜索框只跟着防抖后的值发请求:
function SearchBox() {
const [keyword, setKeyword] = useState('');
const debouncedKeyword = useDebounce(keyword, 300);
useEffect(() => {
if (debouncedKeyword) fetchSearch(debouncedKeyword);
}, [debouncedKeyword]);
return <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />;
}
把「动作」本身防抖,返回一个稳定引用(useCallback 缓存),不会让子组件因每次渲染拿到新函数而重新渲染:
import { useCallback, useEffect, useRef } from 'react';
function useDebouncedCallback<T extends (...args: any[]) => void>(
callback: T,
delay = 300,
) {
const callbackRef = useRef(callback);
const timerRef = useRef<ReturnType<typeof setTimeout>>();
useEffect(() => {
callbackRef.current = callback; // 始终拿到最新闭包
});
useEffect(() => () => clearTimeout(timerRef.current), []); // 卸载清理
return useCallback(
(...args: Parameters<T>) => {
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => callbackRef.current(...args), delay);
},
[delay],
);
}
把「闸门 + 定时器兜底」完整封装:
function useThrottledCallback<T extends (...args: any[]) => void>(
callback: T,
delay = 200,
) {
const callbackRef = useRef(callback);
const timerRef = useRef<ReturnType<typeof setTimeout>>();
const lastTimeRef = useRef(0);
const lastArgsRef = useRef<Parameters<T>>();
useEffect(() => {
callbackRef.current = callback;
});
useEffect(() => () => clearTimeout(timerRef.current), []);
return useCallback(
(...args: Parameters<T>) => {
lastArgsRef.current = args;
const now = Date.now();
const remaining = delay - (now - lastTimeRef.current);
if (remaining <= 0) {
// 间隔已到 → 立即执行,并取消待决的定时器
clearTimeout(timerRef.current);
timerRef.current = undefined;
lastTimeRef.current = now;
callbackRef.current(...args);
} else if (!timerRef.current) {
// 还没到 → 排定时器,兜住最后一次
timerRef.current = setTimeout(() => {
timerRef.current = undefined;
lastTimeRef.current = Date.now();
callbackRef.current(...lastArgsRef.current!);
}, remaining);
}
},
[delay],
);
}
用法——滚动加载更多,节流 + 不丢最后一次:
const loadMore = useThrottledCallback(() => fetchNextPage(), 200);
useEffect(() => {
window.addEventListener('scroll', loadMore);
return () => window.removeEventListener('scroll', loadMore);
}, [loadMore]);
回到开头的问题。节流和防抖不是两个需要背的公式,而是你对同一件事的两个追问:
两个概念到完整形态后,会汇聚到同一对开关上:leading(先做再说)和 trailing(做完收尾)。理解了这对开关,你就能按场景自由选择,而不是背模板。
export { Editor, Element, Location, Node, Operation, Path, PathRef, Point, PointRef, Range, RangeRef, Span, Text, Transforms, createEditor };
Editor
createEditor
Transforms
/**
* Make all properties in T optional
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};
/**
* Make all properties in T required
*/
type Required<T> = {
[P in keyof T]-?: T[P];
};
/**
* Make all properties in T readonly
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
/**
* From T, pick a set of properties whose keys are in the union K
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
/**
* Construct a type with a set of properties K of type T
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T;
/**
* Extract from T those types that are assignable to U
*/
type Extract<T, U> = T extends U ? T : never;
/**
* Construct a type with the properties of T except for those in type K.
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
类型运用存乎一心
比如枚举用在特定场景,且能反向映射
enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
console.log(Days["Sun"] === 0); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true
console.log(Days[0] === "Sun"); // true
console.log(Days[1] === "Mon"); // true
console.log(Days[2] === "Tue"); // true
console.log(Days[6] === "Sat"); // true
事实上,上面的例子会被编译为:
var Days;
(function (Days) {
Days[Days["Sun"] = 0] = "Sun";
Days[Days["Mon"] = 1] = "Mon";
Days[Days["Tue"] = 2] = "Tue";
Days[Days["Wed"] = 3] = "Wed";
Days[Days["Thu"] = 4] = "Thu";
Days[Days["Fri"] = 5] = "Fri";
Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}));
react组件间的状态共享只能通过将state提升至公共祖先实现,带来一个显而易见的问题就是开发效率低
线性代数中最基础最根源的部分就是向量
二维坐标系中的一个箭头,从原点出发
向量是有序的数字列表,分别用 x,y,z 表示向量在坐标系箭头所在那一个点,一般我们竖着写,用方括号包起来:


存在两个向量,为了相加,将第二个向量平移,使得它的起点与第一个向量终点重合,然后画一个从第一个向量起点到第二个向量终点的箭头,就是他们的和。
为什么这样定义???
如果将向量看做在空间中的运动,先从向量 1 方向运动一段距离,再向向量 2 方向移动,总体效果与沿着这两个向量和方向运动一样。当然,顺序换一换也是一样。
从数值计算的角度而言,我们可以这样理解。先沿着 x 轴运动 1,再沿 y 轴运动 2,然后 x 轴运动 3,y 轴-1。
向量长度的伸缩
线性代数为数据分析提供了一条将大量数据列表概念化可视化的途径,另一方面,给计算机图形程序员提供了一种语言,通过处理数字来描述并操作空间
在 xy 坐标系中,有两个特别的向量,单位向量 i,j。
之前我们描述了向量加法的几何意义,所以可以将向量看成是单位向量经过伸缩后求和的结果。因此向量和可以看成是单位向量伸缩求和。
由此引申出一个问题:
我们完全可以选择不同的基向量,获得一个合理的新坐标系,比如两个不垂直的基向量完全可以通过数乘求和获得所有向量
目前我们需要知道的是,当我们用数来描述向量的时候,都依赖我们正在使用的基,两个数乘向量的和被称为这两个向量的线性组合。
可以这么理解:如果上面公式中 a,b 两个标量固定其中一个,逐渐改变另外一个,所产生的向量的终点最终会绘制出一条直线。

当同时变化两个标量,最终能到达平面内所有的点,即张开平面。(这两个向量同方向例外)
本质上是在问:仅通过向量加法和向量数乘这两种基本运算,能获得可能向量的集合是什么
如上所述,两个向量线性组合会张成平面,那么引入第三个向量,就可以张成空间。但是如果第三个向量或者有某两个向量重合,那么就会受困于这一平面,无法张开空间,即这一组向量中至少有一个是多余的,没有对张开空间做出任何贡献。我们引出线性相关的概念。

如果所有向量都给张成空间添加了新的维度,那么就是线性无关。
如果一个向量是另一个向量的线性组合,那么这两个向量是线性相关的
综上所述:
空间的一组基的严格定义是这样的:张成该空间的一个线性无关向量的集合
根据定义我们可以得到:
接下去我们了解:
线性变换的概念以及它和矩阵的关系
矩阵的向量乘法
变换只是函数一种花哨的说法,输入内容并输出相应结果,之所以用变换的说法,是在暗示一个运动的过程,(即一个输入向量经过变换,移动到输出位置)
空间的变换可以很复杂,但线性代数限制在一种特殊类型变换上——线性变换
如果一个变换具备以下性质,称之为线性的:
总的来说,应该把线性变换看做是保持网格线平行且等距分布的变换。
那么如何用数值表示这一过程呢(怎么给出一个公式,能拿到变换后的向量坐标)
实际上,你只需要记录两个基向量变换后的位置,其他向量会随之而动

也就是说,一个线性变换可以完全由两组数据决定——基向量变换后的坐标
因此我们将他们放在一个矩阵中,分别表示这两组数据
从某种意义上说,笛卡尔坐标中的向量本身也隐含了一种线性变换:比如(1,1)这个向量
再来看看旋转矩阵定义(旋转是最好理解的变换)的含义:
我们直接想象如果逆时针旋转 90 度,可以直接想出原来的 x 轴到了原来 y 轴位置,原来 y 轴到了原来-x 轴位置。单位向量的坐标就变成
我们可以这样理解这个过程(充分理解矩阵数乘的意义):
线性变换需要满足以下两个主要条件:
变换矩阵可以看做是基向量的坐标系的变换,即基向量的坐标系经过变换后的坐标系
我们将新的坐标[a,c] 和 [b,d] 代入上面的线性组合可以得到以下过程:
如果是变换多个向量,无非是把此过程进行分区,分别对每一组向量进行变换,然后再组合起来。
如何描述:一个变换之后再进行另一个变换
比如旋转之后再剪切的计算过程为:

其复合总体结果等效变换矩阵为
因此我们可以认为这个新矩阵就是两个变换矩阵的积
我们不必纠结记忆两个矩阵相乘的数学算法,而是明白其几何意义:
我们可以追踪基向量的变化:
然后我们引入通用计算
线性变换的结果可能对空间进行了扩大或缩小,如何测量面积变化的比例?
这个比例称为这个变换的行列式(一个矩阵的行列式是=3 说明面积扩大 3 倍,=0 说明压缩到一条线甚至一个点,只要检验行列式是否为 0 就能知道是否将空间压缩到更小维度)
当行列式为负,则改变了空间定向,一种理解是基向量 j 在 i 左边,如果变换后变到右边,则称为改变定向
如果 3 阶行列式为正,符合右手定则;如果为负,符合左手定则

首先应用 A 变换再应用 A 逆变换,你会回到原始状态
两个变换相继作用在代数上体现为矩阵乘法
所以 A 逆乘以 A 等于一个什么都不做的矩阵(恒等变换)
一旦找到 A 逆(且 A 的行列式不等于 0),然后可以来求解方程:
或许换下位置你会看的更清楚:
当行列式为 0 并不意味着方程无解
当变换结果为一条线时,也就是说结果是一维的,我们称这个变换的秩=1,代表维度
不管是直线、平面还是空间,所有可能的变换结果的集合称为矩阵的列空间,矩阵的列告诉你基向量变换后的位置,这些基向量张成的空间就是所有可能的变换结果,也就是说列空间就是矩阵列所张成的空间。
秩就是列空间的维数,当秩与列数相等称为满秩
当不满秩,会出现降维,会有一系列向量变换后落在原点,变换后落在原点的向量集合称为零空间。
对于一个方程而言,结果如果恰好是零向量,那么解就是零空间
3 行 2 列矩阵:
因为矩阵有两列表明输入空间有两个基向量,有三行表明每一个基向量在变换后都用三个独立坐标描述;
2 行 3 列矩阵:
3 列表明原始空间有 3 个基向量,两行表明这 3 个基向量在变换后都仅用两个坐标描述,一定落在二维空间,因此这是三维到二维空间的变换。
几何意义是

当两个向量相互垂直,点积为 0;方向大致相同,点积为正;点积是不受顺序影响的,这是因为,当两个向量对称,明显可以证明双方投影长度一致,所以结果不受顺序影响,当你放大其中一个向量(要么是投影随相同倍数放大,要么投影不变,向量成倍),显然结果不受顺序影响。
真正的叉积是通过两个三维向量生成一个新的三维向量
计算机 3D 图形的历史可以追溯到 20 世纪 60 年代,几乎和计算机本身的历史一样长。它被广泛应用于工程、教育、培训、建筑、金融、销售、市场、博彩、娱乐等各个领域。曾经 ,3D 图形只能用计算机软件渲染。如今,所有的计算机和移动设备都搭载了 3D 图形处理硬件,普通智能手机甚至有着比五年前的专业图形工作站更为优秀的图形处理能力。更重要的是现代 Web 浏览器也支持了 3D 渲染,相比昂贵的 3D 专用渲染 件,浏览器显然更普遍,更易于获取,并且是免费的。