为什么Custom Element API会创建重复的元素?

时间:2016-03-01 07:54:54

标签: javascript polymer web-component custom-element

我使用document.registerElement API(Chrome中的原生代码)和一些使用motor-scenemotor-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-scenemotor-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 五次登录到控制台。

0 个答案:

没有答案