Skip to content

Commit 814261f

Browse files
committed
Add more type checks, unify auto-redraw logic, optimize element creation better
1 parent 0c99734 commit 814261f

File tree

12 files changed

+105
-80
lines changed

12 files changed

+105
-80
lines changed

src/core.js

Lines changed: 38 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable no-bitwise */
2-
import {hasOwn} from "./util.js"
2+
import {checkCallback, hasOwn, invokeRedrawable} from "./util.js"
33

44
export {m as default}
55

@@ -286,43 +286,23 @@ m.TYPE_SET_CONTEXT = TYPE_SET_CONTEXT
286286
m.TYPE_USE = TYPE_USE
287287
m.TYPE_INLINE = TYPE_INLINE
288288

289-
// Simple and sweet. Also useful for idioms like `onfoo: m.capture` to drop events without
290-
// redrawing.
289+
// Simple and sweet. Also useful for idioms like `onfoo: m.capture` to completely drop events while
290+
// otherwise ignoring them.
291291
m.capture = (ev) => {
292292
ev.preventDefault()
293293
ev.stopPropagation()
294-
return false
294+
return "skip-redraw"
295295
}
296296

297297
m.retain = () => Vnode(TYPE_RETAIN, null, null, null)
298-
299-
m.inline = (callback) => {
300-
if (typeof callback !== "function") {
301-
throw new TypeError("Callback must be a function.")
302-
}
303-
return Vnode(TYPE_INLINE, null, callback, null)
304-
}
305-
306-
m.layout = (callback) => {
307-
if (typeof callback !== "function") {
308-
throw new TypeError("Callback must be a function.")
309-
}
310-
return Vnode(TYPE_LAYOUT, null, callback, null)
311-
}
312-
313-
m.remove = (callback) => {
314-
if (typeof callback !== "function") {
315-
throw new TypeError("Callback must be a function.")
316-
}
317-
return Vnode(TYPE_REMOVE, null, callback, null)
318-
}
298+
m.inline = (view) => Vnode(TYPE_INLINE, null, checkCallback(view, false, "view"), null)
299+
m.layout = (callback) => Vnode(TYPE_LAYOUT, null, checkCallback(callback), null)
300+
m.remove = (callback) => Vnode(TYPE_REMOVE, null, checkCallback(callback), null)
319301

320302
m.Fragment = (attrs) => attrs.children
321303

322304
m.keyed = (values, view) => {
323-
if (view != null && typeof view !== "function") {
324-
throw new TypeError("Callback must be a function if provided")
325-
}
305+
view = checkCallback(view, true, "view")
326306
var map = new Map()
327307
for (var value of values) {
328308
if (typeof view === "function") value = view(value)
@@ -586,19 +566,19 @@ var updateText = (old, vnode) => {
586566

587567
var handleAttributeError = (old, e, force) => {
588568
if (currentRemoveOnThrow || force) {
589-
removeNode(old)
590-
updateFragment(old, null)
569+
if (old) removeElement(old)
591570
throw e
592571
}
593572
console.error(e)
594573
}
595574

596575
var updateElement = (old, vnode) => {
597576
var prevParent = currentParent
577+
var prevRefNode = currentRefNode
598578
var prevNamespace = currentNamespace
599579
var mask = vnode.m
600580
var attrs = vnode.a
601-
var element , oldAttrs
581+
var element, oldAttrs
602582

603583
if (old == null) {
604584
var entry = selectorCache.get(vnode.t)
@@ -608,11 +588,11 @@ var updateElement = (old, vnode) => {
608588
var ns = attrs && attrs.xmlns || nameSpace[tag] || prevNamespace
609589
var opts = is ? {is} : null
610590

611-
insertAfterCurrentRefNode(element = vnode.d = (
591+
element = (
612592
ns
613593
? currentDocument.createElementNS(ns, tag, opts)
614594
: currentDocument.createElement(tag, opts)
615-
))
595+
)
616596

617597
if (ns == null) {
618598
// Doing it this way since it doesn't seem Terser is smart enough to optimize the `if` with
@@ -709,8 +689,17 @@ var updateElement = (old, vnode) => {
709689
}
710690

711691
currentParent = prevParent
712-
currentRefNode = element
692+
currentRefNode = prevRefNode
713693
currentNamespace = prevNamespace
694+
695+
// Do this as late as possible to reduce how much work browsers have to do to reduce style
696+
// recalcs during initial (sub)tree construction. Also will defer `adoptNode` callbacks in
697+
// custom elements until the last possible point (which will help accelerate some of them).
698+
if (old == null) {
699+
insertAfterCurrentRefNode(vnode.d = element)
700+
}
701+
702+
currentRefNode = element
714703
}
715704

716705
var updateComponent = (old, vnode) => {
@@ -757,6 +746,11 @@ var removeNode = (old) => {
757746
}
758747
}
759748

749+
var removeElement = (old) => {
750+
removeNode(old)
751+
updateFragment(old, null)
752+
}
753+
760754
var removeInstance = (old) => updateNode(old.c, null)
761755

762756
// Replaces an otherwise necessary `switch`.
@@ -777,10 +771,7 @@ var removeNodeDispatch = [
777771
removeFragment,
778772
removeKeyed,
779773
removeNode,
780-
(old) => {
781-
removeNode(old)
782-
updateFragment(old, null)
783-
},
774+
removeElement,
784775
removeInstance,
785776
() => {},
786777
(old) => currentHooks.push(old),
@@ -1096,13 +1087,7 @@ var setAttr = (vnode, element, mask, key, old, attrs) => {
10961087
// return a promise that resolves to it.
10971088
class EventDict extends Map {
10981089
async handleEvent(ev) {
1099-
var handler = this.get(`on${ev.type}`)
1100-
if (typeof handler === "function") {
1101-
var result = handler.call(ev.currentTarget, ev)
1102-
if (result === false) return
1103-
if (result && typeof result.then === "function" && (await result) === false) return
1104-
(0, this._)()
1105-
}
1090+
invokeRedrawable(this._, this.get(`on${ev.type}`), ev.currentTarget, ev)
11061091
}
11071092
}
11081093

@@ -1111,13 +1096,16 @@ class EventDict extends Map {
11111096
var currentlyRendering = []
11121097

11131098
m.render = (dom, vnode, {redraw, removeOnThrow} = {}) => {
1114-
if (!dom) throw new TypeError("DOM element being rendered to does not exist.")
1115-
if (currentlyRendering.some((d) => d === dom || d.contains(dom))) {
1116-
throw new TypeError("Node is currently being rendered to and thus is locked.")
1099+
if (!dom) {
1100+
throw new TypeError("DOM element being rendered to does not exist.")
11171101
}
11181102

1119-
if (redraw != null && typeof redraw !== "function") {
1120-
throw new TypeError("Redraw must be a function if given.")
1103+
checkCallback(redraw, true, "redraw")
1104+
1105+
for (var root of currentlyRendering) {
1106+
if (dom.contains(root)) {
1107+
throw new TypeError("Node is currently being rendered to and thus is locked.")
1108+
}
11211109
}
11221110

11231111
var active = dom.ownerDocument.activeElement

src/std/init.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import m from "../core.js"
22

3+
import {checkCallback, invokeRedrawable} from "../util.js"
4+
35
function Init({f}, old) {
46
if (old) return m.retain()
57
var ctrl = new AbortController()
6-
queueMicrotask(async () => {
7-
if ((await f(ctrl.signal)) !== false) this.redraw()
8-
})
8+
queueMicrotask(() => invokeRedrawable(this.redraw, f, undefined, ctrl.signal))
99
return m.remove(() => ctrl.abort())
1010
}
1111

12-
var init = (f) => m(Init, {f})
12+
var init = (f) => m(Init, {f: checkCallback(f)})
1313

1414
export {init as default}

src/std/lazy.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import m from "../core.js"
22

3+
import {checkCallback} from "../util.js"
4+
35
var lazy = (opts) => {
6+
checkCallback(opts.fetch, false, "opts.fetch")
7+
checkCallback(opts.pending, true, "opts.pending")
8+
checkCallback(opts.error, true, "opts.error")
9+
410
// Capture the error here so stack traces make more sense
511
var error = new ReferenceError("Component not found")
612
var redraws = new Set()
713
var Comp = function () {
8-
redraws.add(this.redraw)
14+
redraws.add(checkCallback(this.redraw, false, "context.redraw"))
915
return opts.pending && opts.pending()
1016
}
1117
var init = async () => {

src/std/router.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/* global window: false */
22
import m from "../core.js"
33

4+
import {checkCallback} from "../util.js"
5+
46
var Route = function ({p: prefix}) {
57
var href = this.href
68
var mustReplace, redraw, currentParsedHref
@@ -60,7 +62,7 @@ var Route = function ({p: prefix}) {
6062
updateRouteWithHref()
6163

6264
return function ({v: view}) {
63-
redraw = this.redraw
65+
redraw = checkCallback(this.redraw, false, "context.redraw")
6466

6567
return [
6668
m.remove(() => window.removeEventListener("popstate", updateRoute)),
@@ -74,11 +76,7 @@ export var route = (prefix, view) => {
7476
throw new TypeError("The route prefix must be a string")
7577
}
7678

77-
if (typeof view !== "function") {
78-
throw new TypeError("Router view must be a function.")
79-
}
80-
81-
return m(Route, {v: view, p: prefix})
79+
return m(Route, {v: checkCallback(view, false, "view"), p: prefix})
8280
}
8381

8482
// Let's provide a *right* way to manage a route link, rather than letting people screw up

src/std/tracked.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ why that was removed in favor of this:
4545
work around it, you'd have to do something like this anyways.
4646
*/
4747

48+
import {checkCallback} from "../util.js"
49+
4850
/**
4951
* @template K, V
5052
* @typedef TrackedHandle
@@ -75,6 +77,8 @@ why that was removed in favor of this:
7577
* @returns {Tracked<K, V>}
7678
*/
7779
var tracked = (redraw, initial) => {
80+
checkCallback(redraw, false, "redraw")
81+
7882
/** @type {Map<K, TrackedHandle<K, V> & {_: AbortController}>} */ var state = new Map()
7983
/** @type {Set<TrackedHandle<K, V>>} */ var live = new Set()
8084

src/std/with-progress.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import {checkCallback} from "../util.js"
2+
13
/**
24
* @param {ReadableStream<Uint8Array> | null} source
35
* @param {(current: number) => void} notify
46
*/
57
export default (source, notify) => {
8+
checkCallback(notify, false, "notify")
9+
610
var reader = source && source.getReader()
711
var current = 0
812

src/util.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,18 @@
11
export var hasOwn = {}.hasOwnProperty
2+
3+
export var invokeRedrawable = async (redraw, fn, thisValue, ...args) => {
4+
if (typeof fn === "function") {
5+
thisValue = Reflect.apply(fn, thisValue, args)
6+
if (thisValue === "skip-redraw") return
7+
if (thisValue && typeof thisValue.then === "function" && (await thisValue) === "skip-redraw") return
8+
redraw()
9+
}
10+
}
11+
12+
export var checkCallback = (callback, allowNull, label = "callback") => {
13+
if (allowNull && callback == null || typeof callback === "function") {
14+
return callback
15+
}
16+
17+
throw new TypeError(`\`${label}\` must be a function${allowNull ? " if provided." : "."}`)
18+
}

tests/core/event.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ o.spec("event", function() {
8585
})
8686
})
8787

88-
o("handles onclick returning false", function() {
89-
var spyDiv = eventSpy((e) => { m.capture(e); return false })
88+
o("handles onclick returning `\"skip-redraw\"`", function() {
89+
var spyDiv = eventSpy((e) => { m.capture(e); return "skip-redraw" })
9090
var spyParent = eventSpy()
9191
var div = m("div", {onclick: spyDiv})
9292
var parent = m("div", {onclick: spyParent}, div)
@@ -106,9 +106,9 @@ o.spec("event", function() {
106106
o(e.defaultPrevented).equals(true)
107107
})
108108

109-
o("handles onclick asynchronously returning false", function() {
109+
o("handles onclick asynchronously returning `\"skip-redraw\"`", function() {
110110
var promise
111-
var spyDiv = eventSpy((e) => { m.capture(e); return promise = Promise.resolve(false) })
111+
var spyDiv = eventSpy((e) => { m.capture(e); return promise = Promise.resolve("skip-redraw") })
112112
var spyParent = eventSpy()
113113
var div = m("div", {onclick: spyDiv})
114114
var parent = m("div", {onclick: spyParent}, div)
@@ -132,8 +132,8 @@ o.spec("event", function() {
132132
})
133133
})
134134

135-
o("handles onclick returning false in child then bubbling to parent and not returning false", function() {
136-
var spyDiv = eventSpy(() => false)
135+
o("handles onclick returning `\"skip-redraw\"` in child then bubbling to parent and not returning `\"skip-redraw\"`", function() {
136+
var spyDiv = eventSpy(() => "skip-redraw")
137137
var spyParent = eventSpy()
138138
var div = m("div", {onclick: spyDiv})
139139
var parent = m("div", {onclick: spyParent}, div)

tests/core/hyperscript.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -732,7 +732,7 @@ o.spec("hyperscript", function() {
732732
// Only doing this for the sake of initializing the required fields in the mock.
733733
G.root.dispatchEvent(e)
734734

735-
o(m.capture(e)).equals(false)
735+
o(m.capture(e)).equals("skip-redraw")
736736
o(e.defaultPrevented).equals(true)
737737
o(e.cancelBubble).equals(true)
738738
})

tests/core/mountRedraw.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ o.spec("mount/redraw", function() {
445445
e.initEvent("click", true, true)
446446

447447
m.mount(G.root, () => m("div", {
448-
onclick: () => false,
448+
onclick: () => "skip-redraw",
449449
}, m.layout(layout)))
450450

451451
G.root.firstChild.dispatchEvent(e)

0 commit comments

Comments
 (0)