首页 > 其他 > 详细

MVVM框架解析

时间:2019-11-14 19:23:05      阅读:66      评论:0      收藏:0      [点我收藏+]

1.框架的基本结构

框架分为4个模块:
mvvm:入口函数,负责数据代理,调用其他模块
observer:负责数据劫持
compile:负责编译模版
watcher:为每一个数据绑定的表达式创建watcher,watcher里面包含更新相关节点的页面的回调函数

2.运行流程

(1)在入口函数MVVM中保存传入的配置项,进行数据代理,并调用observe模块进行数据劫持。接着调用compile模块编译模版
(2)observe模块:为data中所有的属性设置get方法和set方法。为每个属性创建一个dep,用来存放watcher
(3)compile模块:对模版进行解析,编译相关的指令。如果编译时发现该节点有数据绑定(v-htm),则为该节点创建一个watcher,并尝试将该watcher添加到对应的dep中
(4)watcher模块:记录页面更新时用到的回调函数,以及修改Dep.target,以便再触发get方法的时候尝试将watcher添加到对应的dep中

3.难点解析

(1)何时创建dep:每次数据劫持时都会创建对应的dep,而数据劫持发生在2个时候:
对初始化的数据进行数据劫持时和数据遭到修改时
(2)何时创建watcher:只有在编译模版时才创建watcher。模版中每一个数据绑定的表达式都对应一个Watcher
(3)如何将watcher添加到对应的dep中:对模版中的表达式进行取值,触发其get方法。然后在get方法中尝试将watcher添加到对应的dep中。但是触发get方法的情形有很多中,例如计算属性,methods中的函数调用,模版表达式取值等等。这些方法都会触发get方法,但是我们只需要在模版表达式取值时尝试将wacther添加到dep中。其他情形时可以忽略的。所以要对get方法的触发情形给予区分:即根据Dep.target的值来判断此次get方法是不是模版表达式取值触发的。Dep.target默认为null,只有在模版表达式取值时才将Dep.target设为!null(当前watcher),模版表达式取值完毕后重置为null。这样的话就可以根据Dep.target=!null来判断是否是模版表达式取值。
(4)何时将Watcher添加到对应的dep中:解析模版和数据更新时都需要尝试将watcher添加到对应的dep中,因为dep可能是数据修改后创建的,所以每次更新数据都需要尝试再添加一次,为了避免重复添加,需要对watcher添加depIds这属性对象记录已经添加过的dep,防止重复添加

4.代码实现

(1)代码引入顺序(mvvm要在最后)

<script src="js/watcher.js"></script>
<script src="js/compile.js"></script>
<script src="js/observer.js"></script>
<script src="js/mvvm.js"></script>

(2)代码

//mvvm.js
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
            }
        })
    }
}
//observer.js
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(){
                // 只有在获取模版表达式的值的时候Dep.target != null
                if(Dep.target){
                    //此时Dep.target = wacther

                    //判断当前wacther是否被添加过
                    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,用来供watcher判断是否被添加过
    this.id = uid++
    // subs这个数组用来装watcher
    this.subs = []
}

Dep.prototype = {
    notify(){
        //通知所有的watcher,并触发他的update()
        this.subs.forEach(watcher => {
            watcher.update()
        })
    }
}

//默认值null,只有在模版表达式取值时才修改为!null
Dep.target = null
//compile.js
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和exp是为了读取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){
        //bind方法将data中的数据同步到页面中
        this.bind(node,exp,vm,'model')
        
        //监听表单事件,将表单的数据同步到data中
        var bindAttr = 'value'
        var eventName = 'input'
        // 只针对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)

            //监听input标签的事件(将文本框的值同步到data中)
            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))

        //创建wacther
        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'){
                // 如果节点类型是文本输入框则更新value属性
                node[bindAttr] = typeof value == 'undefined' ? '' : value
            }else if(node.type == 'radio'){
                bindAttr = 'checked'
                // 单选框的value属性值与绑定的value一致时则为选中状态
                if(node.value === value){
                    node[bindAttr] = true
                }else {
                    node[bindAttr] = false
                }
            }
        }
    }
}
//watcher.js
// 一个数据绑定的表达式对应一个Watcher
// Watcher记录了当前表达式对应的更新函数,为了获取表达式对应的值,还需要传入表达式和vm
function Watcher(vm,exp,cb){
    this.vm = vm
    this.exp = exp
    this.cb = cb
    // depIds这个对象用来记录当前watcher已经添加过的dep,防止重复添加
    this.depIds = {}

    // 初次编译此节点时尝试将当前wacther添加到对应的dep中
    this.value = this.get()
}

Watcher.prototype = {
    get(){
        // 给dep指定当前Watcher
        Dep.target = this
        // 获取表达式对应的值,并触发属性的get方法
        var value = this.getVMval()
        //触发属性的get方法后将Dep.target还原
        Dep.target = null
        return value
    },
    addToDep(dep){
        // 将当前Wacther添加到dep数组中
        dep.subs.push(this)
    },
    update(){
        // 数据发生改变时,获取当前表达式对应的值
        // 同时尝试将当前Watcher添加到对应的dep中(dep可能是后面创建的,所以每次更新数据都需要尝试再添加一次,因此需要对watcher进行depIds属性)
        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
    }
}

(3)基本使用

<body>
    <div id="app">
        <p>{{name}}</p>
        <p>{{age}}</p>
        <p>{{friend.name}}</p>
        <p>{{friend.age}}</p>
        <div>
            <button v-on:click="test">点我</button>
            <button v-on:click="test2">点我2</button>
            <button v-on:click="test3">点我3</button>
        </div>
    </div>
</body>
<script>
    var vm = new MVVM({
        el:"#app",
        data:{
            name:"郭靖",
            age:20,
            friend:""
        },
        methods:{
            test(){
                this.age = 25
            },
            test2(){
                this.friend = {
                    name:"杨康",
                    age:25
                }
            },
            test3(){
                this.friend.age = 30
            }
        }
    })
</script>

MVVM框架解析

原文:https://www.cnblogs.com/OrochiZ-/p/11858797.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!