Javascript 为什么自定义元素API会创建重复的元素?
我使用的是Javascript 为什么自定义元素API会创建重复的元素?,javascript,polymer,web-component,custom-element,Javascript,Polymer,Web Component,Custom Element,我使用的是文档.registereElementAPI(Chrome自带),带有一些标记,如下面使用运动场景和运动节点自定义元素的标记 <body> <div id="one" style="width: 500px; height: 500px; outline: 1px solid green; float: left;"> <motor-scene id="foo"> <motor-node
文档.registereElement
API(Chrome自带),带有一些标记,如下面使用运动场景
和运动节点
自定义元素的标记
<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>
以下是两个自定义元素(也包括)的定义,以供参考: 运动节点:
从“../motor/Scene”导入场景
从“../motor/Node”导入节点
从“../jss”导入jss
/**
*@class-MotorHTMLNode
*/
const MotorHTMLNode=document.registerement('motor-node'{
原型:Object.assign(Object.create(HTMLElement.prototype)){
createdCallback(){
console.log('createdCallback()')
此项。_attached=false
this.attachPromise=null
此值为.\u cleanedUp=true
this.node=this.makeNode()
this.createChildObserver()
this.childObserver.observe(this,{childList:true})
//TODO:mountPromise适用于节点,而不仅仅是场景。
如果(this.nodeName=='MOTOR-SCENE'){
//XXX:“承诺”与“准备就绪”:
//
//“ready”在HTML方面似乎更直观,因为
//如果用户有对电机节点或电机场景的引用
//并且它存在于DOM中,那么它已经从
//HTML API透视图。也许我们可以使用“mountPromise”来
//命令式API和HTML API的“就绪”。例如:
//
//等待$('motor-scene')[0]。准备好//使用HTML API时
//使用命令式API时等待node.mountPromise//
//
//或者,我们可以在这两种情况下都使用“ready”?。。。
this.mountPromise=this.node.mountPromise
这个
}
},
makeNode(){
返回新节点
},
createChildObserver(){
this.childObserver=新突变观察者(突变=>{
突变。forEach(突变=>{
让节点=Array.from(mutation.addedNodes)
节点=节点。过滤器(节点=>{
让我们保持真实
如果(
node.nodeName.match(/^MOTOR-/)
|| (
//忽略MotorDomainSceneContainer,因为我们
//不会移动它(应该留在里面的地方)
//元素的名称)。其他元素
//移动到场景图中(例如,如果
//你把a放在a里面,然后
//它被移植到马达中
//场景图DOM树,它植根于
//.你会明白这意味着什么
//现在,如果您查看元素检查器。
node.className//某些节点没有类名(#text、#comment、#document)。
&&node.className.match(/^MotorDomainSceneContainer/)
)
) {
保留=错误
}
回程保留
})
nodes.forEach(node=>{
//这是一种黑客行为:我们删除内容
//从实际DOM中的motor节点
//它位于节点控制元素中,该元素可能
//使调试变得更加困难,但至少
//就目前而言,它是有效的。
this.node.element.element.appendChild(节点)
})
})
})
},
异步attachedCallback(){
log('attachedCallback()')
这是真的
//如果当前正在连接节点,请等待连接完成
//在再次连接之前,为避免竞争条件。这将
//几乎从来没有发生过,但以防万一,它可以防止
//最终用户端的幼稚编程(例如,如果附加
//将motor节点元素移动到DOM,然后将其移动到新元素
//在同一时间内。
等着吧,我答应你
this.attachPromise=新承诺(异步(解析)=>{
如果(此项已清除){
此参数为.\u cleanedUp=false
this.childObserver.observe(this,{childList:true})
}
//场景没有要附加到的父对象。
if(this.nodeName.toLowerCase()!=‘马达场景’)
this.parentNode.node.addChild(this.node)
解决()
})
},
异步detachedCallback(){
log('detachedCallback()')
此项。_attached=false
//如果当前正在连接节点,请等待连接完成
//在开始分离过程之前(以避免争用情况)。
//如果this.attachPromise为空,则执行将继续,而不进行任何更改
//(托多:这是我们可以信赖的吗
//在语言规范中?)。
如果(这个附件承诺)等待这个附件承诺
this.attachPromise=null
//XXX为性能,请推迟到清理前的下一个滴答声
//如果该元素实际被重新连接到其他位置
//在同一刻度内(分离和连接是同步的,
//因此,通过推迟到下一个滴答声,我们将能够知道
//元件是否重新连接,以便清理或重新连接
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