美文网首页前端
个人手写的MVVM

个人手写的MVVM

作者: OrochiZ | 来源:发表于2019-08-08 11:24 被阅读22次
    1.MVVM(入口函数)
    • 为vm添加数据代理
    • 调用其他函数(数据劫持,模版编译)
    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(数据劫持)
    • 通过对data中所有层次的属性添加get/set方法来添加属性值的变化
    • 为每个属性new一个dep,dep里面的数组用来保存用到该属性的信息(Watcher)
    • 每次更新数据时,如果新的值不是对象,则什么也不执行。但是如果新的值是对象,那么即使新的值与旧值一模一样,他们完全是两个数据(只是里面的属性和值一样罢了),这样先前创建的dep也就失效了,而新的属性也需要再次进行数据劫持,为其创建新的dep,该节点原有的watcher也应该添加到dep数组中
    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
    • 对模版的指令进行解析,事件指令则为其添加事件监听,其他指令则调用相应的函数进行解析
    • 解析数据绑定的表达式时,为其创建一个Watcher。根据表达式用到的属性,触发其get方法,就可以得到这个属性相应的dep,从而将当前Watcher添加到dep里面的数组中
    • 编译v-model比较麻烦,因为input的类型有好几种,对于text需要绑定value属性,同时监听input事件
      对于radio,将绑定的数据与单选框的value属性值进行比较,相等则选中,否则相反。而监听radio选中是change事件,如果被选中则修改绑定的数据为单选框的value属性值
    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._data中对应的数据,获取数据会触发属性的get方法,从而找到其对应的dep,将当前watcher添加到其数组中
    • watcher对应的dep有可能会发生改变(当前绑定的属性指向新的对象,换句话说,就是属性值是新的对象),所以每次数据修改时都要尝试将watcher添加到对应的dep中
    // 一个数据绑定的表达式对应一个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
        }
    }
    

    相关文章

      网友评论

        本文标题:个人手写的MVVM

        本文链接:https://www.haomeiwen.com/subject/kldtjctx.html