MVVM双向绑定机制的原理和代码实现

MVVM

一句话就是 vm层(视图模型层)通过接口从后台m层(model层)请求数据
vm层继而和v(view层)实现数据的双向绑定

数据绑定

单向绑定:数据 =>视图
双向绑定:视图 <=> 数据

双向数据绑定无非就是在单向绑定的基础上给
可输入元素添加了change事件,来动态修改model和view

实现数据绑定的做法大致有如下几种:

发布者-订阅者模式 backbone.js
脏值检查 angular.js
数据劫持 vue.js

发布者-订阅者模式

一般通过sub pub的方式实现数据和视图的绑定监听更新数据的方式通常是vm.set(‘property’,value);

脏值检查

一般通过检测对比的方式查看数据是否有变更,来决定是否更新视图,
最简单的就是通过setInterval()计时器来定时检测数据变动,
但是angular不会这么low,它只有在指定的事件触发时才进入脏值检测,比如DOM事件,XHR响应事件等

数据劫持

vue则是采用数据劫持结合发布订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter getter,在数据变动时发布消息给订阅者,触发监听回调

基本流程:
在这里插入图片描述

MVVM作为绑定入口,整合Observer,Compile和watcher;
通过Observer监听数据变化,通过Compile来解析编译模板指令
利用watcher搭建Observer和Compile之间的通信达到双向绑定

代码实现

创建index.html

<body>
    <div id="app">
        <h2>{{person.name}}--{{person.age}}</h2>
        <h3>{{person.sex}}</h3>
        <div v-text='person.name'></div>
        <div v-html='htmlStr'></div>
        <input type="text" v-model='msg' />
        <ul>
            <li>{{msg}}</li>
            <li>22</li>
            <li>33</li>
            <li>44</li>
        </ul>
        <button v-on:click='funs'>按钮1</button>
        <button @click='funs'>{{msg}}</button>
    </div>

    <!-- <script src="./myVue.js"></script>官方vue.js文件 -->
    <!-- 引入自定义js文件 -->
    <script src="./Obsever.js"></script>
    <script src="./myVue.js"></script>
    <script>
        let vm = new MyVue({
            el: '#app',
            data: {
                person: {
                    name: '小张',
                    age: 23,
                    sex: '男',
                },
                msg: '实现mvvm原理',
                htmlStr: '网页'
            },
            methods: {
                funs() {
                    console.log(this)
                    this.person.name = '2333'
                }
            }
        })
    </script>
</body>

myVue.js
创建一个MyVue类

//MyVue
class MyVue {
    //构造 
    constructor(options) {
        this.$el = options.el;
        this.$data = options.data;
        this.$option = options;

        //如果节点存在
        if (this.$el) {
            //1.数据劫持
            new Observe(this.$data)
            //2.实现一个指令解析器
            new Compile(this.$el, this);
            //添加代理
            this.proxyData(this.$data);
        }

    }
    //添加代理
    proxyData(data) {
        for (const key in data) {
            Object.defineProperty(this, key, {
                get() {
                    return data[key]
                },
                set(newVal) {
                    data[key] = newVal
                }
            })
        }
    }
}

MyVue类中
1.实例化一个指令解析器
2.数据劫持
3.添加对象代理

创建一个指令解析器

作用:
1.初始化视图
1.订阅数据变化,绑定更新函数

//指令解析器
class Compile {
    constructor(el, vm) {
        //节点传给el
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;

        //1.获取文档碎片对象,放入内存中减少回流和重绘
        //文档碎片对象 :document.createDocumentFragment() 用于暂时存放创建的dom元素
        //将需要添加的大量元素 先添加到文档碎片,文档碎片添加到需要插入的位置,大大减少dom操作
        const fragment = this.nodeFragment(this.el);
        // console.log(fragment);
        //2.编译模板
        this.compile(fragment);
        //3.追加子元素到根元素
        this.el.appendChild(fragment);
    }

    //编译模板
    compile(fragment) {
        const childNode = fragment.childNodes;
        //遍历每个子节点
        [...childNode].forEach(childItem => {
            if (this.isElementNode(childItem)) {
                // console.log('元素节点', childItem);
                //处理节点元素
                this.compileElementNode(childItem);

            } else {
                // console.log('文本节点', childItem);
                //处理文本元素
                this.compileElementText(childItem)
            }
            //遍历子节点下的子节点
            if (childItem.childNodes && childItem.childNodes.length) {
                this.compile(childItem);
            }
        })

    }


    //处理节点元素
    compileElementNode(node) {
        //获取节点属性
        const attributes = node.attributes;
        //遍历节点每个属性
        [...attributes].forEach(item => {
            const { name, value } = item;
            //是否指令
            if (this.isDirecative(name)) { //v-text v-html v-model v-on:click 
                // console.log(item)
                const [, dir] = name.split('-'); //text html model on:click
                const [dirName, eventName] = dir.split(':');//text html model on click
                //更新视图,数据驱动视图
                compileUtil[dirName](node, value, this.vm, eventName);
                //删除有指令的标签属性
                node.removeAttribute('v-' + dir);
            }
            //是否事件
            else if (this.isEventName(name)) {  //@click='fun'
                const [, dir] = name.split('@');
                const eventName = dir.split('=');
                compileUtil['on'](node, value, this.vm, eventName);
            }
            // console.log(name, value)
        })
    }


    //处理文本元素
    compileElementText(node) {
        const content = node.textContent;
        if (/\{\{(.+?)\}\}/.test(content)) {
            console.log(content);
            compileUtil['text'](node, content, this.vm);
        }
    }


    //转化为文档节点
    nodeFragment(el) {
        //创建文档对象
        const f = document.createDocumentFragment();
        let firstChild;
        //循环装入第一子节点
        while (firstChild = el.firstChild) {
            f.appendChild(firstChild);
        }
        //
        return f;
    }
    //是否是事件
    isEventName(attrName) {
        return attrName.startsWith('@')
    }
    //是否是指令
    isDirecative(attrName) {
        return attrName.startsWith('v-');
    }
    //判断是否节点
    isElementNode(node) {
        return node.nodeType === 1;
    }


}

添加一个指令对象,处理v-html v-model v-on v-text等指令

const compileUtil = {

    //指令(节点,名称,MyVue对象)
    text(node, expr, vm) {
        //获取MyVue对象 data的某一属性的值
        console.log(expr)
        // const value = this.getVal(expr, vm);
        let value;
        //去除{{}}
        if (expr.indexOf('{{') !== -1) {
            value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
                //挂在一个观察者
                new Watcher(vm, args[1], (newVal) => {
                    console.log(this.getContenVal(expr, vm))
                    this.updater.textUpdater(node, this.getContenVal(expr, vm));
                })
                return this.getVal(args[1], vm);
            })
        } else {
            console.log(this.getContenVal(expr, vm))

            value = this.getVal(expr, vm);
        }
        //更新函数
        this.updater.textUpdater(node, value);
    },
    html(node, expr, vm) {
        const value = this.getVal(expr, vm);
        //挂在一个观察者
        new Watcher(vm, expr, (newVal) => {
            this.updater.htmlUpdater(node, newVal);
        })
        this.updater.htmlUpdater(node, value);
    },
    model(node, expr, vm) {
        const value = this.getVal(expr, vm);
        //数据=>视图
        new Watcher(vm, expr, (newVal) => {
            this.updater.modelUpdater(node, newVal);
        })
        //视图=>数据=>视图
        node.addEventListener('input', (e) => {
            this.setVal(expr, vm, e.target.value);
        })
        this.updater.modelUpdater(node, value);
    },
    //事件
    on(node, expr, vm, eventName) {
        let fn = vm.$option.methods && vm.$option.methods[expr];
        node.addEventListener(eventName, fn.bind(vm), false);
    },
    //更新函数
    updater: {
        modelUpdater(node, value) {
            node.value = value;
        },
        htmlUpdater(node, value) {
            node.innerHTML = value;
        },
        textUpdater(node, value) {
            //返回指定节点的文本内容
            node.textContent = value;
        }
    },
    //
    setVal(expr, vm, inputVal) {
        return expr.split('.').reduce((data, currentVal) => {
            data[currentVal] = inputVal;
            return data[currentVal];
        }, vm.$data);
    },
    //获取值
    getVal(expr, vm) {
        //person.name
        //[person,name]
        return expr.split('.').reduce((data, currentVal) => {
            return data[currentVal]
        }, vm.$data);
    },
    //
    getContenVal(expr, vm) {

        return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            return this.getVal(args[1], vm);
        })
    }
}

在每个指令执行时候为每个对象挂载一个观察者new Watcher()

2.数据劫持

Object.defineProperty劫持数据的get和set方法

//数据劫持
class Observe {
    constructor(data) {
        this.observer(data);
    }
    //遍历所有属性
    observer(data) {
        if (data && typeof data === 'object') {
            console.log(Object.keys(data))
            Object.keys(data).forEach(key => {
                this.defineReactive(data, key, data[key]);
            })
        }
    }

    //数据劫持
    defineReactive(data, key, value) {
        //递归遍历子属性
        this.observer(value);
        //添加dep容器
        const dep = new Dep();
        //劫持所有属性 为每个属性添加get set方法
        Object.defineProperty(data, key, {
            enumerable: true,//是否可遍历
            configurable: false,//是否可更改编写
            get() {
                //订阅数据变化时,往Deo中添加观察者
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set: (newVal) => {
                //对新值也进行劫持
                this.observer(newVal);
                if (newVal !== value) {
                    value = newVal;
                }
                //通知观察者去跟新 
                dep.notify();
            }
        })
    }
}

订阅数据时,往Deo中为每个属性添加观察者
数据变化时通知Dep的属性观察者去更新

创建一个Dep容器

//*Obsever.js*
class Dep {
    constructor() {
        this.subs = [];
    }
    //收集观察者
    addSub(watcher) {
        this.subs.push(watcher);
    }
    //通知观察者去跟新
    notify() {
        console.log('观察者', this.subs);
        this.subs.forEach(watcher => {
            //通知观察者更新数据
            watcher.updata();
        })
    }
}

Dep容器的作用是去存储所有观察者,当数据变化时,通知观察者去跟新

观察者

class Watcher {
    constructor(vm, expr, callback) {
        this.vm = vm;
        this.expr = expr;
        this.callback = callback;
        //先把旧值保存
        this.oldVal = this.getOldVal();

    }
    //保存旧值
    getOldVal() {
        //挂载观察者
        Dep.target = this; //target暂存的this,this指向Watcher对象,把对象push进dep就销毁
        //获取旧值
        const oldVal = compileUtil.getVal(this.expr, this.vm);
        //清除观察者
        Dep.target = null;
        return oldVal;
    }
    //更新数据
    updata() {
        //获取变化值
        const newVal = compileUtil.getVal(this.expr, this.vm);
        //如果改变则执行回调
        if (newVal !== this.oldVal) {
            this.callback(newVal)
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值