написать впереди
React разрабатывается уже больше года, недавно я внимательно изучил исходный код React, и резюмирую здесь принципы. Исходный код React более сложен и не подходит для изучения новичками. Так что эта статья облегчает понимание принципа, реализуя упрощенную версию React (эта статья основана на React v15). включать:
- Несколько компонентов React и первая реализация рендеринга
- Реализация механизма обновления реагирования и алгоритма реагирования на работу
Реагнийный код очень сложен, хотя есть упрощенная версия. Но все же нам нужно иметь хорошее объектно-ориентированное мышление. React Core - в основном следующие точки.
- Объект виртуального дома (Virtual DOM)
- Алгоритм различия виртуальных домов (алгоритм сравнения)
- Однонаправленный поток данных
- жизненный цикл компонента
- обработка событий
Репозиторий кода для этой статьи
- Откройте Main.html прямо в браузере, чтобы увидеть эффект.
- Чтобы изменить код, сначала выполните его
npm i
Установите зависимости (используйте код es6) - После изменения кода выполните
npm run dev
перекомпилировать код
Реализуйте привет, React! оказание
См. следующий код:
// js
React.render('hello React!',document.getElementById("root"))
// html
<div id="root"></div>
// 生成代码
<div id="root">
<span data-reactid="0">hello React!</span>
</div>
Для конкретной реализации вышеуказанного кода
/**
* component 类
* 文本类型
* @param {*} text 文本内容
*/
function ReactDOMTextComponent(text) {
// 存下当前的字符串
this._currentElement = "" + text;
// 用来标识当前component
this._rootNodeID = null;
}
/**
* component 类 装载方法,生成 dom 结构
* @param {number} rootID 元素id
* @return {string} 返回dom
*/
ReactDOMTextComponent.prototype.mountComponent = function(rootID) {
this._rootNodeID = rootID;
return (
'<span data-reactid="' + rootID + '">' + this._currentElement + "</span>"
);
};
/**
* 根据元素类型实例化一个具体的component
* @param {*} node ReactElement
* @return {*} 返回一个具体的component实例
*/
function instantiateReactComponent(node) {
//文本节点的情况
if (typeof node === "string" || typeof node === "number") {
return new ReactDOMTextComponent(node);
}
}
const React = {
nextReactRootIndex: 0,
/**
* 接收一个React元素,和一个dom节点
* @param {*} element React元素
* @param {*} container 负责装载的dom
*/
render: function(element, container) {
// 实例化组件
var componentInstance = instantiateReactComponent(element);
// 组件完成dom装载
var markup = componentInstance.mountComponent(React.nextReactRootIndex++);
// 将装载好的 dom 放入 container 中
$(container).html(markup);
$(document).trigger("mountReady");
}
};
Этот код разделен на три части:
- 1 React.render принимает элемент React как запись, а dom в браузере отвечает за вызов рендеринга, а nextReactRootIndex — это уникальный идентификатор каждого компонента
- 2. Познакомьтесь с концепцией класса компонентов. ReactDOMTextComponent — это определение класса компонентов. ReactDOMTextComponent обрабатывает текстовые узлы. А метод mountComponent реализован на прототипе ReactDOMTextComponent, который используется для рендеринга компонента и возврата dom-структуры компонента. Конечно, компонент также имеет операции обновления и удаления, которые будут объяснены здесь позже.
- 3 instanceiateReactComponent используется для возврата экземпляра компонента в соответствии с типом элемента (теперь существует только один тип строки). По сути, это фабрика класса.
Здесь мы разделяем логику на несколько частей, логика рендеринга определяется внутри компонента, React.render отвечает за планирование всего процесса, вызов instanceiateReactComponent для генерации экземпляра объекта, соответствующего типу компонента, затем вызов mountComponent объекта для возврата к dom и, наконец, запись в узел контейнера
виртуальный дом
Виртуальный дом, несомненно, является основной концепцией React.В коде мы будем использовать React.createElement для создания элемента виртуального дома.
Существует два типа виртуального дома: один — это базовый элемент, который поставляется с браузером, например div, а другой — настраиваемый элемент (текстовые узлы не считаются виртуальным домом).
Как использовать виртуальные узлы
// 绑定事件监听方法
function sayHello(){
alert('hello!')
}
var element = React.createElement('div',{id:'jason',onclick:hello},'click me')
React.render(element,document.getElementById("root"))
// 最终生成的html
<div data-reactid="0" id="jason">
<span data-reactid="0.0">click me</span>
</div>
Мы используем React.createElement для создания виртуального элемента dom, ниже приведена простая реализация.
/**
* ReactElement 就是虚拟节点的概念
* @param {*} key 虚拟节点的唯一标识,后期可以进行优化
* @param {*} type 虚拟节点类型,type可能是字符串('div', 'span'),也可能是一个function,function时为一个自定义组件
* @param {*} props 虚拟节点的属性
*/
function ReactElement(type, key, props) {
this.type = type;
this.key = key;
this.props = props;
}
const React = {
nextReactRootIndex: 0,
/**
* @param {*} type 元素的 component 类型
* @param {*} config 元素配置
* @param {*} children 元素的子元素
*/
createElement: function(type, config, children) {
var props = {};
var propName;
config = config || {};
var key = config.key || null;
for (propName in config) {
if (config.hasOwnProperty(propName) && propName !== "key") {
props[propName] = config[propName];
}
}
var childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = Array.isArray(children) ? children : [children];
} else if (childrenLength > 1) {
var childArray = [];
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
return new ReactElement(type, key, props);
},
/**
* 自行添加上文中的render方法
*/
};
Метод createElement выполняет некоторую обработку входящих параметров и, наконец, возвращает экземпляр виртуального элемента ReactElement. Определение ключа может повысить эффективность обновления.
С экземпляром виртуального элемента нам нужно преобразовать метод instanceiateReactComponent
/**
* 根据元素类型实例化一个具体的component
* @param {*} node ReactElement
* @return {*} 返回一个具体的component实例
*/
function instantiateReactComponent(node) {
//文本节点的情况
if (typeof node === "string" || typeof node === "number") {
return new ReactDOMTextComponent(node);
}
//浏览器默认节点的情况
if (typeof node === "object" && typeof node.type === "string") {
//注意这里,使用了一种新的component
return new ReactDOMComponent(node);
}
}
Мы добавили проверку, чтобы вместо текста отображался базовый элемент браузера. Мы используем другой компонент для обработки того, что он должен возвращать при рендеринге. Это отражает преимущества фабричного метода instanceiateReactComponent.Независимо от того, какой тип узла приходит, он может отвечать за создание экземпляра компонента, отвечающего за рендеринг. Таким образом, рендер вообще не нужно модифицировать, а нужно только сделать другой соответствующий тип компонента (здесь ReactDOMComponent).
ReactDOMComponent
конкретная реализация
/**
* component 类
* react 基础标签类型,类似与html中的('div','span' 等)
* @param {*} element 基础元素
*/
function ReactDOMComponent(element) {
// 存下当前的element对象引用
this._currentElement = element;
this._rootNodeID = null;
}
/**
* component 类 装载方法
* @param {*} rootID 元素id
* @param {string} 返回dom
*/
ReactDOMComponent.prototype.mountComponent = function(rootID) {
this._rootNodeID = rootID;
var props = this._currentElement.props;
// 外层标签
var tagOpen = "<" + this._currentElement.type;
var tagClose = "</" + this._currentElement.type + ">";
// 加上reactid标识
tagOpen += " data-reactid=" + this._rootNodeID;
// 拼接标签属性
for (var propKey in props) {
// 属性为绑定事件
if (/^on[A-Za-z]/.test(propKey)) {
var eventType = propKey.replace("on", "");
// 对当前节点添加事件代理
$(document).delegate(
'[data-reactid="' + this._rootNodeID + '"]',
eventType + "." + this._rootNodeID,
props[propKey]
);
}
// 对于props 上的children和事件属性不做处理
if (
props[propKey] &&
propKey != "children" &&
!/^on[A-Za-z]/.test(propKey)
) {
tagOpen += " " + propKey + "=" + props[propKey];
}
}
// 渲染子节点dom
var content = "";
var children = props.children || [];
var childrenInstances = []; // 保存子节点component 实例
var that = this;
children.forEach((child, key) => {
var childComponentInstance = instantiateReactComponent(child);
// 为子节点添加标记
childComponentInstance._mountIndex = key;
childrenInstances.push(childComponentInstance);
var curRootId = that._rootNodeID + "." + key;
// 得到子节点的渲染内容
var childMarkup = childComponentInstance.mountComponent(curRootId);
// 拼接在一起
content += " " + childMarkup;
});
// 保存component 实例
this._renderedChildren = childrenInstances;
// 拼出整个html内容
return tagOpen + ">" + content + tagClose;
};
Для логики рендеринга виртуального дома это, по сути, рекурсивный рендеринг, и reactElement будет рекурсивно рендерить свои собственные дочерние узлы. Можно видеть, что мы скрываем разницу дочерних узлов с помощью instanceiateReactComponent, и нам нужно использовать только разные классы компонентов, которые могут гарантировать, что визуализированный контент может быть окончательно получен с помощью mountComponent.
Кроме того, события здесь также должны быть упомянуты.Вы можете передавать такие параметры, как {onClick:function(){}} при передаче реквизита, чтобы событие было добавлено к текущему элементу и делегировано в документ. Поскольку React сам по себе сводится к написанию js, доставка отслеживаемой функции становится особенно простой.
Здесь много чего не рассмотрено, для простоты мы не будем его здесь раскрывать, кроме того, обработка событий в React на самом деле очень сложная, и реализован стандартный набор w3c-событий. Здесь мы ленивы и используем прокси событий jQuery непосредственно к документу.
自定义元素
реализация
С развитием фронтенд-технологии базовые элементы браузеров больше не могут удовлетворять наши потребности.Если у вас есть определенное понимание веб-компонентов, вы знаете, что люди пытались расширить некоторые свои собственные разметки.
React выполняет аналогичную функцию через виртуальный дом.Помните, что node.type выше — это просто простая строка, что, если это класс? Если у этого класса есть собственное управление жизненным циклом, то расширяемость очень высока.
Использование пользовательских элементов в React
var CompositeComponent = React.createClass({
getInitialState: function() {
return {
count: 0
};
},
componentWillMount: function() {
console.log("声明周期: " + "componentWillMount");
},
componentDidMount: function() {
console.log("声明周期: " + "componentDidMount");
},
onChange: function(e) {
var count = ++this.state.count;
this.setState({
count: count
});
},
render: function() {
const count = this.state.count;
var h3 = React.createElement(
"h3",
{ onclick: this.onChange.bind(this), class: "h3" },
`click me ${count}`
);
var children = [h3];
return React.createElement("div", null, children);
}
});
var CompositeElement = React.createElement(CompositeComponent);
var root = document.getElementById("container");
React.render(CompositeElement, root);
React.createElement
Принимает уже не строку, а класс.
React.createClass генерирует собственный класс маркеров с базовым жизненным циклом:
- GetInitialState получает значение начального свойства.
- componentWillmount вызывается, когда компонент готов к рендерингу.
- componentDidMount вызывается после завершения рендеринга компонента.
Реализация React.createClass
/**
* 所有自定义组件的超类
* @function render所有自定义组件都有该方法
*/
function ReactClass() {}
ReactClass.prototype.render = function() {};
/**
* 更新
* @param {*} newState 新状态
*/
ReactClass.prototype.setState = function(newState) {
// 拿到ReactCompositeComponent的实例
this._reactInternalInstance.receiveComponent(null, newState);
};
const React = {
nextReactRootIndex: 0,
/**
* 创建 ReactClass
* @param {*} spec 传入的对象
*/
createClass: function(spec) {
var Constructor = function(props) {
this.props = props;
this.state = this.getInitialState ? this.getInitialState() : null;
};
Constructor.prototype = new ReactClass();
Constructor.prototype.constructor = Constructor;
Object.assign(Constructor.prototype, spec);
return Constructor;
},
/**
* 自己上文的createElement方法
*/
/**
* 自己上文的render方法
*/
};
Здесь createClass генерирует подкласс, который наследует ReactClass, и вызывает this.getInitialState в конструкторе, чтобы получить начальное состояние.
Для удобства демонстрации, ReactClass на нашей стороне довольно прост, на самом деле исходный код обрабатывает множество вещей, таких как комбинированная поддержка наследования миксина класса, такого как componentDidMount, который может быть определен несколько раз. , и его нужно объединить и вызвать.Исходный код не является основной целью этой статьи, поэтому я не буду подробно его здесь раскрывать.
Глядя на наши два типа выше, пришло время предоставить класс компонента для наших пользовательских элементов, где мы создаем экземпляр ReactClass и управляем жизненным циклом, а также зависимостями компонентов родитель-потомок.
Первое преобразование экземпляраReactComponent
/**
* 根据元素类型实例化一个具体的component
* @param {*} node ReactElement
* @return {*} 返回一个具体的component实例
*/
function instantiateReactComponent(node) {
// 文本节点的情况
if (typeof node === "string" || typeof node === "number") {
return new ReactDOMTextComponent(node);
}
//浏览器默认节点的情况
if (typeof node === "object" && typeof node.type === "string") {
// 注意这里,使用了一种新的component
return new ReactDOMComponent(node);
}
// 自定义的元素节点
if (typeof node === "object" && typeof node.type === "function") {
// 注意这里,使用新的component,专门针对自定义元素
return new ReactCompositeComponent(node);
}
}
Здесь мы добавляем решение для обработки пользовательских типов компонентов.
Конкретная реализация ReactCompositeComponent выглядит следующим образом.
/**
* component 类
* 复合组件类型
* @param {*} element 元素
*/
function ReactCompositeComponent(element) {
// 存放元素element对象
this._currentElement = element;
// 存放唯一标识
this._rootNodeID = null;
// 存放对应的ReactClass的实例
this._instance = null;
}
/**
* component 类 装载方法
* @param {*} rootID 元素id
* @param {string} 返回dom
*/
ReactCompositeComponent.prototype.mountComponent = function(rootID) {
this._rootNodeID = rootID;
// 当前元素属性
var publicProps = this._currentElement.props;
// 对应的ReactClass
var ReactClass = this._currentElement.type;
var inst = new ReactClass(publicProps);
this._instance = inst;
// 保留对当前 component的引用
inst._reactInternalInstance = this;
if (inst.componentWillMount) {
// 生命周期
inst.componentWillMount();
//这里在原始的 reactjs 其实还有一层处理,就是 componentWillMount 调用 setstate,不会触发 rerender 而是自动提前合并,这里为了保持简单,就略去了
}
// 调用 ReactClass 实例的render 方法,返回一个element或者文本节点
var renderedElement = this._instance.render();
var renderedComponentInstance = instantiateReactComponent(renderedElement);
this._renderedComponent = renderedComponentInstance; //存起来留作后用
var renderedMarkup = renderedComponentInstance.mountComponent(
this._rootNodeID
);
// dom 装载到html 后调用生命周期
$(document).on("mountReady", function() {
inst.componentDidMount && inst.componentDidMount();
});
return renderedMarkup;
};
Пользовательский элемент сам по себе не отвечает за конкретный контент, он больше отвечает за жизненный цикл. Конкретный контент визуализируется виртуальным узлом, возвращаемым его методом рендеринга.
По сути, это также процесс рекурсивного рендеринга контента. В то же время, из-за этой рекурсивной функции, componentWillMount родительского компонента должен вызываться перед componentWillMount дочернего компонента, а componentDidMount родительского компонента должен быть после дочернего компонента.Поскольку событие mountReady отслеживается, дочерний компонент должен сначала слушать.
Следует отметить, что пользовательский элемент не будет обрабатывать дочерние узлы, переданные при создании элемента, он будет обрабатывать только узлы, возвращенные его собственным рендерингом, как свои собственные дочерние узлы. Однако при рендеринге мы можем использовать this.props.children для получения входящих дочерних узлов, которые мы можем обработать сами. По сути, это чем-то похоже на роль shadow dom в веб-компонентах.
Общий процесс инициализации рендеринга выглядит следующим образом:
Реализовать простой механизм обновления
Обычно в React мы вызываем метод setState, когда нам нужно обновить. Таким образом, обновление этой статьи основано на реализации setState. См. метод вызова ниже:
/**
* ReactCompositeComponent组件
*/
var CompositeComponent = React.createClass({
getInitialState: function() {
return {
count: 0
};
},
componentWillMount: function() {
console.log("声明周期: " + "componentWillMount");
},
componentDidMount: function() {
console.log("声明周期: " + "componentDidMount");
},
onChange: function(e) {
var count = ++this.state.count;
this.setState({
count: count
});
},
render: function() {
const count = this.state.count;
var h3 = React.createElement(
"h3",
{ onclick: this.onChange.bind(this), class: "h3" },
`click me ${count}`
);
var children = [h3];
return React.createElement("div", null, children);
}
});
var CompositeElement = React.createElement(CompositeComponent);
var root = document.getElementById("root");
React.render(CompositeElement, root);
// 生成html
<div id="root">
<div data-reactid="0">
<h3 data-reactid="0.0" class="h3">
<span data-reactid="0.0.0">click me 0</span>
</h3>
</div>
</div>
// 点击click me 计数会递增
Щелчок по тексту вызовет setState для прохождения процесса обновления.Давайте рассмотрим ReactClass и взглянем на реализацию setState.
/**
* 更新
* @param {*} newState 新状态
*/
ReactClass.prototype.setState = function(newState) {
// 拿到ReactCompositeComponent的实例
// 在装载的时候保存
// 代码:this._reactInternalInstance = this
this._reactInternalInstance.receiveComponent(null, newState);
};
Видно, что setState в основном вызывает ReceiveComponent соответствующего компонента для реализации обновления. Все монтирования и обновления должны быть переданы в управление соответствующему компоненту. Так же, как все компоненты реализуют mountComponent для обработки первого рендеринга, все классы компонентов должны реализовывать receiveComponent для обработки своих собственных обновлений.
ReceiveComponent для текстовых узлов
Обновление текстового узла относительно простое: получите новый текст для сравнения и сразу замените весь узел, если он отличается.
/**
* component 类 更新
* @param {*} newText
*/
ReactDOMTextComponent.prototype.receiveComponent = function(nextText) {
var nextStringText = "" + nextText;
// 跟以前保存的字符串比较
if (nextStringText !== this._currentElement) {
this._currentElement = nextStringText;
// 替换整个节点
$('[data-reactid="' + this._rootNodeID + '"]').html(this._currentElement);
}
};
ReceiveComponent пользовательского элемента
Давайте сначала посмотрим на реализацию ReceiveComponent пользовательских элементов.
/**
* component 类 更新
* @param {*} nextElement
* @param {*} newState
*/
ReactCompositeComponent.prototype.receiveComponent = function(
nextElement,
newState
) {
// 如果接受了新的element,则直接使用最新的element
this._currentElement = nextElement || this._currentElement;
var inst = this._instance;
// 合并state
var nextState = Object.assign(inst.state, newState);
var nextProps = this._currentElement.props;
// 更新state
inst.state = nextState;
// 生命周期方法
if (
inst.shouldComponentUpdate &&
inst.shouldComponentUpdate(nextProps, nextState) === false
) {
// 如果实例的 shouldComponentUpdate 返回 false,则不需要继续往下执行更新
return;
}
// 生命周期方法
if (inst.componentWillUpdate) inst.componentWillUpdate(nextProps, nextState);
// 获取老的element
var prevComponentInstance = this._renderedComponent;
var prevRenderedElement = prevComponentInstance._currentElement;
// 通过重新render 获取新的element
var nextRenderedElement = this._instance.render();
// 比较新旧元素
if (_shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
// 两种元素为相同,需要更新,执行字节点更新
prevComponentInstance.receiveComponent(nextRenderedElement);
// 生命周期方法
inst.componentDidUpdate && inst.componentDidUpdate();
} else {
// 两种元素的类型不同,直接重新装载dom
var thisID = this._rootNodeID;
this._renderedComponent = this._instantiateReactComponent(
nextRenderedElement
);
var nextMarkup = _renderedComponent.mountComponent(thisID);
// 替换整个节点
$('[data-reactid="' + this._rootNodeID + '"]').replaceWith(nextMarkup);
}
};
/**
* 通过比较两个元素,判断是否需要更新
* @param {*} preElement 旧的元素
* @param {*} nextElement 新的元素
* @return {boolean}
*/
function _shouldUpdateReactComponent(prevElement, nextElement) {
if (prevElement != null && nextElement != null) {
var prevType = typeof prevElement;
var nextType = typeof nextElement;
if (prevType === "string" || prevType === "number") {
// 文本节点比较是否为相同类型节点
return nextType === "string" || nextType === "number";
} else {
// 通过type 和 key 判断是否为同类型节点和同一个节点
return (
nextType === "object" &&
prevElement.type === nextElement.type &&
prevElement.key === nextElement.key
);
}
}
return false;
}
Общий поток приведенного выше кода:
- состояние слияния
- состояние обновления
- Затем посмотрите, реализован ли в бизнес-коде метод жизненного цикла shouldComponentUpdate. Если есть, вызовите его. Если возвращаемое значение ложно, остановите выполнение.
- Затем метод componentWillUpdate
- Затем вызовите метод рендеринга, получив экземпляр нового состояния, чтобы получить новый элемент и сравнить старый элемент.
- Если вы хотите обновиться, просто продолжайте вызывать receiveComponent, соответствующий соответствующему классу компонента.На самом деле, это просто быть хозяином руки, а дело непосредственно оставлено подчиненному. Конечно, также существует ситуация, когда два сгенерированных элемента слишком разные, поэтому они не одного типа, поэтому легко напрямую регенерировать новый код и перерендерить его один раз.
_shouldUpdateReactComponent — это глобальный метод, представляющий собой механизм оптимизации React. Используется, чтобы решить, следует ли заменить все это напрямую или использовать очень тонкое изменение. Когда ключи дочерних узлов из двух рендеров отличаются, просто перерисуйте их все и замените. В противном случае мы должны сделать рекурсивное обновление, чтобы обеспечить минимальный механизм обновления, чтобы не было слишком много мерцания.
По сути, это процесс рекурсивного вызова метода receiveComponent.
receiveComponent базового элемента
Обновление базовых элементов включает в себя два аспекта
- Обновления атрибутов, включая обработку специальных атрибутов, таких как события.
- обновление дочернего узла
Обновление дочерних узлов является более сложным и является ключом к повышению эффективности, поэтому необходимо решить следующие проблемы:
- diff — сравнить новое дерево детей со старым деревом детей и найти различия между ними.
- patch - После того, как будут найдены все отличия, сразу все обновить.
Ниже приведена базовая структура обновления базового элемента.
/**
* component 类 更新
* @param {*} nextElement
*/
ReactDOMComponent.prototype.receiveComponent = function(nextElement) {
var lastProps = this._currentElement.props;
var nextProps = nextElement.props;
this._currentElement = nextElement;
// 处理当前节点的属性
this._updateDOMProperties(lastProps, nextProps);
// 处理当前节点的子节点变动
this._updateDOMChildren(nextElement.props.children);
};
先看看,更新属性怎么变更:
/**
* 更新属性
* @param {*} lastProps
* @param {*} nextProps
*/
ReactDOMComponent.prototype._updateDOMProperties = function(
lastProps,
nextProps
) {
// 当老属性不在新属性的集合里时,需要删除属性
var propKey;
for (propKey in lastProps) {
if (
nextProps.hasOwnProperty(propKey) ||
!lastProps.hasOwnProperty(propKey)
) {
// 新属性中有,且不再老属性的原型中
continue;
}
if (/^on[A-Za-z]/.test(propKey)) {
var eventType = propKey.replace("on", "");
// 特殊事件,需要去掉事件监听
$(document).undelegate(
'[data-reactid="' + this._rootNodeID + '"]',
eventType,
lastProps[propKey]
);
continue;
}
// 删除不需要的属性
$('[data-reactid="' + this._rootNodeID + '"]').removeAttr(propKey);
}
// 对于新的事件,需要写到dom上
for (propKey in nextProps) {
if (/^on[A-Za-z]/.test(propKey)) {
var eventType = propKey.replace("on", "");
// 删除老的事件绑定
lastProps[propKey] &&
$(document).undelegate(
'[data-reactid="' + this._rootNodeID + '"]',
eventType,
lastProps[propKey]
);
// 针对当前的节点添加事件代理,以_rootNodeID为命名空间
$(document).delegate(
'[data-reactid="' + this._rootNodeID + '"]',
eventType + "." + this._rootNodeID,
nextProps[propKey]
);
continue;
}
if (propKey == "children") continue;
// 添加新的属性,重写同名属性
$('[data-reactid="' + this._rootNodeID + '"]').prop(
propKey,
nextProps[propKey]
);
}
};
Изменение атрибутов не представляет особой сложности, главное найти старые неиспользуемые атрибуты и удалить их напрямую, присвоить новые атрибуты, а также обратить внимание на атрибуты особых событий и провести специальную обработку.
子节点更新,也是最复杂的部分:
// 全局的更新深度标识
var updateDepth = 0;
// 全局的更新队列,所有的差异都存在这里
var diffQueue = [];
ReactDOMComponent.prototype._updateDOMChildren = function(
nextChildrenElements
) {
updateDepth++;
// _diff用来递归找出差别,组装差异对象,添加到更新队列diffQueue。
this._diff(diffQueue, nextChildrenElements);
updateDepth--;
if (updateDepth == 0) {
// 在需要的时候调用patch,执行具体的dom操作
this._patch(diffQueue);
diffQueue = [];
}
};
Как мы уже говорили, обновление дочерних узлов состоит из двух частей: рекурсивный анализ различий и добавление различий в очередь. Затем вызовите _patch в нужное время, чтобы применить diff к dom. Так что же такое подходящее время и для чего нужен updateDepth? Здесь следует отметить, что _diff также будет рекурсивно вызывать receiveComponent дочернего узла, поэтому, когда дочерний узел также является обычным узлом браузера, он также выполнит шаг _updateDOMChildren. Поэтому здесь используется updateDepth для записи рекурсивного процесса.Только когда updateDepth равен 0, когда рекурсия возвращается, это означает, что вся разница проанализирована, и patch может использоваться для обработки очереди разницы.
реализация различий
// 差异更新的几种类型
var UPDATE_TYPES = {
MOVE_EXISTING: 1,
REMOVE_NODE: 2,
INSERT_MARKUP: 3
};
/**
* 生成子节点 elements 的 component 集合
* @param {object} prevChildren 前一个 component 集合
* @param {Array} nextChildrenElements 新传入的子节点element数组
* @return {object} 返回一个映射
*/
function generateComponentChildren(prevChildren, nextChildrenElements) {
var nextChildren = {};
nextChildrenElements = nextChildrenElements || [];
$.each(nextChildrenElements, function(index, element) {
var name = element.key ? element.key : index;
var prevChild = prevChildren && prevChildren[name];
var prevElement = prevChild && prevChild._currentElement;
var nextElement = element;
// 调用_shouldUpdateReactComponent判断是否是更新
if (_shouldUpdateReactComponent(prevElement, nextElement)) {
// 更新的话直接递归调用子节点的receiveComponent就好了
prevChild.receiveComponent(nextElement);
// 然后继续使用老的component
nextChildren[name] = prevChild;
} else {
// 对于没有老的,那就重新新增一个,重新生成一个component
var nextChildInstance = instantiateReactComponent(nextElement, null);
// 使用新的component
nextChildren[name] = nextChildInstance;
}
});
return nextChildren;
}
/**
* 将数组转换为映射
* @param {Array} componentChildren
* @return {object} 返回一个映射
*/
function flattenChildren(componentChildren) {
var child;
var name;
var childrenMap = {};
for (var i = 0; i < componentChildren.length; i++) {
child = componentChildren[i];
name =
child && child._currentelement && child._currentelement.key
? child._currentelement.key
: i.toString(36);
childrenMap[name] = child;
}
return childrenMap;
}
/**
* _diff用来递归找出差别,组装差异对象,添加到更新队列diffQueue。
* @param {*} diffQueue
* @param {*} nextChildrenElements
*/
ReactDOMComponent.prototype._diff = function(diffQueue, nextChildrenElements) {
var self = this;
// 拿到之前的子节点的 component类型对象的集合,这个是在刚开始渲染时赋值的,记不得的可以翻上面
// _renderedChildren 本来是数组,我们搞成map
var prevChildren = flattenChildren(self._renderedChildren);
// 生成新的子节点的component对象集合,这里注意,会复用老的component对象
var nextChildren = generateComponentChildren(
prevChildren,
nextChildrenElements
);
// 重新赋值_renderedChildren,使用最新的。
self._renderedChildren = [];
$.each(nextChildren, function(key, instance) {
self._renderedChildren.push(instance);
});
/**注意新增代码**/
var lastIndex = 0; // 代表访问的最后一次的老的集合的位置
var nextIndex = 0; // 代表到达的新的节点的index
// 通过对比两个集合的差异,组装差异节点添加到队列中
for (name in nextChildren) {
if (!nextChildren.hasOwnProperty(name)) {
continue;
}
var prevChild = prevChildren && prevChildren[name];
var nextChild = nextChildren[name];
// 相同的话,说明是使用的同一个component,所以我们需要做移动的操作
if (prevChild === nextChild) {
// 添加差异对象,类型:MOVE_EXISTING
/**注意新增代码**/
prevChild._mountIndex < lastIndex &&
diffQueue.push({
parentId: self._rootNodeID,
parentNode: $("[data-reactid=" + self._rootNodeID + "]"),
type: UPDATE_TYPES.MOVE_EXISTING,
fromIndex: prevChild._mountIndex,
toIndex: nextIndex
});
/**注意新增代码**/
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
} else {
// 如果不相同,说明是新增加的节点
// 但是如果老的还存在,就是element不同,但是component一样。我们需要把它对应的老的element删除。
if (prevChild) {
// 添加差异对象,类型:REMOVE_NODE
diffQueue.push({
parentId: self._rootNodeID,
parentNode: $("[data-reactid=" + self._rootNodeID + "]"),
type: UPDATE_TYPES.REMOVE_NODE,
fromIndex: prevChild._mountIndex,
toIndex: null
});
// 如果以前已经渲染过了,记得先去掉以前所有的事件监听,通过命名空间全部清空
if (prevChild._rootNodeID) {
$(document).undelegate("." + prevChild._rootNodeID);
}
/**注意新增代码**/
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
}
// 新增加的节点,也组装差异对象放到队列里
// 添加差异对象,类型:INSERT_MARKUP
diffQueue.push({
parentId: self._rootNodeID,
parentNode: $("[data-reactid=" + self._rootNodeID + "]"),
type: UPDATE_TYPES.INSERT_MARKUP,
fromIndex: null,
toIndex: nextIndex,
markup: nextChild.mountComponent(self._rootNodeID + "." + name) //新增的节点,多一个此属性,表示新节点的dom内容
});
}
// 更新mount的index
nextChild._mountIndex = nextIndex;
nextIndex++;
}
// 对于老的节点里有,新的节点里没有的那些,也全都删除掉
for (name in prevChildren) {
if (
prevChildren.hasOwnProperty(name) &&
!(nextChildren && nextChildren.hasOwnProperty(name))
) {
// 添加差异对象,类型:REMOVE_NODE
diffQueue.push({
parentId: self._rootNodeID,
parentNode: $("[data-reactid=" + self._rootNodeID + "]"),
type: UPDATE_TYPES.REMOVE_NODE,
fromIndex: prevChildren[name]._mountIndex,
toIndex: null
});
// 如果以前已经渲染过了,记得先去掉以前所有的事件监听
if (prevChildren[name]._rootNodeID) {
$(document).undelegate("." + prevChildren[name]._rootNodeID);
}
}
}
};
Обратите внимание, что в flattenChildren мы конвертируем коллекцию массивов в карту объектов, а в качестве идентификатора используем ключ элемента.Конечно, для текстового текста или элемента без входящего ключа в качестве идентификатора используется непосредственно индекс. С помощью этих идентификаторов мы можем судить, являются ли два компонента одинаковыми с точки зрения типа.
generateComponentChildren будет максимально переиспользовать предыдущие компоненты, то есть те ямки.Когда обнаружит, что компоненты могут быть переиспользованы (то есть ключи одинаковые), то все равно используются предыдущие.Просто вызовите его соответствующий метод обновления receiveComponent.Рекурсивно получить объект разницы дочернего узла и поместить его в очередь. Если обнаруживается, что это новый узел, который нельзя использовать повторно, нам нужно создать экземпляр ReactComponent для повторного создания нового компонента.
lastIndex представляет максимальную позицию последнего посещенного старого узла коллекции. И мы добавили суждение, только _mountIndex меньше, чем lastIndex нужно будет присоединиться к очереди разности. С таким суждением приведенный выше пример 2 не нуждается в перемещении. И программа тоже может работать хорошо, на самом деле, в большинстве случаев 2.
Это оптимизация порядка, lastIndex всегда обновляется и представляет самый посещенный в настоящее время самый правый старый элемент коллекции. Мы предполагаем, что последним элементом является A, а lastIndex обновляется после добавления. Если в это время мы подойдем к новому элементу B, он будет больше, чем lastIndex, что указывает на то, что текущий элемент находится позади предыдущего элемента A в старом наборе. Таким образом, даже если этот элемент не будет добавлен в очередь различий, он не повлияет на других людей и не повлияет на последующие узлы вставки пути. Потому что из патча мы знаем, что новый набор вставляет элементы с самого начала по порядку, и его нужно изменить только тогда, когда новый элемент меньше lastIndex. На самом деле, если вы внимательно рассмотрите приведенный выше пример, вы сможете понять этот метод оптимизации. ПроверитьСтратегия сравнения различий
Реализация _patch
/**
*
* @param {*} parentNode
* @param {*} childNode
* @param {*} index
*/ function insertChildAt(parentNode, childNode, index) {
var beforeChild = parentNode.children().get(index);
beforeChild
? childNode.insertBefore(beforeChild)
: childNode.appendTo(parentNode);
}
/**
*
* @param {*} diffQueue
*/
ReactDOMComponent.prototype._patch = function(diffQueue) {
var update;
var initialChildren = {};
var deleteChildren = [];
for (var i = 0; i < updates.length; i++) {
update = updates[i];
if (
update.type === UPDATE_TYPES.MOVE_EXISTING ||
update.type === UPDATE_TYPES.REMOVE_NODE
) {
var updatedIndex = update.fromIndex;
var updatedChild = $(update.parentNode.children().get(updatedIndex));
var parentID = update.parentID;
// 所有需要更新的节点都保存下来,方便后面使用
initialChildren[parentID] = initialChildren[parentID] || [];
// 使用parentID作为简易命名空间
initialChildren[parentID][updatedIndex] = updatedChild;
// 所有需要修改的节点先删除,对于move的,后面再重新插入到正确的位置即可
deleteChildren.push(updatedChild);
}
}
// 删除所有需要先删除的
$.each(deleteChildren, function(index, child) {
$(child).remove();
});
// 再遍历一次,这次处理新增的节点,还有修改的节点这里也要重新插入
for (var k = 0; k < updates.length; k++) {
update = updates[k];
switch (update.type) {
case UPDATE_TYPES.INSERT_MARKUP:
insertChildAt(update.parentNode, $(update.markup), update.toIndex);
break;
case UPDATE_TYPES.MOVE_EXISTING:
insertChildAt(
update.parentNode,
initialChildren[update.parentID][update.fromIndex],
update.toIndex
);
break;
case UPDATE_TYPES.REMOVE_NODE:
// 什么都不需要做,因为上面已经帮忙删除掉了
break;
}
}
};
_patch в основном состоит в том, чтобы пройти очередь различий один за другим, пройти дважды, удалить все узлы, которые необходимо изменить в первый раз, а затем вставить новые узлы и измененные узлы во второй раз. Почему вы можете напрямую вставлять сюда по одному? Причина в том, что когда мы добавляем разностные узлы в разностную очередь на этапе сравнения, это происходит по порядку, то есть порядок новых узлов (включая перемещение и вставку) в очереди соответствует порядку окончательного DOM. , так что мы можем по одному прямо по индексу вставить узел.
这样整个的更新机制就完成了。我们再来简单回顾下 React 的差异算法:
Первый заключается в том, что все компоненты реализуют ReceiveComponent, который отвечает за свои собственные обновления, а обновление элементов браузера по умолчанию является наиболее сложным, что часто называют алгоритмом сравнения.
React имеет глобальный _shouldUpdateReactComponent для определения необходимости обновления или повторного рендеринга на основе ключа элемента, который является первым суждением о различиях. Например, в пользовательских элементах это суждение используется, и благодаря этому идентификационному суждению оно станет особенно эффективным.
Каждый тип элемента обрабатывает собственное обновление:
-
Обновление пользовательского элемента в основном предназначено для обновления узла, сгенерированного рендерингом, и передачи соответствующего компонента узла, сгенерированного рендерингом, для управления обновлением.
-
Обновление текстового узла очень простое, непосредственное обновление текста.
-
Обновление основных элементов браузера разделено на две части:
- Сначала обновите атрибуты, сравните различия между атрибутами до и после и обновите их локально. И обрабатывать специальные свойства, такие как привязка событий.
- А затем обновите дочерний узел, обновление подузла - это выявление основных различий в объекте, объект - поиск различий во времени, будет использовать указанный выше _shouldUpdateReactComponent, чтобы определить, возможно ли непосредственное обновление, будет рекурсивно обновлять вызов дочерних узлов. , он также будет рекурсивно искать объект различия здесь будет делать это с использованием оптимизированного lastIndex, так что некоторые из узлов сохраняют позицию в соответствии с различиями в целевой операции после элемента dom (изменение позиции, удаление,
end
Это всего лишь игрушка, но основные функции React реализованы, виртуальные узлы, разностные алгоритмы и одностороннее обновление данных — все здесь. В React все еще есть много замечательных вещей, которые не были реализованы, например, управление пулом потоков памяти во время генерации объектов, механизм пакетного обновления, оптимизация событий, рендеринг на стороне сервера, неизменяемые данные и т. д. В связи с ограниченным пространством эти вещи не будут подробно раскрываться.
React как решение, идея виртуальных узлов относительно новая, но я до сих пор не могу принять этот неуклюжий способ написания. Чтобы использовать React, необходимо использовать весь его набор методов разработки, а его основная функция — это фактически просто алгоритм отличия, который уже реализован родственными библиотеками.
Релевантная информация: