likes
comments
collection
share

Million.js:React 应用中高效数据列表渲染的解决方案

作者站长头像
站长
· 阅读数 51

前言

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:React 应用中高效数据列表渲染的解决方案

当然,million.js 的使用场景当然不在于此,它更多作用于大数据列表渲染,通过 playground 体验优化前后的差距。

这是一个有 1000 行 的列表,当我们更改 input 的值时,所有 data 都将重新生成,这是一个极大的性能消耗: Million.js:React 应用中高效数据列表渲染的解决方案

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 优化后渲染速度加快详细介绍可前往 介绍 Million.js:React 应用中高效数据列表渲染的解决方案

安装

  • 安装依赖包 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
评论
请登录