第九章 案例 3 - Valtio 【下】

创建应用代码

接下来,我们要用Valito来 创建一个 小型应用。

首先,我们要定义一个Todo类型:

js 复制代码
type Todo = {
    id: stirng;
    title: string;
    done: boolean;
}

一个Todo 成员,由 类型为 string 的id,类型 为 stirng 的 title 和 类型 为 boolean 的 done 组成。

当我们在定义状态对象时,可以用Todo 类型:

js 复制代码
const state = proxy<{ todos: Todop[] }>({
    todos: []
})

这个初始的状态对象,被一个proxy所包裹。

为了操作 状态对象,我们需要 定义 一些 函数:添加 待办事项的 方法,删除待办事项的 方法,以及改变待办事项状态的方法:

js 复制代码
const createTodo = (title: string) => {
    state.todos.push({
        id: nanoid(),
        title,
        done: false
    })
}

const removeTodo = (id: string) => {
    const index = state.todos.findIndex(
        (item) => item.id === id
    )
    state.todos.splice(index, 1);
}

const togggleTodo = (id: string) => {
    const index = state.todos.findIndex(
        (item) => item.id === id
    )
    state.todos[index].done = !state.todos[index].done;
}

nanoid 是一个用于生成唯一id的函数。注意,这三个方法都是基于常规的JS语法实现的。这个三个方法把状态当成了普通的JS对象。而这是由proxy来实现的。

接下来要实现的,是TodoItem组件。TodoItem组件是一个通过不同样式来标识是否完成的 一个 勾选框 和 一个 删除按钮:

js 复制代码
const TodoItem = ({
    id,
    title,
    done,
}: {
    id: string,
    title: string,
    done: boolean
}) => {
    return (
        <div>
            <input
                type="checkbox"
                checked={done}
                onChange={() => toggleTodo(id)}
            />
            <span
                style={{
                    textDecoration: done ? "line-through" : "none",
                }}
            >
                {title}
            </span>
            <button onClick={() => removeTodo(id)}>
                Delete
            </button>
        </div>
    )
};

const MemoedTodoItem = memo(TodoItem);

注意,这个组件的属性并不是 整个 状态对象,而是 有三个属性,分别是 id、title 和 done属性。这是因为我们使用了memo函数,来创建 MemoedTodoItem 组件。状态使用收集函数,会检测状态属性。而如果我们传递给缓存组件的是一个 对象,那么将无法追踪其可访问性。

为了使用 MemoedTodoItem 组件,TodoList 组件需要使用 useSnapshot来获取状态:

js 复制代码
const TodoList = () => {
    const { todos } = useSnapshot(state);
    
    return (
        <div>
            {todos.map((todo) => (
                <MemoedTodoITem
                    key={todo.id}
                    id={todo.id}
                    title={todo.title}
                    done={todo.done}
                />
            ))
            }
        </div>
    )
}

这个组件以来自useSnapshot的todos为状态,将之渲染为对应的组件。因此,useSnapshot会因为任意一个数组发生变化,而触发重新渲染。这不是一个大问题,而且这是一个被验证可行的模式,因为MemoedTodoITem组件只会在 id, title, 或 done 发生变化时而重新渲染。我们会在这一小节学习另一个模式。

接下来要实现的,是用于新增待办事项的 新增按钮:

js 复制代码
const NewTodo = () => {
    const [text, setText] = useState("");
    const onClick = () => {
        createTodo(text);
        setText("");
    };
    
    return (
        <div>
            <input
                value={text}
                onChange={(e) => setText(e.target.value)}
             />
             <button onClick={onClick} disbale={!text}>
                 Add
             </button>
        </div>
    )
}

之后,我们要实现App组件:

js 复制代码
const App = () => {
    <>
        <TodoList />
        <NewTodo />
    </>
}

让我们看看这个 应用 是如何工作的:

1.首先,它只有一个文本输入框和一个新增按钮:

2.如果我们点击了新增按钮,可以新增一个待办事项:

3.我们可以尽情地添加待办事项:

4.点击勾选框,可以改变事件的状态:

5.点击删除按钮,就可以删除一个待办事项

这个应用目前是运作良好的,但是我们还是改善该应用以减少不必要的重新渲染。当我们改动一个已存在的状态的状态时,不仅是对应的 TodoItem组件会重新渲染,而且TodoList组件也会重新渲染。目前来说,这还不是一个大问题,因为这还是一个清零的应用。

我们还要另一个方法来减少 TodoList 组件 不必要的额外重新渲染。但这不意味着我们整体上提升了应用到性能。至于采用哪一种方法,要视具体条件而定。

在另一种方式中,我们会在每一个 TodoItem组件 中使用 useSnapshot钩子。这个TodoItem组件只接收id属性。这是调整后的TodoItem组件:

js 复制代码
const TodoItem = ({ id }: { id: string }) => {
    const todoState = state.todos.find(
        (todo) => todo.id === id
    );
    if (!todoState) {
        throw new Error("invalid todo id");
    }
    
    const { title, done } = useSnapshot(todoState);
    return (
        <div>
            <input
                type="checkbox"
                checked={done}
                onChange={() => toggleTodo(id)}
            />
            <span
                style={{
                    textDecoration: done ? "line-through" : "none",
                }}
            >
                {title}
            </span>
            <button onClick={() => removeTodo(id)}>
                Delete
            </button>
        </div>
    )
};

const MemoedTodoItem = memo(TodoItem);

我们会用id属性来找到对应的 todoState,通过 useSnapshot来获取 todoState,并获得相应的 title 和 done 属性。这个组件 只会 在 id,title 或者 done 属性变化时才 重新渲染。

现在,让我们看看 更新后的的 TodoList组件。现在,我们只要传递id即可

js 复制代码
const TodoList = () => {
    const { todos } = useSnapshot(state);
    const todoIds = todos.map((todo) => todo.id);
    
    return (
        <div>
            {todoIds.map((todoId) => (
                <MemoedTodoItem = key={todoId} id={todoId} />
            ))}
        </div>
    );
};

所以,todoIds 是每个todo对象的子集。这个组件只会在 todoIds的顺序发生变化时,才进行重新渲染。如果只是一个已存在的列表项的状态发生了变化,这个组件并不会重新渲染。因此,这个模式减少了额外的重新渲染。

在中型应用程序中,这两种方法在性能方面的变化是微妙的。这两种方法对于不同的编码模式更具意义。开发人员可以选择更符合其思维模式的方法。

在这个小节,我们通过一个简单的应用,学习了 useSnapshot钩子的用法。接下来,我们要学习这个库的优点与缺点。

该库的优点与缺点

我们已经知道了 Valito的工作原理。那么首要问题是,何时该使用它,何时不该使用它。

第一个方面,是心智模型。我们有两个状态更新模型。一个是不可变更新方式,另一是可变更新方式。尽管JS本身是允许可变式更新的,React本身是基于不可变状态来构建的。因此,如果我们把这两个模式混合在一起,我们要确保自己没有弄混。一个可行的方案是,把 Valtio 状态 和 React状态 隔离开,以确保我们不会搞混。

可变更新的好处是,我们可以使用原声的JS语法。

比如说,我们可以通过index来删除一个数组成员:

js 复制代码
array.splice(index, 1)

如果是不可变更新,就要麻烦一点:

js 复制代码
[...array.slice(0, index), ...array.slice(index + 1)]

另一场景,是处理深层嵌套对象。如果是基于可变式更新,只要一行代码即可:

js 复制代码
state.a.b.c.text = "hello";

如果是不可变更新,得写这么一坨:

js 复制代码
{
    ...state,
    a: {
        ...state.a,
        b: {
            ...state.a.b,
        },
        c: {
            ...state.a.b.c,
            text: "hello"
        }
    }
}

这样写确实很复杂。Valito能帮助我们更好的更新复杂的对象。

Valtio 还借助基于代理的渲染优化来帮助减少应用程序代码量。假设我们有一个包含 counttext 属性的状态,如下所示:

js 复制代码
cons state = proxy({ count: 0, text: "hello"});

如果我们要做组件中使用 count,我们可以这样写Valito代码:

js 复制代码
const Component = () => {
    const { count } = useSnapshot(state);
    return <>{count}</>
}

相比之下,Zustand需要这么写:

js 复制代码
const Component = () => {
    const count = useStore((state) => state.count)
    return <>{count}</>
}

差异其实很小,但是Zustand里出现了两次 count。

让我们看看这个场景:

js 复制代码
const Component = ({ showText }) => {
    const snap = useSnapshot(state);
    return <>{snap.count} {showText ? snap.text : "" }</>
}

同样的功能,如果由 Zustand来实现,我们需要多次 useStore钩子:

js 复制代码
const Component = ({ showText }) => {
    const count = useStore((state) => state.count)
    const text = useStore(
        (state) => showText ? state.text : ""
    )
    return <>{count} {text}</>
}

这意味着,如果我们有更多的值,就需要更多的钩子了。

而该库的缺点,则在于这种模式的渲染优化,是很不可预测的。Proxy会在幕后优化渲染,但这也会提升定位问题的难度。所以有些人喜欢选择基于选择器的钩子。

总而言之,这世界上并没有一劳永逸的方案。使用哪个方法还是由开发者视具体情况而定。

在本章中,我们了解了一个名为 Valtio 的库,它广泛使用了代理(proxies)。我们通过示例学习了它的用法 ------ 它允许直接修改状态(mutating state),使用起来就像操作普通的 JavaScript 对象一样自然,而基于代理的渲染优化则有助于减少应用程序代码量。这种方法是否是一个好的选择,取决于开发人员的具体需求。

在下一章中,我们将学习另一个库 ------React Tracked。它是一个基于 Context 的库,和 Valtio 一样具备基于代理的渲染优化能力。

相关推荐
东东2335 分钟前
前端开发中如何取消Promise操作
前端·javascript·promise
掘金安东尼10 分钟前
官方:什么是 Vite+?
前端·javascript·vue.js
柒崽12 分钟前
ios移动端浏览器,vh高度和页面实际高度不匹配的解决方案
前端
渣哥28 分钟前
你以为 Bean 只是 new 出来?Spring BeanFactory 背后的秘密让人惊讶
javascript·后端·面试
烛阴36 分钟前
为什么游戏开发者都爱 Lua?零基础快速上手指南
前端·lua
大猫会长1 小时前
tailwindcss出现could not determine executable to run
前端·tailwindcss
Moonbit1 小时前
MoonBit Pearls Vol.10:prettyprinter:使用函数组合解决结构化数据打印问题
前端·后端·程序员
533_1 小时前
[css] border 渐变
前端·css
云中雾丽1 小时前
flutter的dart语言和JavaScript的消息循环机制的异同
前端
地方地方1 小时前
Vue依赖注入:provide/inject 问题解析与最佳实践
前端·javascript·面试