Million.js:React 应用中高效数据列表渲染的解决方案
前言
Million.js 是一个极快且轻量级(<4kb)的虚拟DOM,可以使 React Component 速度提高70%, Million 与 React 一起工作,Million.js 通过使用一个微调的、优化的虚拟DOM, 减少了React的开销。
playground 地址: demo.million.dev。
million.js 使用一种新的虚拟 DOM 方法,称为 block virtual DOM。你可以通过 virtual DOM: Back in block 以及我们如何在React中使用 Behind the block() 来了解更多关于 block virtual DOM 是什么。
本文只是简单介绍 million.js 如何使用,相应原理介绍可以看下这篇文章
什么是 block
Million.js是一个库,可以让你创建 blocks。block 是一种特殊的高阶组件(HOC),可用作超级优化以提高渲染速度的 React 组件,Blocks 本质上是由 block( ) 包装的组件。
const LionBlock = block(function Lion() {
return (
<img src="https://million.dev/lion.svg" />
);
})
Blocks 可以像普通组件一样使用:
export default function App() {
return (
<div>
<h1>mil + LION = million</h1>
<LionBlock />
</div>
);
}
渲染结果:
当然,million.js 的使用场景当然不在于此,它更多作用于大数据列表渲染,通过 playground 体验优化前后的差距。
这是一个有 1000 行 的列表,当我们更改 input 的值时,所有 data 都将重新生成,这是一个极大的性能消耗:
import { useState } from 'react';
import { Table, Input, lotsOfElements } from './ui';
import { buildData } from './data';
function App() {
const [rows, setRows] = useState(1);
const data = buildData(rows); //假设 data 有 1000 行
return (
<div>
<Input value={rows} setValue={setRows} />
<Table showRadar>
{data.map(({ adjective, color, noun }) => (
<tr>
<td>{adjective}</td>
<td>{color}</td>
<td>{noun}</td>
<td>{...lotsOfElements}</td>
</tr>
))}
</Table>
</div>
);
}
export default App;
在下面的例子中,我们将使用block()
和<For />
来优化渲染,首先,我们需要将<tr>
抽象到它自己的组件中。
data.map(({ adjective, color, noun }) => (
<tr>
<td>{adjective}</td>
<td>{color}</td>
<td>{noun}</td>
<td>{...lotsOfElements}</td>
</tr>
))
// 👇👇👇
function Row({ adjective, color, noun }) {
return (
<tr>
<td>{adjective}</td>
<td>{color}</td>
<td>{noun}</td>
{...lotsOfElements}
</tr>
);
}
然后,我们可以用block()
包装它,以优化<Row />
组件。
const RowBlock = block(
function Row({ adjective, color, noun }) {
return (
<tr>
<td>{adjective}</td>
<td>{color}</td>
<td>{noun}</td>
{...lotsOfElements}
</tr>
);
}
);
一旦我们优化了一行,我们需要将其呈现为列表:
data.map(({ adjective, color, noun }) => (
<RowBlock adjective={adjective} color={color} noun={noun}>
));
但是,million.js 为我们提供了内置的渲染方案—— <For/>
<For />
组件用于呈现一个 block 列表。它将一个数组作为每个 prop,并将一个函数作为其子元素。对于数组中的每个项调用该函数,并将该项及其索引作为参数传递给该函数。
Syntax: <For each={array}>{(item, index) => Block}</For>
这是遍历数组的最佳方式(在底层使用 mapArray()
)。随着数组的变化,<For />
会更新或移动DOM 中的项目,而不是重新创建它们。让我们看一个例子:
有了这个想法,我们可以重写我们的表格来使用 <For />
:
<For each={data}>
{({ adjective, color, noun }) => (
<RowBlock adjective={adjective} color={color} noun={noun} />
)}
</For>
借助更快的底层虚拟DOM,Million.js可以大大减轻渲染大型列表的痛苦。 优化后完整示例:
import { useState } from 'react';
import { Table, Input, lotsOfElements } from './ui';
import { buildData } from './data';
import { block, For } from 'million/react';
const RowBlock = block(
function Row({ adjective, color, noun }) {
return (
<tr>
<td>{adjective}</td>
<td>{color}</td>
<td>{noun}</td>
{...lotsOfElements}
</tr>
);
}
);
function App() {
const [rows, setRows] = useState(1);
const data = buildData(rows);
return (
<div>
<Input value={rows} setValue={setRows} />
<Table showRadar>
<For each={data}>
{({ adjective, color, noun }) => (
<RowBlock adjective={adjective} color={color} noun={noun} />
)}
</For>
</Table>
</div>
);
}
export default App;
至于为什么 million.js 优化后渲染速度加快详细介绍可前往 介绍
安装
- 安装依赖包
npm install million
- 启用编译器 以 next.js 为例(其它详见 million.js官网),
// next.config.js
import million from 'million/compiler';
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
};
export default million.next(nextConfig);
- 自定义 config()
interface Options {
mode: 'react' | 'preact' | 'vdom' // default: 'react'
optimize: boolean // default: false
server: boolean // default: false
}
block 使用规则
- 需先声明 block 变量再使用
console.log(block(<div />)) // ❌ Wrong
export default block(<div />) // ❌ Wrong
// 👇👇👇
const Block = block(<div />) // ✅ Correct
console.log(Block);
export default Block;
- 正确调用 block() 函数
const BadBlock = block(<Component />) // ❌ Wrong
const GoodBlock = block(App) // ✅ Correct
- 使用
<For />
而不是map()
<div>
{items.map((item) => (
<div key={item}>{item}</div>
))}
</div>
// 👇👇👇
<For each={items}>
{(item) => <div key={item}>{item}</div>}
</For>
- 返回值必须是“确定性的”
function Component() {
const [count, setCount] = useState(initial.count);
if (count > 10) {
return <div>Too many clicks!</div>; // ❌ Wrong
}
// ❌ Wrong
return count > 5 ? (
'Count is greater than 5'
) : (
<div>Count is {count}.</div>
);
}
const ComponentBlock = block(Component);
- 小心组件库组件 Million.js 要求你使用 DOM 元素而不是组件。这是因为组件可能会引入非确定性返回,从而导致性能下降。
// ❌ Bad
<Stack>
<Text>What's up my fellow components</Text>
</Stack>
// 🤨 Maybe
<div>
<Text>What's up my fellow components</Text>
</div>
// ✅ Good
<div>
<p>What's up my fellow components</p>
</div>
- 禁用 ...拓展符
不能在 million.js中使用
...
属性,因为它们可能会引入非确定性回报。
<div {...props} /> // ❌ Wrong
API 介绍
block()
block 函数实例化一个 block
(一个无状态的“组件”)。它接受一个带有 props
对象参数的函数,该参数返回一个VNode。
语法:block((props) => vnode)
示例:block((props) => <div>{props.foo}</div>)
在使用 block 的时候要遵循一下准则:
- props 是一个具有原始值或 Block 值的不可变对象
someBlock({
one: '1', // ✅ 基础数据类型
two: 1 + 1, // ✅
three: true, // ✅
four: Date.now(), // ✅
five: anotherBlock({ crewmate: true }), // ✅ Block对象
six: { imposter: true }, // ❌
seven: new Date(), // ❌
});
- props的顶级值不能与其他值插值 props 用 Hole 值填充,当 block() 被调用时,这些 Hole 值将被替换为实际值, Hole 值是不可变的,不能用其他值派生。
// Anatomy of a `Hole`
{
$: 'prop';
}
// Example:
block((props) => {
console.log(props.foo); // { $: 'foo' } ✅
console.log(props.foo + ' bar'); // { $: 'foo' } + ' bar' ❌
return <div>{props.foo}</div>;
});
block((props) => {
const { favorite } = props.favorite; // ❌
<div className={props.className /* ✅ */}>
{props.hello /* ✅ */}
{Date.now() /* ✅ */}
<button
onClick={() => {
console.log(props.world); /* ❌ (no holes inside listeners) */
}}
>
{props.count + 1 /* ❌ */}
{props.foo.toString() /* ❌ */}
</button>
</div>;
});
mount()
mount 函数用于将块挂载到 DOM 元素上(类似于 React 中的 ReactDOM.render()
),它接受两个参数:要挂载的块和要挂载到的DOM元素。
语法: mount(Block, el)
示例: mount(block, document.getElementById('root'))
import { block, mount } from 'million';
const display = block(({ text }) => {
return <p>{text}</p>;
});
const element = mount(display, document.getElementById('root'));
patch()
patch 函数用于使用另一个 blocks 重新渲染 blocks, oldBlock 是将要重新渲染的块,newBlock 表示 DOM 的新版本。
语法: patch(oldBlock, newBlock)
示例: patch(block1, block2)
import { block, mount, patch, fragment } from 'million';
const display = block(({ text }) => {
return <p>{text}</p>;
});
// we will patch against this block for updates
const main = display({ text: 'Hello' });
mount(main, document.getElementById('root'));
patch(main, display({ text: 'World' }));
const bigDisplay = block(({ text }) => {
return <h1 style={{ color: 'red' }}>{text}</h1>;
});
patch(main, bigDisplay({ text: 'World' })); // inefficent, but works
mapArray()
语法: mapArray(Block[])
示例: mapArray([block, block, block])
mapArray 函数用于创建 Block 列表。这是呈现来自类数组数据的视图的最佳方式。当数组改变时,mapArray() 会更新或移动 DOM 中的项,而不是重新创建它们。让我们来看一个例子:
import { block, patch, mapArray } from 'million';
const oldList = [1, 2, 3];
const newList = [3, 2, 1];
const list = block(({ item }) => {
return <div>{item}</div>;
});
// updates list efficiently (only 2 moves instead of 3 updates)
patch(
document.body,
mapArray(oldList.map((item) => list({ item }))),
mapArray(newList.map((item) => list({ item }))),
);
stringToDOM()
语法: stringToDOM(htmlString)
示例: stringToDOM('<div>Hello World</div>')
函数的作用是:接受一个 HTML 字符串并返回一个 DOM 对象,这对于从字符串创建 DOM 元素很有用。
import { stringToDOM } from 'million';
const dom = stringToDOM('<div>Hello World</div>');
dom.innerHTML; // 'Hello World'
dom.tagName; // 'DIV'
renderToTemplate()
renderToTemplate() 函数用于将虚拟 DOM 节点渲染为字符串。这用于创建 blocks 的模板,并与stringToDOM() 相似地工作。
import { renderToTemplate } from 'million';
const edits = [];
const template = renderToTemplate(<div>Hello World</div>, edits);
console.log(template); // '<div>Hello World</div>'
console.log(edits); // []
你也可以将Holes传递给renderToTemplate()函数。这将返回带有编辑的模板。
import { renderToTemplate } from 'million';
const edits = [];
const hole = { $: 'hole' };
const template = renderToTemplate(<div>Hello {hole}</div>, edits);
console.log(template); // '<div>Hello </div>'
console.log(edits); // [{ type: 'child', index: 0, hole: 'hole' }]
参考
转载自:https://juejin.cn/post/7243248407535550520