1more 中文文档教程
1more
(另一个)研发项目,使用模板文字将高性能 DOM 渲染带给可接受的开发人员体验。 完全在浏览器中工作,不需要编译器。
Hello world
import { html, render } from "1more";
const Hello = name => html`<div>Hello ${name}</div>`;
render(Hello("World"), document.getElementById("app"));
您可以在 在 Codesandbox 上 尝试
API Reference
Rendering
html
import { html, render } from "1more";
const world = "World";
const element = html`<div>Hello ${world}</div>`;
render(element, document.body);
返回 TemplateNode,包含给定的 props 和已编译的 HTML 模板。 它不创建真正的 DOM 节点,它的主要用途是在渲染阶段比较和应用更新。 支持片段(具有多个根节点的模板)。
Properties and Attributes
可以控制元素属性和属性,并使用节点的事件处理程序:
import { html } from "1more";
const on = true;
html`
<div
class=${on ? "turned-on" : null}
onclick=${() => console.log("Clicked")}
></div>
`;
- Static attributes (that does not have dynamic bindings):
- Should be in the same form as in plain HTML code. Library does not perform processing or normalization of static content. For example
tabindex
instead oftabIndex
or string representation ofstyle
attribute. - Dynamic attributes (that have dynamic bindings):
- Should not have quotes around them;
- Only one binding per attribute can be used, partial attributes are not supported;
- Supports both property and HTML attribute names. For example:
tabIndex
/tabindex
. - No name conversion performed. Names are going to be used exactly as specified in the template.
- If property and its corresponding HTML attribute has same name, values will be assigned to property. For example for
id
attribute will be used nodeid
property. So it's property-first approach, with fallback to attributes when property not found in node instance. - Assigning
null
orundefined
to any property or attribute will result in removal of this attribute. For properties on native elements, library converts property name into corresponding attribute name to perform removal. - There is no behavior around
disabled
or similar boolean attributes to force remove them on gettingfalse
value. Sometimes using direct property has same effect, for example assigningnode.disabled = false
will remove the attribute. CSS selectors seemed to use node's actual property values over HTML definition. For all other cases it's better to usenull
orundefined
to perform removal. - Special dynamic attributes:
class
/className
- accepts only strings. Assigningnull
,undefined
or any non-string value will result in removal of this attribute.style
- accepts objects with dashed CSS properties names. For examplebackground-color
instead ofbackgroundColor
. Browser prefixes and custom CSS properties are supported. Assigningnull
orundefined
tostyle
will remove this attribute. Assigningnull
,undefined
or empty string to CSS property value will remove it from element's style declaration.defaultValue
/defaultChecked
- can be used to assign corresponding value to node on first mount, and skipping it on updates. Thus it's possible to create uncontrolled form elements.innerHTML
is temporarily disabled.- Event handlers should have name starting with
on
and actual event name. For exampleonclick
instead ofonClick
. Handlers are not attached to DOM nodes, instead library use automatic event delegation. - For Custom Elements:
- Element should be registered before call to
html
with template containing this element. - Property-first approach should work fine, as long as property is exposed in element instance. When assigning
null
orundefined
to element property, it is going to be directly assigned to element, not triggering removal. For attributesnull
andundefined
will work as usual, removing attribute from element. - Delegated events will work fine from both inside and outside of Shadow DOM content (even in closed mode) and doesn't require for events to be
composed
. Also, system ensures correct event propagation order for slotted content. - It's possible to render to
shadowRoot
directly, without any container element.
Children
有效的子项是:字符串、数字、空值、未定义、布尔值、TemplateNode、ComponentNode 和任何这些类型的数组(包括嵌套数组)。
注意:null、undefined、true、false 值不呈现任何内容,就像在 React 中一样。
import { html, component } from "1more";
const SomeComponent = component(() => {
return value => {
return html`<div>${value}</div>`;
};
});
// prettier-ignore
html`
<div>
${1}
${"Lorem ipsum"}
${null}
${false && html`<div></div>`}
${SomeComponent("Content")}
${items.map(i => html`<div>${item.label}</div>`)}
</div>
`;
component
import { component, html, render } from "1more";
const App = component(c => {
return ({ text }) => {
return html`<div>${text}</div>`;
};
});
render(App({ text: "Hello" }), document.body);
创建组件工厂,调用时返回 ComponentNode。 组件需要使用钩子、本地状态和 shouldComponentUpdate 优化。
它唯一的参数是渲染回调,它接受组件引用对象并返回渲染函数。 渲染函数接受提供的道具并返回任何有效的子类型,包括根据不同条件切换返回类型。
调用创建的组件函数时,不会立即调用渲染回调。 相反,它在渲染阶段被调用。 外部函数只会在第一次渲染期间执行一次,之后只会调用返回的渲染函数。
注意:尝试使用 props 对象渲染组件,引用等于上一个渲染中使用的对象,将导致没有更新。 这是 shouldComponentUpdate 优化。
render
import { render } from "1more";
render(App(), document.getElementById("app"));
第一次调用时,render
将从组件创建的 HTML 文档装载到提供的容器中。 之后,在每次 render
调用时,它将执行新组件结构与前一个组件结构的差异,并根据需要应用更新。 这种行为类似于 React 和其他虚拟 dom 库。 Render 接受任何有效的子类型。
key
import { html, key } from "1more";
// prettier-ignore
html`
<div>
${items.map(item => key(item.id, Item(item)))}
</div>
`;
在内部创建具有给定值的 KeyNode。 这些密钥用于节点协调算法,以区分节点并执行适当的更新。 有效键是字符串和数字。
注意:如果需要,可以使用原始类型或可空类型的键。 数组不仅限于键控节点,必要时可以混合使用。 没有密钥的节点将就地更新(或替换)。
import { render, key, html } from "1more";
render(
[
null,
undefined,
key(0, true),
false,
key(1, html`<div>First node</div>`),
html`<div>After</div>`,
],
document.body,
);
invalidate
import { component, html, invalidate } from "1more";
const SomeComponent = component(c => {
let localState = 1;
return () => {
return html`
<button
onclick=${() => {
localState++;
invalidate(c);
}}
>
${localState}
</button>
`;
};
});
invalidate
接受组件引用对象并将安排该组件的更新。 它允许在本地对更改做出反应,而无需重新渲染整个应用程序。
在无效时,组件渲染函数将被调用,结果将被区分并相应地应用。
注意:invalidate
不会立即触发更新。 而是更新延迟到当前调用堆栈的末尾。 它允许为不同的组件安排多个更新,并确保组件只重新渲染一次并且没有应用不必要的 DOM 修改。 如果为多个组件安排了更新,它们将按深度顺序应用,即父组件将在其子组件之前重新渲染。
useUnmount
import { component, html, useUnmount } from "1more";
const SomeComponent = component(c => {
useUnmount(c, () => {
console.log("Component unmounted");
});
return () => {
return html`<div>Some</div>`;
};
});
允许附加回调,将在组件卸载之前调用。
Contexts
上下文 API 可用于为子元素提供静态值。 上下文提供程序不是渲染树的一部分,而是附加到某些主机组件。
上下文 API 由三个函数组成:
createContext(defaultValue)
- creates context configuration that can be used to provide and discover them in the tree.addContext(component, context, value)
- create provider for given context and attach it to the host component.useContext(component, context)
- get value from the closest context provider in the rendered tree or return context's default value.
import {
html,
component,
render,
createContext,
addContext,
useContext,
} from "1more";
const ThemeContext = createContext();
const Child = component(c => {
const theme = useContext(c, ThemeContext);
return () => {
// prettier-ignore
return html`
<div style=${{ color: theme.textColor }}>
Hello world!
</div>
`;
};
});
const App = component(c => {
addContext(c, ThemeContext, { textColor: "#111111" });
return () => {
return Child();
};
});
render(App(), container);
注意:上下文不支持传播和更改其中的值。 由于这是在 React 中使用它们时的主要性能问题之一,并且可以通过许多不同的方式解决,系统默认只关注提供和发现静态值。 反应性可以通过使用额外的库来实现,这些库提供了一些方式来订阅变化并将它们的“存储”放入上下文提供者中。
Observables
box
import { box, read, write, subscribe } from "1more/box";
const state = box(1);
read(state); // Returns 1
const unsub = subscribe(value => {
console.log("Current value: ", value);
}, state);
write(2, state); // Logs "Current value: 2"
互补的原始可观察实现,主要用于支持基准测试中的库。 尽量减少内存占用,使用链表有效地管理订阅。 可以用作廉价的状态管理或作为集成其他库和编写钩子的参考。
useSubscription
import { component, html } from "1more";
import { box, useSubscription } from "1more/box";
const items = box([]);
const SomeComponent = component(c => {
const getItemsCount = useSubscription(
// Component reference
c,
// Source
items,
// Optional selector
(items, prop) => items.count,
);
return prop => {
return html`<div>${getItemsCount(prop)}</div>`;
};
});
设置对 source observable 的订阅并返回 getter 函数以读取当前值。 当 observable 发出新值时,它会触发组件的更新。
usePropSubscription
import { component, html, render } from "1more";
import { box, read, usePropSubscription } from "1more/box";
const item = {
value: box(""),
};
const Item = component(c => {
const getValue = usePropSubscription(c);
return item => {
return html`<div>${getValue(item.label)}</div>`;
};
});
render(Item(item), document.body);
允许使用来自组件道具的可观察对象。 当接收到 observable 时,它会设置订阅。
Delegated events
渲染的 DOM 节点没有附加的事件监听器。 相反,渲染器将委托的事件处理程序附加到每个事件类型的渲染根(render
函数中的容器参数),然后将使用渲染的应用程序实例来发现目标事件处理程序。
具有 bubbles: true
的事件将在 bubble
阶段(从下到上)处理,并正确处理 stopPropagation
调用。
具有 bubbles: false
的事件(如 focus
事件)将在目标上的 capture
阶段处理。 这应该不会影响正常使用,但调试时需要注意。
注意:所有事件处理程序都是活动的(使用 passive: false
),并且系统没有内置支持来处理 capture
阶段的事件。
Does this implementation use Virtual DOM?
它类似于 vdom。 在每个渲染应用程序上生成不可变的虚拟树结构,用于与先前的树进行比较以计算更改的部分。 与 vdom 相比,模板节点作为具有插入点的单个实体处理。 这允许压缩内存中的树结构,将静态部分与动态部分分开,从而加快差异阶段。
Examples
Credits
- ivi - inspired component and hooks API and a lot of hi-perf optimizations.
1more
(One more) R&D project to bring performant DOM rendering to acceptable developer experience using template literals. Works completely in-browser, doesn't require compiler.
Hello world
import { html, render } from "1more";
const Hello = name => html`<div>Hello ${name}</div>`;
render(Hello("World"), document.getElementById("app"));
You can try it on Codesandbox
API Reference
Rendering
html
import { html, render } from "1more";
const world = "World";
const element = html`<div>Hello ${world}</div>`;
render(element, document.body);
Returns TemplateNode, containing given props and compiled HTML template. It does not create real DOM node, it's primary use is for diffing and applying updates during rendering phase. Fragments are supported (template with multiple root nodes).
Properties and Attributes
It's possible to control element properties and attributes, and use event handlers with nodes:
import { html } from "1more";
const on = true;
html`
<div
class=${on ? "turned-on" : null}
onclick=${() => console.log("Clicked")}
></div>
`;
- Static attributes (that does not have dynamic bindings):
- Should be in the same form as in plain HTML code. Library does not perform processing or normalization of static content. For example
tabindex
instead oftabIndex
or string representation ofstyle
attribute. - Dynamic attributes (that have dynamic bindings):
- Should not have quotes around them;
- Only one binding per attribute can be used, partial attributes are not supported;
- Supports both property and HTML attribute names. For example:
tabIndex
/tabindex
. - No name conversion performed. Names are going to be used exactly as specified in the template.
- If property and its corresponding HTML attribute has same name, values will be assigned to property. For example for
id
attribute will be used nodeid
property. So it's property-first approach, with fallback to attributes when property not found in node instance. - Assigning
null
orundefined
to any property or attribute will result in removal of this attribute. For properties on native elements, library converts property name into corresponding attribute name to perform removal. - There is no behavior around
disabled
or similar boolean attributes to force remove them on gettingfalse
value. Sometimes using direct property has same effect, for example assigningnode.disabled = false
will remove the attribute. CSS selectors seemed to use node's actual property values over HTML definition. For all other cases it's better to usenull
orundefined
to perform removal. - Special dynamic attributes:
class
/className
- accepts only strings. Assigningnull
,undefined
or any non-string value will result in removal of this attribute.style
- accepts objects with dashed CSS properties names. For examplebackground-color
instead ofbackgroundColor
. Browser prefixes and custom CSS properties are supported. Assigningnull
orundefined
tostyle
will remove this attribute. Assigningnull
,undefined
or empty string to CSS property value will remove it from element's style declaration.defaultValue
/defaultChecked
- can be used to assign corresponding value to node on first mount, and skipping it on updates. Thus it's possible to create uncontrolled form elements.innerHTML
is temporarily disabled.- Event handlers should have name starting with
on
and actual event name. For exampleonclick
instead ofonClick
. Handlers are not attached to DOM nodes, instead library use automatic event delegation. - For Custom Elements:
- Element should be registered before call to
html
with template containing this element. - Property-first approach should work fine, as long as property is exposed in element instance. When assigning
null
orundefined
to element property, it is going to be directly assigned to element, not triggering removal. For attributesnull
andundefined
will work as usual, removing attribute from element. - Delegated events will work fine from both inside and outside of Shadow DOM content (even in closed mode) and doesn't require for events to be
composed
. Also, system ensures correct event propagation order for slotted content. - It's possible to render to
shadowRoot
directly, without any container element.
Children
Valid childrens are: string, number, null, undefined, boolean, TemplateNode, ComponentNode and arrays of any of these types (including nested arrays).
Note: null, undefined, true, false values render nothing, just like in React.
import { html, component } from "1more";
const SomeComponent = component(() => {
return value => {
return html`<div>${value}</div>`;
};
});
// prettier-ignore
html`
<div>
${1}
${"Lorem ipsum"}
${null}
${false && html`<div></div>`}
${SomeComponent("Content")}
${items.map(i => html`<div>${item.label}</div>`)}
</div>
`;
component
import { component, html, render } from "1more";
const App = component(c => {
return ({ text }) => {
return html`<div>${text}</div>`;
};
});
render(App({ text: "Hello" }), document.body);
Creates component factory, that returns ComponentNode when called. Components are needed to use hooks, local state and shouldComponentUpdate optimizations.
Its only argument is rendering callback, that accepts component reference object and returns rendering function. Rendering function accepts provided props and returns any valid children type, including switching return types based on different conditions.
When calling created component function, rendering callback is not invoked immediately. Instead, it's invoked during rendering phase. Outer function going to be executed only once during first render, after that only returned render function will be invoked.
Note: Trying to render component with props object, that referentially equal to the one that was used in previous render, will result in no update. This is shouldComponentUpdate optimization.
render
import { render } from "1more";
render(App(), document.getElementById("app"));
When called first time, render
going to mount HTML document created from component to provided container. After that, on each render
call, it will perform diffing new component structure with previous one and apply updates as necessary. This behavior is similar to React and other virtual dom libraries. Render accepts any valid children types.
key
import { html, key } from "1more";
// prettier-ignore
html`
<div>
${items.map(item => key(item.id, Item(item)))}
</div>
`;
Creates KeyNode with given value inside. These keys are used in nodes reconciliation algorithm, to differentiate nodes from each other and perform proper updates. Valid keys are strings and numbers.
Note: It is possible to use keys with primitive or nullable types if needed. Arrays are not limited to only keyed nodes, it is possible to mix them if necessary. Nodes without keys are going to be updated (or replaced) in place.
import { render, key, html } from "1more";
render(
[
null,
undefined,
key(0, true),
false,
key(1, html`<div>First node</div>`),
html`<div>After</div>`,
],
document.body,
);
invalidate
import { component, html, invalidate } from "1more";
const SomeComponent = component(c => {
let localState = 1;
return () => {
return html`
<button
onclick=${() => {
localState++;
invalidate(c);
}}
>
${localState}
</button>
`;
};
});
invalidate
accepts component reference object and will schedule update of this component. It allows to react on changes locally, without re-rendering the whole app.
On invalidate, component render function will be called and results will be diffed and applied accordingly.
Note: invalidate
does not trigger update immediately. Instead update delayed till the end of current call stack. It allows to schedule multiple updates for different components and ensure that components are re-rendered only once and no unnecessary DOM modifications applied. If updates scheduled for multiple components, they going to be applied in order of depth, i.e. parent going to be re-rendered before its children.
useUnmount
import { component, html, useUnmount } from "1more";
const SomeComponent = component(c => {
useUnmount(c, () => {
console.log("Component unmounted");
});
return () => {
return html`<div>Some</div>`;
};
});
Allows to attach callback, that going to be called before component unmounted.
Contexts
Context API can be used to provide static values for children elements. Context providers are not part of the rendering tree, instead they attached to some host components.
Context API consists of three functions:
createContext(defaultValue)
- creates context configuration that can be used to provide and discover them in the tree.addContext(component, context, value)
- create provider for given context and attach it to the host component.useContext(component, context)
- get value from the closest context provider in the rendered tree or return context's default value.
import {
html,
component,
render,
createContext,
addContext,
useContext,
} from "1more";
const ThemeContext = createContext();
const Child = component(c => {
const theme = useContext(c, ThemeContext);
return () => {
// prettier-ignore
return html`
<div style=${{ color: theme.textColor }}>
Hello world!
</div>
`;
};
});
const App = component(c => {
addContext(c, ThemeContext, { textColor: "#111111" });
return () => {
return Child();
};
});
render(App(), container);
Note: contexts do not support propagating and changing values in them. Since this is one of the main performance problems when using them in React, and it can be solved in a lot of different ways, system defaults to focus only on providing and discovering static values. Reactivity can be achieved by using additional libraries that provide some way to subscribe to changes and putting their "stores" into context provider.
Observables
box
import { box, read, write, subscribe } from "1more/box";
const state = box(1);
read(state); // Returns 1
const unsub = subscribe(value => {
console.log("Current value: ", value);
}, state);
write(2, state); // Logs "Current value: 2"
Complementary primitive observable implementation, used mainly to support library in benchmarks. Tries to have low memory footprint, uses linked-lists to effectively manage subscriptions. Can be used as a cheap state management or as reference for integrating other libraries and writing hooks.
useSubscription
import { component, html } from "1more";
import { box, useSubscription } from "1more/box";
const items = box([]);
const SomeComponent = component(c => {
const getItemsCount = useSubscription(
// Component reference
c,
// Source
items,
// Optional selector
(items, prop) => items.count,
);
return prop => {
return html`<div>${getItemsCount(prop)}</div>`;
};
});
Setup subscription to source observable and returns getter function to read current value. When observable emits new value, it triggers update of the component.
usePropSubscription
import { component, html, render } from "1more";
import { box, read, usePropSubscription } from "1more/box";
const item = {
value: box(""),
};
const Item = component(c => {
const getValue = usePropSubscription(c);
return item => {
return html`<div>${getValue(item.label)}</div>`;
};
});
render(Item(item), document.body);
Allows to consume observable from component props. When receiving observable, it sets up subscription to it.
Delegated events
Rendered DOM nodes don't have attached event listeners. Instead renderer attaches delegated event handlers to rendering root (container argument in render
function) for each event type, then will use rendered app instance to discover target event handler.
Events that have bubbles: true
will be handled in bubble
phase (from bottom to top), with proper handling of stopPropagation
calls.
Events that have bubbles: false
(like focus
event) will be handled in their capture
phase on the target. This should not affect normal usage, but worth keep in mind when debugging.
Note: All event handlers are active (with passive: false
), and system doesn't have built-in support to handle events in capture
phase.
Does this implementation use Virtual DOM?
It is similar to vdom. On each render app generates immutable virtual tree structure that is used to diff against previous tree to calculate changed parts. Comparing to vdom, template nodes handled as one single entity with insertion points. This allows to compact tree structure in memory, separate static parts from dynamic, and as a result speed up diffing phase.
Examples
Credits
- ivi - inspired component and hooks API and a lot of hi-perf optimizations.