美文网首页前端
个人手写的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

    1.MVVM(入口函数) 为vm添加数据代理 调用其他函数(数据劫持,模版编译) 2.observer(数据劫持)...

  • vue的mvvm原理解析及手写一个

    # 手写vue的mvvm实现原理 ## 1:mvc和mvvm的区别? MVC:modal-view-control...

  • 2019-10-31

    手写vue的mvvm实现原理 1:mvc和mvvm的区别? MVC:modal-view-controller,比...

  • Android_传统MVVM_JetPack加持下的MVVM

    本文目标 理解MVVM架构并能手写出来 强调 首先要强调的一点就是,MVVM并不等同与dataBinding,只不...

  • 前端面试经典

    javaScript 理解MVVM等框架,手写伪代码。 ES6新特性,说说class 从编译角度谈谈变量提升 对象...

  • Android架构师

    MVP架构设计 MVVM架构设计 IOC框架与代理模式 泛型及其JSON解析框架 手写ButterKnife框架 ...

  • vue源码分析

    和vue类似的个人mvvm项目https://github.com/DMQ/mvvm

  • Android Kotlin+Jetpack+MVVM

    开篇废话 最近学习了Kotlin,学习了Jetpack,发现是真香,所以就手写了一个MVVM的框架,可以方便开发。...

  • 聊聊iOS开发之MVVM的架构设计

    前言 而MVVM这种新的代码组织方式就可以解决这些问题,本文就MVVM的架构设计做个简单的个人总结。 MVVM概述...

  • 纯kotlin+ViewModel+LiveData+协程MVV

    MVVM大家都了解差不多了,但是我发现MVVM整成架构时,每个人的写法真的是千差万别。 除了MVVM必要的View...

网友评论

    本文标题:个人手写的MVVM

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