我使用document.registerElement
API(Chrome中的原生代码)和一些使用motor-scene
和motor-node
自定义元素的标记。
<body>
<div id="one" style="width: 500px; height: 500px; outline: 1px solid green; float: left;">
<motor-scene id="foo">
<motor-node
rotation="[0,30,0]"
absoluteSize="[100, 100, 0]"
position="[300, 100, 20]"
opacity="0.5"
sizeMode="[absolute, absolute, absolute]">
<div style="background: pink; width: 100%; height: 100%;">
<h2>Hello HTML!</h2>
</div>
<motor-node
rotation="[30,0,0]"
absoluteSize="[50, 50, 0]"
position="[50, -70, -200]"
opacity="0.5"
sizeMode="[absolute, absolute, absolute]">
<div style="background: teal; width: 100%; height: 100%;">
Hello HTML!
</div>
</motor-node>
</motor-node>
</motor-scene>
</div>
<div id="two" style="width: 500px; height: 500px; outline: 1px solid green; float: left;">
</div>
</body>
如您所见,标记中总共有3个自定义元素。 motor-scene
从motor-node
延伸(即提供给motor-scene
的{{1}}原型是使用document.registerElement
创建的。我能够确认浏览器创建了5个自定义元素实例因为Object.create(motorNodePrototype)
原型的createdCallback
被调用了5次。
我相信motor-node
createdCallback应该只发射3次。它为什么会发射5次?
这是我的错。我忘记了为检查motor-node
是否正常工作而创建额外实例的其他一些行:
document.registerElement
供参考,以下是两个自定义元素(也是available on GitHub)的定义:
console.log('<motor-scene> element registered?', document.createElement('motor-scene').constructor.name == 'motor-scene')
console.log('<motor-node> element registered?', document.createElement('motor-node').constructor.name == 'motor-node')
import Scene from '../motor/Scene'
import Node from '../motor/Node'
import jss from '../jss'
/**
* @class MotorHTMLNode
*/
const MotorHTMLNode = document.registerElement('motor-node', {
prototype: Object.assign(Object.create(HTMLElement.prototype), {
createdCallback() {
console.log('<motor-node> createdCallback()')
this._attached = false
this.attachPromise = null
this._cleanedUp = true
this.node = this.makeNode()
this.createChildObserver()
this.childObserver.observe(this, { childList: true })
// TODO: mountPromise for Node, not just Scene.
if (this.nodeName == 'MOTOR-SCENE') {
// XXX: "mountPromise" vs "ready":
//
// "ready" seems to be more intuitive on the HTML side because
// if the user has a reference to a motor-node or a motor-scene
// and it exists in DOM, then it is already "mounted" from the
// HTML API perspective. Maybe we can use "mountPromise" for
// the imperative API, and "ready" for the HTML API. For example:
//
// await $('motor-scene')[0].ready // When using the HTML API
// await node.mountPromise // When using the imperative API
//
// Or, maybe we can just use "ready" for both cases?...
this.mountPromise = this.node.mountPromise
this.ready = this.mountPromise
}
},
makeNode() {
return new Node
},
createChildObserver() {
this.childObserver = new MutationObserver(mutations => {
mutations.forEach(mutation => {
let nodes = Array.from(mutation.addedNodes)
nodes = nodes.filter(node => {
let keep = true
if (
node.nodeName.match(/^MOTOR-/)
|| (
// Ignore the motorDomSceneContainer because we
// won't move it (that should stay in place inside
// of the <motor-scene> element). Other elements
// get moved into the scene graph (for example, if
// you put a <div> inside of a <motor-node>, then
// that <div> gets transplanted into the Motor
// scene graph DOM tree which is rooted in the
// <motor-scene>. You'll understand what this means
// now if you take a look in the element inspector.
node.className // some nodes don't have a class name (#text, #comment, #document).
&& node.className.match(/^motorDomSceneContainer/)
)
) {
keep = false
}
return keep
})
nodes.forEach(node => {
// this is kind of a hack: we remove the content
// from the motor-node in the actual DOM and put
// it in the node-controlled element, which may
// make it a little harder to debug, but at least
// for now it works.
this.node.element.element.appendChild(node)
})
})
})
},
async attachedCallback() {
console.log('<motor-node> attachedCallback()')
this._attached = true
// If the node is currently being attached, wait for that to finish
// before attaching again, to avoid a race condition. This will
// almost never happen, but just in case, it'll protect against
// naive programming on the end-user's side (f.e., if they attach
// the motor-node element to the DOM then move it to a new element
// within the same tick.
await this.attachPromise
this.attachPromise = new Promise(async (resolve) => {
if (this._cleanedUp) {
this._cleanedUp = false
this.childObserver.observe(this, { childList: true })
}
// The scene doesn't have a parent to attach to.
if (this.nodeName.toLowerCase() != 'motor-scene')
this.parentNode.node.addChild(this.node)
resolve()
})
},
async detachedCallback() {
console.log('<motor-node> detachedCallback()')
this._attached = false
// If the node is currently being attached, wait for that to finish
// before starting the detach process (to avoid a race condition).
// if this.attachPromise is null, excution continues without
// going to the next tick (TODO: is this something we can rely on
// in the language spec?).
if (this.attachPromise) await this.attachPromise
this.attachPromise = null
// XXX For performance, deferr to the next tick before cleaning up
// in case the element is actually being re-attached somewhere else
// within this same tick (detaching and attaching is synchronous,
// so by deferring to the next tick we'll be able to know if the
// element was re-attached or not in order to clean up or not), in
// which case we want to preserve the style sheet, preserve the
// animation frame, and keep the scene in the sceneList. {{
await Promise.resolve() // deferr to the next tick.
// If the scene wasn't re-attached, clean up. TODO (performance):
// How can we coordinate this with currently running animations so
// that Garabage Collection doesn't make the frames stutter?
if (!this._attached) {
this.cleanUp()
this._cleanedUp = true
}
// }}
},
cleanUp() {
cancelAnimationFrame(this.rAF)
this.childObserver.disconnect()
},
attributeChangedCallback(attribute, oldValue, newValue) {
console.log('<motor-node> attributeChangedCallback()')
this.updateNodeProperty(attribute, oldValue, newValue)
},
updateNodeProperty(attribute, oldValue, newValue) {
// TODO: Handle actual values (not just string property values as
// follows) for performance; especially when DOMMatrix is supported
// by browsers.
// attributes on our HTML elements are the same name as those on
// the Node class (the setters).
if (newValue !== oldValue) {
if (attribute.match(/opacity/i))
this.node[attribute] = parseFloat(newValue)
else if (attribute.match(/sizemode/i))
this.node[attribute] = parseStringArray(newValue)
else if (
attribute.match(/rotation/i)
|| attribute.match(/scale/i)
|| attribute.match(/position/i)
|| attribute.match(/absoluteSize/i)
|| attribute.match(/proportionalSize/i)
|| attribute.match(/align/i)
|| attribute.match(/mountPoint/i)
|| attribute.match(/origin/i) // TODO on imperative side.
) {
this.node[attribute] = parseNumberArray(newValue)
}
else { /* crickets */ }
}
},
}),
})
export default MotorHTMLNode
function parseNumberArray(str) {
if (!isNumberArrayString(str))
throw new Error(`Invalid array. Must be an array of numbers of length 3.`)
let numbers = str.split('[')[1].split(']')[0].split(',')
numbers = numbers.map(num => window.parseFloat(num))
return numbers
}
function parseStringArray(str) {
let strings = str.split('[')[1].split(']')[0].split(',')
strings = strings.map(str => str.trim())
return strings
}
function isNumberArrayString(str) {
return !!str.match(/^\s*\[\s*(-?((\d+\.\d+)|(\d+))(\s*,\s*)?){3}\s*\]\s*$/g)
}
import Node from '../motor/Node'
import Scene from '../motor/Scene'
import MotorHTMLNode from './node'
import jss from '../jss'
const sceneList = []
let style = null
/**
* @class MotorHTMLScene
* @extends MotorHTMLNode
*/
const MotorHTMLScene = document.registerElement('motor-scene', {
prototype: Object.assign(Object.create(MotorHTMLNode.prototype), {
createdCallback() {
MotorHTMLNode.prototype.createdCallback.call(this)
console.log('<motor-scene> createdCallback()')
sceneList.push(this)
if (!style) {
style = jss.createStyleSheet({
motorSceneElement: {
boxSizing: 'border-box',
display: 'block',
overflow: 'hidden',
},
})
}
style.attach()
this.classList.add(style.classes.motorSceneElement)
},
makeNode() {
return new Scene(this)
},
cleanUp() {
MotorHTMLNode.prototype.cleanUp.call(this)
sceneList.pop(this)
// TODO: unmount the scene
// dispose of the scene style if we no longer have any scenes
// attached anywhere.
// TODO (performance): Would requesting an animation frame when
// detaching or attaching a stylesheet make things perform
// better?
if (sceneList.length == 0) {
style.detach()
style = null
}
},
attributeChangedCallback(attribute, oldValue, newValue) {
MotorHTMLNode.prototype.attributeChangedCallback.call(this)
console.log('<motor-scene> attributeChangedCallback()')
this.updateSceneProperty(attribute, oldValue, newValue)
},
updateSceneProperty(attribute, oldValue, newValue) {
// ...
},
}),
})
export default MotorHTMLScene
五次登录到控制台。