<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Ce Feng</title>
        <link>https://foen.vercel.app</link>
        <description>Ce Feng's blog</description>
        <lastBuildDate>Tue, 07 Apr 2026 02:34:28 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Ce Feng</title>
            <url>https://foen.vercel.app/favicon.ico</url>
            <link>https://foen.vercel.app</link>
        </image>
        <copyright>All rights reserved Ce Feng 2026</copyright>
        <item>
            <title><![CDATA[AI Native 在下一盘干掉 Chrome 的大棋]]></title>
            <link>https://foen.vercel.app/blogs/ai-native</link>
            <guid>https://foen.vercel.app/blogs/ai-native</guid>
            <pubDate>Thu, 14 Aug 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[AI Native 正在逐步取代浏览器，成为数字世界新入口。]]></description>
            <content:encoded><![CDATA[
过去二十多年，互联网应用几乎都诞生在 浏览器 + Web 服务 + HTTP 传输 这一组合之上。

不同场景下，它们会延伸到移动端 App，但核心体验依然依附于浏览器生态。

每天早上，我们打开 Chrome，查邮件、看新闻、刷股票、逛社交媒体；用 Google Docs 写周报、用 YouTube 消磨午休时光。

信息获取、内容创作、社交娱乐——这些数字生活的核心环节，都在浏览器的框架中生长壮大。

当 AI 开始落地时，我们的第一反应，也是把它装进网页：用浏览器承载 AI 模型和各种服务，于是出现了 Chatbot、AI SaaS 的爆发式涌现。
这是一种“浏览器先入为主”的自然延续。

然而，在 AI 主导的新叙事下，格局正在翻转。

AI 不再只是网页里的一个应用，而更像是掌控与调度一切的浏览器本身（以及背后操纵浏览器的人）。

在 AI Native 的设想中：
- AI 客户端发起的 MCP Client 就像浏览器的独立标签页，拥有自己的进程与任务空间；
- MCP Server 则像是专注单一功能的 Web 应用，只提供最纯粹、高效的能力。

应用不再是信息孤岛，而成为 AI 客户端的原生插件。

我们不再“在网页中用 AI”，而是“在 AI 客户端中看网页”。

在这种形态下：

- 信息获取：AI 聚合多源数据，去重、验证，并持续推送动态更新，必要时溯源。这种交互模式已经借助大模型 + RAG 初具雏形。
- 生产创作：从需求定义到工具调用、工作流搭建、验证、自主产出，再到人工验收，形成高效闭环。
- 社交娱乐：AI 释放生产力，你尽情使用你的时间。好吧，这听起来就很科幻，但很多人已经实现了《如何优雅用 AI 摸鱼》。

小学的微机课上，穿上鞋套，踩着机房的木地板，打开台式机里的 IE 浏览器，这曾是一代人走进互联网的第一课。

十年，甚至五年后，AI Native 将是未来数字世界（乃至物理世界）的智能座舱。谁会是那时的 IE 和 Chrome？





---

]]></content:encoded>
            <author>cefeng06@gmail.com (Ce Feng)</author>
        </item>
        <item>
            <title><![CDATA[把 AI 助手接入飞书：OpenClaw 配置实录与避坑指南]]></title>
            <link>https://foen.vercel.app/blogs/openclaw-feishu-setup</link>
            <guid>https://foen.vercel.app/blogs/openclaw-feishu-setup</guid>
            <pubDate>Thu, 26 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[从零开始配置 OpenClaw + 飞书机器人，记录真实踩坑经历、多 Bot 独立记忆方案，以及那些文档里没说清楚的细节。]]></description>
            <content:encoded><![CDATA[
深夜十二点，我终于让 AI 助手在飞书里跑起来了。

这不是一篇宣传软文，是一篇踩坑实录。

---

## 为什么选 OpenClaw

市面上把 AI 接入即时通讯软件的方案不少，但大多数停留在"问答机器人"层面——发一条，等一条，回来一段话。

OpenClaw 不太一样。它更像是一个**驻留在你设备上的 AI 代理**：

- 有自己的 workspace，维护跨会话的长期记忆
- 能主动执行任务（定时检查邮件、整理文件、跑脚本）
- 支持多渠道接入：飞书、Telegram、WhatsApp、Discord……
- 本地 Gateway，不依赖第三方云，数据不离机

对我来说，最吸引人的一点是**心跳机制**：你睡觉的时候，它可以在后台帮你做事。

---

## 配置流程

### 第一步：安装 OpenClaw

```bash
npm install -g openclaw
# 或者
pnpm install -g openclaw
```

安装完成后运行引导：

```bash
openclaw onboard
```

向导会依次引导你完成：AI 模型配置（支持 Claude、GPT、Gemini 等）、渠道接入、Gateway 启动。

### 第二步：创建飞书应用

前往 [飞书开放平台](https://open.feishu.cn/app)，创建一个「企业自建应用」。

需要收集两个东西：
- **App ID**（格式：`cli_xxx`）
- **App Secret**

然后在应用后台完成三件事：

**1. 批量导入权限**

在「权限管理」里点击「批量导入」，粘贴以下 JSON：

```json
{
  "scopes": {
    "tenant": [
      "im:message",
      "im:message:send_as_bot",
      "im:message.p2p_msg:readonly",
      "im:message.group_at_msg:readonly",
      "im:resource",
      "contact:user.employee_id:readonly"
    ]
  }
}
```

**2. 开启 Bot 能力**

在「应用功能」→「机器人」里开启，设置 Bot 名称。

**3. 配置事件订阅**

选择**长连接模式**（不需要公网 URL），添加事件：`im.message.receive_v1`。

> ⚠️ **坑点一**：必须先跑起 Gateway，再来配置长连接。否则验证会失败。

### 第三步：接入 OpenClaw

```bash
openclaw channels add
```

选择 Feishu，粘贴 App ID 和 App Secret 即可。

配置完成后重启 Gateway：

```bash
openclaw gateway restart
```

---

## 踩坑实录

### 坑点一：个人飞书 vs 企业飞书

这是**最大的坑**，也是文档没有强调的地方。

飞书分两种模式：
- **企业版**：有组织架构，成员可以直接搜索到 Bot
- **个人版**：Bot 只对同一个「个人空间/团队」下的成员可见

如果你用的是个人飞书，想让朋友或家人也能使用你的 Bot，**必须先邀请他们加入你的个人团队**，否则他们搜不到这个 Bot。

### 坑点二：配对审批（Pairing）

默认情况下，新用户给 Bot 发消息，Bot 会回复一个**配对码**而不是正常对话。

你需要在终端执行：

```bash
openclaw pairing list feishu
openclaw pairing approve feishu <CODE>
```

通过审批之后才能正常使用。这个机制是好的（防止陌生人滥用），但第一次不知道的话会以为 Bot 坏了。

### 坑点三：App 需要发布才能接收消息

创建应用之后，必须在「版本管理与发布」里**创建版本 → 提交审核 → 发布**，Bot 才会真正生效。

企业自建应用通常可以直接发布（不需要等平台审核），但这一步容易被忽略。

---

## 进阶：多 Bot 独立记忆

我的需求是：给女朋友也配一个 Bot，两边对话**完全独立**，记忆不混淆。

OpenClaw 支持「多 Agent 路由」，配置思路如下：

**1. 在 `~/.openclaw/openclaw.json` 添加第二个账号：**

```json
{
  "channels": {
    "feishu": {
      "accounts": {
        "main": {
          "appId": "cli_xxx",
          "appSecret": "xxx",
          "botName": "小虾"
        },
        "gf": {
          "appId": "cli_yyy",
          "appSecret": "yyy",
          "botName": "小助手"
        }
      }
    }
  }
}
```

**2. 配置独立的 Agent 和 Workspace：**

```json
{
  "agents": {
    "list": [
      { "id": "main", "workspace": "/path/to/workspace-main" },
      { "id": "gf", "workspace": "/path/to/workspace-gf" }
    ]
  }
}
```

**3. 绑定用户 ID 到对应 Agent：**

等用户发出第一条消息后，从日志里拿到她的 `open_id`，然后：

```json
{
  "bindings": [
    {
      "agentId": "gf",
      "match": {
        "channel": "feishu",
        "peer": { "kind": "direct", "id": "ou_xxx" }
      }
    }
  ]
}
```

这样两个 Bot 就彻底独立了——各自的对话历史、记忆文件、人设配置，互不干扰。

---

## 心跳：让 AI 在你睡觉时工作

OpenClaw 有一个「心跳」机制，每隔一段时间会触发 AI 主动检查任务。

在 workspace 里创建 `HEARTBEAT.md`，写下你希望它定期做的事情：

```markdown
# HEARTBEAT.md

- 检查未读重要邮件
- 提醒 24 小时内的日历事项
- 整理今日笔记
```

配置心跳频率（在 `openclaw.json` 里）：

```json
{
  "agents": {
    "defaults": {
      "heartbeat": { "every": "30m" }
    }
  }
}
```

结合 `MEMORY.md`（长期记忆）和 `memory/YYYY-MM-DD.md`（每日日志），AI 助手会真正像一个懂你的人，而不只是一个问答机器。

---

## 总结

整个配置过程花了大概一个小时，其中大半时间在踩坑。

真正花时间的地方：
1. 个人飞书的访问限制（需要先邀请成员加入团队）
2. 配对审批流程不直观
3. 多 Bot 路由的配置需要用户 ID，而 ID 要等对方先发消息才能拿到

配置好之后的体验是值得的。一个能在飞书里随时响应、能主动干活、有记忆的 AI 助手，用着确实不一样。

下次打算写写 OpenClaw 的 Skill 系统，以及怎么让它帮你自动处理 GitHub Issues。

---

*本文基于 OpenClaw v2026.2.24 + 飞书个人版，记录于 2026-02-26 凌晨。*
]]></content:encoded>
            <author>cefeng06@gmail.com (Ce Feng)</author>
        </item>
        <item>
            <title><![CDATA[旧文重读：给被绑架的子组件讨个说法]]></title>
            <link>https://foen.vercel.app/blogs/react-memo</link>
            <guid>https://foen.vercel.app/blogs/react-memo</guid>
            <pubDate>Tue, 21 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[重读系列的第二篇文章，源自我在项目中遇到的子组件重渲染问题，从行文到做图，花了不少于两晚。]]></description>
            <content:encoded><![CDATA[
最近重新回看了早期发表在掘金的技术文章，我对自己的耐心感到陌生。

工程师在变，框架在变，AI 也能替我们讲出答案了，但万幸，这篇文章的思路并不过时。

[原文](https://juejin.cn/post/7222654249133359160)如下：

---

### 前言

沉浸式地写过一个React项目就会发现，不同于一些替你做决定的框架，“潜规则”丰富的React远比看上去要难相处。

React中主要有两类坑点，一种是现象不符合预期，让你措手不及，严重影响开发进度。另一种是看似风平浪静，水下暗流涌动，不动声色地孕育隐患。

官方文档不会介绍花样百出的最差实践，所以下一批开发者又会掉入相同的陷阱。隐藏的坑点需要开发者亲自下地扫雷，经验主义发挥了重要作用，尤其是在Hooks使用中。

这个系列的文章会介绍一些React使用的常见陷阱，带你追溯原因和探索解决方案，帮助新手迅速跳过坑点。

往期文章：

[【React避坑指南】useEffect依赖引用类型](https://juejin.cn/post/7209160083690750008)

上一篇文章我们讲述了`useEffect`依赖引用类型时，即使依赖项的值不变，也会执行引起不必要的重渲染。这次我们关注另一种无意义的重渲染。

### 绑架式重渲染

这里由外向内定义了三层组件，结构是`  App -> A -> B ｜ C `。每个组件都设置了独立的`state`，点击button后会更新`state`，组件之间没有`props`传递。

```typescript
function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const clear = changeBgColor("parent", "green");
    return clear;
  });

  return (
    <div id="parent" className="component">
      <button onClick={() => setCount(count + 1)}> APP</button>
      <CompA />
    </div>
  );
}

function CompA() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const clear = changeBgColor("a", "red");
    return clear;
  });

  return (
    <div id="a" className="component">
      <button onClick={() => setCount(count + 1)}>Component A</button>
      <CompB />
      <CompC />
    </div>
  );
}

function CompB() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const clear = changeBgColor("b", "yellow");
    return clear;
  });

  return (
    <div id="b" className="component">
      <button onClick={() => setCount(count + 1)}>Component B</button>
    </div>
  );
}

function CompC() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const clear = changeBgColor("c", "blue");
    return clear;
  });

  return (
    <div id="c" className="component">
      <button onClick={() => setCount(count + 1)}> Component C</button>
    </div>
  );
}

export default App;
```

我们知道，`useEffect`如果不添加第二个参数，每次组件刷新都会执行回调，借助这个特性，我们为每一个组件绑定背景渐变的动画，便于观察组件是否刷新。

```typescript
const changeBgColor = (key: string, color: string) => {
  const ele = document.getElementById(key) as HTMLElement;
  
  ele.style.backgroundColor = color;

  setTimeout(() => {
    ele.style.transition = "background-color 1s";
    ele.style.backgroundColor = "transparent";
  }, 0);

  const timer = setTimeout(() => {
    ele.style.transition = "";
  }, 1000);

  return () => clearTimeout(timer);
};
```

当我们更新最外层组件App的state时，App会重渲染，作为子组件树的A、B、C会一同刷新。进一步依次更新每一个子组件的状态，可以发现：

-   更新A的`state` -> A、B、C刷新, App不变；
-   更新B的`state` -> B刷新， 其余不变；
-   更新C的`state` -> C刷新， 其余不变；

<img src="/blog/react-memo/1.awebp" width="600" />

根据以上观察，于是有了结论:

**父组件渲染会导致整个子组件树刷新，无关子组件的状态或参数是否改变。子组件渲染不会影响父组件和兄弟组件。**

子组件从属于父组件，随父组件刷新，看似符合直觉。

但在这个案例中，子组件不会接受父组件的`props`，子组件`render`的内容与父组件无关，父组件刷新时，子组件状态也不会改变，但子组件却被“绑架”着执行了一轮重渲染，这貌似也有争议。

因为我们期待的理想状态，就如同`Svelte`文档中讲到的：

**Svelte 编写的代码在应用程序的状态更改时就能像做外科手术一样更新 DOM。**

但是这时不得不提起另一句：

**框架的设计是权衡的艺术，框架之间的差异反应出设计者的认知。**

`React`认为在大多数场景下，子组件并非纯渲染组件，其`props`继承自父组件，父组件状态更新，会影响到子组件`render`的内容，因此子随父变的逻辑适用80%以上的场景，其余场景可以提供API专门应对。

于是就有了`React.memo`。

### 保护罩，可隔离

`React.memo`是一种高阶函数，可以用于优化函数组件的性能。当使用`React.memo`包装一个组件时，`React.memo`会将组件的`props`与前一次渲染的`props`进行浅比较。如果`props`没有发生变化，则`React.memo`会使用上一次渲染的结果，而不重新渲染组件。

我们将上例中的内层组件C用`memo`包裹，重复上述试验，可以观察到, 无论父组件的状态如何变化，C组件岿然不动，脱离了父辈绑架。

```typescript
const MemoC = memo(CompC);
```
<img src="/blog/react-memo/2.awebp" width="600" />

如果我们继续把`Memo`包裹的组件上提至A组件，可以发现A组件及其子组件，都不受最外层App状态更新的影响，`Memo`如同保护罩防止了内层组件的刷新。

<img src="/blog/react-memo/3.awebp" width="600" />

### 保护罩，但镂空

这个问题看似解决了，上例中的组件没有涉及`props`的传递, 我们进一步让组件更加复杂，给内层组件A增加一个`props`，`key`为`value`，值为外层组件的`state`。

```typescript
const App = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const clear = changeBgColor("parent", "green");
    return clear;
  });

  return (
    <div id="parent" className="component">
      <button onClick={() => setCount(count + 1)}>App</button>
      <CompA value={0} />
    </div>
  );
};

const CompA: React.FC<{ value: number }> = memo(
  ({ value }) => {
    useEffect(() => {
      const clear = changeBgColor("a", "red");
      return clear;
    });

    return (
      <div id="a" className="component">
        <div>Memo (Component A) </div>
      </div>
    );
  }
);
```

尽管被`Memo`包裹，但由于`props`变化，内层的组件还是重新执行了一轮渲染。

<img src="/blog/react-memo/4.awebp" width="600" />

如果把`props`值改为常量，内层组件不会刷新，`memo`发挥了缓存组件的作用，以上符合预期。

<img src="/blog/react-memo/5.awebp" width="600" />

但是，当我在`props`中增加一个`object`，值为`{ value: 0 }`:

```typescript
const App = () => {
  // ...
  
  return (
    <div id="parent" className="component">
      <button onClick={() => setCount(count + 1)}>App</button>
      <CompA value={0} object={{ value: 0 }}/>
    </div>
  );
};

const CompA: React.FC<{ value: number, object: { value: number } }> = memo(
  ({ value, object }) => {
  	// ...
  }
);
```

发现尽管每次传递的`props`恒定，而且子组件使用`Memo`包裹，但父组件刷新时还是连带子组件，似乎`memo`这一层保护罩是镂空的。

<img src="/blog/react-memo/6.awebp" width="600" />

看过上一篇文章的同学一定能猜到，这还是由于Javascript中引用类型的储存方式和React Hooks的浅比较机制决定的。

和`useEffect`类似，`memo`也采用浅比较决定是否执行组件的`render()`，对于原始类型，这并没有问题。但当`props`为引用类型时，尽管`object`内部的值相同，父组件每次刷新，都会新建另一个`object`传给子组件: `const object = { value: 0 }`。

由于前后二次创建的`object`在内存中的地址完全不同，`{ value: 0 } === { value: 0 }`浅比较始终为`false`，子组件进行了无效刷新。

所以说，`memo`这层函数组件保护罩，看似坚固，实际是镂空型钢丝网，并非想象中的铁板一张。

### 保护罩，但后果自负

但具体应用中，我们不能放弃在`props`中传递引用类型，但如何让引用类型的内存地址保持一致呢?

这里先给出第一种解法, 从父组件传递的内容入手。

```typescript
const App = () => {
  // ...

  const object = useMemo(() => {
    return { value: 0 }
  }, []);
  
  return (
    <div id="parent" className="component">
      <button onClick={() => setCount(count + 1)}>App</button>
      <CompA value={0} object={object}/>
    </div>
  );
};
```

既然每一次函数组件的刷新导致`props`重新声明，那不如把引用类型的`props`用`useMemo`包裹成可缓存的变量，只在组件挂载时创建这个变量，后续更新组件并不会改变object的保存的地址和值。

如果`object`作为`props`同时传给了多个子组件，这种从上层组件解决问题的方式，就非常适合，对于新组件的拓展，少了很多心智负担。

但是这种思路就像是给镂空保护罩附带了使用说明，要求被过滤的物体尺寸不能小于一个数值，否则后果自负。

### 保护罩，但Plus

第二种方案从子组件的刷新机制入手，深入了解`memo`会发现，它的第二个参数是一个可选的比较函数`areEqual`，`memo`利用这个函数判断组件的`props`是否发生变化。如果`areEqual`函数没有传入，则默认使用浅层比较`shallowEqual`。

```typescript
function memo<Props extends object>(
  Component: (props: Props) => ReactElement | null,
  areEqual?: (prevProps: Props, nextProps: Props) => boolean,
): NamedExoticComponent<Props>;

function areEqual(prevProps: object, nextProps: object): boolean {
  return shallowEqual(prevProps, nextProps);
}

function shallowEqual(objA: unknown, objB: unknown): boolean {
  if (Object.is(objA, objB)) {
    return true;
  }

  if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
    return false;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  for (let i = 0; i < keysA.length; i++) {
    if (
      !Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
      !Object.is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }

  return true;
}
```

所以我们可以自定义这个函数的判断逻辑，使用deepClone或者手动比较对象的值。

```typescript
const CompA: React.FC<{ value: number; object: any }> = memo(
  ({ value, object }) => {

    //...

    return (
      <div id="a" className="component">
        <div>Memo (Component A) </div>
      </div>
    );
  },
  (prev, next) => {
    // 对象的深比较
    return deepCompare(prev, next);
    // 或采用自定义逻辑
    // return prev.value === next.value && prev.object.value === next.object.value;
  }
);
```

这种方法背后的思路是追根溯源的改变`memo`的刷新逻辑，如果很多组件都要使用`memo`，完全可以封装一个`deepMemo`一劳永逸的作为保护罩Plus。

```typescript
const deepMemo = (FunctionComponent) => {
  return memo(FunctionComponent, (prev, next) => {
    return deepCompare(prev, next);
  })
}
```

### 总结

说来一圈，这个问题并不复杂，但疏于关注的重渲染的开发者未必会意识到，而且正如上篇提到的，这篇文章依然遵循我们解决问题的一般思路：

> 由异常现象出发（子组件无效渲染），到文档和源码追根溯源（memo的用法），在语言底层定位原因（基本类型和引用类型的储存方式不同），围绕原因提出治标（修改外部传入props）或治本（修改内部刷新机制）的方案，优化方案形成更好的工程实践（deepMemo），总结形成可推广复用的逻辑]]></content:encoded>
            <author>cefeng06@gmail.com (Ce Feng)</author>
        </item>
        <item>
            <title><![CDATA[旧文重读：那些年，我们一起追过的 useEffect]]></title>
            <link>https://foen.vercel.app/blogs/react-use-effect</link>
            <guid>https://foen.vercel.app/blogs/react-use-effect</guid>
            <pubDate>Mon, 20 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[重读 2023 年的一篇 React 技术文章，从 useEffect 引用类型导致的坑点出发，拆解技术问题的解决路径。]]></description>
            <content:encoded><![CDATA[
最近重新回看了自己在 2023 年初发表在掘金的技术文章，剖析 React 的 useEffect 文档背后那些不易察觉的坑点。那时我还习惯沿着异常现象一路追溯到源码、再推演出解决方案，相信「理解」本身就是一种力量。

两年过去，AI 的普及改变了一切。问题的解决路径从「思考-查阅-推理-验证」变成了「提问—生成—对比—再提问」。我开始怀疑技术文章的意义——当答案被即时生成，我们是否还需要这些记录和分享？

可当我重读那篇旧文，却又感受到一种被唤醒的熟悉感。那些耐心拆解、抽丝剥茧的片段质问我：思考的过程，还是我们与工具的分界线吗？。

[原文](https://juejin.cn/post/7209160083690750008)如下：

---

### 前言
如果你是一个入行不久的前端开发，面试中多半会遇到一个问题：
你认为使用React要注意些什么？

这个问题意在考察你对React的使用深度，因为沉浸式地写过一个项目就会发现，不同于一些替你做决定的框架，“潜规则”丰富的React远比看上去要难相处。

React中主要有两类坑点，一种是让你措手不及，结果对不上预期，严重影响开发进度，另一种更为头痛，表面风平浪静，水下暗流涌动。

官方文档的触角只伸到Demo级别，并不涉及花样百出的最差实践，所以下一批开发者又会掉入相同的陷阱。隐藏的坑点需要开发者亲自下地扫雷，经验主义发挥了重要作用，尤其是在Hooks使用中。

为了避免更多的心智负担，这个系列的文章会介绍一些React使用的常见陷阱，带你追溯原因和探索解决方案，帮助新手迅速跳过坑点。

### 异常现象
```javascript
const Issue = function () {
  const [count, setCount] = useState(0);
  const [person, setPerson] = useState({ name: 'Alice', age: 15 });
  const [array, setArray] = useState([1, 2, 3]);

  useEffect(() => {
    console.log('Component re-rendered by count');
  }, [count]);

  useEffect(() => {
    console.log('Component re-rendered by person');
  }, [person]);

  useEffect(() => {
    console.log('Component re-rendered by array');
  }, [array]);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(1)}>Update Count</button>
      <button onClick={() => setPerson({ name: 'Bob', age: 30 })}>Update Person</button>
      <button onClick={() => setArray([1, 2, 3, 4])}>Update Array</button>
    </div>
  );
};
```
在这个案例中，初始化了三个状态，和对应的三个副作用函数useEffect，理想状态是状态的值更新时才触发useEffect。<br />多次点击Update Count更新State，因为更新后的值还是1，所以第一个useEffect执行第一次后不会重复执行，这符合预期。但是重复点击Update Person和Update Array时，却不是这样，尽管值相同，但useEffect每一次都会触发。当useEffect中的副作用计算量较大时，必然会引起性能问题。
<a name="iSWxJ"></a>
### 原因追溯
为了追溯这个原因，可以首先熟悉一下useEffect的源码：
```javascript
function useEffect(create, deps) {
  const fiber = get();
  const { alternate } = fiber;

  if (alternate !== null) {
    const oldProps = alternate.memoizedProps;
    const [oldDeps, hasSameDeps] = areHookInputsEqual(deps, alternate.memoizedDeps);

    if (hasSameDeps) {
      pushEffect(fiber, oldProps, deps);
      return;
    }
  }

  const newEffect = create();

  pushEffect(fiber, newEffect, deps);
}

function areHookInputsEqual(nextDeps, prevDeps) {
  if (prevDeps === null) {
    return false;
  }

  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (Object.is(nextDeps[i], prevDeps[i])) {
      continue;
    }

    return false;
  }

  return true;
}
```
在上面的代码中，我们着重关注`areHookInputsEqual`的实现，这个函数对比了前后两次传入的依赖项，决定了后续副作用函数`create()`是否会执行。可以明显看到，useEffect对于依赖项执行的是浅比较，即`Object.is (arg1, arg2)`，这可能是出于性能考虑。对于原始类型这没有问题，但对于引用类型（数组、对象、函数等），这意味着即使内部的值保持不变，引用本身也会发生变化，导致 useEffect执行副作用。
<a name="XQd3s"></a>
### 方案探索
<a name="VBQkF"></a>
#### 1.饮鸩止渴
> 缝缝补补只是为了等一个人替你推倒重盖

最直接的思路是把useEffect的依赖项从引用类型换成基本类型：
```javascript
  useEffect(() => {
    console.log('Component re-rendered by person');
  }, [JSON.stringify(person)]);

  useEffect(() => {
    console.log('Component re-rendered by array');
  }, [JSON.stringify(array)]);
```
表面上可行，实际后患无穷（具体参考JSON.stringify为什么不能用来深拷贝），为了避坑而挖另外的坑，显然不是我们期待的解决方案。
```javascript
useEffect(() => {
  console.log('Component re-rendered by person');
}, [person.name, person.age]);
```
对比之下，这样的写法可以容忍，但是person对象如果增加了其他属性，你要确保自己还记得更新依赖，否则依然是掩盖问题。

#### 2.前置拦截
第二种思路：
> 在你决定要出手之前，我已经帮你决定了 —— 格林公式引申

我们可以把问题尽可能前置，手动加一层深对比，如何发现引用值没有变化，就不执行状态更新的逻辑，也就不会触发useEffect重复执行。
```javascript
<button onClick={() => {
    const newPerson = { name: 'Bob', age: 18 };
    if (!isEqual(newPerson, person)) {
      setPerson(newPerson)}
    }
  }
>Update person</button>
```
但这样显然不太优雅，且每一次写setState时心智负担太重，对比逻辑可不可以封装起来。

<a name="mJcI8"></a>
#### 3.他山之石
实际上自定义的Hooks就是为了解决方法级别的逻辑复用，这里我们利用useRef绑定的值可以跨渲染周期的特点，实现一个自定义的useCompare。
```javascript
const useCompare = (value, compare) => {
  const ref = useRef(null);
  if (!compare(value, ref.current)) {
    ref.current = value;
  }
  return ref.current;
}

```
经过ref记录的上一次结果，我们同时拥有了前后两次更新的状态，如果发现值不同，再让ref绑定新的引用类型地址。
```javascript
import { isEqual } from 'lodash';

const comparePerson = useCompare(person, isEqual);

useEffect(() => {
    console.log('Component re-rendered by comparePerson');
}, [comparePerson]);

// 重复执行
useEffect(() => {
  console.log('Component re-rendered by person');
}, [person]);
```
需要注意的是，这里使用了lodash的isEqual函数实现深对比，看似省心实际是一个成本极其不稳定的选择，如果对象过于庞大，可能得不偿失，可以传入简化的compare函数，有取舍的比较常变的key值。<br />而且每次又到单独调用useCompare生成新的对象，这里的逻辑也值得被封装。
<a name="VLgcc"></a>
#### 4.回归本质
> 停止曲线救国，直面问题本身。

说了这么多，实际还是useEffect中对比逻辑问题，本着支持拓展但不支持修改的原则，我们需要支持一个新的useEffect支持深度对比。我们将useRef实现的记忆引用传入useEffect的对比逻辑中：
```javascript
import { useEffect, useRef } from 'react';
import isEqual from 'lodash.isequal';

const useDeepCompareEffect = (callback, dependencies, compare) => {
  // 默认的对比函数采用lodash.isEqual, 支持自定义
  if (!compare) compare = isEqual;
  const memoizedDependencies = useRef([]);
  if (!compare (memoizedDependencies.current, dependencies)) {
    memoizedDependencies.current = dependencies;
  }
  useEffect(callback, memoizedDependencies.current);
};

export default useDeepCompareEffect;


function App({ data }) {
  useDeepCompareEffect(() => {
    // 这里的代码只有在 data 发生深层级的改变时才会执行
    console.log('data 发生了改变', data);
  }, [data]);

  return <div>Hello World</div>;
}
```
考虑到前文提到的复杂对象的深对比隐患，我依然结和个人意志，在useDeepCompareEffect中加了一个可选参数compare函数，把isEqual作为一种默认模式。于是，我们终于有了一劳永逸的方法。
<a name="RWEaC"></a>
### 总结
实际上，react-use和a-hooks等第三方库都已经实现了useDeepCompareEffect，也可以发现自定义hooks解决问题将会是目前体系下一种复用性极高的实践。

[useDeepCompareEffect - ahooks 3.0](https://ahooks.js.org/hooks/use-deep-compare-effect/)

[react-use/useDeepCompareEffect.md at master · streamich/react-use](https://github.com/streamich/react-use/blob/master/docs/useDeepCompareEffect.md)

以上是这个系列的第一篇，通过推导结论的过程，能看出我们解决问题的一般思路：
> <br /> **由异常现象出发（依赖项的值不变但重新渲染），到文档和源码追根溯源（useEffect），在语言底层定位原因（基本类型和引用类型的储存方式不同），围绕原因提出治标（方案1）或治本（方案2，3）的方案，优化方案形成更好的工程实践（方案4），总结形成可推广服用的逻辑（ahooks / react-use）。**




]]></content:encoded>
            <author>cefeng06@gmail.com (Ce Feng)</author>
        </item>
    </channel>
</rss>