Кодовая база React сейчас относительно велика, а с рефакторингом Fiber v16 новичкам легко упасть в море деталей. Это легко заставит людей потерять доверие Сомневаюсь, стоит ли мне продолжать работать над интерфейсом. Так что постарайтесь найти немного уверенности в этой статье (освойте обходные пути).
Preact - это сокращенная версия React. Он очень маленький по размеру, но имеет все внутренние органы. Если вы хотите понять основные принципы React, вы можете изучить исходный код Preact, что и является целью этой статьи.
Уже много отличных статей о принципах реагирования. Эта статья о переосмыслении старого вина в новые бутылки. Это мое собственное резюме, и он также проложит путь для последующих статей.
Длительная статья, время чтения около 20 минут, в основном занято кодом, в то время как также окрашены с помощью блок-схемы, чтобы понять код.
Примечание. Код упрощен, игнорируя svg, replaceNode, контекст и другие функции. Код в этой статье основан на версии Preact v10.
- Virtual-DOM
- Начните с создания элемента
- Реализация компонента
- алгоритм сравнения
- Реализация хуков
- Техническая карта
- расширять
Virtual-DOM
Virtual-DOM на самом деле является деревом объектов, ничего особенного, это дерево объектов в конечном итоге будет сопоставлено с графическими объектами.diff算法
.
Как вы понимаете, естьDOM映射器
, по имени,Задача этого «сопоставителя DOM» состоит в том, чтобы сопоставить дерево объектов Virtual-DOM с DOM страницы браузера, просто чтобы улучшить «производительность операций» DOM. Он не отображает все дерево Virtual-DOM каждый раз, но поддерживает получение двух деревьев объектов Virtual-DOM (одно до обновления, одно после обновления), вычисляет разницу между двумя деревьями Virtual-DOM с помощью алгоритма сравнения, а затем применяет эти различия только к фактическому дереву DOM, тем самым уменьшая Изменение DOM, стоимость.
Virtual-DOM более противоречив, рекомендуется к прочтению«В Интернете говорят, что работа с настоящим DOM медленная, но результаты тестов быстрее, чем у React, почему? 》. Я не хочу покидать сцену, чтобы судить хорошо это или плохо. Когда я узнаю, сколько я сделал React, какой-нибудь беленький почувствует, что Virtual-Dom зависает, jQuery слаб.
Я не думаю, что они сопоставимы с точки зрения производительности,Независимо от того, насколько великолепен фреймворк, ему все равно нужно манипулировать нативным DOM, и он может быть не таким «хорошим», как вы используете JQuery для ручного манипулирования DOM.Необоснованное использование фреймворка также может изменить маленькое состояние, что приведет к лавинообразному рендерингу (крупномасштабному повторному рендерингу). Аналогичным образом, хотя JQuery может улучшить операцию DOM, необоснованная стратегия обновления DOM также может стать узким местом производительности приложение.. Так что ключ зависит от того, как вы его используете.
Итак, зачем вам Virtual-DOM?
Мое личное понимание заключается в освобождении производительности. В настоящее время производительность оборудования становится все лучше и лучше, веб-приложения становятся все более и более сложными, а производительность должна идти в ногу со временем., Хотя ручное манипулирование DOM может обеспечить более высокую производительность и гибкость, это слишком неэффективно для большинства разработчиков, и мы можем пойти на небольшую жертву производительности ради более высокой эффективности разработки.
Следовательно, большее значение Virtual-DOM заключается в изменении метода разработки: декларативный, управляемый данными, чтобы разработчикам не нужно было заботиться о деталях работы с DOM (операция с атрибутами, привязка событий, изменение узла DOM), т.е. то есть способ разработки приложений становитсяview=f(state)
, что значительно способствует высвобождению производительных сил.
Конечно, Virtual-DOM — не единственное решение и не первое такое решение. Можно также сказать, что реализации на основе шаблонов, такие как AngularJS и Vue1.x, добились такого изменения в методах разработки. -Точка покупки DOM Это может быть более высокая производительность.Кроме того, абстракция Virtual-DOM на уровне рендеринга является более тщательной, и она больше не связана с самой DOM.Например, она может отображаться как ReactNative, PDF, пользовательский интерфейс терминала и т. д.
Начните с создания элемента
много новичковJSX
Эквивалент Virtual-DOM, на самом деле между ними нет прямой связи, как мы знаем.JSX — не что иное, как синтаксический сахар.
Например<a href="/"><span>Home</span></a>
в конечном итоге превратится вh('a', { href:'/' }, h('span', null, 'Home'))
эта форма,h
является фабричным методом элемента JSX.
h
Соглашение в React:React.createElement
, в то время как большинство платформ Virtual-DOM используютh
. h
даcreateElement
Псевдоним экосистемы Vue также использует это соглашение, почему оно не разработано (короче?).
можно использовать@jsx
Аннотации или элементы конфигурации babel для настройки фабрики JSX:
/**
* @jsx h
*/
render(<div>hello jsx</div>, el);
Эта статья не является вводной для React или Preact, поэтому щелкните здесь, чтобы получить дополнительные сведения.Официальный учебник.
Проверьте это сейчасcreateElement
, createElement — это не что иное, как создание объекта (VNode):
// ⚛️type 节点的类型,有DOM元素(string)和自定义组件,以及Fragment, 为null时表示文本节点
export function createElement(type, props, children) {
props.children = children;
// ⚛️应用defaultProps
if (type != null && type.defaultProps != null)
for (let i in type.defaultProps)
if (props[i] === undefined) props[i] = type.defaultProps[i];
let ref = props.ref;
let key = props.key;
// ...
// ⚛️构建VNode对象
return createVNode(type, props, key, ref);
}
export function createVNode(type, props, key, ref) {
return { type, props, key, ref, /* ... 忽略部分内置字段 */ constructor: undefined };
}
С помощью JSX и компонентов можно построить сложные деревья объектов:
render(
<div className="container">
<SideBar />
<Body />
</div>,
root,
);
Реализация компонента
Для фреймворка представления компоненты являются его душой, точно так же, как функции для функциональных языков, подобных объектно-ориентированным языкам, и сложные приложения не могут быть составлены без компонентов.
Компонентное мышление рекомендует разделять и захватывать приложение, разделять и комбинировать компоненты на разных уровнях, что может упростить разработку и сопровождение приложения и сделать программу более понятной.С технической точки зренияКомпонент — это настраиваемый тип элемента, который может объявлять ввод (реквизиты) компонента, иметь собственный жизненный цикл, состояние и методы и, наконец, выводить дерево объектов Virtual-DOM, которое существует как ветвь приложения Virtual-DOM. дерево.
Пользовательские компоненты Preact реализованы на основе класса Component.Самая основная вещь для компонентов — это поддержание состояния, которое реализуется через setState:
function Component(props, context) {}
// ⚛️setState实现
Component.prototype.setState = function(update, callback) {
// 克隆下一次渲染的State, _nextState会在一些生命周期方式中用到(例如shouldComponentUpdate)
let s = (this._nextState !== this.state && this._nextState) ||
(this._nextState = assign({}, this.state));
// state更新
if (typeof update !== 'function' || (update = update(s, this.props)))
assign(s, update);
if (this._vnode) { // 已挂载
// 推入渲染回调队列, 在渲染完成后批量调用
if (callback) this._renderCallbacks.push(callback);
// 放入异步调度队列
enqueueRender(this);
}
};
enqueueRender
Помещение компонента в очередь асинхронного пакетного выполнения может объединить частые вызовы setState, и реализация очень проста:
let q = [];
// 异步调度器,用于异步执行一个回调
const defer = typeof Promise == 'function'
? Promise.prototype.then.bind(Promise.resolve()) // micro task
: setTimeout; // 回调到setTimeout
function enqueueRender(c) {
// 不需要重复推入已经在队列的Component
if (!c._dirty && (c._dirty = true) && q.push(c) === 1)
defer(process); // 当队列从空变为非空时,开始调度
}
// 批量清空队列, 调用Component的forceUpdate
function process() {
let p;
// 排序队列,从低层的组件优先更新?
q.sort((a, b) => b._depth - a._depth);
while ((p = q.pop()))
if (p._dirty) p.forceUpdate(false); // false表示不要强制更新,即不要忽略shouldComponentUpdate
}
Хорошо, приведенный выше код можно увидетьsetState
По сути, вызовforceUpdate
Для повторного рендеринга компонентов покопайтесь в реализации forceUpdate.
Игнорировать diff пока,Думайте о diff как о черном ящике, это сопоставитель DOM, например, приведенный выше diff получает два дерева VNode и точку монтирования DOM, в процессе сравнения он может создавать, удалять или обновлять компоненты и элемент DOM, который запускает соответствующий жизненный цикл. метод.
Component.prototype.forceUpdate = function(callback) { // callback放置渲染完成后的回调
let vnode = this._vnode, dom = this._vnode._dom, parentDom = this._parentDom;
if (parentDom) { // 已挂载过
const force = callback !== false;
let mounts = [];
// 调用diff对当前组件进行重新渲染和Virtual-DOM比对
// ⚛️暂且忽略这些参数, 将diff视作一个黑盒,他就是一个DOM映射器,
dom = diff(parentDom, vnode, vnode, mounts, this._ancestorComponent, force, dom);
if (dom != null && dom.parentNode !== parentDom)
parentDom.appendChild(dom);
commitRoot(mounts, vnode);
}
if (callback) callback();
};
посмотри наrender
Метод, реализация которого аналогична forceUpdate, заключается в вызове алгоритма diff для выполнения обновления DOM, но DOM-контейнер указывается извне:
// 简化版
export function render(vnode, parentDom) {
vnode = createElement(Fragment, null, [vnode]);
parentDom.childNodes.forEach(i => i.remove())
let mounts = [];
diffChildren(parentDom, null oldVNode, mounts, vnode, EMPTY_OBJ);
commitRoot(mounts, vnode);
}
Подытожим описанный выше процесс:
До сих пор я не видел других функций компонентов, таких как инициализация, функции жизненного цикла. Эти свойства определяются в функции diff, которая вызывается во время монтирования или обновления компонентов. Мы рассмотрим diff в следующем разделе.
алгоритм сравнения
По прошествии длительного времени из вышеизложенного видно, чтоcreateElement
а такжеComponent
Логика очень тонкая, а основная логика по-прежнему сосредоточена в функции diff.React называет этот процессReconciliation
, вызываемый в PreactDifferantiate
.
Чтобы упростить реализацию Preact, diff и DOM смешаны вместе, но логика все равно очень понятна, просто взгляните на структуру каталогов:
src/diff
├── children.js # 比对children数组
├── index.js # 比对两个节点
└── props.js # 比对两个DOM节点的props
Прежде чем погрузиться в программу сравнения, давайте взглянем на базовую структуру объекта, чтобы облегчить понимание потока программы позже.Давайте взглянем на внешний вид VNode:
type ComponentFactory<P> = preact.ComponentClass<P> | FunctionalComponent<P>;
interface VNode<P = {}> {
// 节点类型, 内置DOM元素为string类型,而自定义组件则是Component类型,Preact中函数组件只是特殊的Component类型
type: string | ComponentFactory<P> | null;
props: P & { children: ComponentChildren } | string | number | null;
key: Key
ref: Ref<any> | null;
/**
* 内部缓存信息
*/
// VNode子节点
_children: Array<VNode> | null;
// 关联的DOM节点, 对于Fragment来说第一个子节点
_dom: PreactElement | Text | null;
// Fragment, 或者组件返回Fragment的最后一个DOM子节点,
_lastDomChild: PreactElement | Text | null;
// Component实例
_component: Component | null;
}
diffChildren
Начнем с самого простого, как уже догадались выше diffChildren используется для сравнения двух списков VNode.
Как показано на рисунке выше, прежде всего нам нужно поддерживать переменную oldDOM, которая представляет текущую позицию вставки.Первоначально она указывает на первый элемент DOM ChildrenNode, и каждый раз, когда вставляется обновление или вставляется newDOM, она будет указывает на следующий родственный элемент newDOM.
В процессе обхода списка newChildren он попытается найти старый VNode с тем же ключом, и отличить его с ним.Если новый VNode и старый VNode находятся в разных позициях, необходимо их переместить; для вновь добавленный DOM, если позиция вставки (oldDOM) Если он достиг конца, он добавляется непосредственно к родительскому узлу, в противном случае он вставляется перед oldDOM.
Наконец, выгрузите неиспользуемые VNode из старого списка VNode.
Рассмотрим подробно исходный код:
export function diffChildren(
parentDom, // children的父DOM元素
newParentVNode, // children的新父VNode
oldParentVNode, // children的旧父VNode,diffChildren主要比对这两个Vnode的children
mounts, // 保存在这次比对过程中被挂载的组件实例,在比对后,会触发这些组件的componentDidMount生命周期函数
ancestorComponent, // children的直接父'组件', 即渲染(render)VNode的组件实例
oldDom, // 当前挂载的DOM,对于diffChildren来说,oldDom一开始指向第一个子节点
) {
let newChildren = newParentVNode._children || toChildArray(newParentVNode.props.children, (newParentVNode._children = []), coerceToVNode, true,);
let oldChildren = (oldParentVNode && oldParentVNode._children) || EMPTY_ARR;
// ...
// ⚛️遍历新children
for (i = 0; i < newChildren.length; i++) {
childVNode = newChildren[i] = coerceToVNode(newChildren[i]); // 规范化VNode
if (childVNode == null) continue
// ⚛️查找oldChildren中是否有对应的元素,如果找到则通过设置为undefined,从oldChildren中移除
// 如果没有找到则保持为null
oldVNode = oldChildren[i];
for (j = 0; j < oldChildrenLength; j++) {
oldVNode = oldChildren[j];
if (oldVNode && childVNode.key == oldVNode.key && childVNode.type === oldVNode.type) {
oldChildren[j] = undefined;
break;
}
oldVNode = null; // 没有找到任何旧node,表示是一个新的
}
// ⚛️ 递归比对VNode
newDom = diff(parentDom, childVNode, oldVNode, mounts, ancestorComponent, null, oldDom);
// vnode没有被diff卸载掉
if (newDom != null) {
if (childVNode._lastDomChild != null) {
// ⚛️当前VNode是Fragment类型
// 只有Fragment或组件返回Fragment的Vnode会有非null的_lastDomChild, 从Fragment的结尾的DOM树开始比对:
// <A> <A>
// <> <> 👈 Fragment类型,diff会递归比对它的children,所以最后我们只需要将newDom指向比对后的最后一个子节点即可
// <a>a</a> <- diff -> <b>b</b>
// <b>b</b> <a>a</a> ----+
// </> </> \
// <div>x</div> 👈oldDom会指向这里
// </A> </A>
newDom = childVNode._lastDomChild;
} else if (oldVNode == null || newDom != oldDom || newDom.parentNode == null) {
// ⚛️ newDom和当前oldDom不匹配,尝试新增或修改位置
outer: if (oldDom == null || oldDom.parentNode !== parentDom) {
// ⚛️oldDom指向了结尾, 即后面没有更多元素了,直接插入即可; 首次渲染一般会调用到这里
parentDom.appendChild(newDom);
} else {
// 这里是一个优化措施,去掉也不会影响正常程序. 为了便于理解可以忽略这段代码
// 尝试向后查找oldChildLength/2个元素,如果找到则不需要调用insertBefore. 这段代码可以减少insertBefore的调用频率
for (sibDom = oldDom, j = 0; (sibDom = sibDom.nextSibling) && j < oldChildrenLength; j += 2) {
if (sibDom == newDom)
break outer;
}
// ⚛️insertBefore() 将newDom移动到oldDom之前
parentDom.insertBefore(newDom, oldDom);
}
}
// ⚛️其他情况,newDom === oldDOM不需要处理
// ⚛️ oldDom指向下一个DOM节点
oldDom = newDom.nextSibling;
}
}
// ⚛️ 卸载掉没有被置为undefined的元素
for (i = oldChildrenLength; i--; )
if (oldChildren[i] != null) unmount(oldChildren[i], ancestorComponent);
}
С картинкой, чтобы понять процесс вызова diffChirlend:
Обобщите блок-схему
diff
diff используется для сравнения двух узлов VNode.Функция diff более многословна, но в ней нет особо сложной логики, в основном обработка жизненного цикла некоторых пользовательских компонентов. Итак, сначала перейдите к блок-схеме, вы можете пропустить код, если вам это не интересно.
Анализ исходного кода:
export function diff(
parentDom, // 父DOM节点
newVNode, // 新VNode
oldVNode, // 旧VNode
mounts, // 存放已挂载的组件, 将在diff结束后批量处理
ancestorComponent, // 直接父组件
force, // 是否强制更新, 为true将忽略掉shouldComponentUpdate
oldDom, // 当前挂载的DOM节点
) {
//...
try {
outer: if (oldVNode.type === Fragment || newType === Fragment) {
// ⚛️ Fragment类型,使用diffChildren进行比对
diffChildren(parentDom, newVNode, oldVNode, mounts, ancestorComponent, oldDom);
// ⚛️记录Fragment的起始DOM和结束DOM
let i = newVNode._children.length;
if (i && (tmp = newVNode._children[0]) != null) {
newVNode._dom = tmp._dom;
while (i--) {
tmp = newVNode._children[i];
if (newVNode._lastDomChild = tmp && (tmp._lastDomChild || tmp._dom))
break;
}
}
} else if (typeof newType === 'function') {
// ⚛️自定义组件类型
if (oldVNode._component) {
// ⚛️ ️已经存在组件实例
c = newVNode._component = oldVNode._component;
newVNode._dom = oldVNode._dom;
} else {
// ⚛️初始化组件实例
if (newType.prototype && newType.prototype.render) {
// ⚛️类组件
newVNode._component = c = new newType(newVNode.props, cctx); // eslint-disable-line new-cap
} else {
// ⚛️函数组件
newVNode._component = c = new Component(newVNode.props, cctx);
c.constructor = newType;
c.render = doRender;
}
c._ancestorComponent = ancestorComponent;
c.props = newVNode.props;
if (!c.state) c.state = {};
isNew = c._dirty = true;
c._renderCallbacks = [];
}
c._vnode = newVNode;
if (c._nextState == null) c._nextState = c.state;
// ⚛️getDerivedStateFromProps 生命周期方法
if (newType.getDerivedStateFromProps != null)
assign(c._nextState == c.state
? (c._nextState = assign({}, c._nextState)) // 惰性拷贝
: c._nextState,
newType.getDerivedStateFromProps(newVNode.props, c._nextState),
);
if (isNew) {
// ⚛️ 调用挂载前的一些生命周期方法
// ⚛️ componentWillMount
if (newType.getDerivedStateFromProps == null && c.componentWillMount != null) c.componentWillMount();
// ⚛️ componentDidMount
// 将组件推入mounts数组,在整个组件树diff完成后批量调用, 他们在commitRoot方法中被调用
// 按照先进后出(栈)的顺序调用, 即子组件的componentDidMount会先调用
if (c.componentDidMount != null) mounts.push(c);
} else {
// ⚛️ 调用重新渲染相关的一些生命周期方法
// ⚛️ componentWillReceiveProps
if (newType.getDerivedStateFromProps == null && force == null && c.componentWillReceiveProps != null)
c.componentWillReceiveProps(newVNode.props, cctx);
// ⚛️ shouldComponentUpdate
if (!force && c.shouldComponentUpdate != null && c.shouldComponentUpdate(newVNode.props, c._nextState, cctx) === false) {
// shouldComponentUpdate返回false,取消渲染更新
c.props = newVNode.props;
c.state = c._nextState;
c._dirty = false;
newVNode._lastDomChild = oldVNode._lastDomChild;
break outer;
}
// ⚛️ componentWillUpdate
if (c.componentWillUpdate != null) c.componentWillUpdate(newVNode.props, c._nextState, cctx);
}
// ⚛️至此props和state已经确定下来,缓存和更新props和state准备渲染
oldProps = c.props;
oldState = c.state;
c.props = newVNode.props;
c.state = c._nextState;
let prev = c._prevVNode || null;
c._dirty = false;
// ⚛️渲染
let vnode = (c._prevVNode = coerceToVNode(c.render(c.props, c.state)));
// ⚛️getSnapshotBeforeUpdate
if (!isNew && c.getSnapshotBeforeUpdate != null) snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
// ⚛️组件层级,会影响更新的优先级
c._depth = ancestorComponent ? (ancestorComponent._depth || 0) + 1 : 0;
// ⚛️递归diff渲染结果
c.base = newVNode._dom = diff(parentDom, vnode, prev, mounts, c, null, oldDom);
if (vnode != null) {
newVNode._lastDomChild = vnode._lastDomChild;
}
c._parentDom = parentDom;
// ⚛️应用ref
if ((tmp = newVNode.ref)) applyRef(tmp, c, ancestorComponent);
// ⚛️调用renderCallbacks,即setState的回调
while ((tmp = c._renderCallbacks.pop())) tmp.call(c);
// ⚛️componentDidUpdate
if (!isNew && oldProps != null && c.componentDidUpdate != null) c.componentDidUpdate(oldProps, oldState, snapshot);
} else {
// ⚛️比对两个DOM元素
newVNode._dom = diffElementNodes(oldVNode._dom, newVNode, oldVNode, mounts, ancestorComponent);
if ((tmp = newVNode.ref) && oldVNode.ref !== tmp) applyRef(tmp, newVNode._dom, ancestorComponent);
}
} catch (e) {
// ⚛️捕获渲染错误,传递给上级组件的didCatch生命周期方法
catchErrorInComponent(e, ancestorComponent);
}
return newVNode._dom;
}
diffElementNodes
Сравнивая два элемента DOM, процесс очень прост:
function diffElementNodes(dom, newVNode, oldVNode, mounts, ancestorComponent) {
// ...
// ⚛️创建DOM节点
if (dom == null) {
if (newVNode.type === null) {
// ⚛️文本节点, 没有属性和子级,直接返回
return document.createTextNode(newProps);
}
dom = document.createElement(newVNode.type);
}
if (newVNode.type === null) {
// ⚛️文本节点更新
if (oldProps !== newProps) dom.data = newProps;
} else {
if (newVNode !== oldVNode) {
// newVNode !== oldVNode 这说明是一个静态节点
let oldProps = oldVNode.props || EMPTY_OBJ;
let newProps = newVNode.props;
// ⚛️ dangerouslySetInnerHTML处理
let oldHtml = oldProps.dangerouslySetInnerHTML;
let newHtml = newProps.dangerouslySetInnerHTML;
if (newHtml || oldHtml)
if (!newHtml || !oldHtml || newHtml.__html != oldHtml.__html)
dom.innerHTML = (newHtml && newHtml.__html) || '';
// ⚛️递归比对子元素
diffChildren(dom, newVNode, oldVNode, context, mounts, ancestorComponent, EMPTY_OBJ);
// ⚛️递归比对DOM属性
diffProps(dom, newProps, oldProps, isSvg);
}
}
return dom;
}
diffProps
diffProps используется для обновления свойств элементов DOM.
export function diffProps(dom, newProps, oldProps, isSvg) {
let i;
const keys = Object.keys(newProps).sort();
// ⚛️比较并设置属性
for (i = 0; i < keys.length; i++) {
const k = keys[i];
if (k !== 'children' && k !== 'key' &&
(!oldProps || (k === 'value' || k === 'checked' ? dom : oldProps)[k] !== newProps[k]))
setProperty(dom, k, newProps[k], oldProps[k], isSvg);
}
// ⚛️清空属性
for (i in oldProps)
if (i !== 'children' && i !== 'key' && !(i in newProps))
setProperty(dom, i, null, oldProps[i], isSvg);
}
Реализация diffProps относительно проста, то есть нужно пройти, изменилось ли свойство, и если есть изменение, установить свойство через setProperty. Для недопустимых реквизитов он также будет пустым через setProperty. Немного сложнее здесь setProperty, который включает в себя обработку событий, преобразование имен и т. д.:
function setProperty(dom, name, value, oldValue, isSvg) {
if (name === 'style') {
// ⚛️样式设置
const set = assign(assign({}, oldValue), value);
for (let i in set) {
// 样式属性没有变动
if ((value || EMPTY_OBJ)[i] === (oldValue || EMPTY_OBJ)[i]) continue;
dom.style.setProperty(
i[0] === '-' && i[1] === '-' ? i : i.replace(CAMEL_REG, '-$&'),
value && i in value
? typeof set[i] === 'number' && IS_NON_DIMENSIONAL.test(i) === false
? set[i] + 'px'
: set[i]
: '', // 清空
);
}
} else if (name[0] === 'o' && name[1] === 'n') {
// ⚛️事件绑定
let useCapture = name !== (name = name.replace(/Capture$/, ''));
let nameLower = name.toLowerCase();
name = (nameLower in dom ? nameLower : name).slice(2);
if (value) {
// ⚛️首次添加事件, 注意这里是eventProxy为事件处理器
// preact统一将所有事件处理器收集在dom._listeners对象中,统一进行分发
// function eventProxy(e) {
// return this._listeners[e.type](options.event ? options.event(e) : e);
// }
if (!oldValue) dom.addEventListener(name, eventProxy, useCapture);
} else {
// 移除事件
dom.removeEventListener(name, eventProxy, useCapture);
}
// 保存事件队列
(dom._listeners || (dom._listeners = {}))[name] = value;
} else if (name !== 'list' && name !== 'tagName' && name in dom) {
// ⚛️DOM对象属性
dom[name] = value == null ? '' : value;
} else if (
typeof value !== 'function' &&
name !== 'dangerouslySetInnerHTML'
) {
// ⚛️DOM元素属性
if (value == null || value === false) {
dom.removeAttribute(name);
} else {
dom.setAttribute(name, value);
}
}
}
ОК Пока алгоритм Diff введен, на самом деле логика тут не особо сложная, конечно Preact это просто предельно упрощенный фреймворк, а сложность React намного выше, особенно после рефакторинга React Fiber. Вы также можете использовать Preact как исторический обзор React, и вам интересно узнать больше о последней архитектуре React.
Реализация хуков
Хуки, официально представленные в React 16.8, предлагают новый способ разработки компонентов React и делают код более лаконичным.React hooks: not magic, just arraysВ этой статье уже был раскрыт базовый принцип реализации хуков, который как раз реализуется на основе массивов. Preact также реализует механизм хуков, а код реализации составляет всего сто строк, давайте попробуем.
Сама функция хуков не интегрирована в кодовую базу Preact, а черезpreact/hooks
импорт
import { h } from 'preact';
import { useEffect } from 'preact/hooks';
function Foo() {
useEffect(() => {
console.log('mounted');
}, []);
return <div>hello hooks</div>;
}
Так как же Preact расширяет алгоритм сравнения для реализации перехватчиков? На самом деле Preact предоставляетoptions
Object для расширения Preact diff, параметры аналогичны хукам жизненного цикла Preact, которые вызываются во время процесса сравнения (для краткости я опустил приведенный выше код). Например:
export function diff(/*...*/) {
// ...
// ⚛️开始diff
if ((tmp = options.diff)) tmp(newVNode);
try {
outer: if (oldVNode.type === Fragment || newType === Fragment) {
// Fragment diff
} else if (typeof newType === 'function') {
// 自定义组件diff
// ⚛️开始渲染
if ((tmp = options.render)) tmp(newVNode);
try {
// ..
c.render(c.props, c.state, c.context),
} catch (e) {
// ⚛️捕获异常
if ((tmp = options.catchRender) && tmp(e, c)) return;
throw e;
}
} else {
// DOM element diff
}
// ⚛️diff结束
if ((tmp = options.diffed)) tmp(newVNode);
} catch (e) {
catchErrorInComponent(e, ancestorComponent);
}
return newVNode._dom;
}
// ...
useState
Начнем с наиболее часто используемого useState:
export function useState(initialState) {
// ⚛️OK只是数组,没有Magic,每个hooks调用都会递增currenIndex, 从当前组件中取出状态
const hookState = getHookState(currentIndex++);
// ⚛️ 初始化
if (!hookState._component) {
hookState._component = currentComponent; // 当前组件实例
hookState._value = [
// ⚛️state, 初始化state
typeof initialState === 'function' ? initialState() : initialState,
// ⚛️dispatch
value => {
const nextValue = typeof value === 'function' ? value(hookState._value[0]) : value;
if (hookState._value[0] !== nextValue) {
// ⚛️ 保存状态并调用setState强制更新
hookState._value[0] = nextValue;
hookState._component.setState({});
}
},
];
}
return hookState._value; // [state, dispatch]
}
Как видно из кода, ключgetHookState
реализация
import { options } from 'preact';
let currentIndex; // 保存当前hook的索引
let currentComponent;
// ⚛️render 钩子, 在组件开始渲染之前调用
// 因为Preact是同步递归向下渲染的,而且Javascript是单线程的,所以可以安全地引用当前正在渲染的组件实例
options.render = vnode => {
currentComponent = vnode._component; // 保存当前正在渲染的组件
currentIndex = 0; // 开始渲染时index重置为0
// 暂时忽略,下面讲到useEffect就能理解
// 清空上次渲染未处理的Effect(useEffect),只有在快速重新渲染时才会出现这种情况,一般在异步队列中被处理
if (currentComponent.__hooks) {
currentComponent.__hooks._pendingEffects = handleEffects(
currentComponent.__hooks._pendingEffects,
);
}
};
// ⚛️no magic!, 只是一个数组, 状态保存在组件实例的_list数组中
function getHookState(index) {
// 获取或初始化列表
const hooks = currentComponent.__hooks ||
(currentComponent.__hooks = {
_list: [], // 放置状态
_pendingEffects: [], // 放置待处理的effect,由useEffect保存
_pendingLayoutEffects: [], // 放置待处理的layoutEffect,有useLayoutEffect保存
});
// 新建状态
if (index >= hooks._list.length) {
hooks._list.push({});
}
return hooks._list[index];
}
Примерный процесс выглядит следующим образом:
useEffect
Посмотрите еще раз на useEffect и useLayoutEffect. useEffect похож на useLayouteEffect, но время срабатывания эффекта другое.
export function useEffect(callback, args) {
const state = getHookState(currentIndex++);
if (argsChanged(state._args, args)) {
// ⚛️状态变化
state._value = callback;
state._args = args;
currentComponent.__hooks._pendingEffects.push(state); // ⚛️推进_pendingEffects队列
afterPaint(currentComponent);
}
}
export function useLayoutEffect(callback, args) {
const state = getHookState(currentIndex++);
if (argsChanged(state._args, args)) {
// ⚛️状态变化
state._value = callback;
state._args = args;
currentComponent.__hooks._pendingLayoutEffects.push(state); // ⚛️推进_pendingLayoutEffects队列
}
}
Посмотрите, как вызвать effect.useEffect и см. выше.enqueueRender
Почти, поставленный в асинхронную очередь,requestAnimationFrame
Чтобы запланировать пакетную обработку:
// 这是一个类似于上面提到的异步队列
afterPaint = component => {
if (!component._afterPaintQueued && // 避免组件重复推入
(component._afterPaintQueued = true) &&
afterPaintEffects.push(component) === 1 // 开始调度
)
requestAnimationFrame(scheduleFlushAfterPaint); // 由requestAnimationFrame调度
};
function scheduleFlushAfterPaint() {
setTimeout(flushAfterPaintEffects);
}
function flushAfterPaintEffects() {
afterPaintEffects.some(component => {
component._afterPaintQueued = false;
if (component._parentDom)
// 清空_pendingEffects队列
component.__hooks._pendingEffects = handleEffects(component.__hooks._pendingEffects);
});
afterPaintEffects = [];
}
function handleEffects(effects) {
// 先清除后调用effect
effects.forEach(invokeCleanup); // 请调用清理
effects.forEach(invokeEffect); // 再调用effect
return [];
}
function invokeCleanup(hook) {
if (hook._cleanup) hook._cleanup();
}
function invokeEffect(hook) {
const result = hook._value();
if (typeof result === 'function') hook._cleanup = result;
}
Давайте посмотрим, как вызвать LayoutEffect, это очень просто, он срабатывает после завершения diff, этот процесс синхронный.
options.diffed = vnode => {
const c = vnode._component;
if (!c) return;
const hooks = c.__hooks;
if (hooks) {
hooks._pendingLayoutEffects = handleEffects(hooks._pendingLayoutEffects);
}
};
👌, основной принцип хуков в принципе понятен, и напоследок подытожим его картинкой.
Техническая карта
Статья очень длинная, в основном из-за того, что слишком много кода. Я сам не люблю читать такие статьи, поэтому не ожидаю, что читатели увидят это здесь. Позже я постараюсь улучшить статью. Спасибо. для чтения этого.
Главный герой этого выпуска — небольшой и красивый фреймворк для просмотра без другого стека технологий.Вот автор PreactdevelopitНекоторые другие небольшие и красивые библиотеки.
- WorkerizeИзящно выполнять и вызывать программы в webWorkers
- microbundleИнструмент упаковки библиотек с нулевой конфигурацией
- greenletПодобно workerize, этот выполняет одну асинхронную функцию в веб-воркере, а workerize — это модуль.
- mitt200 байт EventEmitter
- dlvБезопасный доступ к глубоко вложенным свойствам объекта, подобно методу get в lodash.
- snarkdownПарсер уценки 1 КБ
- unistoreЛаконичный Redux-подобный контейнер состояния для React и Preact
- stockroomПоддержка менеджера состояний в webWorker