美文网首页
Vue - 双向绑定原理

Vue - 双向绑定原理

作者: ElricTang | 来源:发表于2019-10-27 15:51 被阅读0次

目标:理解Vue实现双向数据绑定的主要原理。

最终需求:

  • 1. view -> data 视图变化影响数据
  • 2. data -> view 数据变化影响视图
  • 3. 简单实现Vue的部分指令解析,{{}}v-htmlv-modelv-onv-text
  • 结构
    原理图.png

实现compile

class Compile{
    constructor(el,vm){

        // 保存vm
        this.$vm = vm;

        // 获取el
        vm.$el = document.querySelector(el);

        // 创建文档碎片
        let fragment = document.createDocumentFragment();

        // 将DOM节点移入内存
        while(vm.$el.firstChild){
            let child = vm.$el.firstChild;
            fragment.appendChild(child);
        }

        // 编译fragment
        this.replace(fragment);

        // 将fragment放回el
        vm.$el.appendChild(fragment);
    }
    replace(fragment){

        // 遍历fragment
        Array.from(fragment.childNodes).forEach((node)=>{

            // 获取节点的文本内容
            let text = node.textContent;

            // 正则表达式用于匹配大括号
            let reg = /\{\{(.*)\}\}/;

            // 编译文本节点
            if(node.nodeType === 3 && reg.test(text)){
                // 解决name.age的情况
                let arr = RegExp.$1.split('.');
                let val = this.$vm;
                arr.forEach((k)=>{
                    val = val[k];
                });             
                new Watcher(this.$vm,RegExp.$1,function(newVal){
                    node.textContent = text.replace(/\{\{(.*)\}\}/,newVal);
                });
                node.textContent = text.replace(/\{\{(.*)\}\}/,val);             
            }

            // 编译元素节点
            if(node.nodeType === 1){

                // 取出所有属性
                let attrs = node.attributes;

                // 转化为真数组,遍历所有属性
                Array.from(attrs).forEach((attr)=>{

                    // 取出属性名和属性值
                    let name = attr.name;
                    let exp = attr.value;

                    // 判断是否以'v-'开头
                    if(name.indexOf('v-') == 0){
                        // 截取'v-'后面的属性名
                        let dir = name.slice(2);

                        // 处理绑定事件指令'v-on'
                        if(dir.indexOf('on') === 0){
                            let event = dir.split(':')[1];
                            let method = this.$vm.$option.methods && this.$vm.$option.methods[exp];
                            if(method && event){
                                node.addEventListener(event,method,false);
                            }
                        }

                        // 处理指令v-text
                        else if(dir.indexOf('text') === 0){
                            node.textContent = exp;
                        }
                        
                        // 处理指令v-model
                        else if(dir.indexOf('model') === 0){
                            node.value = this.$vm[exp];
                        }
                        
                        // 处理指令v-html
                        else if(dir.indexOf('html') === 0){
                            node.innerHTML = exp;
                        }
                                         
                        
                    }

                    // 创建watcher,当data发生变化触发view发生变化
                    new Watcher(this.$vm,exp,(newVal)=>{
                        node.value = newVal;
                    })

                    // 绑定事件,当view发生变化触发data发生变化
                    node.addEventListener('input',(e)=>{
                        this.$vm[exp] = e.target.value;
                    })

                })
            }

            // 递归编译所有子节点
            if(node.childNodes){
                this.replace(node);
            }
        })
    }
}

实现watcher

class Dep{
    constructor(){
        // 订阅者队列
        this.subs = [];
    }
    addSub(sub){// 添加watcher
        this.subs.push(sub);
    }
    notify(){// 调用notify执行所有watcher的update方法
        this.subs.forEach(sub=>sub.update());
    }
}

class Watcher{
    constructor(vm,exp,fn){

        // 更新回调
        this.fn = fn;

        // 保存vm
        this.vm = vm;

        // 保存表达式
        this.exp = exp;

        // 触发get方法
        Dep.target = this;
        let arr = exp.split('.');
        let val = vm;
        arr.forEach((k)=>{
            val = val[k];
        })
        Dep.target = null;

    }
    update(){ // 执行更新
        let arr = this.exp.split('.');
        let val = this.vm;
        arr.forEach((k)=>{
            val = val[k];
        })
        this.fn(val);
    }
}

实现observer

function observe(data){
    if(!data || typeof data !== 'object'){
        return 
    }
    return new Observer(data);
}

class Observer{
    constructor(data){
        let dep = new Dep();
        for(let key in data){
            let val = data[key];

            // 监听data内每一个属性值
            observe(val);      

            Object.defineProperty(data,key,{
                enumerable:true,
                configurable:true,
                get(){

                    // watcher内触发get方法,添加watcher到订阅者队列
                    Dep.target && dep.addSub(Dep.target);

                    return val;
                },
                set(newVal){
                    
                    // 值不变时,不需要修改
                    if(val === newVal){
                        return 
                    }

                    val = newVal;

                    observe(newVal);

                    // 修改值时通知dep调用发布函数
                    dep.notify();
                }
            })
        }
    }
}

将data代理到vm上

class MVVM{
    constructor(option = {}){
        this.$option = option;
        let data = this._data = this.$option.data;

        // 初始化MVVM时,监视data
        observe(data);

        // 数据代理,将this._data代理到MVVM实例上
        for(let key in data){
            Object.defineProperty(this,key,{
                enumerable:true,
                configurable:true,
                get(){
                    return this._data[key];
                },
                set(newVal){
                    this._data[key] = newVal;
                }
            })
        }

        // 编译指令和{{}}
        new Compile(option.el,this);
    }
}

使用

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>myVue</title>
    </head>
    <body>
        <div id="app">
            {{name}}
            <button v-on:click="hello" >点击</button>
            <p v-text="ee"></p>
            <p v-html="<span>hello</span>"></p>
            <input type="text" v-model="name">
        </div>
        <script src="compile.js"></script>
        <script src="observer.js"></script>
        <script src="watcher.js"></script>
        <script src="mvvm.js"></script>
        <script>
            var vm = new MVVM({
                el:'#app',
                data:{
                    name: 'tom' ,
                    age:21
                },
                methods:{
                    hello(){
                        console.log('hello');
                    }
                }
            })
        </script>
    </body>
</html>

相关文章

网友评论

      本文标题:Vue - 双向绑定原理

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