Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
757eb403ac | ||
|
|
87c2745395 | ||
|
|
0d3d8ef54c | ||
|
|
53d5ee1960 |
@@ -0,0 +1,154 @@
|
||||
## 1 引言
|
||||
|
||||
使用 React Hooks 的时候,经常出现执行次数过多甚至死循环的情况,我们可以利用 [use-what-changed](https://github.com/simbathesailor/use-what-changed) 进行依赖分析,找到哪个变量引用一直在变化。
|
||||
|
||||
据一个例子,比如你尝试在 Class 组件内部渲染 Function 组件,Class 组件是这么写的:
|
||||
|
||||
```jsx
|
||||
class Parent extends React.PureComponent {
|
||||
state = {
|
||||
text: "text",
|
||||
};
|
||||
|
||||
render() {
|
||||
return <Child setText={(text) => this.setState({ text })} />;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
子组件是这么写的:
|
||||
|
||||
```jsx
|
||||
const Child = ({ setText }) => {
|
||||
useEffect(() => {
|
||||
setText("ok");
|
||||
}, [setText]);
|
||||
|
||||
return null;
|
||||
};
|
||||
```
|
||||
|
||||
那么恭喜你,写出了一个最简单的死循环。这个场景里,我们本意是利用 `useEffect` 调用 `props.setText` 更新父组件的 `text`,但执行 `props.setText` 会导致父组件重渲染,由于父级 `setText={(text) => this.setState({ text })}` 的写法,每次重渲染拿到的 `props.setText` 引用都会变化,因此再次触发了 `useEffect` 回调执行,进而触发死循环。
|
||||
|
||||
仅仅打印出值是看不出变化的,引用的改变很隐蔽,为了判断是否变化还得存储上一次的值做比较,非常麻烦,use-what-changed 就是为了解决这个麻烦的。
|
||||
|
||||
## 2 精读
|
||||
|
||||
use-what-changed 使用方式如下:
|
||||
|
||||
```jsx
|
||||
function App() {
|
||||
useWhatChanged([a, b, c, d]); // debugs the below useEffect
|
||||
|
||||
React.useEffect(() => {
|
||||
// console.log("some thing changed , need to figure out")
|
||||
}, [a, b, c, d]);
|
||||
}
|
||||
```
|
||||
|
||||
将参数像依赖数组一样传入,刷新页面就可以在控制台看到引用或值是否变化,如果变化,对应行会展示 ✅ 并打印出上次的值与当前值:
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1SN7JKbj1gK0jSZFOXXc7GpXa-908-460.png">
|
||||
|
||||
第一步是存储上一次依赖项的值,利用 `useRef` 实现:
|
||||
|
||||
```jsx
|
||||
function useWhatChanged(dependency?: any[]) {
|
||||
const dependencyRef = React.useRef(dependency);
|
||||
}
|
||||
```
|
||||
|
||||
然后利用 `useEffect`,对比 `dependency` 与 `dependencyRef` 的引用即可找到变化项:
|
||||
|
||||
```jsx
|
||||
React.useEffect(() => {
|
||||
let changed = false;
|
||||
const whatChanged = dependency
|
||||
? dependency.reduce((acc, dep, index) => {
|
||||
if (dependencyRef.current && dep !== dependencyRef.current[index]) {
|
||||
changed = true;
|
||||
|
||||
const oldValue = dependencyRef.current[index];
|
||||
dependencyRef.current[index] = dep;
|
||||
acc[`"✅" ${index}`] = {
|
||||
"Old Value": getPrintableInfo(oldValue),
|
||||
"New Value": getPrintableInfo(dep),
|
||||
};
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc[`"⏺" ${index}`] = {
|
||||
"Old Value": getPrintableInfo(dep),
|
||||
"New Value": getPrintableInfo(dep),
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {})
|
||||
: {};
|
||||
|
||||
if (isDevelopment) {
|
||||
console.table(whatChanged);
|
||||
}
|
||||
}, [dependency]);
|
||||
```
|
||||
|
||||
1. 直接对比 deps 引用,不想等则将 `changed` 设为 true。
|
||||
2. 调试模式下,利用 console.table 打印出表格。
|
||||
3. 依赖项是 dependency,当依赖项变化时才打印 whatChanged。
|
||||
|
||||
以上就是其源码的核心逻辑,当然我们还可以简化输出,仅当有引用变化时才打印表格,否则只输出简单的 Log 信息:
|
||||
|
||||
```jsx
|
||||
if (isDevelopment) {
|
||||
if (changed) {
|
||||
console.table(whatChanged);
|
||||
} else {
|
||||
console.log(whatChanged);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### babel 插件
|
||||
|
||||
最后 use-what-changed 还提供了 babel 插件,只通过注释就能打印 `useMemo`、`useEffect` 等依赖变化信息。babel 配置如下:
|
||||
|
||||
```js
|
||||
{
|
||||
"plugins": [
|
||||
[
|
||||
"@simbathesailor/babel-plugin-use-what-changed",
|
||||
{
|
||||
"active": process.env.NODE_ENV === "development" // boolean
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
使用方式简化为:
|
||||
|
||||
```jsx
|
||||
// uwc-debug
|
||||
React.useEffect(() => {
|
||||
// console.log("some thing changed , need to figure out")
|
||||
}, [a, b, c, d]);
|
||||
```
|
||||
|
||||
将 Hooks 的 deps 数组直接转化为 use-what-changed 的入参。
|
||||
|
||||
## 3 总结
|
||||
|
||||
[use-what-changed](https://github.com/simbathesailor/use-what-changed) 补充了 Hooks 依赖变化的调试方法,对于 React 组件重渲染分析可以利用 React Dev Tool,可以参考 [精读《React 性能调试》](https://github.com/dt-fe/weekly/blob/v2/149.%20%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20%E6%80%A7%E8%83%BD%E8%B0%83%E8%AF%95%E3%80%8B.md)。
|
||||
|
||||
还有哪些实用的 Hooks 调试工具呢?欢迎分享。
|
||||
|
||||
> 讨论地址是:[精读《use-what-changed 源码》· Issue #256 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/256)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,272 @@
|
||||
## 1 引言
|
||||
|
||||
[IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) 可以轻松判断元素是否可见,在之前的 [精读《用 React 做按需渲染》](https://github.com/dt-fe/weekly/blob/v2/154.%20%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%94%A8%20React%20%E5%81%9A%E6%8C%89%E9%9C%80%E6%B8%B2%E6%9F%93%E3%80%8B.md) 中介绍了原生 API 的方法,这次刚好看到其 React 封装版本 [react-intersection-observer](https://github.com/thebuilder/react-intersection-observer),让我们看一看 React 封装思路。
|
||||
|
||||
## 2 简介
|
||||
|
||||
[react-intersection-observer](https://github.com/thebuilder/react-intersection-observer) 提供了 Hook `useInView` 判断元素是否在可视区域内,API 如下:
|
||||
|
||||
```jsx
|
||||
import React from "react";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
|
||||
const Component = () => {
|
||||
const [ref, inView] = useInView();
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<h2>{`Header inside viewport ${inView}.`}</h2>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
由于判断元素是否可见是基于 dom 的,所以必须将 `ref` 回调函数传递给 **代表元素轮廓的 DOM 元素**,上面的例子中,我们将 `ref` 传递给了最外层 DIV。
|
||||
|
||||
`useInView` 还支持下列参数:
|
||||
|
||||
- `root`:检测是否可见基于的视窗元素,默认是整个浏览器 viewport。
|
||||
- `rootMargin`:root 边距,可以在检测时提前或者推迟固定像素判断。
|
||||
- `threshold`:是否可见的阈值,范围 0 ~ 1,0 表示任意可见即为可见,1 表示完全可见即为可见。
|
||||
- `triggerOnce`:是否仅触发一次。
|
||||
|
||||
## 3 精读
|
||||
|
||||
首先从入口函数 `useInView` 开始解读,这是一个 Hook,利用 `ref` 存储上一次 DOM 实例,`state` 则存储 `inView` 元素是否可见的 boolean 值:
|
||||
|
||||
```jsx
|
||||
export function useInView(
|
||||
options: IntersectionOptions = {},
|
||||
): InViewHookResponse {
|
||||
const ref = React.useRef<Element>()
|
||||
const [state, setState] = React.useState<State>(initialState)
|
||||
|
||||
// 中间部分..
|
||||
|
||||
return [setRef, state.inView, state.entry]
|
||||
}
|
||||
```
|
||||
|
||||
当组件 ref 被赋值时会调用 `setRef`,回调 `node` 是新的 DOM 节点,因此先 `unobserve(ref.current)` 取消旧节点的监听,再 `observe(node)` 对新节点进行监听,最后 `ref.current = node` 更新旧节点:
|
||||
|
||||
```jsx
|
||||
// 中间部分 1
|
||||
const setRef = React.useCallback(
|
||||
(node) => {
|
||||
if (ref.current) {
|
||||
unobserve(ref.current);
|
||||
}
|
||||
|
||||
if (node) {
|
||||
observe(
|
||||
node,
|
||||
(inView, intersection) => {
|
||||
setState({ inView, entry: intersection });
|
||||
|
||||
if (inView && options.triggerOnce) {
|
||||
// If it should only trigger once, unobserve the element after it's inView
|
||||
unobserve(node);
|
||||
}
|
||||
},
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
// Store a reference to the node, so we can unobserve it later
|
||||
ref.current = node;
|
||||
},
|
||||
[options.threshold, options.root, options.rootMargin, options.triggerOnce]
|
||||
);
|
||||
```
|
||||
|
||||
另一段是,当 `ref` 不存在时会清空 `inView` 状态,毕竟当不存在监听对象时,inView 值只有重设为默认 false 才合理:
|
||||
|
||||
```jsx
|
||||
// 中间部分 2
|
||||
useEffect(() => {
|
||||
if (!ref.current && state !== initialState && !options.triggerOnce) {
|
||||
// If we don't have a ref, then reset the state (unless the hook is set to only `triggerOnce`)
|
||||
// This ensures we correctly reflect the current state - If you aren't observing anything, then nothing is inView
|
||||
setState(initialState);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
这就是入口文件的逻辑,我们可以看到还有两个重要的函数 `observe` 与 `unobserve`,这两个函数的实现在 [intersection.ts](https://github.com/thebuilder/react-intersection-observer/blob/master/src/intersection.ts) 文件中,这个文件有三个核心函数:`observe`、`unobserve`、`onChange`。
|
||||
|
||||
- `observe`:监听 element 是否在可视区域。
|
||||
- `unobserve`:取消监听。
|
||||
- `onChange`:处理 `observe` 变化的回调。
|
||||
|
||||
先看 `observe`,对于同一个 root 下的监听会做合并操作,因此需要生成 `observerId` 作为唯一标识,这个标识由 `getRootId`、`rootMargin`、`threshold` 共同决定。
|
||||
|
||||
对于同一个 root 的监听下,拿到 `new IntersectionObserver()` 创建的 `observerInstance` 实例,调用 `observerInstance.observe` 进行监听。这里存储了两个 Map - `OBSERVER_MAP` 与 `INSTANCE_MAP`,前者是保证同一 root 下 `IntersectionObserver` 实例唯一,后者存储了组件 `inView` 以及回调等信息,在 `onChange` 函数使用:
|
||||
|
||||
```jsx
|
||||
export function observe(
|
||||
element: Element,
|
||||
callback: ObserverInstanceCallback,
|
||||
options: IntersectionObserverInit = {}
|
||||
) {
|
||||
// IntersectionObserver needs a threshold to trigger, so set it to 0 if it's not defined.
|
||||
// Modify the options object, since it's used in the onChange handler.
|
||||
if (!options.threshold) options.threshold = 0;
|
||||
const { root, rootMargin, threshold } = options;
|
||||
// Validate that the element is not being used in another <Observer />
|
||||
invariant(
|
||||
!INSTANCE_MAP.has(element),
|
||||
"react-intersection-observer: Trying to observe %s, but it's already being observed by another instance.\nMake sure the `ref` is only used by a single <Observer /> instance.\n\n%s"
|
||||
);
|
||||
/* istanbul ignore if */
|
||||
if (!element) return;
|
||||
// Create a unique ID for this observer instance, based on the root, root margin and threshold.
|
||||
// An observer with the same options can be reused, so lets use this fact
|
||||
let observerId: string =
|
||||
getRootId(root) +
|
||||
(rootMargin
|
||||
? `${threshold.toString()}_${rootMargin}`
|
||||
: threshold.toString());
|
||||
|
||||
let observerInstance = OBSERVER_MAP.get(observerId);
|
||||
if (!observerInstance) {
|
||||
observerInstance = new IntersectionObserver(onChange, options);
|
||||
/* istanbul ignore else */
|
||||
if (observerId) OBSERVER_MAP.set(observerId, observerInstance);
|
||||
}
|
||||
|
||||
const instance: ObserverInstance = {
|
||||
callback,
|
||||
element,
|
||||
inView: false,
|
||||
observerId,
|
||||
observer: observerInstance,
|
||||
// Make sure we have the thresholds value. It's undefined on a browser like Chrome 51.
|
||||
thresholds:
|
||||
observerInstance.thresholds ||
|
||||
(Array.isArray(threshold) ? threshold : [threshold]),
|
||||
};
|
||||
|
||||
INSTANCE_MAP.set(element, instance);
|
||||
observerInstance.observe(element);
|
||||
|
||||
return instance;
|
||||
}
|
||||
```
|
||||
|
||||
对于 `onChange` 函数,因为采用了多元素监听,所以需要遍历 `changes` 数组,并判断 `intersectionRatio` 超过阈值判定为 `inView` 状态,通过 `INSTANCE_MAP` 拿到对应实例,修改其 `inView` 状态并执行 `callback`。
|
||||
|
||||
这个 `callback` 就对应了 `useInView` Hook 中 `observe` 的第二个参数回调:
|
||||
|
||||
```jsx
|
||||
function onChange(changes: IntersectionObserverEntry[]) {
|
||||
changes.forEach((intersection) => {
|
||||
const { isIntersecting, intersectionRatio, target } = intersection;
|
||||
const instance = INSTANCE_MAP.get(target);
|
||||
|
||||
// Firefox can report a negative intersectionRatio when scrolling.
|
||||
/* istanbul ignore else */
|
||||
if (instance && intersectionRatio >= 0) {
|
||||
// If threshold is an array, check if any of them intersects. This just triggers the onChange event multiple times.
|
||||
let inView = instance.thresholds.some((threshold) => {
|
||||
return instance.inView
|
||||
? intersectionRatio > threshold
|
||||
: intersectionRatio >= threshold;
|
||||
});
|
||||
|
||||
if (isIntersecting !== undefined) {
|
||||
// If isIntersecting is defined, ensure that the element is actually intersecting.
|
||||
// Otherwise it reports a threshold of 0
|
||||
inView = inView && isIntersecting;
|
||||
}
|
||||
|
||||
instance.inView = inView;
|
||||
instance.callback(inView, intersection);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
最后是 `unobserve` 取消监听的实现,在 `useInView` `setRef` 灌入新 Node 节点时,会调用 `unobserve` 对旧节点取消监听。
|
||||
|
||||
首先利用 `INSTANCE_MAP` 找到实例,调用 `observer.unobserve(element)` 销毁监听。最后销毁不必要的 `INSTANCE_MAP` 与 `ROOT_IDS` 存储。
|
||||
|
||||
```jsx
|
||||
export function unobserve(element: Element | null) {
|
||||
if (!element) return;
|
||||
const instance = INSTANCE_MAP.get(element);
|
||||
|
||||
if (instance) {
|
||||
const { observerId, observer } = instance;
|
||||
const { root } = observer;
|
||||
|
||||
observer.unobserve(element);
|
||||
|
||||
// Check if we are still observing any elements with the same threshold.
|
||||
let itemsLeft = false;
|
||||
// Check if we still have observers configured with the same root.
|
||||
let rootObserved = false;
|
||||
/* istanbul ignore else */
|
||||
if (observerId) {
|
||||
INSTANCE_MAP.forEach((item, key) => {
|
||||
if (key !== element) {
|
||||
if (item.observerId === observerId) {
|
||||
itemsLeft = true;
|
||||
rootObserved = true;
|
||||
}
|
||||
if (item.observer.root === root) {
|
||||
rootObserved = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!rootObserved && root) ROOT_IDS.delete(root);
|
||||
if (observer && !itemsLeft) {
|
||||
// No more elements to observe for threshold, disconnect observer
|
||||
observer.disconnect();
|
||||
}
|
||||
|
||||
// Remove reference to element
|
||||
INSTANCE_MAP.delete(element);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
从其实现角度来看,为了保证正确识别到子元素存在,一定要保证 `ref` 能持续传递给组件最外层 DOM,如果出现传递断裂,就会判定当前组件不在视图内,比如:
|
||||
|
||||
```jsx
|
||||
const Component = () => {
|
||||
const [ref, inView] = useInView();
|
||||
|
||||
return <Child ref={ref} />;
|
||||
};
|
||||
|
||||
const Child = ({ loading, ref }) => {
|
||||
if (loading) {
|
||||
// 这一步会判定为 inView:false
|
||||
return <Spin />;
|
||||
}
|
||||
|
||||
return <div ref={ref}>Child</div>;
|
||||
};
|
||||
```
|
||||
|
||||
如果你的代码基于 `inView` 做了阻止渲染的判定,那么这个组件进入 loading 后就无法改变状态了。为了避免这种情况,要么不要让 `ref` 的传递断掉,要么当没有拿到 `ref` 对象时判定 `inView` 为 true。
|
||||
|
||||
## 4 总结
|
||||
|
||||
分析了这么多 React- 类的库,其核心思想有两个:
|
||||
|
||||
1. 将原生 API 转换为框架特有 API,比如 React 系列的 Hooks 与 ref。
|
||||
2. 处理生命周期导致的边界情况,比如 dom 被更新时先 `unobserve` 再重新 `observe`。
|
||||
|
||||
看过 [react-intersection-observer](https://github.com/thebuilder/react-intersection-observer) 的源码后,你觉得还有可优化的地方吗?欢迎讨论。
|
||||
|
||||
> 讨论地址是:[react-intersection-observer 源码》· Issue #257 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/257)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,233 @@
|
||||
## 1 引言
|
||||
|
||||
Object 类型的比较是非常重要的基础知识,通过 [How to Compare Objects in JavaScript](https://dmitripavlutin.com/how-to-compare-objects-in-javascript/) 这篇文章,我们可以学到四种对比方法:引用对比、手动对比、浅对比、深对比。
|
||||
|
||||
## 2 简介
|
||||
|
||||
### 引用对比
|
||||
|
||||
下面三种对比方式用于 Object,皆在引用相同是才返回 `true`:
|
||||
|
||||
- `===`
|
||||
- `==`
|
||||
- `Object.is()`
|
||||
|
||||
```js
|
||||
const hero1 = {
|
||||
name: "Batman",
|
||||
};
|
||||
const hero2 = {
|
||||
name: "Batman",
|
||||
};
|
||||
|
||||
hero1 === hero1; // => true
|
||||
hero1 === hero2; // => false
|
||||
|
||||
hero1 == hero1; // => true
|
||||
hero1 == hero2; // => false
|
||||
|
||||
Object.is(hero1, hero1); // => true
|
||||
Object.is(hero1, hero2); // => false
|
||||
```
|
||||
|
||||
### 手动对比
|
||||
|
||||
写一个自定义函数,按照对象内容做自定义对比也是一种方案:
|
||||
|
||||
```js
|
||||
function isHeroEqual(object1, object2) {
|
||||
return object1.name === object2.name;
|
||||
}
|
||||
|
||||
const hero1 = {
|
||||
name: "Batman",
|
||||
};
|
||||
const hero2 = {
|
||||
name: "Batman",
|
||||
};
|
||||
const hero3 = {
|
||||
name: "Joker",
|
||||
};
|
||||
|
||||
isHeroEqual(hero1, hero2); // => true
|
||||
isHeroEqual(hero1, hero3); // => false
|
||||
```
|
||||
|
||||
如果要对比的对象 key 不多,或者在特殊业务场景需要时,这种手动对比方法其实还是蛮实用的。
|
||||
|
||||
但这种方案不够自动化,所以才有了浅对比。
|
||||
|
||||
### 浅对比
|
||||
|
||||
浅对比函数写法有很多,不过其效果都是标准的,下面给出了一种写法:
|
||||
|
||||
```js
|
||||
function shallowEqual(object1, object2) {
|
||||
const keys1 = Object.keys(object1);
|
||||
const keys2 = Object.keys(object2);
|
||||
|
||||
if (keys1.length !== keys2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let key of keys1) {
|
||||
if (object1[key] !== object2[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
可以看到,浅对比就是将对象每个属性进行引用对比,算是一种性能上的平衡,尤其在 redux 下有特殊的意义。
|
||||
|
||||
下面给出了使用例子:
|
||||
|
||||
```js
|
||||
const hero1 = {
|
||||
name: "Batman",
|
||||
realName: "Bruce Wayne",
|
||||
};
|
||||
const hero2 = {
|
||||
name: "Batman",
|
||||
realName: "Bruce Wayne",
|
||||
};
|
||||
const hero3 = {
|
||||
name: "Joker",
|
||||
};
|
||||
|
||||
shallowEqual(hero1, hero2); // => true
|
||||
shallowEqual(hero1, hero3); // => false
|
||||
```
|
||||
|
||||
如果对象层级再多一层,浅对比就无效了,此时需要使用深对比。
|
||||
|
||||
### 深对比
|
||||
|
||||
深对比就是递归对比对象所有简单对象值,遇到复杂对象就逐个 key 进行对比,以此类推。
|
||||
|
||||
下面是一种实现方式:
|
||||
|
||||
```js
|
||||
function deepEqual(object1, object2) {
|
||||
const keys1 = Object.keys(object1);
|
||||
const keys2 = Object.keys(object2);
|
||||
|
||||
if (keys1.length !== keys2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const key of keys1) {
|
||||
const val1 = object1[key];
|
||||
const val2 = object2[key];
|
||||
const areObjects = isObject(val1) && isObject(val2);
|
||||
if (
|
||||
(areObjects && !deepEqual(val1, val2)) ||
|
||||
(!areObjects && val1 !== val2)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isObject(object) {
|
||||
return object != null && typeof object === "object";
|
||||
}
|
||||
```
|
||||
|
||||
可以看到,只要遇到 Object 类型的 key,就会递归调用一次 `deepEqual` 进行比较,否则对于简单类型直接使用 `!==` 引用对比。
|
||||
|
||||
值得注意的是,数组类型也满足 `typeof object === "object"` 的条件,且 `Object.keys` 可以作用于数组,且 `object[key]` 也可作用于数组,因此数组和对象都可以采用相同方式处理。
|
||||
|
||||
有了深对比,再也不用担心复杂对象的比较了:
|
||||
|
||||
```js
|
||||
const hero1 = {
|
||||
name: "Batman",
|
||||
address: {
|
||||
city: "Gotham",
|
||||
},
|
||||
};
|
||||
const hero2 = {
|
||||
name: "Batman",
|
||||
address: {
|
||||
city: "Gotham",
|
||||
},
|
||||
};
|
||||
|
||||
deepEqual(hero1, hero2); // => true
|
||||
```
|
||||
|
||||
但深对比会造成性能损耗,不要小看递归的作用,在对象树复杂时,深对比甚至会导致严重的性能问题。
|
||||
|
||||
## 3 精读
|
||||
|
||||
### 常见的引用对比
|
||||
|
||||
引用对比是最常用的,一般在做 props 比较时,只允许使用引用对比:
|
||||
|
||||
```js
|
||||
this.props.style !== nextProps.style;
|
||||
```
|
||||
|
||||
如果看到有深对比的地方,一般就要有所警觉,这里是真的需要深对比吗?是不是其他地方写法有问题导致的。
|
||||
|
||||
比如在某处看到这样的代码:
|
||||
|
||||
```js
|
||||
deepEqual(this.props.style, nextProps.style);
|
||||
```
|
||||
|
||||
可能是父组件一处随意拼写导致的:
|
||||
|
||||
```jsx
|
||||
const Parent = () => {
|
||||
return <Child style={{ color: "red" }} />;
|
||||
};
|
||||
```
|
||||
|
||||
一个只解决局部问题的同学可能会采用 `deepEqual`,OK 这样也能解决问题,但一个有全局感的同学会这样解决问题:
|
||||
|
||||
```js
|
||||
this.props.style === nextProps.style;
|
||||
```
|
||||
|
||||
```jsx
|
||||
const Parent = () => {
|
||||
const style = useMemo(() => ({ color: "red" }), []);
|
||||
return <Child style={style} />;
|
||||
};
|
||||
```
|
||||
|
||||
从性能上来看,`Parent` 定义的 `style` 只会执行一次且下次渲染几乎没有对比损耗(依赖为空数组),子组件引用对比性能最佳,这样的组合一定优于 `deepEqual` 的例子。
|
||||
|
||||
### 常见的浅对比
|
||||
|
||||
浅对比也在判断组件是否重渲染时很常用:
|
||||
|
||||
```jsx
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return !shallowEqual(this.props, nextProps)
|
||||
}
|
||||
```
|
||||
|
||||
原因是 `this.props` 这个对象引用的变化在逻辑上是无需关心的,因为应用只会使用到 `this.props[key]` 这一层级,再考虑到 React 组件生态下,Immutable 的上下文保证了任何对象子属性变化一定导致对象整体引用变化,可以放心的进行浅对比。
|
||||
|
||||
最少见的就是手动对比和深对比,如果你看到一段代码中使用了深对比,大概率这段代码可以被优化为浅对比。
|
||||
|
||||
## 4 总结
|
||||
|
||||
虽然今天总结了 4 种比较 Object 对象的方式,但在实际项目中,应该尽可能使用引用对比,其次是浅对比和手动对比,最坏的情况是使用深对比。
|
||||
|
||||
> 讨论地址是:[精读《如何比较 Object 对象》》· Issue #258 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/258)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
Reference in New Issue
Block a user