3 | | object
4 | | string
5 | | number
6 | | boolean
7 | | null
8 | | undefined;
9 | export type ComponentChildren = ComponentChild[] | ComponentChild;
10 |
11 | export interface ComponentClass {
12 | new (props: P, context?: any): Component
;
13 | displayName?: string;
14 | defaultProps?: Partial
;
15 | getDerivedStateFromProps?(
16 | props: Readonly
,
17 | state: Readonly
18 | ): Partial | null;
19 | getDerivedStateFromError?(error: any): Partial | null;
20 | }
21 |
22 | export type ComponentType
= ComponentClass
| FunctionComponent
;
23 |
24 | export type PropsType = {
25 | // createElementのoption
26 | is?: string;
27 | // form
28 | checked?: any;
29 | // form
30 | value?: any;
31 | };
32 |
33 | export type Key = string | number | any;
34 |
35 | interface FunctionComponent
{
36 | (props: RenderableProps
, context?: any): VNode | null;
37 | displayName?: string;
38 | defaultProps?: Partial;
39 | }
40 |
41 | interface Attributes {
42 | key?: Key;
43 | jsx?: boolean;
44 | }
45 |
46 | type RenderableProps
= P &
47 | Readonly;
48 |
49 | export interface FunctionalComponent extends FunctionComponent
{
50 | // Define getDerivedStateFromProps as undefined on FunctionalComponent
51 | // to get rid of some errors in `diff()`
52 | getDerivedStateFromProps?: undefined;
53 | }
54 |
55 | export type ComponentFactory
= ComponentClass
| FunctionalComponent
;
56 |
57 | interface VNode
{
58 | key: Key;
59 | /**
60 | * The time this `vnode` started rendering. Will only be set when
61 | * the devtools are attached.
62 | * Default value: `0`
63 | */
64 | startTime?: number;
65 | /**
66 | * The time that the rendering of this `vnode` was completed. Will only be
67 | * set when the devtools are attached.
68 | * Default value: `-1`
69 | */
70 | endTime?: number;
71 | type: string | ComponentFactory
;
72 | props: P & { children: ComponentChildren };
73 | _children: Array> | null;
74 | _parent: VNode | null;
75 | _depth: number | null;
76 | /**
77 | * The [first (for Fragments)] DOM child of a VNode
78 | */
79 | _dom: PreactElement | null;
80 | /**
81 | * The last dom child of a Fragment, or components that return a Fragment
82 | */
83 | _nextDom: PreactElement | null;
84 | _component: Component | null;
85 | _hydrating: boolean | null;
86 | constructor: undefined;
87 | // 初回レンダリングでは与えられないが、renderComponent から詰め込まれていく
88 | _original?: VNode | null | string | number;
89 | }
90 |
91 | interface Context {
92 | Consumer: Consumer;
93 | Provider: Provider;
94 | }
95 |
96 | interface Consumer
97 | extends FunctionComponent<{
98 | children: (value: T) => ComponentChildren;
99 | }> {}
100 | interface PreactConsumer extends Consumer {}
101 |
102 | interface Provider
103 | extends FunctionComponent<{
104 | value: T;
105 | children: ComponentChildren;
106 | }> {}
107 |
108 | export interface Component {
109 | // When component is functional component, this is reset to functional component
110 | constructor: ComponentType
;
111 | state: S; // Override Component["state"] to not be readonly for internal use, specifically Hooks
112 | base?: PreactElement;
113 |
114 | _dirty: boolean;
115 | _force?: boolean;
116 | _renderCallbacks: Array; // Component は実質 () => void
117 | _globalContext?: any;
118 | _vnode?: VNode | null;
119 | // setStateが呼ばれるとこの値に置き換える
120 | _nextState?: S | null; // Only class components
121 | /** Only used in the devtools to later dirty check if state has changed */
122 | _prevState?: S | null;
123 | /**
124 | * Pointer to the parent dom node. This is only needed for top-level Fragment
125 | * components or array returns.
126 | */
127 | _parentDom?: PreactElement | null;
128 | // Always read, set only when handling error
129 | _processingException?: Component | null;
130 | // Always read, set only when handling error. This is used to indicate at diffTime to set _processingException
131 | _pendingError?: Component | null;
132 | }
133 |
134 | export interface PreactElement extends HTMLElement, Text {
135 | _children?: VNode | null;
136 | /** Event listeners to support event delegation */
137 | _listeners: Record void>;
138 |
139 | // Preact uses this attribute to detect SVG nodes
140 | ownerSVGElement?: SVGElement | null;
141 |
142 | // style: HTMLElement["style"]; // From HTMLElement
143 |
144 | data?: string | number; // From Text node
145 | }
146 |
--------------------------------------------------------------------------------
/packages/oreact/lib/component.ts:
--------------------------------------------------------------------------------
1 | import { assign } from "./util";
2 | import { diff, commitRoot } from "./diff/index";
3 | import { Fragment } from "./create-element";
4 | import { Component as ComponentType, PropsType, VNode } from "./type";
5 |
6 | /**
7 | * コンポーネント
8 | * @param props props
9 | */
10 | export function Component(props: PropsType) {
11 | this.props = props;
12 | }
13 |
14 | /**
15 | * setState メソッド.
16 | * component._nextState に次の状態を保存し、enqueueRenderを呼び出して再レンダリングをトリガーする
17 | */
18 | Component.prototype.setState = function (update: Object) {
19 | // only clone state when copying to nextState the first time.
20 | let s;
21 | if (this._nextState != null && this._nextState !== this.state) {
22 | s = this._nextState;
23 | } else {
24 | s = this._nextState = assign({}, this.state);
25 | }
26 |
27 | if (update) {
28 | assign(s, update);
29 | }
30 |
31 | // Skip update if updater function returned null
32 | if (update == null) return;
33 |
34 | if (this._vnode) {
35 | enqueueRender(this);
36 | }
37 | };
38 |
39 | /**
40 | * render が呼ばれると、Fragment() つまり、props.childrenを返す
41 | */
42 | Component.prototype.render = Fragment;
43 |
44 | /**
45 | * 兄弟DOMを取得する
46 | * @param vnode 対象となるVNode
47 | * @param childIndex 何個目の兄弟かの番号
48 | */
49 | export function getDomSibling(vnode: VNode, childIndex: number | null) {
50 | if (childIndex == null) {
51 | return vnode._parent
52 | ? getDomSibling(vnode._parent, vnode._parent._children.indexOf(vnode) + 1)
53 | : null;
54 | }
55 |
56 | let sibling;
57 | for (; childIndex < vnode._children.length; childIndex++) {
58 | sibling = vnode._children[childIndex];
59 |
60 | if (sibling != null && sibling._dom != null) {
61 | return sibling._dom;
62 | }
63 | }
64 |
65 | // vnode's children からDOMが見つからなかった時の処理
66 | return typeof vnode.type == "function"
67 | ? getDomSibling(vnode, undefined)
68 | : null;
69 | }
70 |
71 | /**
72 | * 際レンダリングのトリガー, diffを呼び出す
73 | * @param component rerender したいコンポーネント
74 | */
75 | function renderComponent(component: ComponentType) {
76 | let vnode = component._vnode,
77 | oldDom = vnode._dom,
78 | parentDom = component._parentDom;
79 |
80 | if (parentDom) {
81 | let commitQueue = [];
82 | const oldVNode = assign({}, vnode) as VNode;
83 | oldVNode._original = oldVNode;
84 |
85 | let newDom = diff({
86 | parentDom: parentDom,
87 | newVNode: vnode,
88 | oldVNode: oldVNode,
89 | excessDomChildren: null,
90 | commitQueue: commitQueue,
91 | oldDom: oldDom == null ? getDomSibling(vnode, undefined) : oldDom,
92 | });
93 |
94 | commitRoot(commitQueue);
95 |
96 | if (newDom != oldDom) {
97 | updateParentDomPointers(vnode);
98 | }
99 | }
100 | }
101 |
102 | /**
103 | * @param {import('./internal').VNode} vnode
104 | */
105 | function updateParentDomPointers(vnode: VNode) {
106 | if ((vnode = vnode._parent) != null && vnode._component != null) {
107 | vnode._dom = vnode._component.base = null;
108 | for (let i = 0; i < vnode._children.length; i++) {
109 | let child = vnode._children[i];
110 | if (child != null && child._dom != null) {
111 | vnode._dom = vnode._component.base = child._dom;
112 | break;
113 | }
114 | }
115 |
116 | return updateParentDomPointers(vnode);
117 | }
118 | }
119 |
120 | // 差分更新後の副作用を管理するリスト
121 | let rerenderQueue: ComponentType[] = [];
122 |
123 | // callbackの非同期スケジューラー
124 | const defer: (cb: () => void) => void =
125 | typeof Promise == "function"
126 | ? Promise.prototype.then.bind(Promise.resolve())
127 | : setTimeout;
128 |
129 | /**
130 | * Enqueue a rerender of a component
131 | * @param {import('./internal').Component} c The component to rerender
132 | */
133 |
134 | /**
135 | * setStateが呼び出すトリガー.
136 | * データの破壊的操作があるので注意
137 | * @param c コンポーネント
138 | */
139 | export function enqueueRender(c: ComponentType) {
140 | if (
141 | (!c._dirty &&
142 | (c._dirty = true) &&
143 | // queueの操作
144 | rerenderQueue.push(c) &&
145 | // counterの増加操作
146 | !process._rerenderCount++) ||
147 | true
148 | ) {
149 | defer(process);
150 | }
151 | }
152 |
153 | /**
154 | * renderQueueを実行する
155 | */
156 | function process() {
157 | let queue;
158 | while ((process._rerenderCount = rerenderQueue.length)) {
159 | queue = rerenderQueue.sort((a, b) => a._vnode._depth - b._vnode._depth);
160 | rerenderQueue = [];
161 | // Don't update `renderCount` yet. Keep its value non-zero to prevent unnecessary
162 | // process() calls from getting scheduled while `queue` is still being consumed.
163 | queue.some((c) => {
164 | if (c._dirty) renderComponent(c);
165 | });
166 | }
167 | }
168 | process._rerenderCount = 0;
169 |
--------------------------------------------------------------------------------
/packages/oreact/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | // "incremental": true, /* Enable incremental compilation */
5 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
6 | "module": "es2015" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
7 | "lib": ["dom", "es5"],
8 | // "allowJs": true, /* Allow javascript files to be compiled. */
9 | // "checkJs": true, /* Report errors in .js files. */
10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
13 | // "sourceMap": true, /* Generates corresponding '.map' file. */
14 | // "outFile": "./", /* Concatenate and emit output to single file. */
15 | "outDir": "./js" /* Redirect output structure to the directory. */,
16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
17 | // "composite": true, /* Enable project compilation */
18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
19 | // "removeComments": true, /* Do not emit comments to output. */
20 | // "noEmit": true, /* Do not emit outputs. */
21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
24 |
25 | /* Strict Type-Checking Options */
26 | "strict": false /* Enable all strict type-checking options. */,
27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
28 | // "strictNullChecks": true, /* Enable strict null checks. */
29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
34 |
35 | /* Additional Checks */
36 | // "noUnusedLocals": true, /* Report errors on unused locals. */
37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
40 |
41 | /* Module Resolution Options */
42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
46 | // "typeRoots": [], /* List of folders to include type definitions from. */
47 | // "types": [], /* Type declaration files to be included in compilation. */
48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
52 |
53 | /* Source Map Options */
54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
58 |
59 | /* Experimental Options */
60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
62 | },
63 | "include": ["lib/"]
64 | }
65 |
--------------------------------------------------------------------------------
/packages/oreact/lib/diff/children.ts:
--------------------------------------------------------------------------------
1 | import { diff, unmount } from "./index";
2 | import { createVNode, Fragment } from "../create-element";
3 | import { EMPTY_OBJ, EMPTY_ARR } from "../constants";
4 | import { getDomSibling } from "../component";
5 | import { Component, ComponentChildren, PreactElement, VNode } from "../type";
6 |
7 | type DiffChildrenArgType = {
8 | parentDom: PreactElement;
9 | /** diffElementNodesからchildrenが渡される */
10 | renderResult: ComponentChildren[];
11 | /** [renderResult]がdiffから渡される */
12 | newParentVNode: VNode;
13 | /** diff が持ってる oldVNode が渡される. 呼び出されるたびに */
14 | oldParentVNode: VNode;
15 | excessDomChildren: PreactElement;
16 | commitQueue: Component[];
17 | oldDom: Element | Text | typeof EMPTY_OBJ;
18 | };
19 |
20 | /**
21 | * VNodeのchildren比較を行う
22 | */
23 | export function diffChildren(arg: DiffChildrenArgType) {
24 | let {
25 | parentDom,
26 | renderResult,
27 | newParentVNode,
28 | oldParentVNode,
29 | excessDomChildren,
30 | commitQueue,
31 | oldDom,
32 | } = arg;
33 | let i,
34 | j,
35 | oldVNode,
36 | childVNode,
37 | newDom,
38 | firstChildDom,
39 | filteredOldDom: Element | Text | null;
40 |
41 | let oldChildren = (oldParentVNode && oldParentVNode._children) || EMPTY_ARR;
42 | let oldChildrenLength = oldChildren.length;
43 |
44 | // top level の render か Fragmentかの識別
45 | if (oldDom == EMPTY_OBJ) {
46 | if (oldChildrenLength) {
47 | filteredOldDom = getDomSibling(oldParentVNode, 0);
48 | } else {
49 | filteredOldDom = null;
50 | }
51 | }
52 |
53 | // renderResult から newVNode として扱いたい childVNode を作り出し、それを配列に詰め込んで newParentVNode._children を作り出す
54 | newParentVNode._children = [];
55 | for (i = 0; i < renderResult.length; i++) {
56 | childVNode = renderResult[i];
57 |
58 | if (childVNode == null || typeof childVNode == "boolean") {
59 | childVNode = newParentVNode._children[i] = null;
60 | } else if (typeof childVNode == "string" || typeof childVNode == "number") {
61 | // child が primitive の場合
62 | childVNode = newParentVNode._children[i] = createVNode(
63 | null,
64 | childVNode,
65 | null,
66 | null,
67 | childVNode
68 | );
69 | } else if (Array.isArray(childVNode)) {
70 | // child が 配列 の場合
71 | childVNode = newParentVNode._children[i] = createVNode(
72 | Fragment,
73 | { children: childVNode },
74 | null,
75 | null,
76 | null
77 | );
78 | } else if (childVNode._dom != null || childVNode._component != null) {
79 | // child が element の場合
80 | childVNode = newParentVNode._children[i] = createVNode(
81 | childVNode.type,
82 | childVNode.props,
83 | childVNode.key,
84 | null,
85 | childVNode._original
86 | );
87 | } else {
88 | // child が コンポーネントの場合
89 | childVNode = newParentVNode._children[i] = childVNode;
90 | }
91 |
92 | // Terser removes the `continue` here and wraps the loop body
93 | // in a `if (childVNode) { ... } condition
94 | if (childVNode == null) {
95 | continue;
96 | }
97 |
98 | childVNode._parent = newParentVNode;
99 | childVNode._depth = newParentVNode._depth + 1;
100 |
101 | // Check if we find a corresponding element in oldChildren.
102 | // If found, delete the array item by setting to `undefined`.
103 | // We use `undefined`, as `null` is reserved for empty placeholders
104 | // (holes).
105 | oldVNode = oldChildren[i];
106 |
107 | if (
108 | oldVNode === null ||
109 | (oldVNode &&
110 | childVNode.key == oldVNode.key &&
111 | childVNode.type === oldVNode.type)
112 | ) {
113 | oldChildren[i] = undefined;
114 | } else {
115 | // Either oldVNode === undefined or oldChildrenLength > 0,
116 | // so after this loop oldVNode == null or oldVNode is a valid value.
117 | for (j = 0; j < oldChildrenLength; j++) {
118 | oldVNode = oldChildren[j];
119 | // If childVNode is unkeyed, we only match similarly unkeyed nodes, otherwise we match by key.
120 | // We always match by type (in either case).
121 | if (
122 | oldVNode &&
123 | childVNode.key == oldVNode.key &&
124 | childVNode.type === oldVNode.type
125 | ) {
126 | oldChildren[j] = undefined;
127 | break;
128 | }
129 | oldVNode = null;
130 | }
131 | }
132 |
133 | oldVNode = oldVNode || EMPTY_OBJ;
134 |
135 | // Morph the old element into the new one, but don't append it to the dom yet
136 | newDom = diff({
137 | parentDom: parentDom, // diff から渡された parentDom を使ってまた diff を呼び出す.
138 | newVNode: childVNode, // diff の renderResult の要素を newVNode として diff に渡す.
139 | oldVNode: oldVNode, // oldVNode はおやから渡されたもの or EMPTY_OBJ. key不一致ならEMPTY_OBJが渡される.
140 | excessDomChildren: excessDomChildren,
141 | commitQueue: commitQueue,
142 | oldDom: filteredOldDom,
143 | });
144 |
145 | // 新しいDOMがあれば挿入する
146 | if (newDom != null) {
147 | if (firstChildDom == null) {
148 | firstChildDom = newDom;
149 | }
150 |
151 | filteredOldDom = placeChild({
152 | parentDom: parentDom,
153 | childVNode: childVNode,
154 | oldVNode: oldVNode,
155 | oldChildren: oldChildren,
156 | excessDomChildren: excessDomChildren,
157 | newDom: newDom,
158 | oldDom: filteredOldDom,
159 | });
160 |
161 | if (typeof newParentVNode.type == "function") {
162 | newParentVNode._nextDom = filteredOldDom as PreactElement;
163 | }
164 | }
165 | }
166 |
167 | newParentVNode._dom = firstChildDom;
168 |
169 | // Remove remaining oldChildren if there are any.
170 | for (i = oldChildrenLength; i--; ) {
171 | if (oldChildren[i] != null) unmount(oldChildren[i], oldChildren[i]);
172 | }
173 | }
174 |
175 | type PlaceChildArgType = {
176 | parentDom: PreactElement;
177 | childVNode: VNode;
178 | oldVNode: VNode;
179 | oldChildren: ComponentChildren;
180 | excessDomChildren: ComponentChildren;
181 | newDom: Node | Text;
182 | oldDom: Node | Text;
183 | };
184 |
185 | /**
186 | * parentDOMにnewDOMを挿入する関数
187 | * @param arg
188 | */
189 | export function placeChild(arg: PlaceChildArgType): PreactElement {
190 | let {
191 | parentDom,
192 | childVNode,
193 | oldVNode,
194 | oldChildren,
195 | excessDomChildren,
196 | newDom,
197 | oldDom,
198 | } = arg;
199 |
200 | let nextDom;
201 | if (childVNode._nextDom !== undefined) {
202 | nextDom = childVNode._nextDom;
203 |
204 | childVNode._nextDom = undefined;
205 | } else if (
206 | excessDomChildren == oldVNode ||
207 | newDom != oldDom ||
208 | newDom.parentNode == null
209 | ) {
210 | outer: if (oldDom == null || oldDom.parentNode !== parentDom) {
211 | // 親が異なるなら兄弟ではないので子要素を追加
212 | parentDom.appendChild(newDom);
213 | nextDom = null;
214 | } else {
215 | // 親が同じなら兄弟要素を追加
216 | if (!Array.isArray(oldChildren)) {
217 | throw new Error("配列であるべき");
218 | }
219 | for (
220 | let sibDom = oldDom, j = 0;
221 | (sibDom = sibDom.nextSibling) && j < oldChildren.length;
222 | j += 2
223 | ) {
224 | if (sibDom == newDom) {
225 | break outer;
226 | }
227 | }
228 | parentDom.insertBefore(newDom, oldDom);
229 | nextDom = oldDom;
230 | }
231 | }
232 |
233 | if (nextDom !== undefined) {
234 | oldDom = nextDom;
235 | } else {
236 | oldDom = newDom.nextSibling;
237 | }
238 |
239 | return oldDom as PreactElement;
240 | }
241 |
--------------------------------------------------------------------------------
/packages/oreact/lib/diff/index.ts:
--------------------------------------------------------------------------------
1 | import { EMPTY_OBJ } from "../constants";
2 | import { Component } from "../component";
3 | import { Fragment } from "../create-element";
4 | import { diffChildren } from "./children";
5 | import { diffProps } from "./props";
6 | import { removeNode } from "../util";
7 | import {
8 | Component as ComponentType,
9 | PreactElement,
10 | PropsType,
11 | VNode,
12 | } from "../type";
13 |
14 | type DiffArgType = {
15 | /** マウント対象のDOM. このうえにVNodeを反映させていく. 初回実行では render から渡されたものが入るが、diff 自体は再帰的に呼ばれ parentDom も置き換えられたりするので様々な値が入りうる。 */
16 | parentDom: PreactElement;
17 | /** 置き換えに使うvnode, renderから呼ばれるときは事前にcreateElementされている。diffCHildrenから呼ばれるときはchildNode(つまり一つ階層を降っている) */
18 | newVNode: VNode;
19 | /** 初回実行ならnullが渡されれる(hydrateされていないなら) */
20 | oldVNode: VNode | typeof EMPTY_OBJ;
21 | excessDomChildren: PreactElement[];
22 | /** commitRoot時に実行されるcallbackを持つコンポーネントのリスト */
23 | commitQueue: ComponentType[];
24 | /** 初回レンダリングではreplaceNodeがそのまま渡される(初回レンダリングでは大抵の場合はEMPTY_OBJECT),ただしdiff 自体は再帰的に呼ばれ oldDom も置き換えられたりするので様々な値が入りうる。 */
25 | oldDom: Element | Text | typeof EMPTY_OBJ;
26 | };
27 |
28 | /**
29 | * 与えられた新旧VNodeの差分をとって、その差分DOMに適用してそのDOMを返す関数
30 | */
31 | export function diff(arg: DiffArgType) {
32 | console.log("diff", arguments);
33 | let {
34 | parentDom,
35 | newVNode,
36 | oldVNode,
37 | excessDomChildren,
38 | commitQueue,
39 | oldDom,
40 | } = arg;
41 | let tmp,
42 | newType = newVNode.type;
43 |
44 | // createElement は constructor を undefined で設定するtので、そこ経由で作られたことを保証する。
45 | if (newVNode.constructor !== undefined) return null;
46 |
47 | // newVNode がコンポーネントかエレメントかで処理が分岐
48 | if (typeof newType == "function") {
49 | // newVNode がコンポーネントの時の分岐
50 | let c, isNew, oldProps, oldState;
51 | let newProps = newVNode.props;
52 |
53 | let componentContext = EMPTY_OBJ;
54 |
55 | // コンポーネントオブジェクトの作成
56 | if (oldVNode._component) {
57 | // すでにコンポーネントがある時(例えばsetStateのとき)
58 | c = newVNode._component = oldVNode._component;
59 | } else {
60 | if ("prototype" in newType && newType.prototype.render) {
61 | newVNode._component = c = new newType(newProps);
62 | } else {
63 | newVNode._component = c = new Component(newProps);
64 | c.constructor = newType;
65 | c.render = doRender;
66 | }
67 |
68 | c.props = newProps;
69 | if (!c.state) c.state = {};
70 | isNew = c._dirty = true;
71 | c._renderCallbacks = [];
72 | }
73 |
74 | // Invoke getDerivedStateFromProps
75 | if (c._nextState == null) {
76 | c._nextState = c.state;
77 | }
78 |
79 | oldProps = c.props;
80 | oldState = c.state;
81 |
82 | // 差分更新前(diff中)に呼び出されるライフサイクルイベントを実行
83 | if (isNew) {
84 | if (c.componentDidMount != null) {
85 | c._renderCallbacks.push(c.componentDidMount);
86 | }
87 | } else {
88 | if (
89 | newType.getDerivedStateFromProps == null &&
90 | newProps !== oldProps &&
91 | c.componentWillReceiveProps != null
92 | ) {
93 | c.componentWillReceiveProps(newProps, componentContext);
94 | }
95 | }
96 |
97 | c.props = newProps;
98 | c.state = c._nextState;
99 |
100 | c._dirty = false;
101 | c._vnode = newVNode;
102 | c._parentDom = parentDom;
103 |
104 | tmp = c.render(c.props);
105 |
106 | // Handle setState called in render, see #2553
107 | c.state = c._nextState;
108 |
109 | let isTopLevelFragment =
110 | tmp != null && tmp.type == Fragment && tmp.key == null;
111 | let renderResult = isTopLevelFragment ? tmp.props.children : tmp;
112 |
113 | diffChildren({
114 | parentDom: parentDom,
115 | renderResult: Array.isArray(renderResult) ? renderResult : [renderResult],
116 | newParentVNode: newVNode,
117 | oldParentVNode: oldVNode,
118 | excessDomChildren: excessDomChildren,
119 | commitQueue: commitQueue,
120 | oldDom: oldDom,
121 | });
122 |
123 | // FIXME: 消しても問題ない
124 | c.base = newVNode._dom;
125 |
126 | // We successfully rendered this VNode, unset any stored hydration/bailout state:
127 | newVNode._hydrating = null;
128 |
129 | if (c._renderCallbacks.length) {
130 | commitQueue.push(c);
131 | }
132 |
133 | c._force = false;
134 | } else if (
135 | excessDomChildren == null &&
136 | newVNode._original === oldVNode._original
137 | ) {
138 | // FIXME: このブロックも消して問題ない
139 | newVNode._children = oldVNode._children;
140 | newVNode._dom = oldVNode._dom;
141 | } else {
142 | // newVNode が Element の時の分岐
143 | newVNode._dom = diffElementNodes({
144 | dom: oldVNode._dom, // この分岐に入るとparentDomではなくoldVNode._domを見るようになる。(階層を下る)
145 | newVNode: newVNode,
146 | oldVNode: oldVNode,
147 | excessDomChildren: excessDomChildren,
148 | commitQueue: commitQueue,
149 | }) as PreactElement;
150 | }
151 |
152 | return newVNode._dom;
153 | }
154 |
155 | /**
156 | * commitQueueを実行する関数
157 | * @param commitQueue コールバックリストを持ったcomponentのリスト
158 | */
159 | export function commitRoot(commitQueue: ComponentType[]) {
160 | commitQueue.some((c) => {
161 | commitQueue = c._renderCallbacks;
162 | c._renderCallbacks = [];
163 | commitQueue.some((cb) => {
164 | cb.call(c);
165 | });
166 | });
167 | }
168 |
169 | type DiffElementArgType = {
170 | /** diffのoldVNode._domが渡される */
171 | dom: PreactElement;
172 | newVNode: VNode;
173 | oldVNode: VNode;
174 | excessDomChildren: any;
175 | commitQueue: ComponentType[];
176 | };
177 |
178 | /**
179 | * newVNode と oldVNode を比較して dom に反映する。
180 | * ツリーではなくDOM Node のプロパティ比較が責務。
181 | */
182 | function diffElementNodes(arg: DiffElementArgType) {
183 | let { dom, newVNode, oldVNode, excessDomChildren, commitQueue } = arg;
184 | let i;
185 | let oldProps = oldVNode.props;
186 | let newProps = newVNode.props;
187 |
188 | if (dom == null) {
189 | // oldVNode._dom は 初回レンダリングはnullなのでこの分岐
190 | if (newVNode.type === null) {
191 | // primitive値であることが保証されているのでキャストする
192 | // 3 も '3' も '3' として扱う
193 | const value = String(newProps);
194 | return document.createTextNode(value);
195 | }
196 |
197 | // 初回レンダリングでDOMツリーを作る
198 | dom = document.createElement(
199 | newVNode.type,
200 | newProps.is && { is: newProps.is }
201 | );
202 | // 新しく親を作ったので既存の子は使いまわさない
203 | excessDomChildren = null;
204 | }
205 |
206 | if (newVNode.type === null) {
207 | // newVNode が primitive の場合
208 | const textNodeProps = (newProps as any) as string | number;
209 | if (oldProps !== newProps && dom.data !== textNodeProps) {
210 | dom.data = textNodeProps;
211 | }
212 | } else {
213 | // newVNode が element の場合
214 | const props: Partial["props"]> =
215 | oldVNode.props || EMPTY_OBJ;
216 |
217 | // VNodeの差分を取る。domは破壊的操作がされる
218 | diffProps(dom, newProps, props);
219 |
220 | // VNode の children に diff を取るためにchildrenを抽出
221 | i = newVNode.props.children;
222 |
223 | // newVNodeがComponentの入れ子でなくてもElementの入れ子の可能性があるので、childrenの比較も行う
224 | diffChildren({
225 | parentDom: dom,
226 | renderResult: Array.isArray(i) ? i : [i],
227 | newParentVNode: newVNode,
228 | oldParentVNode: oldVNode,
229 | excessDomChildren: excessDomChildren,
230 | commitQueue: commitQueue,
231 | oldDom: EMPTY_OBJ,
232 | });
233 | }
234 |
235 | return dom;
236 | }
237 |
238 | /**
239 | * componentWillUnmount の実行と、DOMツリーからNodeをremoveする
240 | * @param vnode
241 | * @param parentVNode
242 | * @param skipRemove
243 | */
244 | export function unmount(vnode, parentVNode, skipRemove) {
245 | let r;
246 |
247 | let dom;
248 | if (!skipRemove && typeof vnode.type != "function") {
249 | skipRemove = (dom = vnode._dom) != null;
250 | }
251 |
252 | // Must be set to `undefined` to properly clean up `_nextDom`
253 | // for which `null` is a valid value. See comment in `create-element.js`
254 | vnode._dom = vnode._nextDom = undefined;
255 |
256 | if ((r = vnode._component) != null) {
257 | if (r.componentWillUnmount) {
258 | r.componentWillUnmount();
259 | }
260 |
261 | r.base = r._parentDom = null;
262 | }
263 |
264 | if ((r = vnode._children)) {
265 | for (let i = 0; i < r.length; i++) {
266 | if (r[i]) unmount(r[i], parentVNode, skipRemove);
267 | }
268 | }
269 |
270 | if (dom != null) removeNode(dom);
271 | }
272 |
273 | /** The `.render()` method for a PFC backing instance. */
274 | function doRender(props) {
275 | return this.constructor(props);
276 | }
277 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | This document is intended for developers interest in making contributions to Preact and document our internal processes like releasing a new version.
4 |
5 | ## Getting Started
6 |
7 | This steps will help you to set up your development environment. That includes all dependencies we use to build Preact and developer tooling like git commit hooks.
8 |
9 | 1. Clone the git repository: `git clone git@github.com:preactjs/preact.git`
10 | 2. Go into the cloned folder: `cd preact/`
11 | 3. Install all dependencies: `npm install`
12 |
13 | ## The Repo Structure
14 |
15 | This repository contains Preact itself, as well as several addons like the debugging package for example. This is reflected in the directory structure of this repository. Each package has a `src/` folder where the source code can be found, a `test` folder for all sorts of tests that check if the code in `src/` is correct, and a `dist/` folder where you can find the bundled artifacts. Note that the `dist/` folder may not be present initially. It will be created as soon as you run any of the build scripts inside `package.json`. More on that later ;)
16 |
17 | A quick overview of our repository:
18 |
19 | ```bash
20 | # The repo root (folder where you cloned the repo into)
21 | /
22 | src/ # Source code of our core
23 | test/ # Unit tests for core
24 | dist/ # Build artifacts for publishing on npm (may not be present)
25 |
26 | # Sub-package, can be imported via `preact/compat` by users.
27 | # Compat stands for react-compatibility layer which tries to mirror the
28 | # react API as close as possible (mostly legacy APIs)
29 | compat/
30 | src/ # Source code of the compat addon
31 | test/ # Tests related to the compat addon
32 | dist/ # Build artifacts for publishing on npm (may not be present)
33 |
34 | # Sub-package, can be imported via `preact/hooks` by users.
35 | # The hooks API is an effect based API to deal with component lifcycles.
36 | # It's similar to hooks in React
37 | hooks/
38 | src/ # Source code of the hooks addon
39 | test/ # Tests related to the hooks addon
40 | dist/ # Build artifacts for publishing on npm (may not be present)
41 |
42 | # Sub-package, can be imported via `preact/debug` by users.
43 | # Includes debugging warnings and error messages for common mistakes found
44 | # in Preact application. Also hosts the devtools bridge
45 | debug/
46 | src/ # Source code of the debug addon
47 | test/ # Tests related to the debug addon
48 | dist/ # Build artifacts for publishing on npm (may not be present)
49 |
50 | # Sub-package, can be imported via `preact/test-utils` by users.
51 | # Provides helpers to make testing Preact applications easier
52 | test-utils/
53 | src/ # Source code of the test-utils addon
54 | test/ # Tests related to the test-utils addon
55 | dist/ # Build artifacts for publishing on npm (may not be present)
56 |
57 | # A demo application that we use to debug tricky errors and play with new
58 | # features.
59 | demo/
60 |
61 | # Contains build scripts and dependencies for development
62 | package.json
63 | ```
64 |
65 | _Note: The code for rendering Preact on the server lives in another repo and is a completely separate npm package. It can be found here: [https://github.com/preactjs/preact-render-to-string](https://github.com/preactjs/preact-render-to-string)_
66 |
67 | ### What does `mangle.json` do?
68 |
69 | It's a special file that can be used to specify how `terser` (previously known as `uglify`) will minify variable names. Because each sub-package has it's own distribution files we need to ensure that the variable names stay consistent across bundles.
70 |
71 | ## What does `options.js` do?
72 |
73 | Unique to Preact we do support several ways to hook into our renderer. All our addons use that to inject code at different stages of a render process. They are documented in our typings in `internal.d.ts`. The core itself doesn't make use of them, which is why the file only contains an empty `object`.
74 |
75 | ## Important Branches
76 |
77 | We merge every PR into the `master` branch which is the one that we'll use to publish code to npm. For the previous Preact release line we have a branch called `8` which is in maintenance mode. As a new contributor you won't have to deal with that ;)
78 |
79 | ## Creating your first Pull-Request
80 |
81 | We try to make it as easy as possible to contribute to Preact and make heavy use of GitHub's "Draft PR" feature which tags Pull-Requests (short = PR) as work in progress. PRs tend to be published as soon as there is an idea that the developer deems worthwhile to include into Preact and has written some rough code. The PR doesn't have to be perfect or anything really ;)
82 |
83 | Once a PR or a Draft PR has been created our community typically joins the discussion about the proposed change. Sometimes that includes ideas for test cases or even different ways to go about implementing a feature. Often this also includes ideas on how to make the code smaller. We usually refer to the latter as "code-golfing" or just "golfing".
84 |
85 | When everything is good to go someone will approve the PR and the changes will be merged into the `master` branch and we usually cut a release a few days/ a week later.
86 |
87 | _The big takeaway for you here is, that we will guide you along the way. We're here to help to make a PR ready for approval!_
88 |
89 | The short summary is:
90 |
91 | 1. Make changes and submit a PR
92 | 2. Modify change according to feedback (if there is any)
93 | 3. PR will be merged into `master`
94 | 4. A new release will be cut (every 2-3 weeks).
95 |
96 | ## Commonly used scripts for contributions
97 |
98 | Scripts can be executed via `npm run [script]` or `yarn [script]` respectively.
99 |
100 | - `build` - compiles all packages ready for publishing to npm
101 | - `build:core` - builds just Preact itself
102 | - `build:debug` - builds the debug addon only
103 | - `build:hooks` - builds the hook addon only
104 | - `build:test-utils` - builds the test-utils addon only
105 | - `test:ts` - Run all tests for TypeScript definitions
106 | - `test:karma` - Run all unit/integration tests.
107 | - `test:karma:watch` - Same as above, but it will automatically re-run the test suite if a code change was detected.
108 |
109 | But to be fair, the only ones we use ourselves are `build` and `test:karma:watch`. The other ones are mainly used on our CI pipeline and we rarely use them.
110 |
111 | _Note: Both `test:karma` and `test:karma:watch` listen to the environment variable `COVERAGE=true`. Disabling code coverage can significantly speed up the time it takes to complete the test suite._
112 |
113 | _Note2: The test suite is based on `karma` and `mocha`. Individual tests can be executed by appending `.only`:_
114 |
115 | ```jsx
116 | it.only('should test something', () => {
117 | expect(1).to.equal(1);
118 | });
119 | ```
120 |
121 | ## Common terminology and variable names
122 |
123 | - `vnode` -> shorthand for `virtual-node` which is an object that specifies how a Component or DOM-node looks like
124 | - `commit` -> A commit is the moment in time when you flush all changes to the DOM
125 | - `c` -> The variable `c` always refers to a `component` instance throughout our code base.
126 | - `diff/diffing` -> Diffing describes the process of comparing two "things". In our case we compare the previous `vnode` tree with the new one and apply the delta to the DOM.
127 | - `root` -> The topmost node of a `vnode` tree
128 |
129 | ## Tips for getting to know the code base
130 |
131 | - Check the JSDoc block right above the function definition to understand what it does. It contains a short description of each function argument and what it does.
132 | - Check the callsites of a function to understand how it's used. Modern editors/IDEs allow you to quickly find those, or use the plain old search feature instead.
133 |
134 | ## FAQ
135 |
136 | ### Why does the JSDoc use TypeScript syntax to specify types?
137 |
138 | Several members of the team are very fond of TypeScript and we wanted to leverage as many of its advantages, like improved autocompletion, for Preact. We even attempted to port Preact to TypeScript a few times, but we ran into many issues with the DOM typings. Those would force us to fill our codebase with many `any` castings, making our code very noisy.
139 |
140 | Luckily TypeScript has a mode where it can somewhat reliably typecheck JavaScript code by reusing the types defined in JSDoc blocks. It's not perfect and it often has trouble inferring the correct types the further one strays away from the function arguments, but it's good enough that it helps us a lot with autocompletion. Another plus is that we can make sure that our TypeScript definitons are correct at the same time.
141 |
142 | Check out the [official TypeScript documentation](https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html) for more information.
143 |
144 | _Note that we have separate tests for our TypeScript definition files. We only use `ts-check` for local development and don't check it anywhere else like on the CI._
145 |
146 | ### Why does the code base often use `let` instead of `const`?
147 |
148 | There is no real reason for that other a historical one. Back before auto-formatting via prettier was a thing and minifiers weren't as advanced as they are today we used a pretty terse code-style. The code-style deliberately was aimed at making code look as concise and short as possible. The `let` keyword is a bit shorter than `const` to write, so we only used that. This was done only for stylistic reasons.
149 |
150 | This helped our minds to not lose sight of focusing on size, but made it difficult for newcomers to start contributing to Preact. For that reason alone we switched to `prettier` and loosened our rule regarding usage of `let` or `const`. Today we use both, but you can still find many existing places where `let` is still in use.
151 |
152 | In the end there is no effect on size regardless if you use `const`, `let` or use both. Our code is downtranspiled to `ES5` for npm so both will be replaced with `var` anyways. Therefore it doesn't really matter at all which one is used in our codebase.
153 |
154 | This will only become important once shipping modern JavaScript code on npm becomes a thing and bundlers follow suit.
155 |
156 | ## How to create a good bug report
157 |
158 | To be able to fix issues we need to see them on our machine. This is only possible when we can reproduce the error. The easiest way to do that is narrow down the problem to specific components or combination of them. This can be done by removing as much unrelated code as possible.
159 |
160 | The perfect way to do that is to make a [codesandbox](https://codesandbox.io/). That way you can easily share the problematic code and ensure that others can see the same issue you are seeing.
161 |
162 | For us a [codesandbox](https://codesandbox.io/) says more than a 1000 words :tada:
163 |
164 | ## I have more questions on how to contribute to Preact. How can I reach you?
165 |
166 | We closely watch our issues and have a pretty active [Slack workspace](https://chat.preactjs.com/). Nearly all our communication happens via these two forms of communication.
167 |
168 | ## Releasing Preact (Maintainers only)
169 |
170 | This guide is intended for core team members that have the necessary
171 | rights to publish new releases on npm.
172 |
173 | 1. [Write the release notes](#writing-release-notes) and keep them as a draft in GitHub
174 | 1. I'd recommend writing them in an offline editor because each edit to a draft will change the URL in GitHub.
175 | 2. Make a PR where **only** the version number is incremented in `package.json` (note: We follow `SemVer` conventions)
176 | 3. Wait until the PR is approved and merged.
177 | 4. Switch back to the `master` branch and pull the merged PR
178 | 5. Run `npm run build && npm publish`
179 | 1. Make sure you have 2FA enabled in npm, otherwise the above command will fail.
180 | 2. If you're doing a pre-release add `--tag next` to the `npm publish` command to publish it under a different tag (default is `latest`)
181 | 6. Publish the release notes and create the correct git tag.
182 | 7. Tweet it out
183 |
184 | ## Legacy Releases (8.x)
185 |
186 | > **ATTENTION:** Make sure that you've cleared the project correctly
187 | > when switching from a 10.x branch.
188 |
189 | 0. Run `rm -rf dist node_modules && npm i` to make sure to have the correct dependencies.
190 |
191 | Apart from that it's the same as above.
192 |
193 | ## Writing release notes
194 |
195 | The release notes have become a sort of tiny blog post about what's
196 | happening in preact-land. The title usually has this format:
197 |
198 | ```txt
199 | Version Name
200 | ```
201 |
202 | Example:
203 |
204 | ```txt
205 | 10.0.0-beta.1 Los Compresseros
206 | ```
207 |
208 | The name is optional, we just have fun finding creative names :wink:
209 |
210 | To keep them interesting we try to be as
211 | concise as possible and to just reflect where we are. There are some
212 | rules we follow while writing them:
213 |
214 | - Be nice, use a positive tone. Avoid negative words
215 | - Show, don't just tell.
216 | - Be honest.
217 | - Don't write too much, keep it simple and short.
218 | - Avoid making promises and don't overpromise. That leads to unhappy users
219 | - Avoid framework comparisons if possible
220 | - Highlight awesome community contributions (or great issue reports)
221 | - If in doubt, praise the users.
222 |
223 | After this section we typically follow with a changelog part that's
224 | divided into 4 groups in order of importance for the user:
225 |
226 | - Features
227 | - Bug Fixes
228 | - Typings
229 | - Maintenance
230 |
231 | We generate it via this handy cli program: [changelogged](https://github.com/marvinhagemeister/changelogged). It will collect and format
232 | the descriptions of all PRs that have been merged between two tags.
233 | The usual command is `changelogged 10.0.0-rc.2..HEAD` similar to how
234 | you'd diff two points in time with git. This will get you 90% there,
235 | but you still need to divide it into groups. It's also a good idea
236 | to unify the formatting of the descriptions, so that they're easier
237 | to read and don't look like a mess.
238 |
--------------------------------------------------------------------------------