Загрузка...
Загрузка...
Reconciliation (Согласование) — это алгоритм, который React использует для сравнения двух деревьев элементов и определения, какие части реального DOM нужно обновить.
Когда state или props компонента меняются, React создаёт новое дерево элементов и сравнивает его с предыдущим, чтобы сделать минимальное количество изменений в DOM.
Обновление DOM — дорогая операция. React оптимизирует этот процесс:
React использует эвристический алгоритм O(n) вместо O(n³). Он основан на двух предположениях:
Когда корневые элементы имеют разные типы, React удаляет старое дерево и строит новое с нуля.
// Старое дерево
<div>
<Counter />
</div>
// Новое дерево
<span>
<Counter />
</span>
React:
<div> и всех детейCounter (вызовет componentWillUnmount)<span>Counter (вызовет componentDidMount)Когда типы совпадают, React сравнивает атрибуты и обновляет только изменённые:
// Старый
<div className="before" title="old" />
// Новый
<div className="after" title="old" />
React обновит только className, title останется без изменений.
// Старый
<div style={{color: 'red', fontWeight: 'bold'}} />
// Новый
<div style={{color: 'blue', fontWeight: 'bold'}} />
React обновит только color.
Когда компонент обновляется, экземпляр остаётся тем же, поэтому state сохраняется:
<Counter count={1} />
// После обновления
<Counter count={2} />
React:
componentWillReceiveProps() и componentWillUpdate()render()render()React рекурсивно обрабатывает детей. Рассмотрим пример:
// Старый
<ul>
<li>first</li>
<li>second</li>
</ul>
// Новый
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
React:
<li> — одинаковые, ничего не делает<li> — одинаковые, ничего не делает<li>Эффективно!
// Старый
<ul>
<li>first</li>
<li>second</li>
</ul>
// Новый
<ul>
<li>zero</li>
<li>first</li>
<li>second</li>
</ul>
React:
<li> — "first" ≠ "zero", обновляет<li> — "second" ≠ "first", обновляет<li>Неэффективно! React не понял, что элементы просто сдвинулись.
key помогает React понять, какие элементы изменились, добавились или удалились.
// Старый
<ul>
<li key="first">first</li>
<li key="second">second</li>
</ul>
// Новый
<ul>
<li key="zero">zero</li>
<li key="first">first</li>
<li key="second">second</li>
</ul>
React:
Эффективно!
Ключи должны быть уникальными только среди соседних элементов:
function Blog(props) {
const sidebar = (
<ul>
{props.posts.map(post =>
<li key={post.id}>{post.title}</li>
)}
</ul>
);
const content = props.posts.map(post =>
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
);
return (
<div>
{sidebar}
<hr />
{content}
</div>
);
}
Одинаковые key в разных массивах — это нормально.
Ключи должны быть стабильными и предсказуемыми. Не используйте случайные значения:
// Плохо
{items.map(item => <Item key={Math.random()} data={item} />)}
// Плохо (при пересортировке потеряется state)
{items.map((item, index) => <Item key={index} data={item} />)}
// Хорошо
{items.map(item => <Item key={item.id} data={item} />)}
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Buy milk', done: false },
{ id: 2, text: 'Clean room', done: true }
]);
// Плохо
return (
<ul>
{todos.map((todo, index) => (
<TodoItem key={index} todo={todo} />
))}
</ul>
);
}
Проблема: при удалении первого элемента, индексы смещаются, и React думает, что изменился контент, а не порядок.
// Хорошо
return (
<ul>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
Можно оптимизировать reconciliation, сказав React, когда компонент НЕ нужно перерисовывать:
class MyComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
// Рендерить только если изменился id
return this.props.id !== nextProps.id;
}
render() {
return <div>{this.props.data}</div>;
}
}
Автоматически делает поверхностное сравнение props и state:
class MyComponent extends React.PureComponent {
render() {
return <div>{this.props.data}</div>;
}
}
Для функциональных компонентов:
const MyComponent = React.memo(function MyComponent(props) {
return <div>{props.data}</div>;
});
// С кастомным сравнением
const MyComponent = React.memo(
function MyComponent(props) {
return <div>{props.data}</div>;
},
(prevProps, nextProps) => {
return prevProps.id === nextProps.id;
}
);
function UserList({ users, filter }) {
const filteredUsers = users.filter(user =>
user.name.includes(filter)
);
return (
<ul>
{filteredUsers.map(user => (
<li key={user.id}>
{user.name}
<input type="text" />
</li>
))}
</ul>
);
}
Если использовать index вместо user.id, то при изменении фильтра значения в <input> перемешаются, потому что React не поймёт, что это другие пользователи.
function App() {
const [tab, setTab] = useState('profile');
return (
<div>
{tab === 'profile' && <Profile />}
{tab === 'settings' && <Settings />}
</div>
);
}
При переключении tab React полностью размонтирует один компонент и монтирует другой, потому что это разные типы.
Если хотите сохранить state, используйте display: none:
function App() {
const [tab, setTab] = useState('profile');
return (
<div>
<div style={{ display: tab === 'profile' ? 'block' : 'none' }}>
<Profile />
</div>
<div style={{ display: tab === 'settings' ? 'block' : 'none' }}>
<Settings />
</div>
</div>
);
}
function Component({ isLoading }) {
if (isLoading) {
return <div>Loading...</div>;
}
return <span>Data</span>;
}
При изменении isLoading React размонтирует весь компонент, потому что div ≠ span.
Решение: используйте одинаковый тип:
function Component({ isLoading }) {
return <div>{isLoading ? 'Loading...' : 'Data'}</div>;
}
// Плохо
function List({ items }) {
return (
<ul>
{items.map(item => <li>{item.name}</li>)}
</ul>
);
}
React будет использовать порядковый номер по умолчанию, что приведёт к проблемам при изменении порядка.
// Плохо
function List({ items }) {
return (
<ul>
{items.map(item => (
<li key={`${item.name}-${Date.now()}`}>
{item.name}
</li>
))}
</ul>
);
}
Key меняется при каждом рендере, React будет пересоздавать элементы.
Reconciliation:
На собеседовании:
Важно уметь: