RubyLouvre/avalon

View on GitHub
src/component/index.js

Summary

Maintainability
D
1 day
Test Coverage
import { avalon, isObject, platform } from '../seed/core'
import { cssDiff } from '../directives/css'
import { dumpTree, groupTree, getRange } from '../renders/share'
var legalTags = { wbr: 1, xmp: 1, template: 1 }
var events = 'onInit,onReady,onViewChange,onDispose,onEnter,onLeave'
var componentEvents = avalon.oneObject(events)

function toObject(value) {
    var value = platform.toJson(value)
    if (Array.isArray(value)) {
        var v = {}
        value.forEach(function(el) {
            el && avalon.shadowCopy(v, el)
        })
        return v
    }
    return value
}
var componentQueue = []
avalon.directive('widget', {
    delay: true,
    priority: 4,
    deep: true,
    init: function() {
        //cached属性必须定义在组件容器里面,不是template中
        var vdom = this.node
        this.cacheVm = !!vdom.props.cached
        if (vdom.dom && vdom.nodeName === '#comment') {
            var comment = vdom.dom
        }
        var oldValue = this.getValue()
        var value = toObject(oldValue)
            //外部VM与内部VM
            // ===创建组件的VM==BEGIN===
        var is = vdom.props.is || value.is
        this.is = is
        var component = avalon.components[is]
            //外部传入的总大于内部
        if (!('fragment' in this)) {
            if (!vdom.isVoidTag) { //提取组件容器内部的东西作为模板
                var text = vdom.children[0]
                if (text && text.nodeValue) {
                    this.fragment = text.nodeValue
                } else {
                    this.fragment = avalon.vdom(vdom.children, 'toHTML')
                }
            } else {
                this.fragment = false
            }
        }
        //如果组件还没有注册,那么将原元素变成一个占位用的注释节点
        if (!component) {
            this.readyState = 0
            vdom.nodeName = '#comment'
            vdom.nodeValue = 'unresolved component placeholder'
            delete vdom.dom
            avalon.Array.ensure(componentQueue, this)
            return
        }
       
            //如果是非空元素,比如说xmp, ms-*, template
        var id = value.id || value.$id
        var hasCache = avalon.vmodels[id]
        var fromCache = false
       // this.readyState = 1
        if (hasCache) {
            comVm = hasCache
            this.comVm = comVm
            replaceRoot(this, comVm.$render)
            fromCache = true
        } else {
            if(typeof component === 'function'){
               component = new component(value)
            }
            var comVm = createComponentVm(component, value, is)
            this.readyState = 1
            fireComponentHook(comVm, vdom, 'Init')
            this.comVm = comVm

            // ===创建组件的VM==END===
            var innerRender = avalon.scan(component.template, comVm)
            comVm.$render = innerRender
            replaceRoot(this, innerRender)
            var nodesWithSlot = []
            var directives = []
            if (this.fragment || component.soleSlot) {
                var curVM = this.fragment ? this.vm : comVm
                var curText = this.fragment || '{{##' + component.soleSlot + '}}'
                var childBoss = avalon.scan('<div>' + curText + '</div>', curVM, function() {
                    nodesWithSlot = this.root.children
                })
                directives = childBoss.directives
                this.childBoss = childBoss
                for (var i in childBoss) {
                    delete childBoss[i]
                }
            }
            Array.prototype.push.apply(innerRender.directives, directives)

            var arraySlot = [],
                objectSlot = {}
                //从用户写的元素内部 收集要移动到 新创建的组件内部的元素
            if (component.soleSlot) {
                arraySlot = nodesWithSlot
            } else {
                nodesWithSlot.forEach(function(el, i) { //要求带slot属性
                    if (el.slot) {
                        var nodes = getRange(nodesWithSlot, el)
                        nodes.push(nodes.end)
                        nodes.unshift(el)
                        objectSlot[el.slot] = nodes
                    } else if (el.props) {
                        var name = el.props.slot
                        if (name) {
                            delete el.props.slot
                            if (Array.isArray(objectSlot[name])) {
                                objectSlot[name].push(el)
                            } else {
                                objectSlot[name] = [el]
                            }
                        }
                    }
                })
            }
            //将原来元素的所有孩子,全部移动新的元素的第一个slot的位置上
            if (component.soleSlot) {
                insertArraySlot(innerRender.vnodes, arraySlot)
            } else {
                insertObjectSlot(innerRender.vnodes, objectSlot)
            }
        }

        if (comment) {
            var dom = avalon.vdom(vdom, 'toDOM')
            comment.parentNode.replaceChild(dom, comment)
            comVm.$element = innerRender.root.dom = dom
            delete this.reInit
        }

        //处理DOM节点

        dumpTree(vdom.dom)
        comVm.$element = vdom.dom
        groupTree(vdom.dom, vdom.children)
        if (fromCache) {
            fireComponentHook(comVm, vdom, 'Enter')
        } else {
            fireComponentHook(comVm, vdom, 'Ready')
        }
    },
    diff: function(newVal, oldVal) {
        if (cssDiff.call(this, newVal, oldVal)) {
            return true
        }
    },

    update: function(vdom, value) {
        //this.oldValue = value //★★防止递归

        switch (this.readyState) {
            case 0:
                if (this.reInit) {
                    this.init()
                    this.readyState++
                }
                break
            case 1:
                this.readyState++
                break
            default:
                this.readyState++
                var comVm = this.comVm
                avalon.viewChanging = true
                avalon.transaction(function() {
                    for (var i in value) {
                        if (comVm.hasOwnProperty(i)) {
                            if (Array.isArray(value[i])) {
                                comVm[i] = value[i].concat()
                            } else {
                                comVm[i] = value[i]
                            }
                        }
                    }
                })

                //要保证要先触发孩子的ViewChange 然后再到它自己的ViewChange
                fireComponentHook(comVm, vdom, 'ViewChange')
                delete avalon.viewChanging
                break
        }
        this.value = avalon.mix(true, {}, value)
    },
    beforeDispose: function() {
        var comVm = this.comVm
        if (!this.cacheVm) {
            fireComponentHook(comVm, this.node, 'Dispose')
            comVm.$hashcode = false
            delete avalon.vmodels[comVm.$id]
            this.innerRender && this.innerRender.dispose()
        } else {
            fireComponentHook(comVm, this.node, 'Leave')
        }
    },
})

function replaceRoot(instance, innerRender) {
    instance.innerRender = innerRender
    var root = innerRender.root
    var vdom = instance.node
    var slot = vdom.props.slot
    for (var i in root) {
        vdom[i] = root[i]
    }
    if (vdom.props && slot) {
        vdom.props.slot = slot
    }
    innerRender.root = vdom
    innerRender.vnodes[0] = vdom
}

function fireComponentHook(vm, vdom, name) {
    var list = vm.$events['on' + name]
    if (list) {
        list.forEach(function(el) {
            setTimeout(function(){
                el.callback.call(vm, {
                    type: name.toLowerCase(),
                    target: vdom.dom,
                    vmodel: vm
                })
            },0)
            
        })
    }
}


export function createComponentVm(component, value, is) {
    var hooks = []
    var defaults = component.defaults
    collectHooks(defaults, hooks)
    collectHooks(value, hooks)
    var obj = {}
    for (var i in defaults) {
        var val = value[i]
        if (val == null) {
            obj[i] = defaults[i]
        } else {
            obj[i] = val
        }
    }
    obj.$id = value.id || value.$id || avalon.makeHashCode(is)
    delete obj.id
    var def = avalon.mix(true, {}, obj)
    var vm = avalon.define(def)
    hooks.forEach(function(el) {
        vm.$watch(el.type, el.cb)
    })
    return vm
}

function collectHooks(a, list) {
    for (var i in a) {
        if (componentEvents[i]) {
            if (typeof a[i] === 'function' &&
                i.indexOf('on') === 0) {
                list.unshift({
                    type: i,
                    cb: a[i]
                })
            }
            //delete a[i] 这里不能删除,会导致再次切换时没有onReady
        }
    }
}

function resetParentChildren(nodes, arr) {
    var dir = arr && arr[0] && arr[0].forDir
    if (dir) {
        dir.parentChildren = nodes
    }
}

function insertArraySlot(nodes, arr) {
    for (var i = 0, el; el = nodes[i]; i++) {
        if (el.nodeName === 'slot') {
            resetParentChildren(nodes, arr)
            nodes.splice.apply(nodes, [i, 1].concat(arr))
            break
        } else if (el.children) {
            insertArraySlot(el.children, arr)
        }
    }
}

function insertObjectSlot(nodes, obj) {
    for (var i = 0, el; el = nodes[i]; i++) {
        if (el.nodeName === 'slot') {
            var name = el.props.name
            resetParentChildren(nodes, obj[name])
            nodes.splice.apply(nodes, [i, 1].concat(obj[name]))
            continue
        } else if (el.children) {
            insertObjectSlot(el.children, obj)
        }
    }
}

avalon.components = {}
avalon.component = function(name, component) {

    component.extend = componentExtend
    return addToQueue(name, component)
    
   
}
function addToQueue(name, component){
    avalon.components[name] = component
    for (var el, i = 0; el = componentQueue[i]; i++) {
        if (el.is === name) {
            componentQueue.splice(i, 1)
            el.reInit = true
            delete el.value
            el.update()
            i--;
        }
    }
    return component
}


function componentExtend(child){
    var name = child.displayName
    delete child.displayName
    var obj = {defaults: avalon.mix(true, {}, this.defaults, child.defaults)}
    if( child.soleSlot){
        obj.soleSlot = child.soleSlot
    }
    obj.template = child.template || this.template
    return avalon.component(name, obj)
}