UNPKG

6.63 kBJavaScriptView Raw
1import { assign } from './util';
2import { diff, commitRoot } from './diff/index';
3import options from './options';
4import { Fragment } from './create-element';
5
6/**
7 * Base Component class. Provides `setState()` and `forceUpdate()`, which
8 * trigger rendering
9 * @param {object} props The initial component props
10 * @param {object} context The initial context from parent components'
11 * getChildContext
12 */
13export function Component(props, context) {
14 this.props = props;
15 this.context = context;
16}
17
18/**
19 * Update component state and schedule a re-render.
20 * @param {object | ((s: object, p: object) => object)} update A hash of state
21 * properties to update with new values or a function that given the current
22 * state and props returns a new partial state
23 * @param {() => void} [callback] A function to be called once component state is
24 * updated
25 */
26Component.prototype.setState = function(update, callback) {
27 // only clone state when copying to nextState the first time.
28 let s;
29 if (this._nextState !== this.state) {
30 s = this._nextState;
31 } else {
32 s = this._nextState = assign({}, this.state);
33 }
34
35 if (typeof update == 'function') {
36 update = update(s, this.props);
37 }
38
39 if (update) {
40 assign(s, update);
41 }
42
43 // Skip update if updater function returned null
44 if (update == null) return;
45
46 if (this._vnode) {
47 if (callback) this._renderCallbacks.push(callback);
48 enqueueRender(this);
49 }
50};
51
52/**
53 * Immediately perform a synchronous re-render of the component
54 * @param {() => void} [callback] A function to be called after component is
55 * re-rendered
56 */
57Component.prototype.forceUpdate = function(callback) {
58 if (this._vnode) {
59 // Set render mode so that we can differentiate where the render request
60 // is coming from. We need this because forceUpdate should never call
61 // shouldComponentUpdate
62 this._force = true;
63 if (callback) this._renderCallbacks.push(callback);
64 enqueueRender(this);
65 }
66};
67
68/**
69 * Accepts `props` and `state`, and returns a new Virtual DOM tree to build.
70 * Virtual DOM is generally constructed via [JSX](http://jasonformat.com/wtf-is-jsx).
71 * @param {object} props Props (eg: JSX attributes) received from parent
72 * element/component
73 * @param {object} state The component's current state
74 * @param {object} context Context object, as returned by the nearest
75 * ancestor's `getChildContext()`
76 * @returns {import('./index').ComponentChildren | void}
77 */
78Component.prototype.render = Fragment;
79
80/**
81 * @param {import('./internal').VNode} vnode
82 * @param {number | null} [childIndex]
83 */
84export function getDomSibling(vnode, childIndex) {
85 if (childIndex == null) {
86 // Use childIndex==null as a signal to resume the search from the vnode's sibling
87 return vnode._parent
88 ? getDomSibling(vnode._parent, vnode._parent._children.indexOf(vnode) + 1)
89 : null;
90 }
91
92 let sibling;
93 for (; childIndex < vnode._children.length; childIndex++) {
94 sibling = vnode._children[childIndex];
95
96 if (sibling != null && sibling._dom != null) {
97 // Since updateParentDomPointers keeps _dom pointer correct,
98 // we can rely on _dom to tell us if this subtree contains a
99 // rendered DOM node, and what the first rendered DOM node is
100 return sibling._dom;
101 }
102 }
103
104 // If we get here, we have not found a DOM node in this vnode's children.
105 // We must resume from this vnode's sibling (in it's parent _children array)
106 // Only climb up and search the parent if we aren't searching through a DOM
107 // VNode (meaning we reached the DOM parent of the original vnode that began
108 // the search)
109 return typeof vnode.type == 'function' ? getDomSibling(vnode) : null;
110}
111
112/**
113 * Trigger in-place re-rendering of a component.
114 * @param {import('./internal').Component} component The component to rerender
115 */
116function renderComponent(component) {
117 let vnode = component._vnode,
118 oldDom = vnode._dom,
119 parentDom = component._parentDom;
120
121 if (parentDom) {
122 let commitQueue = [];
123 const oldVNode = assign({}, vnode);
124 oldVNode._original = oldVNode;
125
126 let newDom = diff(
127 parentDom,
128 vnode,
129 oldVNode,
130 component._globalContext,
131 parentDom.ownerSVGElement !== undefined,
132 null,
133 commitQueue,
134 oldDom == null ? getDomSibling(vnode) : oldDom
135 );
136 commitRoot(commitQueue, vnode);
137
138 if (newDom != oldDom) {
139 updateParentDomPointers(vnode);
140 }
141 }
142}
143
144/**
145 * @param {import('./internal').VNode} vnode
146 */
147function updateParentDomPointers(vnode) {
148 if ((vnode = vnode._parent) != null && vnode._component != null) {
149 vnode._dom = vnode._component.base = null;
150 for (let i = 0; i < vnode._children.length; i++) {
151 let child = vnode._children[i];
152 if (child != null && child._dom != null) {
153 vnode._dom = vnode._component.base = child._dom;
154 break;
155 }
156 }
157
158 return updateParentDomPointers(vnode);
159 }
160}
161
162/**
163 * The render queue
164 * @type {Array<import('./internal').Component>}
165 */
166let rerenderQueue = [];
167let rerenderCount = 0;
168
169/**
170 * Asynchronously schedule a callback
171 * @type {(cb: () => void) => void}
172 */
173/* istanbul ignore next */
174// Note the following line isn't tree-shaken by rollup cuz of rollup/rollup#2566
175const defer =
176 typeof Promise == 'function'
177 ? Promise.prototype.then.bind(Promise.resolve())
178 : setTimeout;
179
180/*
181 * The value of `Component.debounce` must asynchronously invoke the passed in callback. It is
182 * important that contributors to Preact can consistently reason about what calls to `setState`, etc.
183 * do, and when their effects will be applied. See the links below for some further reading on designing
184 * asynchronous APIs.
185 * * [Designing APIs for Asynchrony](https://blog.izs.me/2013/08/designing-apis-for-asynchrony)
186 * * [Callbacks synchronous and asynchronous](https://blog.ometer.com/2011/07/24/callbacks-synchronous-and-asynchronous/)
187 */
188
189let prevDebounce;
190
191/**
192 * Enqueue a rerender of a component
193 * @param {import('./internal').Component} c The component to rerender
194 */
195export function enqueueRender(c) {
196 if (
197 (!c._dirty &&
198 (c._dirty = true) &&
199 rerenderQueue.push(c) &&
200 !rerenderCount++) ||
201 prevDebounce !== options.debounceRendering
202 ) {
203 prevDebounce = options.debounceRendering;
204 (prevDebounce || defer)(process);
205 }
206}
207
208/** Flush the render queue by rerendering all queued components */
209function process() {
210 let queue;
211 while ((rerenderCount = rerenderQueue.length)) {
212 queue = rerenderQueue.sort((a, b) => a._vnode._depth - b._vnode._depth);
213 rerenderQueue = [];
214 // Don't update `renderCount` yet. Keep its value non-zero to prevent unnecessary
215 // process() calls from getting scheduled while `queue` is still being consumed.
216 queue.some(c => {
217 if (c._dirty) renderComponent(c);
218 });
219 }
220}