普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月3日首页

每日一题-给 N x 3 网格图涂色的方案数🔴

2026年1月3日 00:00

你有一个 n x 3 的网格图 grid ,你需要用 红,黄,绿 三种颜色之一给每一个格子上色,且确保相邻格子颜色不同(也就是有相同水平边或者垂直边的格子颜色不同)。

给你网格图的行数 n 。

请你返回给 grid 涂色的方案数。由于答案可能会非常大,请你返回答案对 10^9 + 7 取余的结果。

 

示例 1:

输入:n = 1
输出:12
解释:总共有 12 种可行的方法:

示例 2:

输入:n = 2
输出:54

示例 3:

输入:n = 3
输出:246

示例 4:

输入:n = 7
输出:106494

示例 5:

输入:n = 5000
输出:30228214

 

提示:

  • n == grid.length
  • grid[i].length == 3
  • 1 <= n <= 5000

三种方法:记忆化搜索/递推/矩阵快速幂(Python/Java/C++/Go)

作者 endlesscheng
2025年12月30日 16:17

方法一:记忆化搜索(轮廓线 DP)

考虑暴力搜索,枚举每个格子涂哪种颜色。

从下往上涂色(这样可以在多组测试数据间复用记忆化的结果),我们需要知道:

  • 当前在给哪个格子涂色?用 $(i,j)$ 表示,即 $i$ 行 $j$ 列。
  • $i+1$ 行具体怎么涂色?用 $\textit{preRow}$ 表示。$(i,j)$ 的颜色不能等于 $(i+1,j)$ 的颜色。
  • $i$ 行具体怎么涂色?用 $\textit{curRow}$ 表示。$(i,j)$ 的颜色不能等于 $(i,j-1)$ 的颜色。

三种颜色分别用 $0,1,2$ 表示,存储一个格子的颜色信息需要 $2$ 个比特位,一行的颜色就需要 $2\cdot 3 = 6$ 个比特位。

注意取模。为什么可以在中途取模?原理见 模运算的世界:当加减乘除遇上取模

###py

MOD = 1_000_000_007

# (i, j):当前位置 
# pre_row:上一行(i+1 行)的颜色
# cur_row:当前这一行已填入的颜色
@cache  # 缓存装饰器,避免重复计算 dfs(一行代码实现记忆化)
def dfs(i: int, j: int, pre_row: int, cur_row: int) -> int:
    if i == 0:  # 所有格子都已涂色
        return 1  # 找到一个合法方案

    if j == 3:  # i 行已涂色
        # 开始对 i-1 行涂色,cur_row 变成 pre_row
        return dfs(i - 1, 0, cur_row, 0)

    res = 0
    for color in range(3):  # 枚举 (i, j) 的颜色 color
        # 不能和下面相邻格子 (i+1, j) 颜色相同
        # 不能和左侧相邻格子 (i, j-1) 颜色相同
        if pre_row and color == pre_row >> (j * 2) & 3 or \
                 j and color == cur_row >> ((j - 1) * 2) & 3:
            continue
        res += dfs(i, j + 1, pre_row, cur_row | (color << (j * 2)))
    return res % MOD

class Solution:
    def numOfWays(self, n: int) -> int:
        return dfs(n, 0, 0, 0)  # 从最后一行开始涂色

###java

class Solution {
private static final int MOD = 1_000_000_007;

// 全局 memo,记忆化的内容可以在不同测试数据间共享
private static Map<Integer, Integer> memo = new HashMap<>();

public int numOfWays(int n) {
return dfs(n, 0, 0, 0);
}

    // (i, j):当前位置 
    // preRow:上一行(i+1 行)的颜色
    // curRow:当前这一行已填入的颜色
private int dfs(int i, int j, int preRow, int curRow) {
if (i == 0) { // 所有格子都已涂色
return 1; // 找到一个合法方案
}

if (j == 3) { // i 行已涂色
    // 开始对 i-1 行涂色,curRow 变成 preRow
return dfs(i - 1, 0, curRow, 0);
}

        // 参数压缩到一个 int 中
int key = (i << 14) | (j << 12) | (preRow << 6) | curRow;
if (memo.containsKey(key)) { // 之前计算过
return memo.get(key);
}

int res = 0;
for (int color = 0; color < 3; color++) { // 枚举 (i, j) 的颜色 color
if (preRow > 0 && color == (preRow >> (j * 2) & 3) || // 不能和下面相邻格子 (i+1, j) 颜色相同
j > 0 && color == (curRow >> ((j - 1) * 2) & 3)) { // 不能和左侧相邻格子 (i, j-1) 颜色相同
continue;
}
res = (res + dfs(i, j + 1, preRow, curRow | (color << (j * 2)))) % MOD;
}

memo.put(key, res); // 记忆化
return res;
}
}

###cpp

constexpr int MOD = 1'000'000'007;

// 全局 memo,记忆化的内容可以在不同测试数据间共享
unordered_map<int, int> memo;

// (i, j):当前位置 
// pre_row:上一行(i+1 行)的颜色
// cur_row:当前这一行已填入的颜色
int dfs(int i, int j, int pre_row, int cur_row) {
    if (i == 0) { // 所有格子都已涂色
        return 1; // 找到一个合法方案
    }

    if (j == 3) { // i 行已涂色
        // 开始对 i-1 行涂色,cur_row 变成 pre_row
        return dfs(i - 1, 0, cur_row, 0);
    }

    // 参数压缩到一个 int 中
    int key = (i << 14) | (j << 12) | (pre_row << 6) | cur_row;
    if (memo.contains(key)) { // 之前计算过
        return memo[key];
    }

    int res = 0;
    for (int color = 0; color < 3; color++) { // 枚举 (i, j) 的颜色 color
        if (pre_row > 0 && color == (pre_row >> (j * 2) & 3) || // 不能和下面相邻格子 (i+1, j) 颜色相同
            j > 0 && color == (cur_row >> ((j - 1) * 2) & 3)) { // 不能和左侧相邻格子 (i, j-1) 颜色相同
            continue;
        }
        res = (res + dfs(i, j + 1, pre_row, cur_row | (color << (j * 2)))) % MOD;
    }

    memo[key] = res; // 记忆化
    return res;
}

class Solution {
public:
int numOfWays(int n) {
return dfs(n, 0, 0, 0);
}
};

###go

const mod = 1_000_000_007

type tuple struct{ i, j, preRow, curRow int }

// 全局 memo,记忆化的内容可以在不同测试数据间共享
var memo = map[tuple]int{}

// (i, j):当前位置 
// preRow:上一行(i+1 行)的颜色
// curRow:当前这一行已填入的颜色
func dfs(i, j, preRow, curRow int) int {
if i == 0 { // 所有格子都已涂色
return 1 // 找到一个合法方案
}

if j == 3 { // i 行已涂色
// 开始对 i-1 行涂色,curRow 变成 preRow
return dfs(i-1, 0, curRow, 0)
}

t := tuple{i, j, preRow, curRow}
if res, ok := memo[t]; ok { // 之前计算过
return res
}

res := 0
for color := range 3 { // 枚举 (i, j) 的颜色 color
if preRow > 0 && color == preRow>>(j*2)&3 || // 不能和下面相邻格子 (i+1, j) 颜色相同
j > 0 && color == curRow>>((j-1)*2)&3 { // 不能和左侧相邻格子 (i, j-1) 颜色相同
continue
}
res = (res + dfs(i, j+1, preRow, curRow|color<<(j*2))) % mod
}

memo[t] = res // 记忆化
return res
}

func numOfWays(n int) int {
return dfs(n, 0, 0, 0)
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(nmk^{2m+1})$,其中 $m=3$ 是列数,$k=3$ 是颜色数。由于存在大量不合法的状态,复杂度不会跑满。
  • 空间复杂度:$\mathcal{O}(nmk^{2m})$。

方法二:递推

回顾方法一,总体上看,DP 过程是一个线性递推(从 $(n-1)\times 3$ 网格图最后一行的所有涂色方案线性转移到 $n\times 3$ 网格图的最后一行),所以必然有线性递推式。

定义 $f[n]$ 表示给 $n\times 3$ 网格图涂色的方案数。

根据 Berlekamp-Massey 算法,用方法一求出 $f$ 的连续若干项(具体多少在文章中有解释,本题只需 $4$ 项),就可以用 Berlekamp-Massey 算法直接得到线性递推式

$$
f[i] = 5\cdot f[i-1]-2\cdot f[i-2] \ \ (i \ge 3)
$$

初始值 $f[1] = 12,\ f[2] = 54$。

也可以倒推出 $f[0]=3$,把 $f[0]$ 和 $f[1]$ 作为初始值。

###py

class Solution:
    def numOfWays(self, n: int) -> int:
        MOD = 1_000_000_007
        f = [0] * (n + 1)
        f[0] = 3
        f[1] = 12
        for i in range(2, n + 1):
            f[i] = (f[i - 1] * 5 - f[i - 2] * 2) % MOD
        return f[n]

###java

class Solution {
    public int numOfWays(int n) {
        final int MOD = 1_000_000_007;
        int[] f = new int[n + 1];
        f[0] = 3;
        f[1] = 12;
        for (int i = 2; i <= n; i++) {
            f[i] = (int) ((f[i - 1] * 5L - f[i - 2] * 2L) % MOD); // 注意这里有减法,结果可能是负数
        }
        return (f[n] + MOD) % MOD; // 保证结果非负
    }
}

###cpp

class Solution {
public:
    int numOfWays(int n) {
        constexpr int MOD = 1'000'000'007;
        vector<int> f(n + 1);
        f[0] = 3;
        f[1] = 12;
        for (int i = 2; i <= n; i++) {
            f[i] = (f[i - 1] * 5LL - f[i - 2] * 2LL) % MOD; // 注意这里有减法,结果可能是负数
        }
        return (f[n] + MOD) % MOD; // 保证结果非负
    }
};

###go

func numOfWays(n int) int {
const mod = 1_000_000_007
f := make([]int, n+1)
f[0] = 3
f[1] = 12
for i := 2; i <= n; i++ {
f[i] = (f[i-1]*5 - f[i-2]*2) % mod // 注意这里有减法,结果可能是负数
}
return (f[n] + mod) % mod // 保证结果非负
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$。
  • 空间复杂度:$\mathcal{O}(n)$。注:可以用滚动变量优化至 $\mathcal{O}(1)$。

方法三:矩阵快速幂

把方法二的递推式用矩阵乘法表示,即

$$
\begin{bmatrix}
f[i] \
f[i-1] \
\end{bmatrix}
= \begin{bmatrix}
5 & -2 \
1 & 0 \
\end{bmatrix}
\begin{bmatrix}
f[i-1] \
f[i-2] \
\end{bmatrix}
$$

把上式中的三个矩阵分别记作 $F[i],M,F[i-1]$,即

$$
F[i] = M\times F[i-1]
$$

那么有

$$
\begin{aligned}
F[n] &= M\times F[n-1] \
&= M\times M\times F[n-2] \
&= M\times M\times M\times F[n-3] \
&\ \ \vdots \
&= M^{n-1}\times F[1] \
\end{aligned}
$$

其中 $M^{n-1}$ 可以用快速幂计算,原理请看【图解】一张图秒懂快速幂

初始值

$$
F[1] = \begin{bmatrix}
f[1] \
f[0] \
\end{bmatrix}
= \begin{bmatrix}
12 \
3 \
\end{bmatrix}
$$

答案为 $f[n]$,即 $F[n]$ 的第一项。

###py

MOD = 1_000_000_007

# a @ b,其中 @ 是矩阵乘法
def mul(a: List[List[int]], b: List[List[int]]) -> List[List[int]]:
    return [[sum(x * y for x, y in zip(row, col)) % MOD for col in zip(*b)]
            for row in a]

# a^n @ f0
def pow_mul(a: List[List[int]], n: int, f0: List[List[int]]) -> List[List[int]]:
    res = f0
    while n:
        if n & 1:
            res = mul(a, res)
        a = mul(a, a)
        n >>= 1
    return res

class Solution:
    def numOfWays(self, n: int) -> int:
        m = [[5, -2], [1, 0]]
        f1 = [[12], [3]]
        fn = pow_mul(m, n - 1, f1)
        return fn[0][0]

###py

import numpy as np

class Solution:
    def numOfWays(self, n: int) -> int:
        MOD = 1_000_000_007
        m = np.array([[5, -2], [1, 0]], dtype=object)
        f1 = np.array([12, 3], dtype=object)
        fn = np.linalg.matrix_power(m, n - 1) @ f1
        return fn[0] % MOD

###java

class Solution {
    private static final int MOD = 1_000_000_007;

    public int numOfWays(int n) {
        int[][] m = {
            {5, -2},
            {1, 0},
        };
        int[][] f1 = {{12}, {3}};
        int[][] fn = powMul(m, n - 1, f1);
        return (fn[0][0] + MOD) % MOD; // 保证结果非负
    }

    // a^n * f0
    private int[][] powMul(int[][] a, int n, int[][] f0) {
        int[][] res = f0;
        while (n > 0) {
            if ((n & 1) > 0) {
                res = mul(a, res);
            }
            a = mul(a, a);
            n >>= 1;
        }
        return res;
    }

    // 返回矩阵 a 和矩阵 b 相乘的结果
    private int[][] mul(int[][] a, int[][] b) {
        int[][] c = new int[a.length][b[0].length];
        for (int i = 0; i < a.length; i++) {
            for (int k = 0; k < a[i].length; k++) {
                if (a[i][k] == 0) {
                    continue;
                }
                for (int j = 0; j < b[k].length; j++) {
                    c[i][j] = (int) ((c[i][j] + (long) a[i][k] * b[k][j]) % MOD);
                }
            }
        }
        return c;
    }
}

###cpp

constexpr int MOD = 1'000'000'007;

using matrix = vector<vector<int>>;

// 返回矩阵 a 和矩阵 b 相乘的结果
matrix mul(matrix& a, matrix& b) {
    int n = a.size(), m = b[0].size();
    matrix c = matrix(n, vector<int>(m));
    for (int i = 0; i < n; i++) {
        for (int k = 0; k < a[i].size(); k++) {
            if (a[i][k] == 0) {
                continue;
            }
            for (int j = 0; j < m; j++) {
                c[i][j] = (c[i][j] + 1LL * a[i][k] * b[k][j]) % MOD;
            }
        }
    }
    return c;
}

// a^n * f0
matrix pow_mul(matrix a, int n, matrix& f0) {
    matrix res = f0;
    while (n) {
        if (n & 1) {
            res = mul(a, res);
        }
        a = mul(a, a);
        n >>= 1;
    }
    return res;
}

class Solution {
public:
    int numOfWays(int n) {
        matrix m = {
            {5, -2},
            {1, 0},
        };
        matrix f1 = {{12}, {3}};
        matrix fn = pow_mul(m, n - 1, f1);
        return (fn[0][0] + MOD) % MOD; // 保证结果非负
    }
};

###go

const mod = 1_000_000_007

type matrix [][]int

func newMatrix(n, m int) matrix {
a := make(matrix, n)
for i := range a {
a[i] = make([]int, m)
}
return a
}

// 返回矩阵 a 和矩阵 b 相乘的结果
func (a matrix) mul(b matrix) matrix {
c := newMatrix(len(a), len(b[0]))
for i, row := range a {
for k, x := range row {
if x == 0 {
continue
}
for j, y := range b[k] {
c[i][j] = (c[i][j] + x*y) % mod
}
}
}
return c
}

// a^n * f0
func (a matrix) powMul(n int, f0 matrix) matrix {
res := f0
for ; n > 0; n /= 2 {
if n%2 > 0 {
res = a.mul(res)
}
a = a.mul(a)
}
return res
}

func numOfWays(n int) int {
m := matrix{
{5, -2},
{1, 0},
}
f1 := matrix{{12}, {3}}
fn := m.powMul(n-1, f1)
return (fn[0][0] + mod) % mod // 保证结果非负
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(\log n)$。
  • 空间复杂度:$\mathcal{O}(1)$。

注:也可以用 Kitamasa 算法 计算,在矩阵更大(递推式更长)时有优势。

相似题目

专题训练

见下面动态规划题单的「§9.5 轮廓线 DP」和「§11.6 矩阵快速幂优化 DP」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

给 N x 3 网格图涂色的方案数

2020年4月19日 09:53

方法一:递推

我们可以用 $f[i][\textit{type}]$ 表示当网格的大小为 $i \times 3$ 且最后一行的填色方法为 $\textit{type}$ 时的方案数。由于我们在填充第 $i$ 行时,会影响我们填充方案的只有它上面的那一行(即 $i - 1$ 行),因此用 $f[i][\textit{type}]$ 表示状态是合理的。

那么我们如何计算 $f[i][\textit{type}]$ 呢?可以发现:

  • 首先,$\textit{type}$ 本身是要满足要求的。每一行有 $3$ 个网格,如果我们用 $0, 1, 2$ 分别代表红黄绿,那么 $\textit{type}$ 可以看成一个三进制数,例如 $\textit{type} = (102)_3$ 时,表示 $3$ 个网格从左到右的颜色分别为黄、红、绿;

    • 这样以来,我们可以预处理出所有满足要求的 $\textit{type}$。具体地,我们使用三重循环分别枚举每一个格子的颜色,只有相邻的格子颜色不相同时,$\textit{type}$ 才满足要求。
  • 其次,$f[i][\textit{type}]$ 应该等于所有 $f[i - 1][\textit{type}']$ 的和,其中 $\textit{type'}$ 和 $\textit{type}$ 可以作为相邻的行。也就是说,$\textit{type'}$ 和 $\textit{type}$ 的对应位置不能相同。

递推解法的本身不难想出,难度在于上述的预处理以及编码实现。下面给出包含详细注释的 C++JavaPython 代码。

###C++

class Solution {
private:
    static constexpr int mod = 1000000007;

public:
    int numOfWays(int n) {
        // 预处理出所有满足条件的 type
        vector<int> types;
        for (int i = 0; i < 3; ++i) {
            for (int j = 0; j < 3; ++j) {
                for (int k = 0; k < 3; ++k) {
                    if (i != j && j != k) {
                        // 只要相邻的颜色不相同就行
                        // 将其以十进制的形式存储
                        types.push_back(i * 9 + j * 3 + k);
                    }
                }
            }
        }
        int type_cnt = types.size();
        // 预处理出所有可以作为相邻行的 type 对
        vector<vector<int>> related(type_cnt, vector<int>(type_cnt));
        for (int i = 0; i < type_cnt; ++i) {
            // 得到 types[i] 三个位置的颜色
            int x1 = types[i] / 9, x2 = types[i] / 3 % 3, x3 = types[i] % 3;
            for (int j = 0; j < type_cnt; ++j) {
                // 得到 types[j] 三个位置的颜色
                int y1 = types[j] / 9, y2 = types[j] / 3 % 3, y3 = types[j] % 3;
                // 对应位置不同色,才能作为相邻的行
                if (x1 != y1 && x2 != y2 && x3 != y3) {
                    related[i][j] = 1;
                }
            }
        }
        // 递推数组
        vector<vector<int>> f(n + 1, vector<int>(type_cnt));
        // 边界情况,第一行可以使用任何 type
        for (int i = 0; i < type_cnt; ++i) {
            f[1][i] = 1;
        }
        for (int i = 2; i <= n; ++i) {
            for (int j = 0; j < type_cnt; ++j) {
                for (int k = 0; k < type_cnt; ++k) {
                    // f[i][j] 等于所有 f[i - 1][k] 的和
                    // 其中 k 和 j 可以作为相邻的行
                    if (related[k][j]) {
                        f[i][j] += f[i - 1][k];
                        f[i][j] %= mod;
                    }
                }
            }
        }
        // 最终所有的 f[n][...] 之和即为答案
        int ans = 0;
        for (int i = 0; i < type_cnt; ++i) {
            ans += f[n][i];
            ans %= mod;
        }
        return ans;
    }
};

###Java

class Solution {
    static final int MOD = 1000000007;

    public int numOfWays(int n) {
        // 预处理出所有满足条件的 type
        List<Integer> types = new ArrayList<Integer>();
        for (int i = 0; i < 3; ++i) {
            for (int j = 0; j < 3; ++j) {
                for (int k = 0; k < 3; ++k) {
                    if (i != j && j != k) {
                        // 只要相邻的颜色不相同就行
                        // 将其以十进制的形式存储
                        types.add(i * 9 + j * 3 + k);
                    }
                }
            }
        }
        int typeCnt = types.size();
        // 预处理出所有可以作为相邻行的 type 对
        int[][] related = new int[typeCnt][typeCnt];
        for (int i = 0; i < typeCnt; ++i) {
            // 得到 types[i] 三个位置的颜色
            int x1 = types.get(i) / 9, x2 = types.get(i) / 3 % 3, x3 = types.get(i) % 3;
            for (int j = 0; j < typeCnt; ++j) {
                // 得到 types[j] 三个位置的颜色
                int y1 = types.get(j) / 9, y2 = types.get(j) / 3 % 3, y3 = types.get(j) % 3;
                // 对应位置不同色,才能作为相邻的行
                if (x1 != y1 && x2 != y2 && x3 != y3) {
                    related[i][j] = 1;
                }
            }
        }
        // 递推数组
        int[][] f = new int[n + 1][typeCnt];
        // 边界情况,第一行可以使用任何 type
        for (int i = 0; i < typeCnt; ++i) {
            f[1][i] = 1;
        }
        for (int i = 2; i <= n; ++i) {
            for (int j = 0; j < typeCnt; ++j) {
                for (int k = 0; k < typeCnt; ++k) {
                    // f[i][j] 等于所有 f[i - 1][k] 的和
                    // 其中 k 和 j 可以作为相邻的行
                    if (related[k][j] != 0) {
                        f[i][j] += f[i - 1][k];
                        f[i][j] %= MOD;
                    }
                }
            }
        }
        // 最终所有的 f[n][...] 之和即为答案
        int ans = 0;
        for (int i = 0; i < typeCnt; ++i) {
            ans += f[n][i];
            ans %= MOD;
        }
        return ans;
    }
}

###Python

class Solution:
    def numOfWays(self, n: int) -> int:
        mod = 10**9 + 7
        # 预处理出所有满足条件的 type
        types = list()
        for i in range(3):
            for j in range(3):
                for k in range(3):
                    if i != j and j != k:
                        # 只要相邻的颜色不相同就行
                        # 将其以十进制的形式存储
                        types.append(i * 9 + j * 3 + k)
        type_cnt = len(types)
        # 预处理出所有可以作为相邻行的 type 对
        related = [[0] * type_cnt for _ in range(type_cnt)]
        for i, ti in enumerate(types):
            # 得到 types[i] 三个位置的颜色
            x1, x2, x3 = ti // 9, ti // 3 % 3, ti % 3
            for j, tj in enumerate(types):
                # 得到 types[j] 三个位置的颜色
                y1, y2, y3 = tj // 9, tj // 3 % 3, tj % 3
                # 对应位置不同色,才能作为相邻的行
                if x1 != y1 and x2 != y2 and x3 != y3:
                    related[i][j] = 1
        # 递推数组
        f = [[0] * type_cnt for _ in range(n + 1)]
        # 边界情况,第一行可以使用任何 type
        f[1] = [1] * type_cnt
        for i in range(2, n + 1):
            for j in range(type_cnt):
                for k in range(type_cnt):
                    # f[i][j] 等于所有 f[i - 1][k] 的和
                    # 其中 k 和 j 可以作为相邻的行
                    if related[k][j]:
                        f[i][j] += f[i - 1][k]
                        f[i][j] %= mod
        # 最终所有的 f[n][...] 之和即为答案
        ans = sum(f[n]) % mod
        return ans

###C#

public class Solution {
    private const int mod = 1000000007;
    
    public int NumOfWays(int n) {
        // 预处理出所有满足条件的 type
        List<int> types = new List<int>();
        for (int i = 0; i < 3; ++i) {
            for (int j = 0; j < 3; ++j) {
                for (int k = 0; k < 3; ++k) {
                    if (i != j && j != k) {
                        // 只要相邻的颜色不相同就行
                        // 将其以十进制的形式存储
                        types.Add(i * 9 + j * 3 + k);
                    }
                }
            }
        }
        int type_cnt = types.Count;
        // 预处理出所有可以作为相邻行的 type 对
        int[][] related = new int[type_cnt][];
        for (int i = 0; i < type_cnt; ++i) {
            related[i] = new int[type_cnt];
            // 得到 types[i] 三个位置的颜色
            int x1 = types[i] / 9, x2 = types[i] / 3 % 3, x3 = types[i] % 3;
            for (int j = 0; j < type_cnt; ++j) {
                // 得到 types[j] 三个位置的颜色
                int y1 = types[j] / 9, y2 = types[j] / 3 % 3, y3 = types[j] % 3;
                // 对应位置不同色,才能作为相邻的行
                if (x1 != y1 && x2 != y2 && x3 != y3) {
                    related[i][j] = 1;
                }
            }
        }
        // 递推数组
        int[][] f = new int[n + 1][];
        for (int i = 0; i <= n; ++i) {
            f[i] = new int[type_cnt];
        }
        // 边界情况,第一行可以使用任何 type
        for (int i = 0; i < type_cnt; ++i) {
            f[1][i] = 1;
        }
        for (int i = 2; i <= n; ++i) {
            for (int j = 0; j < type_cnt; ++j) {
                for (int k = 0; k < type_cnt; ++k) {
                    // f[i][j] 等于所有 f[i - 1][k] 的和
                    // 其中 k 和 j 可以作为相邻的行
                    if (related[k][j] == 1) {
                        f[i][j] = (f[i][j] + f[i - 1][k]) % mod;
                    }
                }
            }
        }
        // 最终所有的 f[n][...] 之和即为答案
        int ans = 0;
        for (int i = 0; i < type_cnt; ++i) {
            ans = (ans + f[n][i]) % mod;
        }
        return ans;
    }
}

###Go

func numOfWays(n int) int {
    // 预处理出所有满足条件的 type
    mod := 1000000007
    types := []int{}
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            for k := 0; k < 3; k++ {
                if i != j && j != k {
                    // 只要相邻的颜色不相同就行
                    // 将其以十进制的形式存储
                    types = append(types, i*9 + j*3 + k)
                }
            }
        }
    }
    type_cnt := len(types)
    // 预处理出所有可以作为相邻行的 type 对
    related := make([][]int, type_cnt)
    for i := range related {
        related[i] = make([]int, type_cnt)
    }
    for i := 0; i < type_cnt; i++ {
        // 得到 types[i] 三个位置的颜色
        x1 := types[i] / 9
        x2 := types[i] / 3 % 3
        x3 := types[i] % 3
        for j := 0; j < type_cnt; j++ {
            // 得到 types[j] 三个位置的颜色
            y1 := types[j] / 9
            y2 := types[j] / 3 % 3
            y3 := types[j] % 3
            // 对应位置不同色,才能作为相邻的行
            if x1 != y1 && x2 != y2 && x3 != y3 {
                related[i][j] = 1
            }
        }
    }
    // 递推数组
    f := make([][]int, n+1)
    for i := range f {
        f[i] = make([]int, type_cnt)
    }
    // 边界情况,第一行可以使用任何 type
    for i := 0; i < type_cnt; i++ {
        f[1][i] = 1
    }
    for i := 2; i <= n; i++ {
        for j := 0; j < type_cnt; j++ {
            for k := 0; k < type_cnt; k++ {
                // f[i][j] 等于所有 f[i - 1][k] 的和
                // 其中 k 和 j 可以作为相邻的行
                if related[k][j] == 1 {
                    f[i][j] = (f[i][j] + f[i-1][k]) % mod
                }
            }
        }
    }
    // 最终所有的 f[n][...] 之和即为答案
    ans := 0
    for i := 0; i < type_cnt; i++ {
        ans = (ans + f[n][i]) % mod
    }
    return ans
}

###C

int numOfWays(int n) {
    // 预处理出所有满足条件的 type
    const int mod = 1000000007;
    int types[12];
    int type_cnt = 0;
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 3; ++j) {
            for (int k = 0; k < 3; ++k) {
                if (i != j && j != k) {
                    // 只要相邻的颜色不相同就行
                    // 将其以十进制的形式存储
                    types[type_cnt++] = i * 9 + j * 3 + k;
                }
            }
        }
    }
    // 预处理出所有可以作为相邻行的 type 对
    int related[12][12] = {0};
    for (int i = 0; i < type_cnt; ++i) {
        // 得到 types[i] 三个位置的颜色
        int x1 = types[i] / 9, x2 = types[i] / 3 % 3, x3 = types[i] % 3;
        for (int j = 0; j < type_cnt; ++j) {
            // 得到 types[j] 三个位置的颜色
            int y1 = types[j] / 9, y2 = types[j] / 3 % 3, y3 = types[j] % 3;
            // 对应位置不同色,才能作为相邻的行
            if (x1 != y1 && x2 != y2 && x3 != y3) {
                related[i][j] = 1;
            }
        }
    }
    // 递推数组
    int f[n + 1][type_cnt];
    // 初始化
    for (int i = 0; i <= n; ++i) {
        for (int j = 0; j < type_cnt; ++j) {
            f[i][j] = 0;
        }
    }
    // 边界情况,第一行可以使用任何 type
    for (int i = 0; i < type_cnt; ++i) {
        f[1][i] = 1;
    }
    for (int i = 2; i <= n; ++i) {
        for (int j = 0; j < type_cnt; ++j) {
            for (int k = 0; k < type_cnt; ++k) {
                // f[i][j] 等于所有 f[i - 1][k] 的和
                // 其中 k 和 j 可以作为相邻的行
                if (related[k][j]) {
                    f[i][j] = (f[i][j] + f[i - 1][k]) % mod;
                }
            }
        }
    }
    // 最终所有的 f[n][...] 之和即为答案
    int ans = 0;
    for (int i = 0; i < type_cnt; ++i) {
        ans = (ans + f[n][i]) % mod;
    }
    return ans;
}

###JavaScript

var numOfWays = function(n) {
    // 预处理出所有满足条件的 type
    const mod = 1000000007;
    const types = [];
    for (let i = 0; i < 3; ++i) {
        for (let j = 0; j < 3; ++j) {
            for (let k = 0; k < 3; ++k) {
                if (i !== j && j !== k) {
                    // 只要相邻的颜色不相同就行
                    // 将其以十进制的形式存储
                    types.push(i * 9 + j * 3 + k);
                }
            }
        }
    }
    const type_cnt = types.length;
    // 预处理出所有可以作为相邻行的 type 对
    const related = Array.from({length: type_cnt}, () => new Array(type_cnt).fill(0));
    for (let i = 0; i < type_cnt; ++i) {
        // 得到 types[i] 三个位置的颜色
        const x1 = Math.floor(types[i] / 9);
        const x2 = Math.floor(types[i] / 3) % 3;
        const x3 = types[i] % 3;
        for (let j = 0; j < type_cnt; ++j) {
            // 得到 types[j] 三个位置的颜色
            const y1 = Math.floor(types[j] / 9);
            const y2 = Math.floor(types[j] / 3) % 3;
            const y3 = types[j] % 3;
            // 对应位置不同色,才能作为相邻的行
            if (x1 !== y1 && x2 !== y2 && x3 !== y3) {
                related[i][j] = 1;
            }
        }
    }
    // 递推数组
    const f = Array.from({length: n + 1}, () => new Array(type_cnt).fill(0));
    // 边界情况,第一行可以使用任何 type
    for (let i = 0; i < type_cnt; ++i) {
        f[1][i] = 1;
    }
    for (let i = 2; i <= n; ++i) {
        for (let j = 0; j < type_cnt; ++j) {
            for (let k = 0; k < type_cnt; ++k) {
                // f[i][j] 等于所有 f[i - 1][k] 的和
                // 其中 k 和 j 可以作为相邻的行
                if (related[k][j]) {
                    f[i][j] = (f[i][j] + f[i - 1][k]) % mod;
                }
            }
        }
    }
    // 最终所有的 f[n][...] 之和即为答案
    let ans = 0;
    for (let i = 0; i < type_cnt; ++i) {
        ans = (ans + f[n][i]) % mod;
    }
    return ans;
};

###TypeScript

function numOfWays(n: number): number {
    // 预处理出所有满足条件的 type
    const mod: number = 1000000007;
    const types: number[] = [];
    for (let i = 0; i < 3; ++i) {
        for (let j = 0; j < 3; ++j) {
            for (let k = 0; k < 3; ++k) {
                if (i !== j && j !== k) {
                    // 只要相邻的颜色不相同就行
                    // 将其以十进制的形式存储
                    types.push(i * 9 + j * 3 + k);
                }
            }
        }
    }
    const type_cnt: number = types.length;
    // 预处理出所有可以作为相邻行的 type 对
    const related: number[][] = Array.from({length: type_cnt}, () => new Array(type_cnt).fill(0));
    for (let i = 0; i < type_cnt; ++i) {
        // 得到 types[i] 三个位置的颜色
        const x1: number = Math.floor(types[i] / 9);
        const x2: number = Math.floor(types[i] / 3) % 3;
        const x3: number = types[i] % 3;
        for (let j = 0; j < type_cnt; ++j) {
            // 得到 types[j] 三个位置的颜色
            const y1: number = Math.floor(types[j] / 9);
            const y2: number = Math.floor(types[j] / 3) % 3;
            const y3: number = types[j] % 3;
            // 对应位置不同色,才能作为相邻的行
            if (x1 !== y1 && x2 !== y2 && x3 !== y3) {
                related[i][j] = 1;
            }
        }
    }
    // 递推数组
    const f: number[][] = Array.from({length: n + 1}, () => new Array(type_cnt).fill(0));
    // 边界情况,第一行可以使用任何 type
    for (let i = 0; i < type_cnt; ++i) {
        f[1][i] = 1;
    }
    for (let i = 2; i <= n; ++i) {
        for (let j = 0; j < type_cnt; ++j) {
            for (let k = 0; k < type_cnt; ++k) {
                // f[i][j] 等于所有 f[i - 1][k] 的和
                // 其中 k 和 j 可以作为相邻的行
                if (related[k][j]) {
                    f[i][j] = (f[i][j] + f[i - 1][k]) % mod;
                }
            }
        }
    }
    // 最终所有的 f[n][...] 之和即为答案
    let ans: number = 0;
    for (let i = 0; i < type_cnt; ++i) {
        ans = (ans + f[n][i]) % mod;
    }
    return ans;
}

###Rust

impl Solution {
    pub fn num_of_ways(n: i32) -> i32 {
        // 预处理出所有满足条件的 type
        let mod_val = 1000000007;
        let n = n as usize;
        let mut types = Vec::new();
        for i in 0..3 {
            for j in 0..3 {
                for k in 0..3 {
                    if i != j && j != k {
                        // 只要相邻的颜色不相同就行
                        // 将其以十进制的形式存储
                        types.push(i * 9 + j * 3 + k);
                    }
                }
            }
        }
        let type_cnt = types.len();
        // 预处理出所有可以作为相邻行的 type 对
        let mut related = vec![vec![0; type_cnt]; type_cnt];
        for i in 0..type_cnt {
            // 得到 types[i] 三个位置的颜色
            let x1 = types[i] / 9;
            let x2 = types[i] / 3 % 3;
            let x3 = types[i] % 3;
            for j in 0..type_cnt {
                // 得到 types[j] 三个位置的颜色
                let y1 = types[j] / 9;
                let y2 = types[j] / 3 % 3;
                let y3 = types[j] % 3;
                // 对应位置不同色,才能作为相邻的行
                if x1 != y1 && x2 != y2 && x3 != y3 {
                    related[i][j] = 1;
                }
            }
        }
        // 递推数组
        let mut f = vec![vec![0; type_cnt]; n + 1];
        // 边界情况,第一行可以使用任何 type
        for i in 0..type_cnt {
            f[1][i] = 1;
        }
        for i in 2..=n {
            for j in 0..type_cnt {
                for k in 0..type_cnt {
                    // f[i][j] 等于所有 f[i - 1][k] 的和
                    // 其中 k 和 j 可以作为相邻的行
                    if related[k][j] == 1 {
                        f[i][j] = (f[i][j] + f[i - 1][k]) % mod_val;
                    }
                }
            }
        }
        // 最终所有的 f[n][...] 之和即为答案
        let mut ans = 0;
        for i in 0..type_cnt {
            ans = (ans + f[n][i]) % mod_val;
        }
        ans
    }
}

复杂度分析

  • 时间复杂度:$O(T^2N)$,其中 $T$ 是满足要求的 $\textit{type}$ 的数量,在示例一中已经给出了 $T = 12$。在递推的过程中,我们需要计算所有的 $f[i][\textit{type}]$,并且需要枚举上一行的 $\textit{type}'$。

  • 空间复杂度:$O(T^2 + TN)$。我们需要 $T * T$ 的二维数组存储 $\textit{type}$ 之间的关系,$T * N$ 的数组存储递推的结果。注意到由于 $f[i][\textit{type}]$ 只和上一行的状态有关,我们可以使用两个一维数组存储当前行和上一行的 $f$ 值,空间复杂度降低至 $O(T^2 + 2T) = O(T^2)$。

方法二:递推优化

如果读者有一些高中数学竞赛基础,就可以发现上面的这个递推式是线性的,也就是说:

  • 我们可以进行一些化简;

  • 它存在通项公式。

直观上,我们怎么化简方法一中的递推呢?

我们把满足要求的 $\textit{type}$ 都写出来,一共有 $12$ 种:

010, 012, 020, 021, 101, 102, 120, 121, 201, 202, 210, 212

我们可以把它们分成两类:

  • ABC 类:三个颜色互不相同,一共有 $6$ 种:012, 021, 102, 120, 201, 210

  • ABA 类:左右两侧的颜色相同,也有 $6$ 种:010, 020, 101, 121, 202, 212

这样我们就可以把 $12$ 种 $\textit{type}$ 浓缩成了 $2$ 种,尝试写出这两类之间的递推式。我们用 $f[i][0]$ 表示 ABC 类,$f[i][1]$ 表示 ABA 类。在计算时,我们可以将任意一种满足要求的涂色方法带入第 i - 1 行,并检查第 i 行的方案数,这是因为同一类的涂色方法都是等价的:

  • i - 1 行是 ABC 类,第 i 行是 ABC 类:以 012 为例,那么第 i 行只能是120201,方案数为 $2$;

  • i - 1 行是 ABC 类,第 i 行是 ABA 类:以 012 为例,那么第 i 行只能是 101121,方案数为 $2$;

  • i - 1 行是 ABA 类,第 i 行是 ABC 类:以 010 为例,那么第 i 行只能是 102201,方案数为 2

  • i - 1 行是 ABA 类,第 i 行是 ABA 类:以 010 为例,那么第 i 行只能是 101121202,方案数为 3

因此我们就可以写出递推式:

$$
\begin{cases}
f[i][0] = 2 * f[i - 1][0] + 2 * f[i - 1][1] \
f[i][1] = 2 * f[i - 1][0] + 3 * f[i - 1][1]
\end{cases}
$$

###C++

class Solution {
private:
    static constexpr int mod = 1000000007;

public:
    int numOfWays(int n) {
        int fi0 = 6, fi1 = 6;
        for (int i = 2; i <= n; ++i) {
            int new_fi0 = (2LL * fi0 + 2LL * fi1) % mod;
            int new_fi1 = (2LL * fi0 + 3LL * fi1) % mod;
            fi0 = new_fi0;
            fi1 = new_fi1;
        }
        return (fi0 + fi1) % mod;
    }
};

###Java

class Solution {
    static final int MOD = 1000000007;

    public int numOfWays(int n) {
        long fi0 = 6, fi1 = 6;
        for (int i = 2; i <= n; ++i) {
            long newFi0 = (2 * fi0 + 2 * fi1) % MOD;
            long newFi1 = (2 * fi0 + 3 * fi1) % MOD;
            fi0 = newFi0;
            fi1 = newFi1;
        }
        return (int) ((fi0 + fi1) % MOD);
    }
}

###Python

class Solution:
    def numOfWays(self, n: int) -> int:
        mod = 10**9 + 7
        fi0, fi1 = 6, 6
        for i in range(2, n + 1):
            fi0, fi1 = (2 * fi0 + 2 * fi1) % mod, (2 * fi0 + 3 * fi1) % mod
        return (fi0 + fi1) % mod

###C#

public class Solution {
    private const int mod = 1000000007;
    
    public int NumOfWays(int n) {
        long fi0 = 6, fi1 = 6;
        for (int i = 2; i <= n; ++i) {
            long new_fi0 = (2 * fi0 + 2 * fi1) % mod;
            long new_fi1 = (2 * fi0 + 3 * fi1) % mod;
            fi0 = new_fi0;
            fi1 = new_fi1;
        }
        return (int)((fi0 + fi1) % mod);
    }
}

###Go

func numOfWays(n int) int {
    mod := 1000000007
    fi0, fi1 := 6, 6
    for i := 2; i <= n; i++ {
        new_fi0 := (2*fi0 + 2*fi1) % mod
        new_fi1 := (2*fi0 + 3*fi1) % mod
        fi0, fi1 = new_fi0, new_fi1
    }
    return (fi0 + fi1) % mod
}

###C

int numOfWays(int n) {
    const int mod = 1000000007;
    long long fi0 = 6, fi1 = 6;
    for (int i = 2; i <= n; ++i) {
        long long new_fi0 = (2 * fi0 + 2 * fi1) % mod;
        long long new_fi1 = (2 * fi0 + 3 * fi1) % mod;
        fi0 = new_fi0;
        fi1 = new_fi1;
    }
    return (fi0 + fi1) % mod;
}

###JavaScript

var numOfWays = function(n) {
    const mod = 1000000007;    
    let fi0 = 6, fi1 = 6;
    for (let i = 2; i <= n; i++) {
        const new_fi0 = (2 * fi0 + 2 * fi1) % mod;
        const new_fi1 = (2 * fi0 + 3 * fi1) % mod;
        fi0 = new_fi0;
        fi1 = new_fi1;
    }
    return (fi0 + fi1) % mod;
};

###TypeScript

function numOfWays(n: number): number {
    const mod: number = 1000000007;    
    let fi0: number = 6, fi1: number = 6;
    for (let i = 2; i <= n; i++) {
        const new_fi0: number = (2 * fi0 + 2 * fi1) % mod;
        const new_fi1: number = (2 * fi0 + 3 * fi1) % mod;
        fi0 = new_fi0;
        fi1 = new_fi1;
    }
    return (fi0 + fi1) % mod;
}

###Rust

impl Solution {
    pub fn num_of_ways(n: i32) -> i32 {
        let mod_val: i64 = 1000000007;
        let mut fi0: i64 = 6;
        let mut fi1: i64 = 6;
        
        for _ in 2..= n {
            let new_fi0 = (2 * fi0 + 2 * fi1) % mod_val;
            let new_fi1 = (2 * fi0 + 3 * fi1) % mod_val;
            fi0 = new_fi0;
            fi1 = new_fi1;
        }
        
        ((fi0 + fi1) % mod_val) as i32
    }
}

复杂度分析

  • 时间复杂度:$O(N)$。

  • 空间复杂度:$O(1)$。

数学解决非常快乐

作者 LindsayWong
2020年4月12日 12:12

1.观察LEETCODE给的官方N=1示例,可以抽象区分为2种类型,ABA和ABC
image.png

2.分情况讨论,可知,在下方增加1行时,有9种情况,又可以分为ABA和ABC两个大类
image.png

本层的结果 = ABA类的个数m + ABC类的个数n

本层的每个ABA类 => 下层演化 3个ABA + 2个ABC
本层的每个ABC类 => 下层演化 2个ABA + 2个ABC

下层的结果 = ABA类的个数 + ABC类的个数 = (3m+2n) + (2m+2n)

3.数学计算
image.png

4.最后给出代码

###csharp

public class Solution {
    public int NumOfWays(int n) {
            if (n == 0)
                return 0;
            else if (n == 1)
                return 12;
            var temp = 1000000007;
            long  repeat = 6;
            long  unrepeat = 6;
            for(int i = 2; i <=n; i++)
            {
                long  newrep = (repeat * 3) % temp + unrepeat * 2 % temp;
                long  newunrep = repeat * 2 % temp + unrepeat * 2 % temp;
                repeat = newrep;
                unrepeat = newunrep;
            }
            return (int)((repeat + unrepeat)%temp);
    }
}
昨天 — 2026年1月2日首页

Make Good New Things

2026年1月2日 08:00

Paul Graham 在 What to do 中探讨了一个看似简单却极具深意的问题:人的一生应该做什么?除了「帮助他人」和「爱护世界」这两个显而易见的道德责任外,他提出了第三个关键点:创造美好的新事物(Make good new things)。

读到这段话时,我马上想到的是 Make Something Wonderful 这本书。某种程度上,两者共享了同一个核心理念:「创造美好」不应只是一次性的行为,而是一种值得毕生追求的生活方式。

Steve Jobs 曾这样描述 Make Something Wonderful 这句话背后的动机:

There’s lots of ways to be as a person, and some people express their deep appreciation in different ways, but one of the ways that I believe people express their appreciation to the rest of humanity is to make something wonderful and put it out there.

And you never meet the people, you never shake their hands, you never hear their story or tell yours, but somehow, in the act of making something with a great deal of care and love, something is transmitted there.

And it’s a way of expressing to the rest of our species our deep appreciation. So, we need to be true to who we are and remember what’s really important to us. That’s what’s going to keep Apple Apple: is if we keep us us.

创造的产物不限形式,它可以是宏大的牛顿力学定律,也可以是一把精致的维京椅。文章也是一种常见的创作。在 AI 时代,「是否还有写博客的必要」成为了备受热议的话题。博客的独特价值在于其内容的多样性——它可以是一篇游记一篇散文一次技术折腾的记录一本好书的读后感,甚至是稍纵即逝的灵感碎片。个体独特的经历与细腻的感受,是 AI 无法替代的。或者,也可以像 Paul GrahamGwern 那样,通过写作对某一话题进行深度挖掘,以确保自己真正掌握了真理。

除了写作,还可以开发 App。AI Coding Assistants 的崛起极大地降低了编程门槛,普通人只需花时间熟悉这些助手,便能在短时间内构建出像模像样的产品。而随着各类 AI 图像生成工具(如 Nano Banana Pro 等)的出现,绘画创作也不再遥不可及。这正是 AI 时代对个体的最大赋能:曾经专属于专业人士的领域,如今已向所有人敞开大门。

但我们为什么非得「做点什么」呢?躺在沙发上刷剧岂不更舒服?的确,消费内容看起来更惬意,但「整天躺着刷剧」与「辛苦创作一天后再躺下刷剧」,这两种体验有着天壤之别。那种完成了一件作品后内心产生的通透感与充实感,是任何单纯的消费行为都无法比拟的。

从价值投资的角度看,「创作」是一项既有安全边际,又具备潜在高回报的行为。假设你花一周时间做了一个小工具,即便最后无人问津,你的安全边际依然存在:你在过程中学到了新知识、巩固了旧体系,解决了自己的痛点,收获了亲手造物的成就感。而潜在回报则是巨大的:它可能真的帮助了他人,改善了某些人的生活,甚至让你结识了同频的伙伴。

要让创作带来巨大的回报,有一个核心要点:高标准。在 AI 时代,我们面临一个残酷的现实:Average is over(平庸时代的终结)。 因为 AI 让生产 60 分的「合格品」变得几乎零成本,平庸的内容将迅速泛滥成灾。

在市场蓝海期,产品或许可以靠便宜、新奇或仅仅是「能用(Just Works)」来取胜;但一旦门槛被 AI 踏平,大量玩家涌入,最终能脱颖而出的,唯有那些超越了「平均线」、不仅能用而且好用的精品。因此,「高标准」不仅是竞争优势,更是生存线。

要达到高标准,高质量的 Input(输入) 必不可少。如果连什么是「好产品」都看不出来,就更不可能做出来。因此,我们需要花时间去多多研究优秀的 Input。当高标准成为习惯,你会发现市面上有太多产品不尽如人意。带着这把「高标准」的放大镜,你能找到无数瑕疵和痛点,而这些,就可以是创作的起点。

阻碍创作的因素通常有三:好奇心匮乏、完美主义倾向、精力被分散。其中最大的阻碍往往是好奇心的缺失。好奇心可分为两类:感知性好奇心(关注 What,如八卦新闻)和认知性好奇心(关注 How 和 Why,如探究事件背后的逻辑与影响)。Hard work is the magnitude of the vector and curiosity is the direction.(努力是矢量的长度,而好奇心是方向)。认知性好奇心,可以为创作指引方向,高标准则决定了矢量的长度。

此外,创作还有一个美好的「副作用」:它能让你更专注于当下,而不是被纷繁的新闻和社交网络裹挟。每一次创作的产出,都像是给人生这条绳索打了一个结。当你回望这些作品时,当时的记忆与点滴便会瞬间涌上心头,让你的人生有迹可循。

最后说说 AI。如果 GPT-3.5 的发布是航空史上的「莱特兄弟时刻」,那么随之而来的 AI 浪潮,则让飞机成为了大众的交通工具。它的操作逻辑与传统的地面交通截然不同,能力也更强悍。要发挥它的最大价值,你需要熟悉与它交互的最佳实践,找到属于你的那架「飞机」,然后让它载着你,飞往以前想都不敢想的地方。

面试官 : “ Vue 选项式api 和 组合式api 什么区别? “

作者 千寻girling
2026年1月2日 18:12

Vue 的选项式 API (Options API)  和组合式 API (Composition API)  是两种核心的代码组织方式,前者侧重 “按选项分类”,后者侧重 “按逻辑分类”,核心差异体现在设计理念、代码结构、复用性等维度,以下是全面对比和实用解读:

一、核心区别对比表

维度 选项式 API (Options API) 组合式 API (Composition API)
设计理念 按 “选项类型” 组织代码(data、methods、computed 等) 按 “业务逻辑” 组织代码(同一逻辑的代码聚合)
代码结构 分散式:同一逻辑的代码分散在不同选项中 聚合式:同一逻辑的代码集中在 setup 或 <script setup>
适用场景 小型组件、简单业务(快速上手) 中大型组件、复杂业务(逻辑复用 / 维护)
逻辑复用 依赖 mixin(易命名冲突、来源不清晰) 依赖组合式函数(Composables,纯函数复用,清晰可控)
类型推导 对 TypeScript 支持弱(需手动标注) 天然适配 TS,类型推导更完善
响应式写法 声明式:data 返回对象,Vue 自动响应式 手动式:用 ref/reactive 创建响应式数据
生命周期 直接声明钩子(created、mounted 等) setup 中通过 onMounted 等函数调用(无 beforeCreate/created)
代码可读性 简单场景清晰,复杂场景 “碎片化” 复杂场景逻辑聚合,可读性更高
上手成本 低(符合传统前端思维) 稍高(需理解 ref/reactive/setup 等概念)

二、代码示例直观对比

1. 选项式 API(Vue 2/3 兼容)

<template>
  <div>{{ count }} <button @click="add">+1</button></div>
</template>

<script>
export default {
  // 数据(响应式)
  data() {
    return {
      count: 0
    };
  },
  // 方法
  methods: {
    add() {
      this.count++;
    }
  },
  // 生命周期
  mounted() {
    console.log('组件挂载:', this.count);
  }
};
</script>

特点:代码按 data/methods/mounted 等选项拆分,同一 “计数逻辑” 分散在不同区块。

2. 组合式 API(Vue 3 推荐,<script setup> 语法糖)

<template>
  <div>{{ count }} <button @click="add">+1</button></div>
</template>

<script setup>
import { ref, onMounted } from 'vue';

// 响应式数据(ref 用于基本类型)
const count = ref(0);

// 方法(与数据聚合)
const add = () => {
  count.value++; // ref 需通过 .value 访问
};

// 生命周期
onMounted(() => {
  console.log('组件挂载:', count.value);
});
</script>

特点:“计数逻辑” 的数据、方法、生命周期全部聚合在一处,逻辑边界清晰。

三、核心差异深度解读

1. 逻辑复用:从 mixin 到 Composables(组合式函数)

  • 选项式 API 的痛点(mixin) :复用逻辑需写 mixin 文件,多个 mixin 易出现命名冲突,且无法清晰知道属性来源:

    // mixin/countMixin.js
    export default {
      data() { return { count: 0 }; },
      methods: { add() { this.count++; } }
    };
    // 组件中使用
    export default {
      mixins: [countMixin], // 引入后,count/add 混入组件,但来源不直观
    };
    
  • 组合式 API 的优势(Composables) :复用逻辑封装为纯函数,按需导入,属性来源清晰,无命名冲突:

    // composables/useCount.js
    import { ref } from 'vue';
    export const useCount = () => {
      const count = ref(0);
      const add = () => count.value++;
      return { count, add };
    };
    // 组件中使用
    <script setup>
    import { useCount } from './composables/useCount';
    const { count, add } = useCount(); // 明确导入,来源清晰
    </script>
    

2. 响应式原理:声明式 vs 手动式

  • 选项式 API:data 返回的对象会被 Vue 递归劫持(Object.defineProperty/Proxy),自动变成响应式,直接通过 this.xxx 访问;
  • 组合式 API:需手动用 ref(基本类型)/reactive(引用类型)创建响应式数据,ref 需通过 .value 访问(模板中自动解包),更灵活且可控。

3. 大型项目适配性

  • 选项式 API:组件复杂度提升后,同一业务逻辑的代码会分散在 data/methods/computed/watch 等多个选项中,形成 “面条代码”,维护成本高;
  • 组合式 API:可将复杂业务拆分为多个 Composables(如 useUser/useCart/useOrder),每个函数负责一个逻辑模块,代码结构清晰,便于多人协作和维护。

四、选型建议

场景 推荐 API
小型组件 / 快速原型 选项式 API
中大型项目 / 复杂逻辑 组合式 API
需兼容 Vue 2 + Vue 3 选项式 API(过渡)
用 TypeScript 开发 组合式 API

总结

  • 选项式 API 是 “面向选项” 的思维,适合入门和简单场景,符合传统前端的代码组织习惯;
  • 组合式 API 是 “面向逻辑” 的思维,解决了选项式 API 在复杂场景下的复用、维护痛点,是 Vue 3 的核心升级,也是大型项目的最佳实践。

两者并非互斥,Vue 3 完全兼容选项式 API,可根据项目规模和团队习惯灵活选择(甚至同一项目中混合使用)。

🔥3 kB 换 120 ms 阻塞? Axios 还是 fetch?

2026年1月2日 17:50

0. 先抛结论,再吵不迟

指标 Axios 1.7 fetch (原生)
gzip 体积 ≈ 3.1 kB 0 kB
阻塞时间(M3/4G) 120 ms 0 ms
内存峰值(1000 并发) 17 MB 11 MB
生产 P1 故障(过去一年) 2 次(拦截器顺序 bug) 0 次
开发体验(DX) 10 分 7 分

结论:

  • 极致性能/SSG/Edge → fetch 已足够;
  • 企业级、需要全局拦截、上传进度 → Axios 仍值得;
  • 二者可共存:核心链路与首页用 fetch,管理后台用 Axios。

1. 3 kB 到底贵不贵?

2026 年 1 月,HTTP Archive 最新采样(Chrome 桌面版)显示:

  • 中位 JS 体积 580 kB,3 kB 似乎“九牛一毛”;
  • 但放到首屏预算 100 kB 的站点(TikTok 推荐值),3 kB ≈ 3 % 预算,再加 120 ms 阻塞,LCP 直接从 1.5 s 飙到 1.62 s,SEO 评级掉一档。

“ bundle 每 +1 kB,4G 下 FCP +8 ms”——Lighthouse 2025 白皮书。


2. 把代码拍桌上:差异只剩这几行

下面 4 个高频场景,全部给出“可直接复制跑”的片段,差异一目了然。

2.1 自动 JSON + 错误码

// Axios:零样板
const {data} = await axios.post('/api/login', {user, pwd});

// fetch:两行样板
const res = await fetch('/api/login', {
  method:'POST',
  headers:{'Content-Type':'application/json'},
  body:JSON.stringify({user, pwd})
});
if (!res.ok) throw new Error(res.status);
const data = await res.json();

争议

  • Axios 党:少写两行,全年少写 3000 行。
  • fetch 党:gzip 后 3 kB 换两行?ESLint 模板一把就补全。

2.2 超时 + 取消

// Axios:内置
const source = axios.CancelToken.source();
setTimeout(() => source.cancel('timeout'), 5000);
await axios.get('/api/big', {cancelToken: source.token});

// fetch:原生 AbortController
const ctl = new AbortController();
setTimeout(() => ctl.abort(), 5000);
await fetch('/api/big', {signal: ctl.signal});

2025 之后 Edge/Node 22 已全支持,AbortSignal.timeout(5000) 一行搞定:

await fetch('/api/big', {signal: AbortSignal.timeout(5000)});

结论:语法差距已抹平。

2.3 上传进度条

// Axios:progress 事件
await axios.post('/upload', form, {
  onUploadProgress: e => setProgress(e.loaded / e.total)
});

// fetch:借助 `xhr` 或 `ReadableStream`
// 2026 仍无原生简易方案,需要封装 `xhr` 才能拿到 `progress`。

结论:大文件上传场景 Axios 仍吊打 fetch。

2.4 拦截器(token、日志)

// Axios:全局拦截
axios.interceptors.request.use(cfg => {
  cfg.headers.Authorization = `Bearer ${getToken()}`;
  return cfg;
});

// fetch:三行封装
export const $get = (url, opts = {}) => fetch(url, {
  ...opts,
  headers: {...opts.headers, Authorization: `Bearer ${getToken()}`}
});

经验:拦截器一旦>2 个,Axios 顺序地狱频发;fetch 手动链式更直观。


3. 实测!同一个项目,两套 bundle

测试场景

  • React 18 + Vite 5,仅替换 HTTP 层;
  • 构建目标:es2020 + gzip + brotli;
  • 网络:模拟 4G(RTT 150 ms);
  • 采样 10 次取中位。
指标 Axios fetch
gzip bundle 46.7 kB 43.6 kB
首屏阻塞时间 120 ms 0 ms
Lighthouse TTI 2.1 s 1.95 s
内存峰值(1000 并发请求) 17 MB 11 MB
生产报错(过去一年) 2 次拦截器顺序错乱 0

数据来自 rebrowser 2025 基准 ;阻塞时间差异与 51CTO 独立测试吻合 。


4. 什么时候一定要 Axios?

  1. 需要上传进度(onUploadProgress)且不想回退 xhr;
  2. 需要请求/响应拦截链 >3 层,且团队对“黑盒”可接受;
  3. 需要兼容 IE11(2026 年政务/银行仍存);
  4. 需要Node 16 以下老版本(fetch 需 18+)。

5. 共存方案:把 3 kB 花在刀刃上

// core/http.js
export const isSSR = typeof window === 'undefined';
export const HTTP = isSSR || navigator.connection?.effectiveType === '4g'
  ? { get: (u,o) => fetch(u,{...o, signal: AbortSignal.timeout(5000)}) }
  : await import('axios');   // 动态 import,只在非 4G 或管理后台加载

结果:

  • 首屏 0 kB;
  • 管理后台仍享受 Axios 拦截器;
  • 整体 bundle 下降 7 %,LCP −120 ms。

6. 一句话收尸

2026 年的浏览器,fetch 已把“缺的课”补完:取消、超时、Node 原生、TypeScript 完美。
3 kB 的 Axios 不再是“默认”,而是“按需”。
上传进度、深链拦截、老浏览器——用 Axios;
其余场景,让首页飞一把,把 120 ms 还给用户。

挪威2025年新车销量的96%为电动汽车

2026年1月2日 17:45
上周五的官方数据显示,去年在挪威注册的几乎所有新车都是纯电动汽车,其中特斯拉的销量更是一路飙升,使这个北欧国家在逐步淘汰汽油和柴油动力汽车方面巩固了全球领先地位。在税收优惠政策的推动下,2025年注册的所有新车中有95.9%是电动汽车,12月份这一数字接近98%。挪威道路联合会(OFV)的数据显示,这一年度数字高于2024年的88.9%。OFV称,挪威今年登记的新车数量达到创纪录的179549辆,比2024年增长了40%。(新浪财经)

在ios上动态插入元素的列表使用:last-child样式可能不能及时生效

作者 tjswk2008
2026年1月2日 17:39

这是一个在 iOS Safari(Webkit 引擎)中比较经典的渲染 Bug。当通过 JavaScript 动态向容器末尾添加元素时,Webkit 有时未能正确触发重绘(Repaint)或样式重新计算(Style Recalculation),导致 :last-child 伪类仍然停留在之前的元素上。

以下是解决这个问题的几种常用方案,按推荐程度排序:

1. 强制触发重绘 (Force Reflow)

这是最简单直接的“黑科技”。在插入元素的代码后面,读取一次容器或新元素的某个布局属性(如 offsetHeight),强制浏览器同步计算样式。

JavaScript

const container = document.getElementById('container');
const newItem = document.createElement('div');
container.appendChild(newItem);

// 强制触发重绘
container.offsetHeight; 

2. 使用 Flex/Grid 的 gap 属性 (推荐)

如果你的 :last-child 主要是为了处理间距(例如 margin-bottom: 0),那么弃用 :last-child 改用 gap 是最现代且根本的解决方案。gap 会自动处理元素间的间距,不需要判断谁是最后一个。

CSS

.container {
  display: flex;
  flex-direction: column;
  gap: 10px; /* 元素之间自动产生 10px 间距,最后一个元素后方不会有间距 */
}

3. 反向思路:使用 :not(:last-child)

有时候 Webkit 对“谁是最后一个”反应迟钝,但对“谁不是最后一个”反应较快。尝试给所有非末尾元素设置样式:

CSS

/* 不推荐 */
/* .item { margin-bottom: 10px; } */
/* .item:last-child { margin-bottom: 0; } */

/* 推荐 */
.item:not(:last-child) {
  margin-bottom: 10px;
}

4. 手动切换 Class

如果逻辑比较复杂,伪类失效频繁,建议放弃 CSS 伪类,改用 JavaScript 在插入时手动维护一个 .is-last 类。

JavaScript

// 插入逻辑
const items = container.querySelectorAll('.item');
items.forEach(el => el.classList.remove('is-last'));
const lastItem = items[items.length - 1];
lastItem.classList.add('is-last');

为什么会发生这种情况?

Webkit 引擎为了性能优化,会尽量减少样式重新计算的频率。当 DOM 树发生变化时,它本应标记该容器为 "dirty" 并重新检查伪类状态,但在某些复杂的嵌套布局或特定的 iOS 版本中,这个触发机制会漏掉对 :last-child 的检查。

建议: 如果你的项目环境允许(iOS 14.1+),优先使用 Flexbox/Grid 的 gap。它不仅性能更好,还能彻底规避此类由于动态插入导致的伪类失效问题。

Vue 组件通信的 8 种最佳实践,你知道几种?

作者 刘大华
2026年1月2日 17:11

经常写 Vue 的朋友应该很熟悉,在 Vue 的应用中,组件化开发可以让我们的代码更容易维护,而组件之间的数据传递事件通信也是我们必须要解决的问题。

经过多个项目的实践,我逐渐摸清了Vue3中8种组件通信方式和适用场景。

下面来给大家分享一下。

1. Props / Emits:最基础的父子传值

这是 Vue 的官方推荐通信方式,遵循单向数据流原则,数据只能从上往下流,事件从下往上传。

Props:父传子的单向数据流

适用场景:当你需要把配置、用户信息、状态等数据从父组件传递给子组件时。

<!-- 父组件 Parent.vue -->
<template>
  <div class="parent">
    <h2>父组件</h2>
    <!-- 传递静态和动态数据 -->
    <ChildComponent 
      title="用户信息" 
      :user="userData"
      :count="clickCount"
    />
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'
import ChildComponent from './ChildComponent.vue'

const userData = reactive({
  name: '张三',
  age: 25,
  email: 'zhangsan@example.com'
})

const clickCount = ref(0)
</script>
<!-- 子组件 ChildComponent.vue -->
<template>
  <div class="child">
    <h3>{{ title }}</h3>
    <div class="user-card">
      <p>姓名:{{ user.name }}</p>
      <p>年龄:{{ user.age }}</p>
      <p>邮箱:{{ user.email }}</p>
    </div>
    <p>点击次数:{{ count }}</p>
  </div>
</template>

<script setup>
// 方式1:简单定义
// defineProps(['title', 'user', 'count'])

// 方式2:带类型验证(推荐)
defineProps({
  title: {
    type: String,
    required: true
  },
  user: {
    type: Object,
    default: () => ({})
  },
  count: {
    type: Number,
    default: 0
  }
})

// 方式3:使用 TypeScript(最佳实践)
interface Props {
  title: string
  user: {
    name: string
    age: number
    email: string
  }
  count?: number
}

defineProps<Props>()
</script>

为什么推荐带验证?

它能提前发现传参错误,比如把字符串传给了 count,Vue 会在控制台报错,避免线上bug。


Emits:子传父的事件机制

适用场景:子组件需要通知父组件有事发生,比如表单提交、按钮点击、输入变化等。

<!-- 子组件 ChildComponent.vue -->
<template>
  <div class="child">
    <button @click="handleButtonClick">通知父组件</button>
    <input 
      :value="inputValue" 
      @input="handleInputChange"
      placeholder="输入内容..."
    />
  </div>
</template>

<script setup>
import { ref } from 'vue'

// 定义可触发的事件
const emit = defineEmits(['button-clicked', 'input-changed', 'update:modelValue'])

const inputValue = ref('')

const handleButtonClick = () => {
  // 触发事件并传递数据
  emit('button-clicked', {
    message: '按钮被点击了!',
    timestamp: new Date().toISOString()
  })
}

const handleInputChange = (event) => {
  inputValue.value = event.target.value
  emit('input-changed', inputValue.value)
  
  // 支持 v-model 的更新方式
  emit('update:modelValue', inputValue.value)
}
</script>
<!-- 父组件 Parent.vue -->
<template>
  <div class="parent">
    <ChildComponent 
      @button-clicked="handleChildButtonClick"
      @input-changed="handleChildInputChange"
    />
    
    <div v-if="lastEvent">
      <p>最后收到的事件:{{ lastEvent.type }}</p>
      <p>数据:{{ lastEvent.data }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const lastEvent = ref(null)

const handleChildButtonClick = (data) => {
  lastEvent.value = {
    type: 'button-clicked',
    data: data
  }
  console.log('收到子组件消息:', data)
}

const handleChildInputChange = (value) => {
  lastEvent.value = {
    type: 'input-changed',
    data: value
  }
  console.log('输入内容:', value)
}
</script>

关键点:

  • 子组件不直接修改父组件数据,而是发出请求,由父组件决定如何处理。
  • 这种解耦设计让组件更可复用、更易测试。

2. v-model:双向绑定的语法糖

v-model 在 Vue3 中变得更加强大,支持多个 v-model 绑定。

基础用法

<!-- 父组件 -->
<template>
  <div>
    <CustomInput v-model="username" />
    <p>当前用户名:{{ username }}</p>
    
    <!-- 多个 v-model -->
    <UserForm
      v-model:name="userName"
      v-model:email="userEmail"
      v-model:age="userAge"
    />
  </div>
</template>

<script setup>
import { ref } from 'vue'

const username = ref('')
const userName = ref('')
const userEmail = ref('')
const userAge = ref(0)
</script>
<!-- 子组件 CustomInput.vue -->
<template>
  <div class="custom-input">
    <label>用户名:</label>
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
      class="input-field"
    />
  </div>
</template>

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<!-- 子组件 UserForm.vue -->
<template>
  <div class="user-form">
    <div class="form-group">
      <label>姓名:</label>
      <input
        :value="name"
        @input="$emit('update:name', $event.target.value)"
      />
    </div>
    <div class="form-group">
      <label>邮箱:</label>
      <input
        :value="email"
        @input="$emit('update:email', $event.target.value)"
        type="email"
      />
    </div>
    <div class="form-group">
      <label>年龄:</label>
      <input
        :value="age"
        @input="$emit('update:age', parseInt($event.target.value) || 0)"
        type="number"
      />
    </div>
  </div>
</template>

<script setup>
defineProps({
  name: String,
  email: String,
  age: Number
})

defineEmits(['update:name', 'update:email', 'update:age'])
</script>

v-model的核心优势:

  • 语法简洁,减少样板代码
  • 符合双向绑定的直觉
  • 支持多个v-model绑定
  • 类型安全(配合TypeScript)

适用场景:自定义表单控件(如日期选择器、富文本编辑器)需要双向绑定。


3. Ref / 模板引用:直接操作组件

当需要直接访问子组件或 DOM 元素时,模板引用是最佳选择。

<!-- 父组件 -->
<template>
  <div class="parent">
    <ChildComponent ref="childRef" />
    <CustomForm ref="formRef" />
    <video ref="videoRef" controls>
      <source src="./movie.mp4" type="video/mp4">
    </video>
    
    <div class="controls">
      <button @click="focusInput">聚焦输入框</button>
      <button @click="getChildData">获取子组件数据</button>
      <button @click="playVideo">播放视频</button>
      <button @click="validateForm">验证表单</button>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, nextTick } from 'vue'

// 创建引用
const childRef = ref(null)
const formRef = ref(null)
const videoRef = ref(null)

// 确保 DOM 更新后访问
const focusInput = async () => {
  await nextTick()
  childRef.value?.focusInput()
}

const getChildData = () => {
  if (childRef.value) {
    const data = childRef.value.getData()
    console.log('子组件数据:', data)
  }
}

const playVideo = () => {
  videoRef.value?.play()
}

const validateForm = () => {
  formRef.value?.validate()
}

// 组件挂载后访问
onMounted(() => {
  console.log('子组件实例:', childRef.value)
})
</script>
<!-- 子组件 ChildComponent.vue -->
<template>
  <div class="child">
    <input ref="inputEl" type="text" placeholder="请输入..." />
    <p>内部数据:{{ internalData }}</p>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'

const inputEl = ref(null)
const internalData = ref('这是内部数据')

// 暴露给父组件的方法和数据
defineExpose({
  focusInput: () => {
    inputEl.value?.focus()
  },
  getData: () => {
    return {
      internalData: internalData.value,
      timestamp: new Date().toISOString()
    }
  },
  internalData
})
</script>

适用场景:需要调用子组件方法(如弹窗打开)、聚焦输入框、操作原生元素(如 video 播放)。

4. Provide / Inject:跨层级数据传递

解决"prop 逐级传递"问题,实现祖先与后代组件的直接通信。

<!-- 根组件 App.vue -->
<template>
  <div id="app">
    <Header />
    <div class="main-content">
      <Sidebar />
      <ContentArea />
    </div>
    <Footer />
  </div>
</template>

<script setup>
import { provide, ref, reactive, computed } from 'vue'

// 提供用户信息
const currentUser = ref({
  id: 1,
  name: '张三',
  role: 'admin',
  permissions: ['read', 'write', 'delete']
})

// 提供应用配置
const appConfig = reactive({
  theme: 'dark',
  language: 'zh-CN',
  apiBaseUrl: import.meta.env.VITE_API_URL
})

// 提供方法
const updateUser = (newUserData) => {
  currentUser.value = { ...currentUser.value, ...newUserData }
}

const updateConfig = (key, value) => {
  appConfig[key] = value
}

// 计算属性
const userPermissions = computed(() => currentUser.value.permissions)

// 提供数据和方法
provide('currentUser', currentUser)
provide('appConfig', appConfig)
provide('updateUser', updateUser)
provide('updateConfig', updateConfig)
provide('userPermissions', userPermissions)
</script>
<!-- 深层嵌套的组件 ContentArea.vue -->
<template>
  <div class="content-area">
    <UserProfile />
    <ArticleList />
  </div>
</template>

<script setup>
// 这个组件不需要处理 props,直接渲染子组件
</script>
<!-- 使用注入的组件 UserProfile.vue -->
<template>
  <div class="user-profile">
    <h3>用户信息</h3>
    <div class="profile-card">
      <p>姓名:{{ currentUser.name }}</p>
      <p>角色:{{ currentUser.role }}</p>
      <p>权限:{{ userPermissions.join(', ') }}</p>
      <p>主题:{{ appConfig.theme }}</p>
    </div>
    <button @click="handleUpdateProfile">更新资料</button>
  </div>
</template>

<script setup>
import { inject } from 'vue'

// 注入数据和方法
const currentUser = inject('currentUser')
const appConfig = inject('appConfig')
const userPermissions = inject('userPermissions')
const updateUser = inject('updateUser')

const handleUpdateProfile = () => {
  updateUser({
    name: '李四',
    role: 'user'
  })
}
</script>

Provide/Inject的优势

  • 避免Props逐层传递的繁琐
  • 实现跨层级组件通信
  • 提供全局状态和方法的统一管理
  • 提高代码的可维护性

适用场景:当数据需要从顶层组件传递到底层组件,中间隔了好几层(比如主题、用户信息、语言设置)。


5. Pinia:现代化状态管理

对于复杂应用,Pinia 提供了更优秀的状态管理方案。

创建 Store

// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    user: null,
    isLoggedIn: false,
    token: '',
    permissions: []
  }),
  
  getters: {
    userName: (state) => state.user?.name || '未登录用户',
    isAdmin: (state) => state.user?.role === 'admin',
    hasPermission: (state) => (permission) => 
      state.permissions.includes(permission)
  },
  
  actions: {
    async login(credentials) {
      try {
        // 模拟 API 调用
        const response = await mockLoginApi(credentials)
        
        this.user = response.user
        this.token = response.token
        this.isLoggedIn = true
        this.permissions = response.permissions
        
        // 保存到 localStorage
        localStorage.setItem('token', this.token)
        
        return { success: true }
      } catch (error) {
        console.error('登录失败:', error)
        return { success: false, error: error.message }
      }
    },
    
    logout() {
      this.user = null
      this.token = ''
      this.isLoggedIn = false
      this.permissions = []
      
      localStorage.removeItem('token')
    },
    
    async updateProfile(userData) {
      if (!this.isLoggedIn) {
        throw new Error('请先登录')
      }
      
      this.user = { ...this.user, ...userData }
      // 这里可以调用 API 更新后端数据
    }
  }
})

// 模拟登录 API
const mockLoginApi = (credentials) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        user: {
          id: 1,
          name: credentials.username,
          role: 'admin'
        },
        token: 'mock-jwt-token',
        permissions: ['read', 'write', 'delete']
      })
    }, 1000)
  })
}

在组件中使用 Store

<!-- UserProfile.vue -->
<template>
  <div class="user-profile">
    <div v-if="userStore.isLoggedIn" class="logged-in">
      <h3>欢迎回来,{{ userStore.userName }}!</h3>
      <div class="user-info">
        <p>角色:{{ userStore.user.role }}</p>
        <p>权限:{{ userStore.permissions.join(', ') }}</p>
      </div>
      
      <div class="actions">
        <button 
          @click="updateName" 
          :disabled="!userStore.hasPermission('write')"
        >
          更新姓名
        </button>
        <button @click="userStore.logout" class="logout-btn">
          退出登录
        </button>
      </div>
    </div>
    
    <div v-else class="logged-out">
      <LoginForm />
    </div>
  </div>
</template>

<script setup>
import { useUserStore } from '@/stores/user'
import LoginForm from './LoginForm.vue'

const userStore = useUserStore()

const updateName = () => {
  userStore.updateProfile({
    name: `用户${Math.random().toString(36).substr(2, 5)}`
  })
}
</script>
<!-- LoginForm.vue -->
<template>
  <div class="login-form">
    <h3>用户登录</h3>
    <form @submit.prevent="handleLogin">
      <div class="form-group">
        <input 
          v-model="credentials.username" 
          placeholder="用户名"
          required
        />
      </div>
      <div class="form-group">
        <input 
          v-model="credentials.password" 
          type="password" 
          placeholder="密码"
          required
        />
      </div>
      <button type="submit" :disabled="loading">
        {{ loading ? '登录中...' : '登录' }}
      </button>
    </form>
    
    <div v-if="message" class="message" :class="messageType">
      {{ message }}
    </div>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

const credentials = reactive({
  username: '',
  password: ''
})

const loading = ref(false)
const message = ref('')
const messageType = ref('')

const handleLogin = async () => {
  loading.value = true
  message.value = ''
  
  const result = await userStore.login(credentials)
  
  if (result.success) {
    message.value = '登录成功!'
    messageType.value = 'success'
  } else {
    message.value = `登录失败:${result.error}`
    messageType.value = 'error'
  }
  
  loading.value = false
}
</script>

Pinia 优势:

  • 无 mutations,直接修改 state
  • 完美支持 TypeScript
  • DevTools 调试友好
  • 模块化设计,易于拆分

适用场景:中大型应用,多个组件需要共享复杂状态(如用户登录态、购物车、全局配置)。

6. 事件总线:轻量级全局通信

Vue3 移除了实例上的 onon、off 方法,不再支持这种模式,但我们可以使用 mitt 库实现。

// utils/eventBus.js
import mitt from 'mitt'

// 创建全局事件总线
const eventBus = mitt()

// 定义事件类型
export const EVENTS = {
  USER_LOGIN: 'user:login',
  USER_LOGOUT: 'user:logout',
  NOTIFICATION_SHOW: 'notification:show',
  MODAL_OPEN: 'modal:open',
  THEME_CHANGE: 'theme:change'
}

export default eventBus
<!-- 发布事件的组件 -->
<template>
  <div class="publisher">
    <h3>事件发布者</h3>
    <div class="buttons">
      <button @click="sendNotification">发送通知</button>
      <button @click="openModal">打开模态框</button>
      <button @click="changeTheme">切换主题</button>
    </div>
  </div>
</template>

<script setup>
import eventBus, { EVENTS } from '@/utils/eventBus'

const sendNotification = () => {
  eventBus.emit(EVENTS.NOTIFICATION_SHOW, {
    type: 'success',
    title: '操作成功',
    message: '这是一个来自事件总线的通知',
    duration: 3000
  })
}

const openModal = () => {
  eventBus.emit(EVENTS.MODAL_OPEN, {
    component: 'UserForm',
    props: { userId: 123 },
    title: '用户表单'
  })
}

const changeTheme = () => {
  const themes = ['light', 'dark', 'blue']
  const randomTheme = themes[Math.floor(Math.random() * themes.length)]
  
  eventBus.emit(EVENTS.THEME_CHANGE, {
    theme: randomTheme,
    timestamp: new Date().toISOString()
  })
}
</script>
<!-- 监听事件的组件 -->
<template>
  <div class="listener">
    <h3>事件监听者</h3>
    <div class="events-log">
      <div 
        v-for="(event, index) in events" 
        :key="index"
        class="event-item"
      >
        <strong>{{ event.type }}</strong>
        <span>{{ event.data }}</span>
        <small>{{ event.timestamp }}</small>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import eventBus, { EVENTS } from '@/utils/eventBus'

const events = ref([])

// 事件处理函数
const handleNotification = (data) => {
  events.value.unshift({
    type: EVENTS.NOTIFICATION_SHOW,
    data: `通知: ${data.title} - ${data.message}`,
    timestamp: new Date().toLocaleTimeString()
  })
}

const handleModalOpen = (data) => {
  events.value.unshift({
    type: EVENTS.MODAL_OPEN,
    data: `打开模态框: ${data.component}`,
    timestamp: new Date().toLocaleTimeString()
  })
}

const handleThemeChange = (data) => {
  events.value.unshift({
    type: EVENTS.THEME_CHANGE,
    data: `主题切换为: ${data.theme}`,
    timestamp: new Date().toLocaleTimeString()
  })
}

// 注册事件监听
onMounted(() => {
  eventBus.on(EVENTS.NOTIFICATION_SHOW, handleNotification)
  eventBus.on(EVENTS.MODAL_OPEN, handleModalOpen)
  eventBus.on(EVENTS.THEME_CHANGE, handleThemeChange)
})

// 组件卸载时移除监听
onUnmounted(() => {
  eventBus.off(EVENTS.NOTIFICATION_SHOW, handleNotification)
  eventBus.off(EVENTS.MODAL_OPEN, handleModalOpen)
  eventBus.off(EVENTS.THEME_CHANGE, handleThemeChange)
})
</script>

不太推荐使用。为什么?

  • 数据流向不透明,难以追踪
  • 容易忘记 off 导致内存泄漏
  • 大型项目维护困难
  • 建议:优先用 Pinia 或 provide/inject

适用场景:小型项目中,两个无关联组件需要临时通信(如通知弹窗、模态框控制)。


7. 属性透传($attrs)和边界处理

当你封装一个组件,并希望把未声明的属性自动传递给内部元素时,就用 $attrs。

<!-- 基础组件 BaseButton.vue -->
<template>
  <button 
    v-bind="filteredAttrs"
    class="base-button"
    @click="handleClick"
  >
    <slot></slot>
  </button>
</template>

<script setup>
import { computed, useAttrs } from 'vue'

const attrs = useAttrs()

// 过滤掉不需要透传的属性
const filteredAttrs = computed(() => {
  const { class: className, style, ...rest } = attrs
  return rest
})

const emit = defineEmits(['click'])

const handleClick = (event) => {
  emit('click', event)
}

// 也可以选择性地暴露 attrs
defineExpose({
  attrs
})
</script>
</style>
<!-- 使用基础组件 -->
<template>
  <div>
    <!-- 透传 class、style、data-* 等属性 -->
    <BaseButton
      class="custom-btn"
      style="color: red;"
      data-testid="submit-button"
      title="提交按钮"
      @click="handleSubmit"
    >
      提交表单
    </BaseButton>
    
    <!-- 多个按钮使用相同的基组件 -->
    <BaseButton
      class="secondary-btn"
      data-testid="cancel-button"
      @click="handleCancel"
    >
      取消
    </BaseButton>
  </div>
</template>

<script setup>
const handleSubmit = () => {
  console.log('提交表单')
}

const handleCancel = () => {
  console.log('取消操作')
}
</script>

<style>
.custom-btn {
  background: blue;
  color: white;
}

.secondary-btn {
  background: gray;
  color: white;
}
</style>

特性

  • 用户传的 class 和 style 会和组件内部的样式合并(Vue 自动处理)。
  • 所有 data-、title、aria- 等原生 HTML 属性都能正常生效。
  • 你不用提前知道用户会传什么,也能支持!

适用场景:封装通用组件(如按钮、输入框),希望保留原生 HTML 属性(class、style、data-* 等)。

8. 组合式函数:逻辑复用

对于复杂的通信逻辑,可以使用组合式函数封装。

// composables/useCommunication.js
import { ref, onUnmounted } from 'vue'

export function useCommunication() {
  const messages = ref([])
  const listeners = new Map()

  const sendMessage = (type, data) => {
    messages.value.unshift({
      type,
      data,
      timestamp: new Date().toISOString()
    })
    
    // 通知监听者
    if (listeners.has(type)) {
      listeners.get(type).forEach(callback => {
        callback(data)
      })
    }
  }

  const onMessage = (type, callback) => {
    if (!listeners.has(type)) {
      listeners.set(type, new Set())
    }
    listeners.get(type).add(callback)
  }

  const offMessage = (type, callback) => {
    if (listeners.has(type)) {
      listeners.get(type).delete(callback)
    }
  }

  // 清理函数
  const cleanup = () => {
    listeners.clear()
  }

  onUnmounted(cleanup)

  return {
    messages,
    sendMessage,
    onMessage,
    offMessage,
    cleanup
  }
}
<!-- 使用组合式函数 -->
<template>
  <div class="communication-demo">
    <div class="senders">
      <MessageSender />
      <EventSender />
    </div>
    <div class="receivers">
      <MessageReceiver />
      <EventReceiver />
    </div>
    <div class="message-log">
      <h4>消息日志</h4>
      <div 
        v-for="(msg, index) in messages" 
        :key="index"
        class="log-entry"
      >
        [{{ formatTime(msg.timestamp) }}] {{ msg.type }}: {{ msg.data }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { useCommunication } from '@/composables/useCommunication'
import MessageSender from './MessageSender.vue'
import MessageReceiver from './MessageReceiver.vue'
import EventSender from './EventSender.vue'
import EventReceiver from './EventReceiver.vue'

const { messages } = useCommunication()

const formatTime = (timestamp) => {
  return new Date(timestamp).toLocaleTimeString()
}
</script>

优势

  • 逻辑高度复用
  • 类型安全(配合 TS)
  • 易于单元测试

适用场景:将复杂的通信逻辑抽象成可复用的函数,比如 WebSocket 连接、本地存储同步等。


避坑指南

1. Props 设计原则

// 好的 Props 设计
defineProps({
  // 必需属性
  title: { type: String, required: true },
  
  // 可选属性带默认值
  size: { type: String, default: 'medium' },
  
  // 复杂对象
  user: { 
    type: Object, 
    default: () => ({ name: '', age: 0 }) 
  },
  
  // 验证函数
  count: {
    type: Number,
    validator: (value) => value >= 0 && value <= 100
  }
})

2. 事件命名规范

// 使用 kebab-case 事件名
defineEmits(['update:title', 'search-change', 'form-submit'])

// 避免使用驼峰命名
// defineEmits(['updateTitle']) // 不推荐

3. Provide/Inject 的响应性

// 保持响应性
const data = ref({})
provide('data', readonly(data))

// 提供修改方法
const updateData = (newData) => {
  data.value = { ...data.value, ...newData }
}
provide('updateData', updateData)

4. 内存泄漏预防

// 及时清理事件监听
onUnmounted(() => {
  eventBus.off('some-event', handler)
})

// 清理定时器
const timer = setInterval(() => {}, 1000)
onUnmounted(() => clearInterval(timer))

总结

经过上面的详细讲解,相信大家对 Vue3 的组件通信有了更深入的理解。让我最后做个总结:

  • 核心原则:根据组件关系选择合适方案
  • 父子组件:优先使用 Props/Emits,简单直接
  • 表单控件:v-model是最佳选择,语法优雅
  • 深层嵌套:Provide/Inject 避免 prop 透传地狱
  • 全局状态:Pinia 专业强大,适合复杂应用
  • 临时通信:事件总线可用但需谨慎
  • 组件封装:属性透传提供更好用户体验
  • 逻辑复用:组合式函数提升代码质量

在实际开发中,可以这样:

  1. 先从 Props/Emits 开始,这是基础
  2. 熟练掌握 v-model 的表单处理
  3. 在需要时引入 Pinia,不要过度设计
  4. 保持代码的可读性和可维护性

简单的需求用简单的方案,复杂的需求才需要复杂的工具。

本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!

📌往期精彩

《重构了20个SpringBoot项目后,总结出这套稳定高效的架构设计》

《代码里全是 new 对象,真的很 Low 吗?我认真想了一晚》

《这 5 个冷门 HTML 标签,让我直接删了100 行 JS 代码》

《这 10 个 Vue3 性能优化技巧很实用,但很多项目都没用上》

CSS 写 SQL 查询?后端慌了!

作者 小小荧
2026年1月2日 17:09

CSS 写 SQL 查询?后端慌了!

cover_image

初次接触到这个项目时,我的第一反应只有四个字

这也行?

最近在 X 上大火的一个叫 TailwindSQL 的项目,引发了广泛讨论。

其核心玩法非常简单——通过 CSS 的 className 来实现 SQL 查询功能。

前端发展到这个地步了吗?

让我们先看一个示例:

<DB className="db-users-name-where-id-1" />

如果你是前端开发者,可能会下意识地认为这是在定义样式

但如果你是后端开发者,估计已经开始皱眉了。

然而实际上,这段代码执行的是:

SELECT name FROM users WHERE id = 1;

看到这里,我确实愣了一下。

TailwindSQL 的本质

简而言之,它将 SQL 语句拆解为一个个「类名」片段。

这种做法类似于 TailwindCSSCSS 的处理方式:

db-users
db-users-name
db-users-name-where-id-1
db-products-orderby-price-desc

这些 className 最终会被解析为 SQL 语句,并在 React Server Components 中直接执行。

你甚至无需编写 API 接口,也无需使用 ORM 框架。

这个方案可靠吗?

从工程实践的角度来看,答案其实很明确:

并不可靠。

SQL 的复杂性,从来不是语法层面的问题。

真正的挑战在于:

  • 表关系管理

  • 复杂 JOIN 操作

  • 嵌套子查询

  • 事务控制

  • 权限验证

  • 边界条件处理

一旦查询逻辑稍显复杂,className 就会变得越来越冗长,最终形成一串难以维护的代码片段。

说实话,我很难想象在实际项目中,会有开发者认真地写出这样的代码:

className="db-orders-user-products-joinwhere-user-age-gt-18and-order-status-paidgroupby-user-id"

这已经不再是 DSL(领域特定语言)了,而是一种折磨。

我认为 TailwindSQL 很难在生产环境中得到应用,它更像是 vibe coding(氛围编程)的产物。

是否使用?可以了解一下,然后继续编写你熟悉的 SQL 吧。

  • TailwindSQL 官网https://tailwindsql.com/

分析师:三星SDI或将持续亏损直至2026年底

2026年1月2日 16:54
NH Investment & Securities以Ju Min-woo为首的分析师表示,三星SDI在2025年第四财季因美国电动汽车需求疲软而录得大于预期的亏损后,可能会持续亏损直至2026年底。这些分析师表示,由于在欧洲的电动汽车电池市场份额不断下滑,这家韩国电池制造商可能比竞争对手受到更大冲击。NH Investment & Securities预计,该公司第四财季将录得3,387亿韩元的营业亏损,而市场平均预期为亏损2,690亿韩元,并预计其2025年年度亏损为1.765万亿韩元。NH Investment & Securities预测,该公司2026年的营业亏损可能收窄至9,670亿韩元。(新浪财经)

前端面试题整理(方便自己看的)

2026年1月2日 16:46

JavaScript题

1.JavaScript中的数据类型?

JavaScript中,分两种类型:

  • 基本类型
  • 引用类型

基本类型主要有以下6种:Number、String、Boolean、Undefined、null、symbol。 引用类型主要有Object、Array、Function。其它的有Date、RegExp、Map等

2.DOM

文档对象模型(DOM)HTMLXML文档的编程接口。 日常开发离不开DOM的操作,对节点的增删改查等操作。在以前,使用Jquery,zepto等库来操作DOM,之后在vue,Angular,React等框架出现后,通过操作数据来控制DOM(多数情况下),越来越少的直接去操作DOM

3.BOM

3.1 BOM是什么?

BOM(Browser Object Model),浏览器对象模型,提供了独立于内容与浏览器窗口进行交互的对象。其作用就是跟浏览器做一些交互效果,比如:进行页面的后退、前进、刷新、浏览器窗口发生变化,滚动条滚动等。

3.2 window

Bom的核心对象是window,它表示浏览器的一个实例。 在浏览器中,window即是浏览器窗口的一个接口,又是全局对象。

4 == 和 === 区别,分别在什么情况使用

image.png

等于操作符用俩个等于号(==)表示,如果操作数相等,则会返回true。 等于操作符(==)在比较中会先进行类型转换,再确定操作数是否相等。

全等操作符由3个等于号(===)表示,只有俩个操作数在不转换的前提下相等才返回true,即类型相同,值也相同。

区别:等于操作符(==)会做类型转换再进行值的比较,全等操作符不会做类型转换。 nullundefined 比较,相等操作符为true 全等为false

5 typeof 和 instanceof 的区别

typeof 操作符返回一个字符串,表示未经计算的操作数的类型。

instanceof 运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。

区别:

  • typeof 会返回一个变量的基本类型,instanceof 返回的是一个Boolean.
  • instanceof 可以准确的判断复杂引用数据类型,但是不能正确判断基础数据类型。
  • 如果需要通用检测数据类型,可以通过Object.prototype.toString,调用该方法,统一返回格式 [object XXX]的字符串。

6 JavaScript 原型,原型链?有什么特点?

原型

JavaScript常被描述为一种基于原型的语言---每个对象拥有一个原型对象。访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或达到原型链的末尾。

原型链

原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链,它解释了为何一个对象会拥有定义在其他对象中的属性和方法。

在对象实例和它的构造器之间建立一个链接(它是_proto_属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。

  • 一切对象都是继承自Object对象,object对象直接继承根源对象null
  • 一切的函数对象(包括object对象),都是继承自Function对象
  • Object 对象直接继承自 Function 对象
  • Function 对象的 _proto_ 会指向自己的原型对象,最终还是继承自 Object 对象

7.对作用域链的理解

作用域,即变量和函数生效的区域或集合,作用域决定了代码区块中变量和其他资源的可见性。

  • 全局作用域:任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在任意位置访问。
  • 函数作用域:函数作用域也叫局部作用域,如果一个变量是在函数内部声明的,它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问。
  • 块级作用域:ES6引入了letconst关键字,和var关键字不同,在大括号中使用letconst声明的变量存在于块级作用域中。在大括号外面不能访问这些变量。

作用域链

当在JavaScript中使用一个变量的时候,首先JavaScript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。

8. 谈谈对this对象的理解

8.1定义

函数的this关键字在JavaScript中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别。在绝大数情况下,函数的调用方式决定了this的值。 this关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象。

8.2 new绑定

通过构造函数new 关键字生成一个实例对象,此时this指向这个实例对象。

apply()、call()、bind()、是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时this指的就是这个第一个参数。

8.3 箭头函数

在ES6的语法中,提供了箭头函数法,让我们在代码书写时就能确定this的指向。

9.new操作符具体干了什么

  • 创建一个新的对象
  • 将对象与构建函数通过原型链链接起来
  • 将构建函数中的this绑定到新建的对象上
  • 根据构建函数返回类型做判断,如果原始值则被忽略,如果是返回对象,需要正常处理。

10.bind、call、apply区别?

bindcallapply、作用是改变函数执行时的上下文,改变函数运行时的this指向。

区别:

  • 三者都可以改变函数的this指向
  • 三者第一个参数都是this要指向的对象,如果没有这个参数或者参数为undefinednull,则默认指向全局window
  • 三者都可以传参,但是apply是数组,而call是参数列表,且applycall是一次性传入参数,而bind可以分多次传入
  • bind是返回绑定this之后的函数,applycall则是立即执行

11.闭包的理解?闭包使用场景?

11.1 闭包是什么?

一个函数和对其周围状态的引用捆绑在一起,这样的组合就是闭包。闭包让你可以在一个内层函数中访问到其外层函数的作用域。

11.2 闭包使用场景

  • 创建私有变量
  • 延长变量的生命周期

11.3 柯里化函数

柯里化的目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用。

11.4 闭包的缺点

如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能有负面影响。

12.深拷贝浅拷贝的区别?实现一个深拷贝?

12.1 浅拷贝

Object.assignArray.prototype.slice()Array.prototype.concat()拓展运算符实现复制。

var obj = {
    name: 'xxx',
    age: 17
}
var newObj = Object.assign({}, obj);
const a = [1,2,3];
const b = a.slice(0);
b[1] = 4;
console.log(a, b);// [1,2,3] [1,4,3]
const a = [1,2,3];
const b = [...a];
b[1] = 4;
console.log(a, b);// [1,2,3] [1,4,3]

12.2 深拷贝

常见深拷贝方式:

  • _.cloneDeep()
  • jQuery.extend()
  • JSON.stringify()
  • 手写循环递归
const _ = require('lodash');
const obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f); // false

JSON.stringify()

// 有缺点 会忽略undefined、symbol、函数
const obj2=JSON.parse(JSON.stringify(obj1));

循环递归

function deepClone(obj, hash = new WeakMap()) {
    if (obj === null) return obj; //null或者undefined就不拷贝
    if (obj instanceof Date) return new Date(obj);
    if (obj instanceof RegExp) return new RegExp(obj);
    // 可能是对象或者普通的值 如果是函数的话不拷贝
    if (typeof obj !== "object") return obj;
    // 是对象的话就要进行深拷贝
    if (hash.get(obj)) return hash.get(obj);
    let cloneObj = new obj.constructor();
    hash.set(obj, cloneObj);
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            // 实现一个递归拷贝
            cloneObj[key] = deepClone(obj[key], hash);
        }
    }
    return cloneObj;
}

12.3 区别

浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享一块内存,修改对象属性会影响原对象。

深拷贝会另外创建一个一模一样的对象,不共享内存,不影响原对象。

13. JavaScript字符串的常用方法

let stringValue = "hello world"; 
console.log(stringValue.slice(3, 7)); // "lo w"
console.log(stringValue.substring(3,7)); // "lo w"
console.log(stringValue.substr(3, 7)); // "lo worl"
// 删除前、后或者前后所有空格符,再返回新的字符串
let stringValue = " hello world ";
let trimmedStringValue = stringValue.trim();
console.log(stringValue); // " hello world "
console.log(trimmedStringValue); // "hello world"
// 接收一个整数参数,将字符串复制多少次,返回拼接所有副本后的结果
let stringValue = "na ";
let copyResult = stringValue.repeat(2) // na na
  • toUpperCase()、toLowerCase() 大小写转化
  • indexOf() 从字符串开头去搜索传入的字符串,并返回位置(没找到返回-1)
  • includes() 字符串是否包含传入的字符串
  • split() 把字符串按照指定分隔符,拆分成数组
  • replace() 接收俩个参数,第一个参数为匹配的内容,第二个参数为替换的元素

14.数组常用方法

  • push() 添加到数组末尾
  • unshift() 在数组开头添加
  • splice() 传入3个参数,开始位置、0(要删除的元素数量)、插入的元素
  • concat() 合并数组,返回一个新数组

  • pop() 删除数组最后一项,返回被删除的项。
  • shift() 删除数组的第一项,返回被删除的项。
  • splice()传入两个参数,开始位置,删除元素的数量,返回包含删除元素的数组。
  • slice() 用于创建一个包含原有数组中一个或多个元素的新数组,不会影响原始数组。

  • indexOf() 返回要查找元素在数组中的位置,如果没找到则返回 -1.
  • includes() 返回查找元素是否在数组中,有返回true,否则false.
  • find() 返回第一个匹配的元素。

排序方法

  • reverse() 将数组元素方向反转
  • sort() 接受一个比较函数,用于判断那个值在前面
function compare(value1, value2) {
    if (value1 < value2) {
        return -1;
    } else if (value1 > value2) {
        return 1;
    } else {
        return 0;
    }
}
let values = [0, 1, 5, 10, 15];
values.sort(compare);
alert(values); // 0,1,5,10,15

转换方法

join() 方法接收一个参数,即字符串分隔符,返回包含所有项的字符串。

循环方法

some() 和 every() 方法一样

对数组每一项都运行传入的测试函数,如果至少有一个元素返回true,则这个方法返回true.

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let someResult = numbers.some((item, index, array) => item > 2);
console.log(someResult) // true
forEach()

对数组每一项都运行传入的函数,没有返回值

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.forEach((item, index, array) => {
    //执行操作
});
filter()

函数返回true 的项会组成数组之后返回。

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let filterResult = numbers.filter((item, index, array) => item > 2);
console.log(filterResult); // 3,4,5,4,3
map()

返回由每次函数调用的结果构成的数组。

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let mapResult = numbers.map((item, index, array) => item * 2);
console.log(mapResult) // 2,4,6,8,10,8,6,4,2

15.事件循环的理解?

事件循环

JavaScript是一门单线程的语言,实现单线程非阻塞的方法就是事件循环。 在JavaScript中,所有的任务都可以分为:

  • 同步任务:立即执行的任务,同步任务一般会直接进入到主线程执行。
  • 异步任务:异步的比如ajax网络请求,setTimeout定时函数等。

image.png

同步任务进入主线程,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。上述过程不断重复就是事件循环

宏任务与微任务
console.log(1)
setTimeout(()=>{
    console.log(2)
}, 0)
new Promise((resolve, reject)=>{
    console.log('new Promise')
    resolve()
}).then(()=>{
    console.log('then')
})
console.log(3)
  • 遇到 console.log(1),直接打印1
  • 遇到定时器,属于新的宏任务,留着后面执行
  • 遇到 new Promise,这个是直接执行的,打印'newPromise
  • .then 属于微任务,放入微任务队列,后面再执行
  • 遇到 console.log(3)直接打印 3
  • 好了本轮宏任务执行完毕,现在去微任务列表查看是否有微任务,发现.then 的回调,执行它打印'then'
  • 当一次宏任务执行完,再去执行新的宏任务,这里就剩一个定时器的宏任务了,执行它,打印 2

结果是:1=>'new Promise'=> 3 => 'then' => 2

异步任务执行顺序,事件队列其实是一个“先进先出”的数据结构,排在前面的事件会优先被主线程读取。

微任务

常见的微任务有:

  • Promise.then
  • MutaionObserver
  • process.nextTice(node.js)
宏任务

宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合.

常见的宏任务有:

  • script(可以理解为外层同步代码)
  • setTimeout/setInterval
  • Ul rendering/Ul事件
  • postMessage、MessageChannel
  • setlmmediate、1/0(Node.is) 这时候,事件循环,宏任务,微任务的关系如图所示

image.png

它的执行机制是:

  • 执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中
  • 当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完
async 与 await

async就是用来声明一个异步方法,await是用来等待异步方法执行。

async函数返回一个promise对象,下面代码是等效的:

function f() {
    return Promise.resolve('TEST');
}
async function asyncF() {
    return 'TEST';
}

正常情况下, await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

async function f() {
    // 等同于 return 123
    return await 123
}
f().then(i => console.log(i)) // 123

不管 await 后面跟着的是什么,await 都会阻塞后面的代码。

async function fn1 (){
    console.log(1)
    await fn2()
    console.log(2) // 阻塞
}
async function fn2 (){
    console.log('fn2')
}
fn1()
console.log(3)

await 会阻塞下面的代码(即加入微任务队列),上面的例子中,先执行 async 外面的同步代码同步代码执行完,再回到 async函数中,再执行之前阻塞的代码

输出:1,fn2,3,2

async function async1() {
    console.log('1')
    await async2()
    console.log('2')
}
async function async2() {
    console.log('3')
}
console.log('4')
setTimeout(function () {
    console.log('settimeout')
})
async1()
new Promise(function (resolve) {
    console.log('5')
    resolve()
}).then(function () {
    console.log('6')
})
console.log('7');
// 输出结果: 4 1 3 5 7 2 6 settimeout

分析过程:

  • 1.执行整段代码,遇到 console.log('4')直接打印结果,输出 4;
  • 2.遇到定时器了,它是宏任务,先放着不执行;
  • 3.遇到 async1(),执行 async1 函数,先打印 1 ,下面遇到 await 怎么办?先执行 async2,打印 3,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码
  • 4.跳到 new Promise 这里,直接执行,打印 5,下面遇到 .then(),它是微任务,放到微任务列表等待执行;
  • 5.最后一行直接打印 7 ,现在同步代码执行完了,开始执行微任务,即 await 下面的代码,打印 2;
  • 6.继续执行下一个微任务,即执行 then 的回调,6;
  • 7.上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印 settimeout所以最后的结果是: 4 1 3 5 7 2 6 settimeout

16.JavaScript本地存储方式有哪些?区别及应用场景?

16.1 方式

javaScript 本地缓存的方法主要讲述以下四种:

  • cookie
  • sessionStorage
  • localStorage
  • indexedDB
16.1.1.cookie

Cookie ,类型为「小型文本文件」,指某些网站为了辨别用户身份而储存在用户本地终端上的数据。是为了解决 HTTP 无状态导致的问题。 作为一段一般不超过 4KB 的小型文本数据,它由一个名称(Name)、一个值(Value)和其它几个用于控制 cookie 有效期、安全性、使用范围的可选属性组成。

但是 cookie 在每次请求中都会被发送,如果不使用 HTTPS 并对其加密,其保存的信息很容易被窃取,导致安全风险。

16.1.2 localStorage
  • 生命周期:持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。
  • 存储的信息在同一域中是共享的。
  • 当本页操作(新增、修改、删除)了 localStorage 的时候,本页面不会触发 storage 事件,但是别的页面会触发 storage 事件。
  • 大小:5M(跟浏览器厂商有关系)。
  • localstorage 本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡。
  • 受同源策略的限制。
localStorage.setItem('username','你的名字');
localStorage.getItem('username');
localStorage.key(0) // 获取第一个键名
localStorage.removeItem('username');
localStorage.clear(); // 清空localStorage
16.1.3 sessionStorage

sessionStoragelocalstorage 使用方法基本一致,唯一不同的是生命周期,一旦页面(会话)关闭,sessionStorage 将会删除数据。

16.1.4 indexedDB

indexedDB 是一种低级AP,用于客户端存储大量结构化数据(包括,文件/blobs)。该API使用索引来 实现对该数据的高性能搜索。

虽然 Web Storage 对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太有用。

优点:

  • 储存量理论上没有上限
  • 所有操作都是异步的,相比LocalStorage 同步操作性能更高,尤其是数据量较大时
  • 原生支持储存 JS 的对象
  • 是个正经的数据库,意味着数据库能干的事它都能干

缺点:

  • 操作非常繁琐
  • 本身有一定门槛
区别
  • 存储大小: cookie 数据大小不能超过 4ksessionStorage 和 localStorage 虽然也有存储大小的限制,但比cookie 大得多,可以达到5M或更大。
  • 有效时间: localStorage 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据; sessionStorage 数据在当前浏览器窗口关闭后自动删除; cookie 设置的 cookie 过期时间之前一直有效,即使窗口或浏览器关闭。
  • 数据与服务器之间的交互方式,cookie 的数据会自动的传递到服务器,服务器端也可以写 cookie 到客户端;sessionStorage 和 localStorage 不会自动把数据发给服务器,仅在本地保存

17.Ajax 原理是什么?如何实现?

Ajax 的原理简单来说通过 XmlHttpRequest 对象来向服务器发异步请求,从服务器获得数据,然后用 JavaScript 来操作 DOM 而更新页面。

简单封装一个ajax请求:

function ajax(options) {
    //创建XMLHttpRequest对象
    const xhr = new XMLHttpRequest();
    //初始化参数的内容
    options = options || {};
    options.type = (options.type || 'GET').toUpperCase();
    options.dataType = options.dataType 'json';
    const params = options.data;

    // 发送请求
    if (options.type === 'GET') {
        xhr.open('GET', options.url + '?' + params, true) xhr.send(null)
    } else if (options.type === 'POST') {
        xhr.open('POST', options.url, true) xhr.send(params)
        // 接收请求
        xhr.onreadystatechange = function() {
            if (xhr.readyState === 4) {
                let status = xhr.status;
                if (status >= 200 && status < 300) {
                    options.success && options.success(xhr.responseText, xhr.responseXML)
                } else {
                    options.fail && options.fail(status)
                }
            }
        }
    }
}

// 调用
ajax({
    type: 'post',
    dataType: 'json',
    data: {},
    url: 'https://xxxx',
    success: function(valse, xml){ 
        console.log(valse)
    },
    fail: function(status){ 
        console.log(status)
    }
})

18. 防抖和节流?区别?如何实现?

  • 节流: n秒内只运行一次,若在n秒内重复触发,只有一次生效。
  • 防抖: n 秒后在执行该事件,若在n秒内被重复触发,则重新计时

应用场景:

防抖在连续的事件,只需触发一次回调的场景有:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求
  • 手机号、邮箱验证输入检测
  • 窗口大小 resize 。只需窗口调整完成后,计算窗口大小。防止重复渲染。

节流在间隔一段时间执行一次回调的场景有:

  • 滚动加载,加载更多或滚到底部监听
  • 搜索框,搜索联想功能

节流

function throttled(fn, delay) {
    let timer = null;
    let starttime = Date.now();
    return function () {
        let curTime = Date.now(); // 当前时间
        let remaining = delay - (curTime - starttime); // 从上一次到现在,还剩下多少多余事件
        let context = this; // 保存this指向
        let args = arguments; // 拿到event对象
        clearTimeout(timer);
        if (remaining <= 0) {
            fn.apply(context, args);
            starttime = Date.now();
        } else {
            timer = setTimeout(fn, remaining);
        }
    }
}

防抖

function debounce(func, wait) {
    let timeout;
    return function () {
        let context = this; // 保存this指向
        let args = arguments; // 拿到event对象
        clearTimeout(timeout)
        timeout = setTimeout(() => {
            func.apply(context, args)
        }, wait);
    }
}

如果需要立即执行防抖,可加入第三个参数

function debounce(func, wait, immediate) {
    let timeout;
    return function() {
        let context = this; // 保存this指向
        let args = arguments; // 拿到event对象
        if (timeout) clearTimeout(timeout); // timeout 不为 null
        if (immediate) {
            let callNow = !timeout; // 第一次会立即执行,以后只有事件执行后才会触发
            timeout = setTimeout(function() {
                timeout = null;
            },
            wait);
            if (callNow) {
                func.apply(context, args)
            }
        } else {
            timeout = setTimeout(function() {
                func.apply(context, args)
            },
            wait);
        }
    }
}

区别

相同点

  • 都可以通过使用 setTimeout 实现
  • 目的都是,降低回调执行频率。节省计算资源

不同点

  • 函数防抖,在一段连续操作结束后,处理回调,利用clearTimeout和 setTimeout 实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能。
  • 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次例如,都设置时间频率为500ms,在2秒时间内,频繁触发函数,节流,每隔500ms 就执行一次。防抖,则不管调动多少次方法,在2s后,只会执行一次。

19. web常见的攻击方式有哪些?如何防御?

常见的有:

  • XSS 跨站脚本攻击
  • CSRF 跨站请求伪造
  • SQL 注入攻击

防止csrf常用方案如下:

  • 阻止不明外域的访问,同源检测,Samesite Coolkie
  • 提交时要求附加本域才能获取信息 CSRF Token, 双重Cookie验证

预防SQL如下:

  • 严格检查输入变量的类型和格式
  • 过滤和转义特殊字符
  • 对访问数据库的web应用程序采用web应用防火墙

20.JavaScript内存泄露的几种情况?

内存泄漏(Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存。

Javascript 具有自动垃圾回收机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。

常见的内存泄露情况:

  • 意外的全局变量。a='我是未声明的变量'.
  • 定时器

21. JavaScript数字精度丢失的问题?如何解决?

0.1 + 0.2 === 0.3; // false

可以使用parseFloat解决

CSS题型整理

1.盒模型

盒模型:由4个部分组成,content,padding,border,margin.

2.BFC的理解

BFC:即块级格式化上下文。

常见页面情况有:

  • 元素高度没了
  • 俩栏布局没法自适应
  • 元素间距奇怪
2.1清除内部浮动

元素添加overflow: hidden;

3.元素水平垂直居中的方法有哪些?

实现方式如下:

  • 利用定位+margin:auto
  • 利用定位+margin: 负值
  • 利用定位+transform
  • flex布局等

4.实现两栏布局,右侧自适应?三栏布局中间自适应?

两栏布局的话:

  • 使用float左浮动布局
  • 右边模块使用margin-left 撑出内容块做内容展示
  • 为父级元素添加BFC,防止下方元素跟上方内容重叠。

flex布局:

  • 简单易用代码少

三栏布局:

  • 两边用float,中间用margin
  • 两边用absolute,中间用margin
  • display: table
  • flex
  • grid网格布局

5.css中,有哪些方式隐藏页面元素?

例如:

  • display: none; 最常用,页面彻底消失,会导致浏览器重排和重绘
  • visibility: hidden; dom存在,不会重排,但是会重绘
  • opacity: 0; 元素透明 元素不可见,可以响应点击事件
  • position: absolute; 将元素移出可视区域,不影响页面布局

6.如何实现单行/多行文本溢出的省略样式

单行:

<style>
p {
    overflow: hidden;
    line-height: 40px;
    width:400px;
    height:40px;
    border:1px solid red;
    text-overflow: ellipsis;
    white-space: nowrap;
}

</style>
<p>文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本</p>

多行

<style>
.demo {
    position: relative;
    line-height: 20px;
    height: 40px;
    overflow: hidden;
}
.demo::after {
    content: "...";
    position: absolute;
    bottom: 0;
    right: 0;
    padding: 0 20px 0 10px;
}
</style>
<body>
    <div class="demo">文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本</div>
</body>

css实现

<style>
p {
    width: 400px;
    border-radius: 1px solid red;
    -webkit-line-clamp: 2;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    overflow: hidden;
    text-overflow: ellipsis;
}
</styl

7.CSS3新增了哪些新特性?

选择器:

  • nth-child(n)
  • nth-last-child(n)
  • last-child

新样式:

  • border-radius; 创建圆角边框
  • box-shadow; 为元素添加阴影
  • border-image; 图片绘制边框
  • background-clip; 确定背景画区
  • background-size; 调整背景图大小

文字:

  • word-wrap: normal|break-word; 使用浏览器默认的换行 | 允许在单词内换行;
  • text-overflow; clip | ellipsis; 修剪文本 | 显示省略符号来代表被修剪的文本;
  • text-decoration; text-fill-color| text-stroke-color | text-stroke-width;

transition 过渡、transform 转换、animatin动画、渐变、等

8.CSS提高性能的方法有哪些?

如下:

  • 内联首屏关键css
  • 异步加载css
  • 资源压缩(webpack/gulp/grunt)压缩代码
  • 合理的使用选择器
  • 不要使用@import
  • icon图片合成等

ES6

1.var,let, const的区别?

  • var 声明的变量会提升为全局变量,多次生成,会覆盖。
  • let let声明的变量只在代码块内有效。
  • const 声明一个只读常量,常量的值不能改变。

区别:

  • 变量提升,var会提升变量到全局。let, const直接报错
  • 暂时性死区
  • 块级作用域
  • 重复声明
  • 修改声明的变量

2.ES6中数组新增了哪些扩展?

  • 扩展运算符...
  • 构造函数新增的方法 Array.from(),Array.of()
  • 数组实例新增方法有:copyWithin(),find(),findIndex(),fill(),includes(),keys(),values()等

3.对象新增了哪些扩展

对象名跟对应值名相等的时候,可以简写。 const a = {foo: foo} == const a = {foo}

属性的遍历:

  • for...in:循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)
  • Object.keys(obj):返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)的键名
  • Object.getOwnPropertyNames(obj):回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)的键名
  • Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所有Symbol属性的键名----- Reflect.ownKeys(obj):返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是Symbol或字符串,也不管是否可枚举.

对象新增的方法

  • Object.is();
  • Object.assign();
  • Object.getOwnPropertyDescriptors() ;
  • Object.keys(), Object.values(),Object.entries();
  • Object.fromEntries();

4.理解ES6中Promise的?

优点:

  • 链式操作减低了编码难度
  • 代码可读性增强

promise对象仅有三种状态,pending(进行中),fulfilled(已成功),rejected(已失败)。一旦状态改变(从 pending变为 fulfilled和从 pending变为 rejected),就不会再变,任何时候都可以得到这个结果。

使用方法

const promise = new Promise(function(resolve, reject) {});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject

  • resolve函数的作用是,将Promise对象的状态从"未完成"变为"成功"
  • reject函数的作用是,将Promise对象的状态从"未完成"变为"失败"

实例方法:

  • then() 是实例状态发生改变时的回调函数。
  • catch() 指定发生错误时的回调函数。
  • finally() 不管Prosime对象最后状态如何,都会执行。

构造函数方法 Promise构造函数存在以下方法:

  • all() 将多个Promise实例包装成一个新的Promise实例。
  • race() 将多个Promise实例包装成一个新的Promise实例。
  • allSettled() 接受一组Promise实例作为参数,只有等所有这些参数返回结果,实例才会结束。
  • resolve() 将现有对象转为Promise对象。
  • reject() 返回一个新的Promise实例,状态为rejected。

Vue2面试题

1.生命周期?

beforeCreate -> created -> beforeMount -> mounted -> beforeUpdate -> updated -> beforeDestroy -> destroyed

1.4 数据请求在created和mouted的区别

created 是在组件实例一旦创建完成的时候立刻调用,这时候页面 dom 节点并未生成; mounted是在页面dom节点渲染完毕之后就立刻执行的。触发时机上created是比mounted要更早的,

两者的相同点:都能拿到实例对象的属性和方法。讨论这个问题本质就是触发的时机,放在mounted中的请求有可能导致页面闪动(因为此时页面dom结构已经生成),但如果在页面加载前完成请求,则不会出现此情况。建议对页面内容的改动放在 created 生命周期当中。

2.双向数据绑定是什么?

释义:当js代码更新Model时,view也会自动更新,用户更新view,Model的数据也会自动被更新,就是双向绑定。

3.Vue组件之间的通信方式有哪些?

  • 1.通过props传递 (父给子组件传递)
  • 2.通过$emit触发自定义事件 (子传父)
  • 3.使用ref (父组件使用子组件的时候)
    1. EventBus (兄弟组件传值)
    1. attrs 与 listeners (祖先传递给子孙)
    1. Provide 与 Inject (在祖先组件定义provide)返回传递的值,在后代组件通过inject 接收组件传递过来的值。
    1. Vuex (复杂关系组件数据传递,存放共享变量)

4.v-if和v-for的优先级是什么?

v-for的优先级比v-if的高

注意

不能把v-if 和 v-for 同时在同一个元素上,带来性能方面的浪费。必须使用的话可以在外层套一个template

5. 未完待续。。。

防抖(Debounce)实战解析:如何用闭包优化频繁 AJAX 请求,提升用户体验

2026年1月2日 16:16

在现代 Web 开发中,用户交互越来越丰富,但随之而来的性能问题也日益突出。一个典型场景是:搜索框实时建议功能。当用户在输入框中快速打字时,如果每按一次键就立即向服务器发送一次 AJAX 请求,不仅会造成大量无效网络开销,还可能导致页面卡顿、响应错乱,甚至压垮后端服务。本文将以“百度搜索建议”为例,通过对比未防抖防抖两种实现方式,深入浅出地讲解防抖技术的原理、实现及其带来的显著优势。


一、问题引入:不防抖的“蛮力请求”有多糟糕?

假设我们正在开发一个类似百度搜索的自动补全功能。用户在输入框中输入关键词,前端实时将内容发送到服务器,获取匹配建议并展示。

❌ 不防抖的实现(反面教材)

const input = document.getElementById('search');
input.addEventListener('input', function(e) {
    ajax(e.target.value); // 每次输入都立刻发请求
});

function ajax(query) {
    console.log('发送请求:', query);
    // 实际项目中这里是 fetch 或 XMLHttpRequest
}

用户输入 “javascript” 的过程:

表格

输入步骤 触发次数 发送的请求
j 1 "j"
ja 2 "ja"
jav 3 "jav"
java 4 "java"
javascript 10 "javascript"

后果分析:

  • 资源浪费:前9次请求几乎无意义(用户还没输完),却消耗了带宽、CPU 和服务器连接。
  • 响应错乱:如果“j”的响应比“javascript”晚到,页面会先显示“j”的结果,再跳变到最终结果,体验极差。
  • 页面卡顿:高频 DOM 操作 + 网络回调,容易导致主线程阻塞,输入框变得“卡手”。

这就是典型的“执行太密集、任务太复杂”问题——事件触发频率远高于实际需求


二、解决方案:用防抖(Debounce)优雅降频

✅ 什么是防抖?

防抖(Debounce)  是一种函数优化技术:在事件被频繁触发时,仅在最后一次触发后等待指定时间,才真正执行函数。

通俗理解:

用户打字时,我不急着查;等他停手500毫秒,我才认为他“打完了”,这时才发请求。

🔧 防抖的核心实现(基于闭包)

function debounce(fn, delay) {
    let timer; // 闭包变量:保存定时器ID
    return function(...args) {
        const context = this;
        clearTimeout(timer); // 清除上一次的定时器
        timer = setTimeout(() => {
            fn.apply(context, args); // 延迟执行,并保持this和参数
        }, delay);
    };
}

关键点解析:

  • 闭包作用timer 被内部函数引用,不会被垃圾回收,可跨多次调用共享。
  • 清除旧定时器:每次触发都重置倒计时,确保只执行“最后一次”。
  • 保留上下文:通过 apply 保证原函数的 this 和参数正确传递。

✅ 防抖后的使用

const debouncedAjax = debounce(ajax, 500);
input.addEventListener('input', function(e) {
    debouncedAjax(e.target.value);
});

用户输入 “javascript” 的效果:

  • 快速打完10个字母 → 只触发1次请求(“javascript”)
  • 中途停顿超过500ms → 触发当前值的请求(如打到“java”停住)

三、对比实验:防抖 vs 不防抖

我们在 HTML 中放置两个输入框:

<input id="undebounce" placeholder="不防抖(危险!)">
<input id="debounce" placeholder="防抖(推荐)">

绑定不同逻辑:

// 不防抖:每输入一个字符就请求
undebounce.addEventListener('input', e => ajax(e.target.value));

// 防抖:500ms 内只执行最后一次
debounce.addEventListener('input', e => debouncedAjax(e.target.value));

打开浏览器控制台,分别快速输入 “react”:

  • 不防抖输入框:控制台瞬间打印 5 条日志(r, re, rea, reac, react)
  • 防抖输入框:控制台仅在你停止输入后 0.5 秒打印 1 条日志(react)

用户体验差异:

  • 不防抖:页面可能闪烁、卡顿,建议列表频繁跳动。
  • 防抖:输入流畅,结果稳定,资源消耗降低 80% 以上。

四、为什么防抖能解决性能问题?

  1. 减少无效请求
    用户输入过程中产生的中间状态(如“j”、“ja”)通常无需处理,防抖直接忽略它们。
  2. 避免竞态条件(Race Condition)
    后发的请求覆盖先发的结果,确保 UI 始终显示最新、最完整的查询结果。
  3. 降低服务器压力
    假设每天有 10 万用户使用搜索,平均每人输入 10 次,不防抖产生 100 万请求;防抖后可能仅 10 万请求,节省 90% 计算资源。
  4. 提升前端性能
    减少 JavaScript 执行、DOM 更新和网络回调的频率,主线程更“轻盈”,页面更流畅。

五、防抖的适用场景

表格

场景 说明
搜索框建议 用户输入时延迟请求,等输入稳定后再查
窗口 resize 防止调整窗口大小时频繁触发布局计算
表单提交 防止用户狂点“提交”按钮导致重复提交
按钮点击 如“点赞”功能,避免快速连点

⚠️ 注意:滚动加载(scroll)更适合用节流(Throttle) ,因为用户持续滚动时仍需定期触发(如每 200ms 检查是否到底部),而防抖会在滚动结束才触发,可能错过加载时机。


六、总结:防抖是前端性能优化的基石

通过本文的对比实验,我们可以清晰看到:不加控制的事件监听是性能杀手,而防抖则是优雅的“减速阀” 。它利用闭包保存状态,通过定时器智能合并高频操作,在不牺牲用户体验的前提下,大幅降低系统开销。

在实际项目中,建议:

  • 对 inputkeyupresize 等高频事件默认使用防抖或节流
  • 使用成熟的工具库(如 Lodash 的 _.debounce)避免手写 bug
  • 根据业务调整延迟时间(搜索建议常用 300–500ms)

记住:好的前端工程师,不仅要让功能跑起来,更要让它跑得稳、跑得快、跑得省。  而防抖,正是你工具箱中不可或缺的一把利器。

🌟 小提示:下次当你看到百度搜索框在你打字时不急不躁、等你停手才给出建议时,就知道——背后一定有防抖在默默守护性能!

cloudflare使用express实现api防止跨域cors

作者 1024小神
2026年1月2日 15:49

大家好,我是1024小神,想进 技术群 / 私活群 / 股票群 或 交朋友都可以私信我,如果你觉得本文有用,一键三连 (点赞、评论、关注),就是对我最大的支持~

在 Cloudflare Workers 上,必须自己处理 CORS,Express 默认的 cors 中间件 并不会自动生效。

在中间件中写一个cors.ts文件,里面的代码如下:

import { Request, Response, NextFunction } from 'express';

export function corsMiddleware(req: Request, res: Response, next: NextFunction) {
// ⚠️ in production, write the specific domain
res.setHeader('Access-Control-Allow-Origin', '*');

res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');

res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');

// handle preflight request
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}

// next middleware
next();
}

然后配置中间件在所有的路由前面:

然后重启项目,再次发送请求就没事了:

如果你有好的想法或需求,都可以私信我,我这里有很多程序员朋友喜欢用代码来创造丰富多彩的计算机世界

从定时器管理出发,彻底搞懂防抖与节流的实现逻辑

作者 烟袅破辰
2026年1月2日 15:01

在前端开发中,高频事件(如输入、滚动、窗口缩放)若不加控制,极易引发性能问题。为应对这一挑战,防抖(debounce)节流(throttle) 成为必备工具。


一、防抖:每次触发都重置定时器

假设我们要实现一个功能:用户在输入框打字时,只有当他停止输入超过 1 秒,才发送请求。

第一步:我需要延迟执行

显然,要用 setTimeout

setTimeout(() => {
    ajax(value);
}, 1000);

第二步:但如果用户继续输入,之前的请求就不该发

→ 所以必须取消之前的定时器,再建一个新的。

这就要求我们保存定时器 ID

let timerId;
// 每次触发时:
if (timerId) clearTimeout(timerId);
timerId = setTimeout(() => {
    ajax(value);
}, 1000);

第三步:处理 this 和参数

因为 ajax 可能依赖上下文或多个参数,不能直接写死。我们需要在触发时捕获当前的 thisarguments

function debounce(fn, delay) {
    let timerId;
    return function(...args) {
        const context = this;
        if (timerId) clearTimeout(timerId);
        timerId = setTimeout(() => {
            fn.apply(context, args);
        }, delay);
    };
}

到此,防抖完成。它的全部逻辑就源于一句话: “每次触发,先删旧定时器,再建新定时器。”
所谓“停手后执行”,只是这种操作的自然结果。


二、节流:控制执行频率,必要时预约补发

现在需求变了:不管用户多快输入,每 1 秒最多只发一次请求,且最后一次输入不能丢

第一步:我能立即执行吗?

用时间戳判断是否已过 delay

const now = Date.now();
if (now - last >= delay) {
    fn(); 
    last = now; // 记录执行时间
}

这能保证最小间隔,但有个致命缺陷:如果用户快速输入后立刻停止,最后一次可能永远不会执行。

第二步:如何不丢尾?

→ 在冷却期内,预约一次未来的执行。这就要用到 setTimeout

于是逻辑分裂为两条路径:

  • 路径 A(可立即执行) :时间到了,马上执行,更新 last
  • 路径 B(还在冷却) :清除之前的预约,重新预约一次执行

第三步:管理预约定时器

我们需要一个变量 deferTimer 来保存预约任务的 ID:

let last = 0;
let deferTimer = null;

当处于冷却期时:

clearTimeout(deferTimer); // 清除旧预约
deferTimer = setTimeout(() => {
    last = Date.now(); // 关键:这次执行也要记录时间!
    fn.apply(this, args);
}, delay - (now - last)); // 精确计算剩余等待时间

第四步:整合逻辑

function throttle(fn, delay) {
    let last = 0;
    let deferTimer = null;

    return function(...args) {
        const context = this;
        const now = Date.now();

        if (now - last >= delay) {
            // 路径 A:立即执行
           
            last = now;
            fn.apply(context, args);
        } else {
            // 路径 B:预约执行
            if (deferTimer) clearTimeout(deferTimer);
            deferTimer = setTimeout(() => {
                last = Date.now(); // 必须更新!
               
                fn.apply(context, args);
            }, delay - (now - last));
        }
    };
}

节流的核心,是两种执行方式的协同

  • 立即执行靠时间戳判断
  • 补发执行靠 setTimeout 预约
    而两者共享同一个 last 状态,确保整体节奏不乱。

三、对比总结:防抖 vs 节流的机制差异

维度 防抖(Debounce) 节流(Throttle)
核心操作 每次触发都 clearTimeout + setTimeout 冷却期内 clearTimeout + setTimeout,否则立即执行
状态变量 仅需 timerId last(时间) + deferTimer(预约ID)
执行特点 只执行最后一次 固定间隔执行,且不丢尾
适用场景 搜索建议、表单校验 滚动加载、按钮限频、实时位置上报
❌
❌