MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Solid.js列表渲染的核心概念与实践

2022-07-183.9k 阅读

Solid.js 列表渲染基础概念

在前端开发中,列表渲染是一个常见的需求,无论是展示商品列表、用户列表还是其他任何集合数据。Solid.js 作为一种新兴的前端框架,提供了高效且独特的列表渲染方式。

1. 响应式数据驱动列表

Solid.js 基于响应式编程模型,这意味着当数据发生变化时,与之相关的视图会自动更新。在列表渲染中,我们将列表数据定义为响应式数据。例如,假设我们有一个简单的待办事项列表:

import { createSignal } from 'solid-js';

// 创建响应式的待办事项列表数据
const [todos, setTodos] = createSignal([
    { id: 1, text: '学习 Solid.js', completed: false },
    { id: 2, text: '完成项目任务', completed: false }
]);

这里通过 createSignal 创建了一个信号 todos,它包含了初始的待办事项列表数据,并且 setTodos 用于更新这个列表。

2. 列表渲染的基本语法

在 Solid.js 中,我们使用 map 方法结合 JSX 来渲染列表。例如,继续上面的待办事项列表,我们可以这样渲染:

import { createSignal } from'solid-js';
import { render } from'solid-js/web';

const [todos, setTodos] = createSignal([
    { id: 1, text: '学习 Solid.js', completed: false },
    { id: 2, text: '完成项目任务', completed: false }
]);

const App = () => {
    return (
        <ul>
            {todos().map(todo => (
                <li key={todo.id}>
                    {todo.text} - {todo.completed? '已完成' : '未完成'}
                </li>
            ))}
        </ul>
    );
};

render(() => <App />, document.getElementById('app'));

在这个例子中,todos() 用于获取当前的列表数据,然后通过 map 方法遍历每个待办事项,并为每个事项生成一个 <li> 元素。注意,这里为每个列表项设置了 key 属性,key 在列表渲染中起着至关重要的作用。

列表项的 Key

1. Key 的作用

在 Solid.js 列表渲染中,key 是一个必须的属性。它的主要作用是帮助 Solid.js 高效地识别列表中的每个项,以便在数据发生变化时,能够准确地更新和复用 DOM 元素。当列表数据发生变化,例如添加、删除或重新排序时,Solid.js 会根据 key 来确定哪些项是新的,哪些项需要更新,哪些项需要移除。

2. Key 的选择原则

key 应该是列表项中唯一且稳定的标识。通常,使用数据项的唯一 ID 作为 key 是一个很好的选择,就像我们在待办事项列表中使用 todo.id 作为 key。避免使用数组索引作为 key,因为当列表发生插入、删除操作时,索引会发生变化,这可能导致 Solid.js 做出错误的 DOM 更新决策。例如,以下是一个错误使用索引作为 key 的示例:

import { createSignal } from'solid-js';
import { render } from'solid-js/web';

const [todos, setTodos] = createSignal([
    { text: '学习 Solid.js', completed: false },
    { text: '完成项目任务', completed: false }
]);

const App = () => {
    return (
        <ul>
            {todos().map((todo, index) => (
                <li key={index}>
                    {todo.text} - {todo.completed? '已完成' : '未完成'}
                </li>
            ))}
        </ul>
    );
};

render(() => <App />, document.getElementById('app'));

假设我们在列表开头插入一个新的待办事项,由于索引变化,后面所有项的 key 都会改变,Solid.js 会认为所有项都是新的,从而不必要地重新渲染所有 DOM 元素,这会降低性能。

动态更新列表

1. 添加列表项

在待办事项列表的场景中,添加新的待办事项是一个常见的操作。我们可以通过更新 todos 信号来实现。例如,添加一个新的待办事项按钮:

import { createSignal } from'solid-js';
import { render } from'solid-js/web';

const [todos, setTodos] = createSignal([
    { id: 1, text: '学习 Solid.js', completed: false },
    { id: 2, text: '完成项目任务', completed: false }
]);

const addTodo = () => {
    const newTodo = { id: Date.now(), text: '新的待办事项', completed: false };
    setTodos([...todos(), newTodo]);
};

const App = () => {
    return (
        <div>
            <button onClick={addTodo}>添加待办事项</button>
            <ul>
                {todos().map(todo => (
                    <li key={todo.id}>
                        {todo.text} - {todo.completed? '已完成' : '未完成'}
                    </li>
                ))}
            </ul>
        </div>
    );
};

render(() => <App />, document.getElementById('app'));

addTodo 函数中,我们创建一个新的待办事项对象,并使用 setTodos 将其添加到现有的待办事项列表中。由于 todos 是响应式信号,视图会自动更新以显示新的待办事项。

2. 删除列表项

删除列表项同样通过更新 todos 信号来实现。例如,为每个待办事项添加一个删除按钮:

import { createSignal } from'solid-js';
import { render } from'solid-js/web';

const [todos, setTodos] = createSignal([
    { id: 1, text: '学习 Solid.js', completed: false },
    { id: 2, text: '完成项目任务', completed: false }
]);

const deleteTodo = (id) => {
    setTodos(todos().filter(todo => todo.id!== id));
};

const App = () => {
    return (
        <div>
            <ul>
                {todos().map(todo => (
                    <li key={todo.id}>
                        {todo.text} - {todo.completed? '已完成' : '未完成'}
                        <button onClick={() => deleteTodo(todo.id)}>删除</button>
                    </li>
                ))}
            </ul>
        </div>
    );
};

render(() => <App />, document.getElementById('app'));

deleteTodo 函数中,我们使用 filter 方法创建一个新的数组,不包含要删除的待办事项,然后通过 setTodos 更新列表。同样,由于响应式系统,视图会自动更新,移除被删除的项。

3. 更新列表项数据

更新列表项数据也是常见的操作。例如,在待办事项列表中,我们可能想要标记一个事项为已完成或未完成。

import { createSignal } from'solid-js';
import { render } from'solid-js/web';

const [todos, setTodos] = createSignal([
    { id: 1, text: '学习 Solid.js', completed: false },
    { id: 2, text: '完成项目任务', completed: false }
]);

const toggleTodo = (id) => {
    setTodos(todos().map(todo =>
        todo.id === id? { ...todo, completed:!todo.completed } : todo
    ));
};

const App = () => {
    return (
        <div>
            <ul>
                {todos().map(todo => (
                    <li key={todo.id}>
                        {todo.text} - {todo.completed? '已完成' : '未完成'}
                        <input type="checkbox" checked={todo.completed} onChange={() => toggleTodo(todo.id)} />
                    </li>
                ))}
            </ul>
        </div>
    );
};

render(() => <App />, document.getElementById('app'));

toggleTodo 函数中,我们使用 map 方法遍历列表,找到要更新的项并修改其 completed 属性,然后通过 setTodos 更新整个列表。视图会根据新的数据状态自动更新。

列表渲染中的性能优化

1. 减少不必要的渲染

在 Solid.js 中,由于其细粒度的响应式系统,默认情况下已经能够有效地减少不必要的渲染。但是,在复杂的列表场景中,我们仍然可以采取一些额外的措施。例如,对于大型列表,我们可以使用 shouldUpdate 函数来进一步控制组件的更新。假设我们有一个包含复杂子组件的列表项:

import { createSignal } from'solid-js';
import { render } from'solid-js/web';

const [items, setItems] = createSignal([
    { id: 1, data: '数据1' },
    { id: 2, data: '数据2' }
]);

const ItemComponent = ({ item }) => {
    return (
        <div>
            <p>{item.data}</p>
        </div>
    );
};

const shouldUpdate = (prevProps, nextProps) => {
    return prevProps.item.data!== nextProps.item.data;
};

const App = () => {
    return (
        <div>
            {items().map(item => (
                <ItemComponent item={item} shouldUpdate={shouldUpdate} key={item.id} />
            ))}
        </div>
    );
};

render(() => <App />, document.getElementById('app'));

在这个例子中,shouldUpdate 函数比较前后两次 props 中的 item.data,只有当 item.data 发生变化时,ItemComponent 才会重新渲染,从而避免了不必要的渲染。

2. 虚拟列表

对于非常长的列表,一次性渲染所有项可能会导致性能问题,特别是在移动设备上。此时,我们可以使用虚拟列表技术。Solid.js 本身没有内置虚拟列表组件,但可以借助第三方库如 react - virtualized (虽然名字中有 React,但可以在 Solid.js 中使用部分功能) 或 react - window。以 react - virtualized 为例,我们可以这样实现虚拟列表:

  1. 安装 react - virtualized
npm install react - virtualized
  1. 使用 List 组件进行虚拟列表渲染:
import { createSignal } from'solid-js';
import { render } from'solid-js/web';
import { List } from'react - virtualized';

const [items, setItems] = createSignal(Array.from({ length: 1000 }, (_, i) => ({ id: i, text: `项 ${i}` })));

const rowRenderer = ({ index, key, style }) => {
    const item = items()[index];
    return (
        <div key={key} style={style}>
            {item.text}
        </div>
    );
};

const App = () => {
    return (
        <List
            height={400}
            rowCount={items().length}
            rowHeight={50}
            rowRenderer={rowRenderer}
            width={300}
        />
    );
};

render(() => <App />, document.getElementById('app'));

在这个例子中,react - virtualizedList 组件只渲染当前可见区域内的列表项,大大提高了性能。rowRenderer 函数定义了每个列表项的渲染方式,heightrowCountrowHeightwidth 等属性配置了列表的基本参数。

嵌套列表渲染

1. 简单嵌套列表

在实际应用中,我们经常会遇到嵌套列表的情况。例如,一个包含分类和子项目的菜单。假设我们有如下数据结构:

const menuData = [
    {
        id: 1,
        title: '菜单一',
        subItems: [
            { id: 11, text: '子项一' },
            { id: 12, text: '子项二' }
        ]
    },
    {
        id: 2,
        title: '菜单二',
        subItems: [
            { id: 21, text: '子项三' },
            { id: 22, text: '子项四' }
        ]
    }
];

我们可以这样在 Solid.js 中渲染这个嵌套列表:

import { createSignal } from'solid-js';
import { render } from'solid-js/web';

const menuData = [
    {
        id: 1,
        title: '菜单一',
        subItems: [
            { id: 11, text: '子项一' },
            { id: 12, text: '子项二' }
        ]
    },
    {
        id: 2,
        title: '菜单二',
        subItems: [
            { id: 21, text: '子项三' },
            { id: 22, text: '子项四' }
        ]
    }
];

const [menu, setMenu] = createSignal(menuData);

const App = () => {
    return (
        <ul>
            {menu().map(category => (
                <li key={category.id}>
                    {category.title}
                    <ul>
                        {category.subItems.map(subItem => (
                            <li key={subItem.id}>{subItem.text}</li>
                        ))}
                    </ul>
                </li>
            ))}
        </ul>
    );
};

render(() => <App />, document.getElementById('app'));

在这个例子中,外层 map 遍历菜单分类,内层 map 遍历每个分类下的子项,从而实现了嵌套列表的渲染。

2. 动态更新嵌套列表

动态更新嵌套列表与普通列表类似,但需要注意正确处理多层数据结构。例如,添加一个新的子项到某个分类中:

import { createSignal } from'solid-js';
import { render } from'solid-js/web';

const menuData = [
    {
        id: 1,
        title: '菜单一',
        subItems: [
            { id: 11, text: '子项一' },
            { id: 12, text: '子项二' }
        ]
    },
    {
        id: 2,
        title: '菜单二',
        subItems: [
            { id: 21, text: '子项三' },
            { id: 22, text: '子项四' }
        ]
    }
];

const [menu, setMenu] = createSignal(menuData);

const addSubItem = (categoryId) => {
    setMenu(menu().map(category =>
        category.id === categoryId? {
           ...category,
            subItems: [...category.subItems, { id: Date.now(), text: '新子项' }]
        } : category
    ));
};

const App = () => {
    return (
        <div>
            <ul>
                {menu().map(category => (
                    <li key={category.id}>
                        {category.title}
                        <button onClick={() => addSubItem(category.id)}>添加子项</button>
                        <ul>
                            {category.subItems.map(subItem => (
                                <li key={subItem.id}>{subItem.text}</li>
                            ))}
                        </ul>
                    </li>
                ))}
            </ul>
        </div>
    );
};

render(() => <App />, document.getElementById('app'));

addSubItem 函数中,我们找到对应的分类,并为其 subItems 添加一个新的子项,然后通过 setMenu 更新整个菜单数据。视图会根据新的数据结构自动更新。

列表排序

1. 简单排序

对列表进行排序是常见的需求。例如,在待办事项列表中,我们可能想要根据事项的完成状态或文本内容进行排序。假设我们要根据待办事项的文本内容进行升序排序:

import { createSignal } from'solid-js';
import { render } from'solid-js/web';

const [todos, setTodos] = createSignal([
    { id: 1, text: '学习 Solid.js', completed: false },
    { id: 2, text: '完成项目任务', completed: false }
]);

const sortTodos = () => {
    setTodos([...todos()].sort((a, b) => a.text.localeCompare(b.text)));
};

const App = () => {
    return (
        <div>
            <button onClick={sortTodos}>按文本排序</button>
            <ul>
                {todos().map(todo => (
                    <li key={todo.id}>
                        {todo.text} - {todo.completed? '已完成' : '未完成'}
                    </li>
                ))}
            </ul>
        </div>
    );
};

render(() => <App />, document.getElementById('app'));

sortTodos 函数中,我们先复制当前的待办事项列表,然后使用 sort 方法根据 text 进行排序,最后通过 setTodos 更新列表。由于 todos 是响应式信号,视图会自动按照新的顺序渲染。

2. 多条件排序

有时我们可能需要根据多个条件进行排序。例如,先根据完成状态排序(未完成的在前),然后再根据文本内容排序:

import { createSignal } from'solid-js';
import { render } from'solid-js/web';

const [todos, setTodos] = createSignal([
    { id: 1, text: '学习 Solid.js', completed: false },
    { id: 2, text: '完成项目任务', completed: true }
]);

const multiSortTodos = () => {
    setTodos([...todos()].sort((a, b) => {
        if (a.completed!== b.completed) {
            return a.completed? 1 : -1;
        } else {
            return a.text.localeCompare(b.text);
        }
    }));
};

const App = () => {
    return (
        <div>
            <button onClick={multiSortTodos}>多条件排序</button>
            <ul>
                {todos().map(todo => (
                    <li key={todo.id}>
                        {todo.text} - {todo.completed? '已完成' : '未完成'}
                    </li>
                ))}
            </ul>
        </div>
    );
};

render(() => <App />, document.getElementById('app'));

multiSortTodos 函数中,sort 方法的回调函数首先比较完成状态,然后在完成状态相同时比较文本内容。这样就实现了多条件排序,并且视图会根据新的排序结果自动更新。

列表过滤

1. 简单过滤

在列表渲染中,过滤数据是另一个常见的操作。例如,在待办事项列表中,我们可能只想显示已完成或未完成的事项。假设我们要显示未完成的待办事项:

import { createSignal } from'solid-js';
import { render } from'solid-js/web';

const [todos, setTodos] = createSignal([
    { id: 1, text: '学习 Solid.js', completed: false },
    { id: 2, text: '完成项目任务', completed: true }
]);

const filterUncompleted = () => {
    setTodos(todos().filter(todo =>!todo.completed));
};

const App = () => {
    return (
        <div>
            <button onClick={filterUncompleted}>显示未完成事项</button>
            <ul>
                {todos().map(todo => (
                    <li key={todo.id}>
                        {todo.text} - {todo.completed? '已完成' : '未完成'}
                    </li>
                ))}
            </ul>
        </div>
    );
};

render(() => <App />, document.getElementById('app'));

filterUncompleted 函数中,我们使用 filter 方法创建一个新的数组,只包含未完成的待办事项,然后通过 setTodos 更新列表。视图会根据过滤后的数据自动更新。

2. 复杂过滤

对于更复杂的过滤需求,我们可能需要根据多个条件进行过滤。例如,在一个商品列表中,我们可能要根据价格范围和类别进行过滤。假设我们有如下商品数据结构:

const productData = [
    { id: 1, name: '商品一', price: 100, category: '电子产品' },
    { id: 2, name: '商品二', price: 200, category: '家居用品' },
    { id: 3, name: '商品三', price: 150, category: '电子产品' }
];

我们可以这样实现复杂过滤:

import { createSignal } from'solid-js';
import { render } from'solid-js/web';

const productData = [
    { id: 1, name: '商品一', price: 100, category: '电子产品' },
    { id: 2, name: '商品二', price: 200, category: '家居用品' },
    { id: 3, name: '商品三', price: 150, category: '电子产品' }
];

const [products, setProducts] = createSignal(productData);
const [minPrice, setMinPrice] = createSignal(0);
const [maxPrice, setMaxPrice] = createSignal(Infinity);
const [selectedCategory, setSelectedCategory] = createSignal('');

const filterProducts = () => {
    setProducts(productData.filter(product => {
        return product.price >= minPrice() && product.price <= maxPrice() &&
            (selectedCategory() === '' || product.category === selectedCategory());
    }));
};

const App = () => {
    return (
        <div>
            <input type="number" placeholder="最小价格" onChange={(e) => setMinPrice(Number(e.target.value))} />
            <input type="number" placeholder="最大价格" onChange={(e) => setMaxPrice(Number(e.target.value))} />
            <select onChange={(e) => setSelectedCategory(e.target.value)}>
                <option value="">所有类别</option>
                <option value="电子产品">电子产品</option>
                <option value="家居用品">家居用品</option>
            </select>
            <button onClick={filterProducts}>过滤</button>
            <ul>
                {products().map(product => (
                    <li key={product.id}>
                        {product.name} - 价格: {product.price} - 类别: {product.category}
                    </li>
                ))}
            </ul>
        </div>
    );
};

render(() => <App />, document.getElementById('app'));

filterProducts 函数中,我们根据 minPricemaxPriceselectedCategory 的值对商品数据进行过滤。通过输入框和下拉框可以动态调整过滤条件,每次点击过滤按钮时,视图会根据新的过滤结果自动更新。

列表渲染与表单结合

1. 列表项内的表单元素

在待办事项列表中,我们可能想要为每个事项添加一个输入框,以便用户可以编辑事项文本。例如:

import { createSignal } from'solid-js';
import { render } from'solid-js/web';

const [todos, setTodos] = createSignal([
    { id: 1, text: '学习 Solid.js', completed: false },
    { id: 2, text: '完成项目任务', completed: false }
]);

const editTodo = (id, newText) => {
    setTodos(todos().map(todo =>
        todo.id === id? { ...todo, text: newText } : todo
    ));
};

const App = () => {
    return (
        <div>
            <ul>
                {todos().map(todo => (
                    <li key={todo.id}>
                        <input type="text" value={todo.text} onChange={(e) => editTodo(todo.id, e.target.value)} />
                        <input type="checkbox" checked={todo.completed} />
                    </li>
                ))}
            </ul>
        </div>
    );
};

render(() => <App />, document.getElementById('app'));

在这个例子中,每个列表项都包含一个输入框和一个复选框。当输入框内容发生变化时,editTodo 函数会更新对应的待办事项文本。由于 todos 是响应式信号,视图会自动反映这些变化。

2. 基于表单输入添加列表项

我们也可以通过表单输入来添加新的列表项。例如,在待办事项列表中,添加一个输入框和按钮来创建新的待办事项:

import { createSignal } from'solid-js';
import { render } from'solid-js/web';

const [todos, setTodos] = createSignal([
    { id: 1, text: '学习 Solid.js', completed: false },
    { id: 2, text: '完成项目任务', completed: false }
]);
const [newTodoText, setNewTodoText] = createSignal('');

const addTodo = () => {
    if (newTodoText()) {
        const newTodo = { id: Date.now(), text: newTodoText(), completed: false };
        setTodos([...todos(), newTodo]);
        setNewTodoText('');
    }
};

const App = () => {
    return (
        <div>
            <input type="text" placeholder="新待办事项" value={newTodoText()} onChange={(e) => setNewTodoText(e.target.value)} />
            <button onClick={addTodo}>添加待办事项</button>
            <ul>
                {todos().map(todo => (
                    <li key={todo.id}>
                        {todo.text} - {todo.completed? '已完成' : '未完成'}
                    </li>
                ))}
            </ul>
        </div>
    );
};

render(() => <App />, document.getElementById('app'));

在这个例子中,用户在输入框中输入文本,点击按钮后,addTodo 函数会创建一个新的待办事项并添加到列表中。同时,输入框会被清空,等待下一次输入。由于 todosnewTodoText 都是响应式信号,视图会准确地反映这些操作带来的变化。

通过以上对 Solid.js 列表渲染核心概念与实践的详细介绍,包括基础概念、动态更新、性能优化、嵌套列表、排序、过滤以及与表单结合等方面,相信开发者能够熟练掌握并运用 Solid.js 进行高效的列表渲染开发。无论是简单的列表展示,还是复杂的交互式列表功能,Solid.js 都提供了强大且灵活的解决方案。