RubyLouvre/avalon

View on GitHub
src/renders/domRender.js

Summary

Maintainability
D
1 day
Test Coverage
import { avalon, config, inBrowser, delayCompileNodes, directives } from '../seed/core'
import { fromDOM } from '../vtree/fromDOM'
import { fromString } from '../vtree/fromString'

import { VFragment } from '../vdom/VFragment'
import { Directive } from './Directive'

import { orphanTag } from '../vtree/orphanTag'
import { parseAttributes, eventMap } from '../parser/attributes'
import { parseInterpolate } from '../parser/interpolate'

import { startWith, groupTree, dumpTree, getRange } from './share'


/**
 * 生成一个渲染器,并作为它第一个遇到的ms-controller对应的VM的$render属性
 * @param {String|DOM} node
 * @param {ViewModel|Undefined} vm
 * @param {Function|Undefined} beforeReady
 * @returns {Render}
 */
avalon.scan = function(node, vm, beforeReady) {
    return new Render(node, vm, beforeReady || avalon.noop)
}

/**
 * avalon.scan 的内部实现
 */
function Render(node, vm, beforeReady) {
    this.root = node //如果传入的字符串,确保只有一个标签作为根节点
    this.vm = vm
    this.beforeReady = beforeReady
    this.bindings = [] //收集待加工的绑定属性
    this.callbacks = []
    this.directives = []
    this.init()
}

Render.prototype = {
    /**
     * 开始扫描指定区域
     * 收集绑定属性
     * 生成指令并建立与VM的关联
     */
    init() {
        var vnodes
        if (this.root && this.root.nodeType > 0) {
            vnodes = fromDOM(this.root) //转换虚拟DOM
                //将扫描区域的每一个节点与其父节点分离,更少指令对DOM操作时,对首屏输出造成的频繁重绘
            dumpTree(this.root)
        } else if (typeof this.root === 'string') {
            vnodes = fromString(this.root) //转换虚拟DOM
        } else {
            return avalon.warn('avalon.scan first argument must element or HTML string')
        }

        this.root = vnodes[0]
        this.vnodes = vnodes
        this.scanChildren(vnodes, this.vm, true)
    },

    scanChildren(children, scope, isRoot) {
        for (var i = 0; i < children.length; i++) {
            var vdom = children[i]
            switch (vdom.nodeName) {
                case '#text':
                    scope && this.scanText(vdom, scope)
                    break
                case '#comment':
                    scope && this.scanComment(vdom, scope, children)
                    break
                case '#document-fragment':
                    this.scanChildren(vdom.children, scope, false)
                    break
                default:
                    this.scanTag(vdom, scope, children, false)
                    break
            }
        }
        if (isRoot) {
            this.complete()
        }
    },

    /**
     * 从文本节点获取指令
     * @param {type} vdom 
     * @param {type} scope
     * @returns {undefined}
     */
    scanText(vdom, scope) {
        if (config.rexpr.test(vdom.nodeValue)) {
            this.bindings.push([vdom, scope, {
                nodeValue: vdom.nodeValue
            }])
        }
    },

    /**
     * 从注释节点获取指令
     * @param {type} vdom 
     * @param {type} scope
     * @param {type} parentChildren
     * @returns {undefined}
     */
    scanComment(vdom, scope, parentChildren) {
        if (startWith(vdom.nodeValue, 'ms-for:')) {
            this.getForBinding(vdom, scope, parentChildren)
        }
    },

    /**
     * 从元素节点的nodeName与属性中获取指令
     * @param {type} vdom 
     * @param {type} scope
     * @param {type} parentChildren
     * @param {type} isRoot 用于执行complete方法
     * @returns {undefined}
     */
    scanTag(vdom, scope, parentChildren, isRoot) {
        var dirs = {},
            attrs = vdom.props,
            hasDir, hasFor
        for (var attr in attrs) {
            var value = attrs[attr]
            var oldName = attr
            if (attr.charAt(0) === ':') {
                attr = 'ms-' + attr.slice(1)
            }
            if (startWith(attr, 'ms-')) {
                dirs[attr] = value
                var type = attr.match(/\w+/g)[1]
                type = eventMap[type] || type
                if (!directives[type]) {
                    avalon.warn(attr + ' has not registered!')
                }
                hasDir = true
            }
            if (attr === 'ms-for') {
                hasFor = value
                delete attrs[oldName]
            }
        }
        var $id = dirs['ms-important'] || dirs['ms-controller']
        if ($id) {
            /**
             * 后端渲染
             * serverTemplates后端给avalon添加的对象,里面都是模板,
             * 将原来后端渲染好的区域再还原成原始样子,再被扫描
             */
            var templateCaches = avalon.serverTemplates
            var temp = templateCaches && templateCaches[$id]
            if (temp) {
                avalon.log('前端再次渲染后端传过来的模板')
                var node = fromString(temp)[0]
                for (var i in node) {
                    vdom[i] = node[i]
                }
                delete templateCaches[$id]
                this.scanTag(vdom, scope, parentChildren, isRoot)
                return

            }
            //推算出指令类型
            var type = dirs['ms-important'] === $id ? 'important' : 'controller'
                //推算出用户定义时属性名,是使用ms-属性还是:属性
            var attrName = ('ms-' + type) in attrs ? 'ms-' + type : ':' + type

            if (inBrowser) {
                delete attrs[attrName]
            }
            var dir = directives[type]
            scope = dir.getScope.call(this, $id, scope)
            if (!scope) {
                return
            } else {
                var clazz = attrs['class']
                if (clazz) {
                    attrs['class'] = (' ' + clazz + ' ').replace(' ms-controller ', '').trim()
                }
            }
            var render = this
            scope.$render = render
            this.callbacks.push(function() {
                //用于删除ms-controller
                dir.update.call(render, vdom, attrName, $id)
            })

        }
        if (hasFor) {
            if (vdom.dom) {
                vdom.dom.removeAttribute(oldName)
            }
            return this.getForBindingByElement(vdom, scope, parentChildren, hasFor)
        }

        if (/^ms\-/.test(vdom.nodeName)) {
            attrs.is = vdom.nodeName
        }

        if (attrs['is']) {
            if (!dirs['ms-widget']) {
                dirs['ms-widget'] = '{}'
            }
            hasDir = true
        }
        if (hasDir) {
            this.bindings.push([vdom, scope, dirs])
        }
        var children = vdom.children
            //如果存在子节点,并且不是容器元素(script, stype, textarea, xmp...)
        if (!orphanTag[vdom.nodeName] &&
            children &&
            children.length &&
            !delayCompileNodes(dirs)
        ) {
            this.scanChildren(children, scope, false)
        }
    },


    /**
     * 将绑定属性转换为指令
     * 执行各种回调与优化指令
     * @returns {undefined}
     */
    complete() {
        this.yieldDirectives()
        this.beforeReady()
        if (inBrowser) {
            var root = this.root
            if (inBrowser) {
                var rootDom = avalon.vdom(root, 'toDOM')
                groupTree(rootDom, root.children)
            }
        }

        this.mount = true
        var fn
        while (fn = this.callbacks.pop()) {
            fn()
        }
        this.optimizeDirectives()
    },

    /**
     * 将收集到的绑定属性进行深加工,最后转换指令
     * @returns {Array<tuple>}
     */
    yieldDirectives() {
        var tuple
        while (tuple = this.bindings.shift()) {
            var vdom = tuple[0],
                scope = tuple[1],
                dirs = tuple[2],
                bindings = []
            if ('nodeValue' in dirs) {
                bindings = parseInterpolate(dirs)
            } else if (!('ms-skip' in dirs)) {
                bindings = parseAttributes(dirs, tuple)
            }
            for (var i = 0, binding; binding = bindings[i++];) {
                var dir = directives[binding.type]
                if (!inBrowser && /on|duplex|active|hover/.test(binding.type)) {
                    continue
                }
                if (dir.beforeInit) {
                    dir.beforeInit.call(binding)
                }

                var directive = new Directive(scope, binding, vdom, this)
                this.directives.push(directive)
            }
        }
    },

    /**
     * 修改指令的update与callback方法,让它们以后执行时更加高效
     * @returns {undefined}
     */
    optimizeDirectives() {
        for (var i = 0, el; el = this.directives[i++];) {
            el.callback = directives[el.type].update
            el.update = newUpdate
            el._isScheduled = false
        }
    },
    update: function() {
        for (var i = 0, el; el = this.directives[i++];) {
            el.update()
        }
    },

    /**
     * 销毁所有指令
     * @returns {undefined}
     */
    dispose() {
        var list = this.directives || []
        for (let i = 0, el; el = list[i++];) {
            el.dispose()
        }
        //防止其他地方的this.innerRender && this.innerRender.dispose报错
        for (let i in this) {
            if (i !== 'dispose')
                delete this[i]
        }
    },

    /**
     * 将循环区域转换为for指令
     * @param {type} begin 注释节点
     * @param {type} scope
     * @param {type} parentChildren
     * @param {type} userCb 循环结束回调
     * @returns {undefined}
     */
    getForBinding(begin, scope, parentChildren, userCb) {
        var expr = begin.nodeValue.replace('ms-for:', '').trim()
        begin.nodeValue = 'ms-for:' + expr
        var nodes = getRange(parentChildren, begin)
        var end = nodes.end
        var fragment = avalon.vdom(nodes, 'toHTML')
        parentChildren.splice(nodes.start, nodes.length)
        begin.props = {}
        this.bindings.push([
            begin, scope, {
                'ms-for': expr
            }, {
                begin,
                end,
                expr,
                userCb,
                fragment,
                parentChildren
            }
        ])
    },


    /**
     * 在带ms-for元素节点旁添加两个注释节点,组成循环区域
     * @param {type} vdom
     * @param {type} scope
     * @param {type} parentChildren
     * @param {type} expr
     * @returns {undefined}
     */
    getForBindingByElement(vdom, scope, parentChildren, expr) {
        var index = parentChildren.indexOf(vdom) //原来带ms-for的元素节点
        var props = vdom.props
        var begin = {
            nodeName: '#comment',
            nodeValue: 'ms-for:' + expr
        }
        if (props.slot) {
            begin.slot = props.slot
            delete props.slot
        }
        var end = {
            nodeName: '#comment',
            nodeValue: 'ms-for-end:'
        }
        parentChildren.splice(index, 1, begin, vdom, end)
        this.getForBinding(begin, scope, parentChildren, props['data-for-rendered'])

    }

}
var viewID

function newUpdate() {
    var oldVal = this.beforeUpdate()
    var newVal = this.value = this.get()
    if (this.callback && this.diff(newVal, oldVal)) {
        this.callback(this.node, this.value)
        var vm = this.vm
        var $render = vm.$render
        var list = vm.$events['onViewChange']
            /* istanbul ignore if */
        if (list && $render &&
            $render.root &&
            !avalon.viewChanging) {
            if (viewID) {
                clearTimeout(viewID)
                viewID = null
            }
            viewID = setTimeout(function() {
                list.forEach(function(el) {
                    el.callback.call(vm, {
                        type: 'viewchange',
                        target: $render.root,
                        vmodel: vm
                    })
                })
            })

        }

    }
    this._isScheduled = false
}