RubyLouvre/anu

View on GitHub
packages/render/server/Renderer.js

Summary

Maintainability
F
6 days
Test Coverage
/**
 * Copyright (c) 2013-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import {
    Children
} from 'react-core/Children';
import {
    isFn,
    emptyObject,
    typeNumber,
    miniCreateClass,
    oneObject
} from 'react-core/util';
import {
    createOpenTagMarkup
} from './html';
import {
    duplexMap
} from './duplex';
import {
    isValidElement
} from 'react-core/createElement';

/*
import {
    REACT_FORWARD_REF_TYPE,
    REACT_FRAGMENT_TYPE,
    REACT_STRICT_MODE_TYPE,
    REACT_ASYNC_MODE_TYPE,
    REACT_PORTAL_TYPE,
    REACT_PROFILER_TYPE,
    REACT_PROVIDER_TYPE,
    REACT_CONTEXT_TYPE
} from 'shared/ReactSymbols';
*/
function AsyncMode(children) {
    return children;
}

function StrictMode(children) {
    return children;
}

function Fragment(children) {
    return children;
}

import {
    encodeEntities
} from './encode';
import {
    Namespaces,
    getIntrinsicNamespace,
    getChildNamespace
} from './namespaces';

function invariant(a, condition) {
    if (a === false) {
        console.warn(condition);
    }
}

const omittedCloseTags = oneObject(
    'area,base,br,col,embed,hr,img,input,keygen,link,meta,param,source,track,wbr'
);

// Based on reading the React.Children implementation. TODO: type this somewhere?
const toArray = Children.toArray;

//https://html.com/tags/listing/
const newlineEatingTags = oneObject('listing,pre,textarea');

function shouldConstruct(Component) {
    return Component.prototype && Component.prototype.isReactComponent;
}

function getNonChildrenInnerMarkup(props) {
    const innerHTML = props.dangerouslySetInnerHTML;
    if (innerHTML != null) {
        if (innerHTML.__html != null) {
            return innerHTML.__html;
        }
    } else {
        const content = props.children;
        var n = typeNumber(content)
        if (n === 3 || n === 4) {
            return encodeEntities(content);
        }
    }
    return null;
}

//数组扁平化
function flattenTopLevelChildren(children) {
    if (!isValidElement(children)) {
        return toArray(children);
    }
    const element = children;
    if (element.type !== REACT_FRAGMENT_TYPE) {
        return [element];
    }
    const fragmentChildren = element.props.children;
    if (!isValidElement(fragmentChildren)) {
        return toArray(fragmentChildren);
    }
    return [fragmentChildren];
}

function processContext(type, context) {
    const contextTypes = type.contextTypes;
    if (!contextTypes) {
        return emptyObject;
    }
    const maskedContext = {};
    for (const contextName in contextTypes) {
        maskedContext[contextName] = context[contextName];
    }
    return maskedContext;
}

function ReactDOMServerRenderer(children, makeStaticMarkup) {
    const flatChildren = flattenTopLevelChildren(children);

    const topFrame = {
        type: null,
        // Assume all trees start in the HTML namespace (not totally true, but
        // this is what we did historically)
        domNamespace: Namespaces.html,
        children: flatChildren,
        childIndex: 0,
        context: emptyObject,
        footer: ''
    };

    this.stack = [topFrame];
    this.exhausted = false;
    this.currentSelectValue = null;
    this.previousWasTextNode = false;
    this.makeStaticMarkup = makeStaticMarkup;

    // Context (new API)
    this.contextIndex = -1;
    this.contextStack = [];
    this.contextValueStack = [];
}
ReactDOMServerRenderer.prototype = {
    constructor: ReactDOMServerRenderer,
    /**
     * Note: We use just two stacks regardless of how many context providers you have.
     * Providers are always popped in the reverse order to how they were pushed
     * so we always know on the way down which provider you'll encounter next on the way up.
     * On the way down, we push the current provider, and its context value *before*
     * we mutated it, onto the stacks. Therefore, on the way up, we always know which
     * provider needs to be "restored" to which value.
     * https://github.com/facebook/react/pull/12985#issuecomment-396301248
     */

    pushProvider(provider) {
        const index = ++this.contextIndex;
        const context = provider.type._context;
        const previousValue = context._currentValue;

        // Remember which value to restore this context to on our way up.
        this.contextStack[index] = context;
        this.contextValueStack[index] = previousValue;

        // Mutate the current value.
        context._currentValue = provider.props.value;
    },

    popProvider() {
        const index = this.contextIndex;

        const context = this.contextStack[index];
        const previousValue = this.contextValueStack[index];

        // "Hide" these null assignments from Flow by using `any`
        // because conceptually they are deletions--as long as we
        // promise to never access values beyond `this.contextIndex`.
        this.contextStack[index] = null;
        this.contextValueStack[index] = null;

        this.contextIndex--;

        // Restore to the previous value we stored as we were walking down.
        context._currentValue = previousValue;
    },

    read(bytes) {
        if (this.exhausted) {
            return null;
        }

        let out = '';
        while (out.length < bytes) {
            if (this.stack.length === 0) {
                this.exhausted = true;
                break;
            }
            const frame = this.stack[this.stack.length - 1];
            if (frame.childIndex >= frame.children.length) {
                const footer = frame.footer;
                out += footer;
                if (footer !== '') {
                    this.previousWasTextNode = false;
                }
                this.stack.pop();
                if (frame.type === 'select') {
                    this.currentSelectValue = null;
                } else if (
                    frame.type != null &&
                    frame.type.type != null &&
                    frame.type.type.$$typeof === REACT_PROVIDER_TYPE
                ) {
                    const provider = frame.type;
                    this.popProvider(provider);
                }
                continue;
            }
            const child = frame.children[frame.childIndex++];

            out += this.render(child, frame.context, frame.domNamespace);
        }
        return out;
    },

    render(child, context, parentNamespace) {
        var t  = typeNumber(child)
        if (t === 3 || t === 4) {
            const text = '' + child;
            if (text === '') {
                return '';
            }
            if (this.makeStaticMarkup) {
                return encodeEntities(text);
            }
            if (this.previousWasTextNode) {
                return '<!-- -->' + encodeEntities(text);
            }
            this.previousWasTextNode = true;
            return encodeEntities(text);
        } else {
            let nextChild;
            ({
                child: nextChild,
                context
            } = resolve(child, context));
            if (nextChild === null || nextChild === false) {
                return '';
            } else if (!isValidElement(nextChild)) {
                if (nextChild != null && nextChild.$$typeof != null) {
                    // Catch unexpected special types early.
                    const $$typeof = nextChild.$$typeof;
                    invariant(
                        $$typeof !== REACT_PORTAL_TYPE,
                        'Portals are not currently supported by the server renderer. ' +
                        'Render them conditionally so that they only appear on the client render.'
                    );
                    // Catch-all to prevent an infinite loop if React.Children.toArray() supports some new type.
                    invariant(
                        false,
                        'Unknown element-like object type: ' + $$typeof + '. This is likely a bug in React. ' +
                        'Please file an issue.'
                    );
                }
                const nextChildren = toArray(nextChild);
                const frame = {
                    type: null,
                    domNamespace: parentNamespace,
                    children: nextChildren,
                    childIndex: 0,
                    context: context,
                    footer: ''
                };

                this.stack.push(frame);
                return '';
            }
            // Safe because we just checked it's an element.
            const nextElement = nextChild;
            const elementType = nextElement.type;

            if (typeNumber(elementType) === 4) {
                return this.renderDOM(nextElement, context, parentNamespace);
            }

            switch (elementType) {
                case REACT_STRICT_MODE_TYPE:
                case REACT_ASYNC_MODE_TYPE:
                case REACT_PROFILER_TYPE:
                case REACT_FRAGMENT_TYPE:
                    {
                        const nextChildren = toArray(nextChild.props.children);
                        const frame = {
                            type: null,
                            domNamespace: parentNamespace,
                            children: nextChildren,
                            childIndex: 0,
                            context: context,
                            footer: ''
                        };

                        this.stack.push(frame);
                        return '';
                    }
                    // eslint-disable-next-line-no-fallthrough
                default:
                    break;
            }
            if (typeof elementType === 'object' && elementType !== null) {
                switch (elementType.$$typeof) {
                    case REACT_FORWARD_REF_TYPE:
                        {
                            const element = nextChild;
                            const nextChildren = toArray(
                                elementType.render(element.props, element.ref)
                            );
                            const frame = {
                                type: null,
                                domNamespace: parentNamespace,
                                children: nextChildren,
                                childIndex: 0,
                                context: context,
                                footer: ''
                            };

                            this.stack.push(frame);
                            return '';
                        }
                    case REACT_PROVIDER_TYPE:
                        {
                            const provider = nextChild;
                            const nextProps = provider.props;
                            const nextChildren = toArray(nextProps.children);
                            const frame = {
                                type: provider,
                                domNamespace: parentNamespace,
                                children: nextChildren,
                                childIndex: 0,
                                context: context,
                                footer: ''
                            };

                            this.pushProvider(provider);

                            this.stack.push(frame);
                            return '';
                        }
                    case REACT_CONTEXT_TYPE:
                        {
                            const consumer = nextChild;
                            const nextProps = consumer.props;
                            const nextValue = consumer.type._currentValue;

                            const nextChildren = toArray(nextProps.children(nextValue));
                            const frame = {
                                type: nextChild,
                                domNamespace: parentNamespace,
                                children: nextChildren,
                                childIndex: 0,
                                context: context,
                                footer: ''
                            };

                            this.stack.push(frame);
                            return '';
                        }
                    default:
                        break;
                }
            }

            let info = '';

            invariant(
                false,
                'Element type is invalid: expected a string (for built-in ' +
                'components) or a class/function (for composite components) ' +
                'but got: %s.%s',
                elementType == null ? elementType : typeof elementType,
                info
            );
        }
    },

    renderDOM(element, context, parentNamespace) {
        const tag = element.type.toLowerCase();

        let namespace = parentNamespace;
        if (parentNamespace === Namespaces.html) {
            namespace = getIntrinsicNamespace(tag);
        }

        let props = element.props;
        //input,select,textarea,option元素需要特殊处理
        if (isFn(duplexMap[tag])) {
            props = duplexMap[tag](props, this);
        }

        let out = createOpenTagMarkup(
            element.type,
            tag,
            props,
            namespace,
            this.makeStaticMarkup,
            this.stack.length === 1
        );
        let footer = '';
        if (omittedCloseTags.hasOwnProperty(tag)) {
            out += '/>';
        } else {
            out += '>';
            footer = '</' + element.type + '>';
        }
        let children;
        const innerMarkup = getNonChildrenInnerMarkup(props);
        if (innerMarkup != null) {
            children = [];
            if (newlineEatingTags[tag] && innerMarkup.charAt(0) === '\n') {
                // text/html ignores the first character in these tags if it's a newline
                // Prefer to break application/xml over text/html (for now) by adding
                // a newline specifically to get eaten by the parser. (Alternately for
                // textareas, replacing "^\n" with "\r\n" doesn't get eaten, and the first
                // \r is normalized out by HTMLTextAreaElement#value.)
                // See: <http://www.w3.org/TR/html-polyglot/#newlines-in-textarea-and-pre>
                // See: <http://www.w3.org/TR/html5/syntax.html#element-restrictions>
                // See: <http://www.w3.org/TR/html5/syntax.html#newlines>
                // See: Parsing of "textarea" "listing" and "pre" elements
                //  from <http://www.w3.org/TR/html5/syntax.html#parsing-main-inbody>
                out += '\n';
            }
            out += innerMarkup;
        } else {
            children = toArray(props.children);
        }
        const frame = {
            domNamespace: getChildNamespace(parentNamespace, element.type),
            type: tag,
            children,
            childIndex: 0,
            context: context,
            footer: footer
        };
        this.stack.push(frame);
        this.previousWasTextNode = false;
        return out;
    }
};

function resolve(child, context) {
    while (isValidElement(child)) {
        // Safe because we just checked it's an element.
        let element = child;
        let Component = element.type;

        if (!isFn(Component)) {
            break;
        }
        processChild(element, Component);
    }

    // Extra closure so queue and replace can be captured properly
    function processChild(element, Component) {
        let publicContext = processContext(Component, context);

        let queue = [];
        let replace = false;
        let updater = {
            isMounted: function() {
                return false;
            },
            enqueueForceUpdate: function() {
                if (queue === null) {
                    return null;
                }
            },
            enqueueReplaceState: function(publicInstance, completeState) {
                replace = true;
                queue = [completeState];
            },
            enqueueSetState: function(publicInstance, currentPartialState) {
                if (queue === null) {
                    return null;
                }
                queue.push(currentPartialState);
            }
        };

        let inst;
        var hasGSFP = isFn(Component.getDerivedStateFromProps);
        //创建实例
        if (shouldConstruct(Component)) {
            inst = new Component(element.props, publicContext, updater);
            if (hasGSFP) {
                let partialState = Component.getDerivedStateFromProps.call(
                    null,
                    element.props,
                    inst.state
                );
                if (partialState != null) {
                    inst.state = Object.assign({}, inst.state, partialState);
                }
            }
        } else {
            inst = Component(element.props, publicContext, updater);
            if (inst == null || inst.render == null) {
                child = inst;
                return;
            }
        }

        inst.props = element.props;
        inst.context = publicContext;
        inst.updater = updater;

        let initialState = inst.state;
        if (initialState === undefined) {
            inst.state = initialState = null;
        }
        //执行componentWillMount钩子
        var willMountHook = hasGSFP ?
            'UNSAFE_componentWillMount' :
            'componentWillMount';

        if (isFn(inst[willMountHook])) {
            inst[willMountHook]();
            if (queue.length) {
                let oldQueue = queue;
                let oldReplace = replace;
                queue = null;
                replace = false;

                if (oldReplace && oldQueue.length === 1) {
                    inst.state = oldQueue[0];
                } else {
                    let nextState = oldReplace ? oldQueue[0] : inst.state;
                    let dontMutate = true;
                    for (let i = oldReplace ? 1 : 0; i < oldQueue.length; i++) {
                        let partial = oldQueue[i];
                        let partialState = isFn(partial) ?
                            partial.call(inst, nextState, element.props, publicContext) :
                            partial;
                        if (partialState != null) {
                            if (dontMutate) {
                                dontMutate = false;
                                nextState = Object.assign({}, nextState, partialState);
                            } else {
                                Object.assign(nextState, partialState);
                            }
                        }
                    }
                    inst.state = nextState;
                }
            } else {
                queue = null;
            }
        }
        //执行render
        child = inst.render();
        //执行getChildContext
        if (isFn(inst.getChildContext)) {
            let childContext = inst.getChildContext();
            if (childContext) {
                context = Object.assign({}, context, childContext);
            }
        }
    }
    return {
        child,
        context
    };
}

export default ReactDOMServerRenderer;