前端

个人手写的MVVM

2019-08-08  本文已影响22人  OrochiZ
1.MVVM(入口函数)
function MVVM(options){
    // 保存传入的配置
    this.$options = options
    // 保存data对象
    var data = this._data = options.data
    // 遍历data中所有的key
    Object.keys(data).forEach(key => {
        // 为vm添加相同的key属性来对data进行数据代理
        this._proxyData(data,key)
    })

    // 数据劫持,监听所有data中所有层次属性值的变动
    observe(data)

    // 模版解析
    new Compile(options.el || document.body, this)
}

MVVM.prototype = {
    _proxyData: function(data,key){
        // 保存vm
        var me = this
        // 为vm添加属性,代理同名的data属性数据
        Object.defineProperty(me,key,{
            configurable: false, // 不可重定义
            enumerable: true, // 可枚举 该属性名能被Object.keys()获取
            get(){
                return data[key]
            },
            set(newVal){
                data[key] = newVal
            }
        })
    }
}
2.observer(数据劫持)
function observe(value){
    // 只有value为对象类型才进行数据劫持
    if(value instanceof Object){
        new Observer(value)
    }
}

function Observer(data){
    // 保存data
    this.data = data
    // 为data所有的key添加数据劫持
    Object.keys(data).forEach(key => {
        this.defineReactive(data,key,data[key])
    })
}

Observer.prototype = {
    defineReactive: function(data,key,val){
        // val:在添加get/set方法前保存属性值,而这个属性值也将供get/set方法return和修改

        // 间接递归调用为该属性值进行数据劫持
        observe(val)

        // 为每个属性new 一个 dep
        var dep = new Dep()

        // 为属性添加get/set方法
        Object.defineProperty(data,key,{
            configurable: false,
            enumerable: true,
            get(){
                // 只有在new Watcher的时候Dep.target != null
                if(Dep.target){
                    if(!Dep.target.hasOwnProperty(dep.id)){
                        // 将当前watcher添加到dep.subs中
                        Dep.target.addToDep(dep)
                        // 为watcher添加属性,防止重复添加到同一个dep中
                        Dep.target[dep.id] = dep
                    }
                }
                return val
            },
            set(newVal){
                if(newVal !== val){
                    val = newVal

                    // 为新的值添加数据劫持
                    observe(val)

                    // 通知所有订阅者(当前dep里面的所有watcher)
                    dep.notify()
                }
            }
        })
    }
}

var uid =0

function Dep(){
    // 每个new出来的Dep都有自己独有的id
    this.id = uid++
    // subs这个数组用来装watcher
    this.subs = []
}

Dep.prototype = {
    notify(){
        this.subs.forEach(watcher => {
            watcher.update()
        })
    }
}

Dep.target = null
3.compile 编译模版,创建Watcher
function Compile(el,vm){
    // 保存vm,以便访问vm._data或者vm.$opstions.methods
    this.$vm = vm
    this.$el = document.querySelector(el)

    // 只有这个dom元素存在才进行编译解析
    if(this.$el){
        // 将这个dom元素的所有子节点移入到fragment中
        this.$fragment = this.nodeToFragment(this.$el)
        // 调用初始化函数,编译fragment
        this.init()
        // 将编译好的fragment插入到el中
        this.$el.appendChild(this.$fragment)
    }
}

Compile.prototype = {
    nodeToFragment: function(el){
        // 创建fragment
        var fragment = document.createDocumentFragment()
        var child
        while(child = el.firstChild){
            // 将原生节点移动到fragment中
            fragment.appendChild(child)
        }
        // 返回fragment
        return fragment
    },
    init: function(){
        // 编译this.$fragment的子节点
        this.compileElement(this.$fragment);
    },
    compileElement: function(el){  // 此函数用来编译el的所有子节点
        // 获取el的所有子节点
        var childNodes = el.childNodes
        // 遍历所有子节点
        Array.from(childNodes).forEach(node => {
            // 匹配 {{}} 的正则表达式 禁止贪婪
            var reg = /\{\{(.*?)\}\}/

            // 如果该节点是 元素节点
            if(node.nodeType === 1){
                // 编译此元素属性中的指令
                this.compileOrder(node)
            }else if(node.nodeType === 3 && reg.test(node.textContent)){
                // 如果是该节点是文本节点且匹配到 大括号 表达式

                // 获取大括号内的表达式
                var exp = RegExp.$1.trim()
                // 调用数据绑定的方法 编译此文本节点 传入vm是为了读取vm._data
                compileUtil.text(node,exp,this.$vm)
            }
            // 如果该元素存在子节点 则调用递归 编译此节点
            if(node.childNodes && node.childNodes.length) {
                this.compileElement(node)
            }
        })
    },
    compileOrder: function(node){
        // 获取该节点所有属性节点
        var nodeAttrs = node.attributes
        // 遍历所有属性
        Array.from(nodeAttrs).forEach(attr => {
            // 获取属性名
            var attrName = attr.name
            // 判断属性是否是我们自定的指令
            if(this.isDirective(attrName)){
                // 获取指令对应的表达式
                var exp = attr.value
                // 获取指令 v-text => text (截去前两个字符)
                var dir = attrName.substring(2)
                // 判断指令类型 是否是事件指令
                if(this.isEventDirective(dir)){
                    // 调用指令处理对象的相应方法 dir == on:click
                    compileUtil.eventHandler(node,dir,exp,this.$vm)
                }else {
                    // 普通指令 v-text
                    compileUtil[dir] && compileUtil[dir](node,exp,this.$vm)
                }
                // 指令编译完成之后移除指令
                node.removeAttribute(attrName)
            }
        })
    },
    isDirective: function(attrName){
        // 只有 v- 开头的属性名才是我们定义的指令
        return attrName.indexOf('v-') == 0
        // attrName.startsWith("v-")
    },
    isEventDirective: function(dir){
        // 事件指令以 on 开头
        return dir.indexOf('on') == 0
    }
}


// 指令处理集合
// 凡事涉及数据绑定的指令统一调用bind方法
var compileUtil = {
    text: function(node,exp,vm){
        this.bind(node,exp,vm,'text')
    },
    html: function(node,exp,vm){
        this.bind(node,exp,vm,'html')
    },
    model: function(node,exp,vm){
        this.bind(node,exp,vm,'model')
        
        var bindAttr = 'value'
        var eventName = 'input'
        // 只针对输入框进行处理
        if(node.nodeName.toLowerCase() == 'input'){
            // 如果是单选框和复选框,则绑定的属性为checked,事件为change
            if(node.type == 'radio' || node.type == 'checkbox'){
                bindAttr = 'checked'
                // oninput 事件在元素值发生变化是立即触发, onchange 在元素失去焦点时触发
                eventName = 'change'
            }
            //保存一个val值,避免input事件触发重复读取
            var val = this._getValue(exp,vm)

            node.addEventListener(eventName,function(e){
                if(node.type === 'text'){
                    // 获取输入框的值
                    var newVal = e.target[bindAttr]
                    // 对比输入框与绑定数据的值
                    if(newVal !== val){
                        // 绑定的值发生改变,修改vm._data对应的值
                        compileUtil._setValue(exp,newVal,vm)
                        // 更新val
                        val = newVal
                    }
                }else if(node.type === 'radio'){
                    // 获取当前单选框的选中状态
                    var checked = e.target[bindAttr]
                    // 如果当前单选框被选中,则修改vm._data对应的值
                    if(checked){
                        compileUtil._setValue(exp,e.target.value,vm)
                    }
                }
            },false)
        }
    },
    bind(node,exp,vm,dir){
        // 根据指令获取更新节点的方法
        var updaterFn = updater[dir + 'Updater']
        // 获取exp表达式的值并调用更新节点的方法
        updaterFn && updaterFn(node,this._getValue(exp,vm))

        new Watcher(vm,exp,function(value){
            updaterFn && updaterFn(node,value)
        })
    },
    eventHandler: function(node,dir,exp,vm){
        // 为节点绑定事件 (哪个节点,哪个事件,触发哪个回调)

        // 获取事件名称 on:click => click
        var eventName = dir.split(':')[1]
        // 根据exp获取其在在vm中对应的函数
        var fn = vm.$options.methods && vm.$options.methods[exp]

        // 只有事件名称和回调同时存在才添加事件监听
        if(eventName && fn){
            // 回调函数强制绑定this为vm
            node.addEventListener(eventName,fn.bind(vm),false)
        }
    },
    _getValue(exp,vm){
        var val = vm._data
        // 例如 a.b 先获取到a的值,再根据a的值获取到a.b的值
        var expArr = exp.split('.')
        expArr.forEach(key => {
            val = val[key]
        })
        return val
    },
    _setValue(exp,newVal,vm){
        var val = vm._data
        var expArr = exp.split('.')
        expArr.forEach((key,index) => {
            // 如果不是最后一个key,则获取值
            if(index < expArr.length - 1){
                val = val[key]
            }else {
                // 如果是最后一个key,则为该key赋予新的值
                val[key] = newVal
            }
        })
    }
}

// 更新元素节点的方法
var updater = {
    textUpdater: function(node,value){
        node.textContent = typeof value == 'undefined' ? '' : value
    },
    htmlUpdater: function(node,value){
        node.innerHTML = typeof value == 'undefined' ? '' : value
    },
    modelUpdater: function(node,value){
        var bindAttr = 'value'
        // 根据节点类型绑定不同的属性

        if(node.nodeName.toLowerCase() == 'input'){
            if(node.type === 'text'){
                // text输入框则更新value属性
                node[bindAttr] = typeof value == 'undefined' ? '' : value
            }else if(node.type == 'radio'){
                // 单选框的value属性值与绑定的value一致时则为选中状态
                bindAttr = 'checked'
                if(node.value === value){
                    node[bindAttr] = true
                }else {
                    node[bindAttr] = false
                }
            }
        }
    }
}
4.watcher 每个watcher里面配置了与属性值绑定相关的节点,更新函数等信息
// 一个数据绑定的表达式对应一个Watcher
// Watcher记录了当前表达式对应的更新函数,还有表达式本身,为了后面获取表达式对应的值,还需要传入vm
function Watcher(vm,exp,cb){
    this.vm = vm
    this.exp = exp
    this.cb = cb
    // depIds这个对象用来记录当前watcher已经添加过的dep,防止重复添加
    this.depIds = {}

    // 初次编译此节点时为dep.subs添加watcher
    this.value = this.get()
}

Watcher.prototype = {
    get(){
        // 给dep指定当前Watcher
        Dep.target = this
        // 获取表达式对应的值,并触发get方法
        var value = this.getVMval()
        Dep.target = null
        return value
    },
    addToDep(dep){
        // 将当前Wacther添加到dep数组中
        dep.subs.push(this)
    },
    update(){
        // 数据发生改变时,获取当前表达式对应的值
        // 同时将当前Watcher添加到dep.subs中(dep可能是后面添加的,所以每次更新数据都需要尝试再添加一次)
        var value = this.get()
        // 调用回调函数更新界面
        this.cb.call(this.vm, value)
    },
    getVMval(){
        var val = this.vm._data
        // 例如 a.b 先获取到a的值,再根据a的值获取到a.b的值
        var expArr = this.exp.split('.')
        expArr.forEach(key => {
            val = val[key]
        })
        return val
    }
}
上一篇 下一篇

猜你喜欢

热点阅读