предисловие
Эта статья подойдет для небольших партнеров, которые используют React около года. Я надеюсь, что его можно использовать как ключ к исходному коду React. Я начну с настройки среды и шаг за шагом проведу вас через простой фреймворк React. Код, который я размещу на своемgithubначальство, Для справки в процессе чтения.
среда сборки
настроить webpack.config.js
- Добавить к
babel-loader
Преобразование высокоуровневого синтаксиса в синтаксис, понятный браузерам. - использовать
@babel/preset-env
как плагин перед транспиляцией - использовать
@babel/plugin-transform-react-jsx
Разобрать синтаксический сахар jsx
module.exports = {
entry: {
main: './main.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: [[
'@babel/plugin-transform-react-jsx'
]]
}
}
}
]
},
optimization: {
minimize: false
},
mode: 'development'
}
Создайте новый html файл и импортируйте main.js
Чтобы отобразить эффект более интуитивно, мы можем ввести упакованный файл скрипта в html.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script src="dist/main.js" ></script>
</html>
Написать jsx.
Пишем секцию jsx в main.js, а потом просматриваем в консолиmain.js
Результат после компиляции.
let a = <div id="hello">hello world!</div>
После компиляции с помощью webpack мы обнаружили, что jsx был скомпилирован в:
var a = /*#__PURE__*/React.createElement("div", {
id: "hello"
}, "hello world!");
При этом мы обнаружили, что консоль сообщила об ошибке ❌.
Uncaught ReferenceError: React is not defined
at eval (main.js:1)
at Object../main.js (main.js:96)
at __webpack_require__ (main.js:20)
at main.js:84
at main.js:87
мы обнаружили, что когда@babel/plugin-transform-react-jsx
При разборе jsx он автоматически запускается сReact
вызыватьcreateElement
метод. Тогда, если вы хотитеReact.createElement
изменился наToyReact.createElement
Что мы можем сделать по этому поводу?
Мы можем увидеть в деталях, просмотрев список плагинов на официальном сайте Babel.babel-plugin-transform-react-jsxПрименение.
- прагма
получить
string
введите строку, по умолчаниюReact.createElement
. Когда встречается тег jsx, он будет заменен значением прагмы. Если мы установим значение прагмы какToyReact.createElement
, тогда код в main.js будет разобран на:
var a = ToyReact.createElement("div", {
id: "hello"
}, "hello world!");
-
прагмафраг получить
string
введите строку, по умолчаниюReact.Fragment
При синтаксическом анализе в пустой тег он заменяется значением pragmaFrag. Например, мы будемmain.js
содержание изменено наlet a = <>hello world!</>
. Тогда результат после разбора:var a = ToyReact.createElement(React.Fragment, null, "hello world!");
-
использование встроенных модулей получить
Boolean
значение типа, значение по умолчаниюfalse
При передаче реквизита используйте его напрямуюObject.assign()
метод, а не другие плагины Babel. -
useSpread
-
throwIfNamespace
давайте обновимwebpack.config.js
Конфигурация,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: [[
'@babel/plugin-transform-react-jsx',
{
pragma: 'ToyReact.createElement',
pragmaFrag: 'ToyReact.Fragment'
}
]]
}
}
Хорошо!На этом построение нашей среды завершено, и мы можем счастливо развиваться дальше!
Написание ToyReact
Создать ToyReact.js
Поскольку мы не писали ничего, связанного с ToyReact, консоль сообщит об ошибке. Далее создаем новыйToyReact.js
,
и добавитьcreateElement
метод для создания объекта DOM.
ТакcreateElement
Как написать метод?
var a = ToyReact.createElement("div", {
id: "hello"
}, "hello world!");
мы обнаруживаем,createElement
передать три параметра
- тип этикетки
- Атрибуты
- дочерний узел
export const ToyReact = {
createElement(type, attributes, ...children) {
const element = document.createElement(type);
for (let name in attributes) {
element.setAttribute(name, attributes[name])
}
return element;
}
}
мы называемdocument.createElement
Метод создает DOM объекта, проходит через пользовательские атрибуты в jsx и монтирует каждый атрибут в DOM объекта. мы можем распечатать в консолиcreateElement
Значение, возвращаемое методом.
<div id="hello"></div>
В это время возвращается сущность DOM, в которой вроде бы отсутствует ❓emm, дочерние узлы вроде не учитываются, можно просто разобраться с дочерними (позже оптимизация)
```js
export const ToyReact = {
createElement(type, attributes, ...children) {
const element = document.createElement(type);
for (let name in attributes) {
element.setAttribute(name, attributes[name])
}
for (let child of children) {
const node = document.createTextNode(child)
element.appendChild(node)
}
return element;
}
}
```
Затем мы вставляем настоящий DOM в документ, чтобы увидеть hello world!.
import { ToyReact } from './ToyReact';
let a = <div id="hello">hello world!</div>
document.body.appendChild(a);
Рефакторинг ToyReact.js
Чтобы стиль соответствовал API React, нам нужно изменить код main.js.
import { ToyReact } from './ToyReact';
class TestComponent {
render() {
return <div id="hello">hello world!</div>
}
}
ToyReact.render(<TestComponent />, document.body)
Мы обнаружили, что в новом коде на один метод рендеринга больше, чем в предыдущем коде, а также используется метод написания класса для возврата jsx через рендер.Так с чего нам начать изменять код в ToyReact?
- Создайте экземпляр TestComponent, получите jsx, который он возвращает
- визуализировать или отвечать в реальном DOM, вставленном в document.body
Изменить createElement
Сначала нам нужно судитьtype
Тип, поскольку переданный тип больше не является строкой предыдущего тега элемента (например, div). но стал function
.Во-вторых, лучше убрать элементы и текстовые узлы. К узлам элементов можно добавлять атрибуты и дочерние узлы, а с текстовыми узлами мы ничего не можем сделать. Цель разделения состоит в том, чтобы позволитьcreateElement
Метод становится менее раздутым, при этом элементы и текст извлекаются, а соответствующая логика более удобна в сопровождении.
export const ToyReact = {
createElement(type, attributes, ...children) {
const element = document.createElement(type);
for (let name in attributes) {
element.setAttribute(name, attributes[name])
}
for (let child of children) {
const node = document.createTextNode(child)
element.appendChild(node)
}
return element;
}
}
Определите класс ElementWrapper
Создайте новый класс ElementWrapper и добавьте创建实体DOM
метод иsetAttribute
а такжеappendChild
метод.
class ElementWrapper {
constructor(type) {
this.root = document.createElement(type)
}
setAttribute(name, value) {
this.root.setAttribute(name, value)
}
appendChild(component) {
this.root.appendChild(component.root)
}
}
В приведенном выше методе appendChild
component
на всем протяженииElementWrapper
или
TextNodeWrapper
Созданное значение.component.root
Относится к узлу элемента или текстовому узлу.
Определите класс TextNodeWrapper
Создайте новый класс TextNodeWrapper, и вам нужно будет создать только текстовый узел.
class TextNodeWrapper {
constructor(content) {
this.root = document.createTextNode(content);
}
}
Измените метод createElement.
createElement(type, attributes, ...children) {
let element;
// 判断element的类型, 如果是元素标签的字符串类型, 那么就通过ElementWrapper创建实DOM, 否则就直接实例化本身返回其render的jsx, 进行重新调用createElement构建元素。
if(typeof type === 'string') {
element = new ElementWrapper(type);
} else {
element = new type;
}
for (let name in attributes) {
element.setAttribute(name, attributes[name])
}
for (let child of children) {
// 如果child是字符串那么直接实例化TextNodeWrapper,得到文本节点。
if(typeof child === 'string') {
child = new TextNodeWrapper(child)
}
element.appendChild(child)
}
return element;
},
Добавить класс компонента
Некоторые учащиеся здесь могут сомневаться, зачем нужно добавлять дополнительный класс.
Когда babel выполняет синтаксический анализTestComponent
Когда мы создаем экземпляр напрямую, а значения после создания экземпляраsetAttribute
свойства и appendChild
дочерний узел. Конечно, мы не можем написать конкретную реализацию этих методов в main.js. Итак, мы отпускаем TestComponent继承
Component, пусть класс Component реализует эти два метода.
export class Component {
constructor(props) {
this.props = Object.create(null);
this._root = null;
this.children = []
}
setAttribute(name, value) {
this.props[name] = value;
}
appendChild(component) {
this.children.push(component);
}
get root() {
if(!this._root) {
this._root = this.render().root;
}
return this._root
}
}
Добавить метод рендеринга
render(component, parentElement) {
parentElement.appendChild(component.root)
}
Измените код main.js
По сравнению с предыдущим, мы позволяем TestComponent наследовать класс Component, так чтоTestComponent
Имеет возможность получать реквизиты и добавлять дочерние узлы.
import { ToyReact, Component } from './ToyReact';
class TestComponent extends Component {
render() {
return <div id="hello">hello world!</div>
}
}
ToyReact.render(<TestComponent name="123" />, document.body)
Мы обнаружили, что написанная нами страница может нормально отображатьсяhello world!
. мы пытаемсяTestComponent
Добавьте детей.
import { ToyReact, Component } from './ToyReact';
class TestComponent extends Component {
render() {
return <div id="hello">hello world!{this.children}</div>
}
}
ToyReact.render(
<TestComponent name="123">
<div>i</div>
<div>am</div>
</TestComponent>,
document.body)
Мы снова запустили код и обнаружили, что страница сообщила об ошибке. В самом деле, мы можем легко догадаться, почему? мы пытаемся напечататьthis.children
значение ,
this.children — это массив, содержащий два элемента. Однако наш предыдущий код не учитывал эту ситуацию, поэтому нам нужно модифицировать метод createElement, чтобы он мог разбирать массив в дочерних элементах.
createElement(type, attributes, ...children) {
let element;
if(typeof type === 'string') {
element = new ElementWrapper(type);
} else {
element = new type;
}
for (let name in attributes) {
element.setAttribute(name, attributes[name])
}
function insertChildren(children) {
for (let child of children) {
if(typeof child === 'string') {
child = new TextNodeWrapper(child)
}
if(typeof child === 'object' && child instanceof Array) {
insertChildren(child);
return;
}
element.appendChild(child)
}
}
insertChildren(children);
return element;
},
Мы обрабатываем случай, когда дочерний элемент представляет собой массив с простой рекурсивной функцией. Теперь наша страница может нормально отображать структуру DOM и имеет возможность использования свойств.
Далее нам нужно сделать так, чтобы страница могла动起来
, то есть мы можем использовать метод, аналогичный this.setState() в React, чтобы изменить отображение страницы.
Заставьте ToyReact «двигаться»
Прежде чем заставить его двигаться, давайте взглянем на менее часто используемый API.range
.
Определение диапазона
MDN определяет это так:Range 接口表示一个包含节点与文本节点的一部分的文档片段
.Я думаю, что это будет лучше понято с небольшой модификацией на этой основе. Range 接口能够表示文档中任意节点之间的一部分文档(HTML DOM)片段。
.
Простое использование API диапазонов
<p id="p1"> hello<span> world !</span><span> world !</span></p>
-
Range.setStart(startNode, startOffset) Установить начальную точку диапазона.
Принимает два параметра: первый параметр — это узел, а второй — смещение узла. Возьмем пример выше, например:
const p1 = document.getElementById('p1'); range.setStart(p1, 1)
Начальная позиция диапазона должна быть
range起始位置 | | | <p id="p1">hello <span> world !</span><span> world !</span></p>
то если
setStart
Второй параметр равен 0, тогда начальная позиция диапазона:range起始位置 | | | <p id="p1">hello <span> world !</span><span> world !</span></p>
На самом деле легко понять, что под узлом элемента p1 есть три дочерних узла. Один — текстовый узел hello, два других — узлы элементов
<span> world !</span>
. -
Range.setEnd(startNode, startOffset) Устанавливает конечную позицию диапазона.
Принимает два параметра: первый параметр — это узел, а второй — смещение узла. Давайте снова возьмем приведенный выше пример:
const p1 = document.getElementById('p1'); range.setEnd(p1, p1.childNodes.length)
range结束位置 | | | <p id="p1">hello <span> world !</span><span> world !</span> </p>
-
Range.insertNode(Node) Вставляет узел в начало диапазона.
<p id="p1"> hello<span> world !</span><span> world !</span><span> world!</span></p>
```js
const range = document.createRange();
const p1 = document.getElementById('p1');
const element = document.createElement('p');
element.appendChild(document.createTextNode('123'));
range.setStart(p1, 0);
range.setEnd(p1, p1.childNodes.length);
range.insertNode(element)
```
при исполненииinsertNode
После метода узел элемента p будет добавлен перед текстовым узлом hello.
- Range.deleteContents() удаляет содержимое диапазона из документа.
Вызов этого метода удаляет все узлы в пределах диапазона.
<p id="p1"> hello<span> world !</span><span> world !</span><span> world!</span></p>
const range = document.createRange();
const p1 = document.getElementById('p1');
range.setStart(p1, 0)
range.setEnd(p1, p1.childNodes.length)
range.deleteContents()
Все узлы под узлом P1 ниже узла P1 будут удалены.
Другие API-интерфейсы диапазона не будут рассматриваться в этой статье, поэтому они не будут представлены один за другим.
Рефакторинг ToyReact с диапазоном
Почему мы используем диапазон для рефакторинга предыдущего кода? Я думаю, что это в основном из-за следующих соображений:
-
1. Используя диапазон, мы можем вставить DOM в любой узел
-
2. Подготовить почву для следующего повторного рендеринга и сравнения виртуального DOM
Основная идея нашей модификации:
- Начните с того места, где отображается DOM, и используйте диапазон, чтобы завершить фактическую работу DOM.
- Внимательно прочитайте предыдущий код, и вы увидите, что он не может быть перерендерен. Итак, нам нужно определить закрытый метод для повторного рендеринга дерева DOM.
Чтобы сделать метод рендеринга DOM-дерева менее простым для внешнего вызова, мы используемSymbol
Возвращает уникальный идентификатор в качестве имени функции.
const RENDER_TO_DOM = Symbol('render to dom')
Изменить класс компонента
Нам нужно добавить закрытый метод в класс Component, потому что значение, возвращаемое this.render(), может быть Component, ElementWrapper или TextNodeWrapper. Итак, в оставшихся двух классах мы
также нужно добавитьRENDER_TO_DOM
метод.
[RENDER_TO_DOM](range) {
this.render()[RENDER_TO_DOM](range);
}
Измените классы ElementWrapper и TextNodeWrapper.
В этих двух классах мы отображаем настоящий DOM на странице. Таким образом, вRENDER_TO_DOM
Нам нужно вставить узлы в диапазон.
[RENDER_TO_DOM](range) {
range.deleteContents();
range.insertNode(this.root);
}
Изменить функцию рендеринга
Поскольку мы больше не используемget root()
метод для получения реального DOM, поэтому мы вызываемRENDER_TO_DOM
чтобы вставить узел.
render(component, parentElement) {
const range = document.createRange();
range.setStart(parentElement, 0);
range.setEnd(parentElement, parentElement.childNodes.length);
range.deleteContents();
component[RENDER_TO_DOM](range)
}
Мы завершили предварительную реконструкцию, и еще немного расстояние до того, как страница может быть перемещена, но в это время обычный дисплей страницы не проблема. Если код не запускается, вы можете проверить этоКод для ответвления функции/диапазона. Просмотрите код, чтобы увидеть, что пошло не так.
Изменить main.js
Нам нужно активное поведение для обновления страницы. Добавляем на страницу счетчик, при каждом нажатии на кнопку число на странице увеличивается на единицу.
import { ToyReact, Component } from './ToyReact';
class TestComponent extends Component {
constructor() {
super();
this.state = {
count: 1
}
}
render() {
return <div id="hello">
hello world!
<span>{
this.state.count.toString()
}
<button onClick={() => this.count ++ }>点击</button>
</span>
{
this.children
}
</div>
}
}
ToyReact.render(<TestComponent></TestComponent>, document.body)
Поддержка привязки событий и новая функция повторного рендеринга
Кажется, ничего не происходит, когда мы нажимаем на страницу onClick. На самом деле, есть два очень важных момента, которые здесь не были затронуты:
- Нам нужно иметь дело с чем-то вроде
onClick
событие - Нам нужно повторно отобразить измененное значение счетчика на странице.
Прежде всего, события могут быть привязаны только к узлам элементов, поэтому мы должны быть вElementWrapper
Изменено в классе. Мы пишем простое регулярное выражение для соответствия всем событиям, начинающимся с on, таким как onClick, onHover, onMouseUp... .
setAttribute(name, value) {
if(name.match(/^on([\s\S]+)/)) {
this.root.addEventListener(RegExp.$1.replace(/^[\s\S]/, s => s.toLowerCase()), value)
}
this.root.setAttribute(name, value)
}
Следующий шаг — подумать о том, как написать метод повторного рендеринга.Когда мы нажимаем кнопку, значение count действительно меняется. Просто содержимое не изменилось, поэтому, если мы хотим обновлять счетчик в режиме реального времени, нам нужно каждый раз обновлять содержимое диапазона.
Добавляем новый в класс Componentrerender
метод для выполнения операции обновления.
constructor(props) {
...
this._range = null;
}
[RENDER_TO_DOM](range) {
this._range = range;
this.render()[RENDER_TO_DOM](range);
}
rerender() {
this._range.deleteContents();
this[RENDER_TO_DOM](this._range)
}
Процесс реализации очень прост, по сути, это удаление всего содержимого диапазона (если не удалить, то сохранится предыдущее содержимое), а затем повторное выполнение метода добавления Node.
мы вmain.js
Событие нажатия кнопки изменено на <button onClick={() => { this.state.count ++; this.rerender()} }>点击</button>
. Пока что страницу удалось переместить. но для
В соответствии с API React нам нужноthis.state.count ++; this.rerender()
слился вthis.setState({ count: count++ })
.
Добавлен метод setState.
Метод setState в основном сравнивает новое состояние со старым состоянием, а затем выполняет операцию глубокого копирования. Если this.state не существует или тип не является объектом, мы напрямую заменяем его новым состоянием. Затем значение в новом состоянии напрямую присваивается старому соответствующему значению состояния посредством рекурсии.
setState(newState) {
if(this.state === null && typeof this.state !== 'object') {
this.state = newState;
this.rerender();
return;
}
let merge = (oldState, newState) => {
for (const key in newState) {
if(oldState[key] === null || typeof oldState[key] !== 'object') {
oldState[key] = newState[key]
} else {
merge(oldState[key], newState[key]);
}
}
}
merge(this.state, newState);
this.rerender();
}
Пример официального сайта Integrate React Tic Tac Toe
Чтобы сделать ToyReact более надежным, мы будем использовать пример официального веб-сайта React в качестве демонстрации ToyReact и, кстати, внесем небольшие исправления в ToyReact.
- Изменить main.js
Нам нужно изменить функциональное написание на официальном сайте на написание класса, потому что ToyReact пока не может обрабатывать поступающие функциональные компоненты.
import { ToyReact, Component } from './ToyReact';
class Square extends Component {
render() {
return (
<button className="square" onClick={this.props.onClick}>
{this.props.value}
</button>
);
}
}
class Board extends Component {
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}
render() {
return (
<div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
class Game extends Component {
constructor(props) {
super(props);
this.state = {
history: [
{
squares: Array(9).fill(null)
}
],
stepNumber: 0,
xIsNext: true
};
}
handleClick(i) {
const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? "X" : "O";
this.setState({
history: history.concat([
{
squares: squares
}
]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext
});
}
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0
});
}
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (this.state.xIsNext ? "X" : "O");
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={i => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
}
ToyReact.render(<Game />, document.getElementById("root"));
function calculateWinner(squares) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}
- Изменить main.html
Вводим стиль официального сайта и корневой узел root.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
body {
font: 14px "Century Gothic", Futura, sans-serif;
margin: 20px;
}
ol, ul {
padding-left: 30px;
}
.board-row:after {
clear: both;
content: "";
display: table;
}
.status {
margin-bottom: 10px;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.square:focus {
outline: none;
}
.kbd-navigation .square:focus {
background: #ddd;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
</style>
<body>
<div id="root"></div>
</body>
<script src="dist/main.js" ></script>
<script >
</script>
</html>
-
Измените класс ElementWrapper для поддержки className.
Мне нужно справиться с этим в одиночку
className
Этот атрибут, потому что имя класса узла элемента может вступить в силу, назначив его классу.setAttribute(name, value) { // ... if(name === 'className') { name = 'class' } // ... }
Пробуем запаковать и проверить отрисовку страницы, пример официального сайта тоже может нормально работать.
Пусть у ToyReact есть виртуальный DOM и алгоритм Diff
Что такое виртуальный DOM
Виртуальный DOM — это, по сути, отношение отображения к реальному DOM. Это DOM, который представляет реальное существование в виде объектов. Например:
<ul id="ul1">
<li name="1">world !</li>
<li name="2">world !</li>
<li name="3">world !</li>
</ul>
Если приведенный выше html-код представить в виде виртуального DOM, то это:
{
type: 'ul',
props: {
id: 'ul1'
},
children: [
{ type: 'li', props: {name: '1'}, children: ['world !']},
{ type: 'li', props: {name: '2'}, children: ['world !']},
{ type: 'li', props: {name: '3'}, children: ['world !']},
]
}
Что такое алгоритм сравнения
Алгоритм Diff фактически сравнивает старое и новое виртуальные деревья DOM, а затем визуализирует разные части на странице, чтобы свести к минимуму цель обновления DOM.
Следующий рисунок DOM является примером:
Алгоритм Diff проходится в соответствии с правилами обхода глубины, поэтому процесс обхода выглядит следующим образом:
-
- Контрастный узел 1 (без изменений)
-
- Контрастный узел 2 (без изменений)
-
- Контрастный узел 4 (без изменений)
-
- Контрастный узел 5
(节点5被移除, 记录一个删除的操作)
- Контрастный узел 5
-
- Контрастный узел 3 (без изменений)
-
- Сравните потомков узла 3
(新增节点5, 记录一个新增操作)
- Сравните потомков узла 3
Поэтому в реальном процессе рендеринга будет выполняться удаление и добавление узла 5, а остальные узлы не изменятся.
ToyReact включает в себя функциональность виртуального дома
После понимания виртуального DOM и алгоритма Diff нам нужно провести рефакторинг ToyReact.js. Попутно мы занимались рефакторингом ToyReact.js.
Перед рефакторингом давайте сравним разницу между официальным примером и нашим примером:
Пример официального сайта:
Каждый раз, когда мы нажимаем кнопку, он обновляет только свой собственный узел. Никакие другие узлы не перерисовываются. Давайте продолжим смотреть на процесс рендеринга ToyReact.
Каждый раз, когда мы нажимаем кнопку, все дерево DOM перерисовывается, что будет очень дорого для сложных страниц. Поэтому нам срочно нужно внедрить виртуальный алгоритм DOM+Diff для решения этой проблемы.
Определить виртуальный DOM
Для узлов элементов виртуальный DOM должен содержать три вещи:
- тип узла (например, div, span, p)
- реквизит на узлах
- дети узла
Однако для текстового узла его тип фиксирован, единственное отличие состоит в его содержимом, поэтому его виртуальный DOM относительно прост.
- тип узла (текст)
- Содержимое узла (content)
Тогда фрагмент кода, соответствующий ToyReact, должен бытьElementWrapper
а такжеTextNodeWrapper
эти два класса.
class ElementWrapper {
// ...
get vdom() {
return {
type: this.type,
props: ???,
children: ???
}
}
// ...
}
В дополнение к атрибуту типа мы можем получить через параметры конструктора, остальные реквизиты и дочерние элементы мы не можем получить. Но тогда эти два свойства в классе Component имеют, поэтому мы можем сделать класс ElementWrapper наследником класса Component. ,
class ElementWrapper {
// ...
get vdom() {
return {
type: this.type,
props: this.props,
children: this.children.map(item => item.vdom)
}
}
// ...
}
В то же время, мы также должны закомментироватьElementWrapper
в классеsetAttribute
а такжеappendChild
метод. В противном случае виртуальный дом нашего дочернего узла не будет вставлен, потому что метод в классе ElementWrapper имеет то же имя, что и метод в классе Component.
Поскольку это виртуальный DOM, его дочерние элементы также должны быть виртуальными дочерними элементами.TextNodeWrapper
В классе вам также необходимо определить метод для получения виртуального DOM.
class TextNodeWrapper {
constructor(content) {
this.root = document.createTextNode(content);
this.content = content;
}
// ...
get vdom() {
return {
type: '#text',
content: this.content
}
}
// ...
}
Хорошо, мне наконец нужно определить метод для получения виртуального дома в классе Component и получить дерево виртуального дома с помощью рекурсивного метода.
export class Component {
// ...
get vdom() {
return this.render().vdom;
}
// ..
}
Просмотр виртуального DOM
Мы можем изменить код в main.js для вывода виртуального дома.
let game = <Game />
console.log(game.vdom);
Хорошо, это структура виртуального дома, который нам нужен. Мы добавим два метода, которые были закомментированы первыми, потому чтоsetAttribute
а такжеappendChild
Все методы — это операции с реальным DOM, поэтому я планирую использовать эти два
Реализация функции находится вRENDER_TO_DOM
в функции.
[RENDER_TO_DOM](range) {
range.deleteContents();
let root = document.createElement(this.type);
for (const name in this.props) {
let value = this.props[name];
if (name.match(/^on([\s\S]+)/)) {
root.addEventListener(RegExp.$1.replace(/^[\s\S]/, s => s.toLowerCase()), value)
}
if (name === 'className') {
root.setAttribute('class', value)
} else {
root.setAttribute(name, value)
}
}
for (const child of this.children) {
const childRange = document.createRange();
childRange.setStart(root, root.childNodes.length);
childRange.setEnd(root, root.childNodes.length);
child[RENDER_TO_DOM](childRange);
}
range.insertNode(root);
}
Что на самом деле делает цикл for в первой части?setAttribute
, назначьте атрибут элементу, а цикл for во второй части рекурсивно вставит дочерний элемент.
Итак, как совместить виртуальный DOM с реальным DOM. На самом деле это очень просто: мы строим виртуальное дерево, проходя по виртуальным дочерним элементам. Наконец, это виртуальное дерево превращается в настоящее дерево. Так как же следует изменить соответствующий код?
Мы по-прежнему получаем от КомпонентаRENDER_TO_DOM
Начиная с основной функции, мы внимательно изучили следующий код и обнаружили, что дочерние элементы здесь по-прежнему являются реальными дочерними элементами, а не виртуальными дочерними элементами, поэтому нам нужно определить vchildren в классе Component.
get vchildren() {
return this.children.map(child => child.vdom)
}
Точно так же при обходе мы используем vchildren для обхода, так что наше дерево является виртуальным деревом.
for (const child of this.vchildren) {
const childRange = document.createRange();
childRange.setStart(root, root.childNodes.length);
childRange.setEnd(root, root.childNodes.length);
child[RENDER_TO_DOM](childRange);
}
Код реализованный виртуальным домом можно посмотретьGitHub.com/summer-Энди…. Эта часть действительно сбивает с толку. Я долго думал об этом в то время. Заинтересованные студенты могут проверить это.feature/vdom
Читайте исходный код ветки.
ToyReact интегрирует алгоритм Diff
Мы уже представили алгоритм Diff чуть ранее, мы не будем реализовывать его алгоритм сравнения, как React, потому что это не тема данной статьи. Цель этой статьи — дать вам представление о том, как алгоритм Diff работает в виртуальном DOM. Но мы постараемся максимально подробно рассмотреть случай перерисовки Диффа. Итак, какие ситуации приведут к перерисовке нашего дерева DOM?
- различные типы узлов
- Значения реквизита старого и нового узлов отличаются
- Реквизит нового узла меньше реквизита старого узла
- Содержимое текстового узла отличается
мы определяемisSameNode
способ сделать diff виртуального DOM
let isSameNode = (oldNode, newNode) => {
if(oldNode.type !== newNode.type) {
return false
}
for (const key in newNode.props) {
if (oldNode.props[key] !== newNode.props[key]) {
return false
}
}
if(Object.keys(oldNode.props).length > Object.keys(newNode.props).length)
return false
if(newNode.type === "#text") {
if(newNode.content !== oldNode.content) {
return false;
}
}
return true;
}
Итак, теперь все, что нам нужно сделать, это пройтись по нашему виртуальному дереву DOM.Сначала определить новый узел виртуального DOM и старый узел виртуального DOM.
Это то же самое, если это то же самое, замените его напрямуюrange
и рекурсивно сравнить дочерние узлы. Если узлы не совпадают, то обновите диапазон под текущим узлом, чтобы добиться эффекта частичного обновления.
let update = (oldNode, newNode) => {
if(!isSameNode(oldNode, newNode)) {
newNode[RENDER_TO_DOM](oldNode._range)
return;
}
newNode._range = oldNode._range;
let newChildren = newNode.vchildren;
let oldChildren = oldNode.vchildren;
for (let index = 0; index < newChildren.length; index++) {
const newChild = newChildren[index];
const oldChild = oldChildren[index];
if(index < oldChildren.length) {
update(oldChild, newChild);
} else {
// ...
}
}
}
Остался последний шаг: после каждого обновления виртуальное DOM-дерево нужно обновлять, чтобы подготовиться к следующему Diff. мы определяемvdom
Чтобы получить последнее виртуальное дерево DOM, после выполнения функции обновления виртуального нам нужно заменить старое виртуальное дерево DOM (this._vdom) на обновленное виртуальное дерево DOM.
Дерево виртуального дома. Пока весь процесс обновления завершен, давайте проверим окончательный эффект.
let vdom = this.vdom;
update(this._vdom, vdom);
this._vdom = vdom;
Мы обнаружили, что дерево DOM больше не перерисовывается. Область его обновления была сужена доBoard
Класс соответствует области дерева DOM.
Полный код этой статьи можно посмотретьGitHub.com/summer-Энди…. Но здесь есть небольшая проблема, почему мы не можем обновить сетку одной кнопки, я планирую использовать метод отладки, чтобы проиллюстрировать эту проблему. Первый вisSameNode
Поставьте точку останова на функцию, чтобы облегчить отладку.
let isSameNode = (oldNode, newNode) => {
debugger;
if(oldNode.type !== newNode.type) {
return false
}
for (const key in newNode.props) {
if (oldNode.props[key] !== newNode.props[key]) {
return false
}
}
if(Object.keys(oldNode.props).length > Object.keys(newNode.props).length)
return false
if(newNode.type === "#text") {
if(newNode.content !== oldNode.content) {
return false;
}
}
return true;
}
Щелкните верхнюю левую сетку, чтобы открыть режим отладки браузера, и программа может войти в режим отладчика. Идем прямо туда, где было сделано последнее сравнение сетки.
старый виртуальный DOMcontent
является нулевым значением, согласно нормальной логике, значение содержимого в соответствующем новом виртуальном DOM в это время должно бытьx
, Давайте проверим результат.
Хорошо, тогда нам нужно знать следующее: когда сравнивается где, есть разница, которая приводит к перерисовке. Мы обнаружили, что при сравнении реквизитов старой и новой ноды значения были разными. и тот же ключ, естьonClick
.
Отлаживая здесь, мы должны понять, почему область, которую он перерисовывает, отличается от того, что мы себе представляли. При сравнении событий мы каждый раз создаем новую функцию события. Из-за этого наш ToyReact не мог обрабатывать различия в планировании событий. Затем, если вы хотите добиться React, обновите только определенный узел С таким эффектом мы можем применить самый жестокий подход и просто игнорировать все события.
for (const key in newNode.props) {
if (oldNode.props[key] !== newNode.props[key]) {
if(typeof newNode.props[key] !== 'function') {
return false
}
}
}
Давайте продолжим смотреть, каков конечный эффект?
Это круто!Мы также добились эффекта обновления только определенного узла. Но все довольны, а для обучения технологии все равно придется смотреть на официальный исходный код React, ха-ха.
Эпилог
Если вы внимательно прочитаете эту статью и наберете код построчно, я думаю, вы многое выиграете. Интересно, у вас часто возникает такая мысль в процессе чтения: Эй, а что такое процесс обновления React? Что такое алгоритм сравнения React? и т.п. Я думаю, что это Движимые этим любопытством, мы можем идти дальше и дальше по дороге обучения.Я надеюсь, что после прочтения этой статьи вы сможете пробудить свое любопытство к React и поможет вам идти дальше и дальше по пути чтения исходного кода React.