人人都能学编程
English 开始学习
核心 · 前端第 8 课 / 共 8 课免费阅读

状态与渲染 · 前端:HTML、CSS 与 JavaScript

应用是怎么让屏幕和数据保持一致的?

课程目录

为什么需要它

待办清单、购物车、聊天:数据一直在变,屏幕必须始终和它一致。一个元素一个元素地手动更新,很快就会乱成一团。React、Vue、Svelte 都基于同一个简单的想法来管住它,而你用普通函数就能学会。

直觉理解

屏幕是数据的函数:屏幕 = render(state)。

  1. 把所有数据放在一个地方:状态(state)。
  2. 写一个 render(state),把状态变成 HTML。
  3. 发生了什么事(点击、按键)时,用 update(state, action) 算出一个新状态,然后重新渲染。

update 从不修改旧状态。它用 ... 展开和 map/filter 造出一个新状态返回。这样 bug 很好找:每个动作都把你从一个已知状态带到下一个状态。

在真实页面里,把它们接起来只要几行:

let state = [];
button.addEventListener("click", () => {
  state = update(state, { type: "add", title: input.value });
  list.innerHTML = render(state);
});

框架帮你做最后一行,而且只更新页面上变了的部分。

render 有一条规矩:来自用户的文字,放进 HTML 之前必须转义。否则有人在评论里输入一个类似 <script> 的标签,就能在别人的浏览器里运行代码:这就是 XSS。

定义

状态

All the data the screen depends on, kept in one place.
屏幕依赖的所有数据,放在一个地方。

渲染

Turning state into what's on screen: here, a function from state to an HTML string.
把状态变成屏幕上的内容:这里是一个从状态到 HTML 字符串的函数。

不可变更新

Building a new object or array instead of changing the old one, with spread, map and filter.
用展开、map、filter 造一个新对象或新数组,而不是修改旧的。

示例

渲染一个列表

动手试试

一个函数把数据变成 HTML。

function render(todos) {
  const items = todos.map(t => `<li>${t.done ? "[x]" : "[ ]"} ${t.title}</li>`);
  return `<ul>${items.join("")}</ul>`;
}

const todos = [
  { title: "Buy milk", done: true },
  { title: "Call mum", done: false },
];
console.log(render(todos));
输出
<ul><li>[x] Buy milk</li><li>[ ] Call mum</li></ul>
逐步执行
  1. L2map 把每个待办变成一个 <li> 字符串;? : 选出勾选框的样子。
  2. L3join("") 把各项拼在一起,中间什么都不加。

用动作来更新

动手试试

每次修改都是一个动作;update 返回下一个状态。

function update(todos, action) {
  if (action.type === "add") {
    return [...todos, { title: action.title, done: false }];
  }
  if (action.type === "toggle") {
    return todos.map((t, i) => (i === action.index ? { ...t, done: !t.done } : t));
  }
  return todos;
}

const s0 = [];
const s1 = update(s0, { type: "add", title: "Buy milk" });
const s2 = update(s1, { type: "toggle", index: 0 });
console.log(s0.length, s1.length);
console.log(s1[0].done, s2[0].done);
输出
0 1
false true
逐步执行
  1. L3一个新数组:旧的各项,再加一项。
  2. L6一个新数组,只有被切换的那个待办是新对象。
  3. L14s0 还是空的:update 没有改它。0 1
  4. L15s1 还是旧值;s2 是新值。false true

转义用户输入

动手试试

包含 HTML 的评论必须显示成文字,而不是被执行。

function escapeHtml(text) {
  return text
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;");
}

const comment = '<img src=x onerror="alert(1)">';
console.log(`<p>${comment}</p>`);
console.log(`<p>${escapeHtml(comment)}</p>`);
输出
<p><img src=x onerror="alert(1)"></p>
<p>&lt;img src=x onerror=&quot;alert(1)&quot;&gt;</p>
逐步执行
  1. L3先处理 &,这样 &lt; 里的 & 就不会被再转义一次。
  2. L10原样插入:浏览器会加载那个坏图片并运行 alert(1)。
  3. L11转义后:浏览器把这些字符当普通文字显示。

常见错误

原地修改状态

state.push(item) 会连旧状态一起改掉,你就没法比较前后,框架也可能察觉不到变化。返回新数组:[...state, item]。

忘了 join

把数组直接放进模板字符串,会用逗号连接:<li>a</li>,<li>b</li>。要用 .join("")。

直接插入用户输入

用户输入的任何内容,放进 HTML 之前都要转义(或者用 textContent 代替 innerHTML)。XSS 攻击就是这样防住的。

小测验

1. 这段代码会输出什么?

const items = ["a", "b"].map(x => `<li>${x}</li>`);
console.log(`<ul>${items}</ul>`);
  1. A
    <ul><li>a</li><li>b</li></ul>
  2. B
    <ul><li>a</li>,<li>b</li></ul>
  3. C
    <ul>[object Array]</ul>
查看答案

答案:B

数组放进 ${ } 会变成用逗号连接的各项。

2. 这段代码会输出什么?

const before = [1, 2];
const after = before;
after.push(3);
console.log(before.length);
  1. A
    2
  2. B
    3
  3. C
    undefined
查看答案

答案:B

after = before 并没有复制:两个名字指向同一个数组,所以 push 也改了 before。

动手练习

3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。

  1. 预测输出重放这些动作

    准确写出它打印的内容。

  2. 改需求修好渲染函数

    render(names) 应该返回 <ul><li>Ann</li><li>Bo</li></ul> 这样的列表。它有两个 bug:各项之间出现了逗号;名字里的 HTML 没有被转义。把两个都修好(escapeHtml 已经写好了)。

  3. 从零写一个购物车

    给购物车写 update(cart, action):购物车是 { id, price, qty } 的数组。

    • { type: "add", id, price }:商品已在购物车里就把 qty 加 1;否则在末尾加上 { id, price, qty: 1 }
    • { type: "remove", id }:把它的 qty 减 1,减到 0 就移除
    • 其他动作:原样返回购物车

    不要修改传进来的购物车:返回一个新数组。

在互动课里练这一课

打开互动课,这些例子都能直接改、直接运行,练习题提交马上判题。免费开始,不用注册。

互动课按顺序解锁:前面的课还没学,会先带你到课程地图。

本课术语

术语中文英文定义
State状态The data a screen is drawn from.
Reducerreducer 更新函数A function (state, action) → new state.
Escaping转义Turning < > & " into entities so text can't become HTML.