源码分析——02_数据双向绑定的原理

本文介绍了MVVM框架中的数据代理和数据劫持机制。数据代理通过Object.defineProperty实现,创建了一个浅代理,使得VM实例可以直接访问data中的属性。而数据劫持则是通过Observer深度遍历data,为每个属性添加getter和setter,实现数据的响应式。此外,还涉及到Watcher和Dep类的使用,以确保数据变化时能够通知到相关的视图更新。
摘要由CSDN通过智能技术生成

在这里插入图片描述

数据代理

数据代理是浅代理,将data中的数据读写的权限交给了vm

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div id="app"></div>
</body>
<script src="./MVVM/mvvm.js"></script>
<script src="./MVVM/compile.js"></script>
<script src="./MVVM/observer.js"></script>
<script src="./MVVM/watcher.js"></script>
<script>
    //数据转绑(数据代理 它是浅代理)  
    //并没有进行存值操作  也没有进行拷贝 所以是浅代理
    var vm = new MVVM({
        el:"#app",
        data:{
            msg:"hello vue",
            a:{
                b:{
                    c:{
                        name:"i am damu"
                    }
                }
            }
        }
    })

    console.log(vm.msg);//msg只是一个空壳,数据代理
    console.log(vm.a.b.c.name);//a是代理
</script>
</html>

mvvm.js:

function MVVM(options) {
    //外面配置项
    this.$options = options;
    //data : 外面配置项中data数据
    var data = this._data = this.$options.data;
    //this劫持(因为下面的this会丢)  me是一个鸡肋闭包
    var me = this;
    // Object.keys(data): 返回一个数组 数组中存放data所有属性名
    Object.keys(data).forEach(function(key) {
    //内部本身的this指向window
        //key : data中每一个key(属性名)
        me._proxy(key);
    });

    //数据劫持
    observe(data, this);

    //模板解析
    this.$compile = new Compile(options.el || document.body, this)
}

MVVM.prototype = {
    $watch: function(key, cb, options) {
        new Watcher(this, key, cb);
    },

    //key : data中每一个key(属性名)
    _proxy: function(key) {
        //this劫持  me是一个鸡肋闭包
        //this:MVVM实例对象;vm
        var me = this;
        //给me 也就是MVVM实例对象  vm 新增属性
        Object.defineProperty(me, key, {
            configurable: false,
            enumerable: true,
            get: function proxyGetter() {
                return me._data[key];//me和key都是闭包
            },
            set: function proxySetter(newVal) {
                me._data[key] = newVal;
            }
        });
    }
};

数据劫持

数据劫持是深度的,将data中的每一个属性都拿出来进行重新定义,让他具备get和set方法

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div id="app"></div>
</body>
<script src="./MVVM/mvvm.js"></script>
<script src="./MVVM/observer.js"></script>
<script src="./MVVM/compile.js"></script>
<script src="./MVVM/watcher.js"></script>
<script>
    //数据转绑(数据代理 它是浅代理)

    /*
        dep的分配
            msg damu damuName damuAge wife wifeName wifeAge son sonName sonAge
        改变属性的本质(数据描述符->访问描述符)
        数据劫持顺序:
            msg damuName damuAge wifeName wifeAge sonName sonAge son wife damu
    */

    var vm = new MVVM({
        el:"#app",
        data:{
            msg:"hello world", //数据描述符
            damu:{
                damuName:"达姆",
                damuAge:18,
                wife:{
                    wifeName:"冬雨",
                    wifeAge:18,
                    son:{
                        sonName:"小乐",
                        sonAge:8
                    }
                }
            }
        }
    })

    console.log(vm.damu.wife.son.sonName)
</script>
</html>





observer.js:

//value:外部的data配置(data数据)
//vm:MVVM的实例对象
function observe(value, vm) {
    // 外部的data配置有值而且必须得是对象 才不会被咬住
    if (!value || typeof value !== 'object') {return;}
    //value:外部的data配置(data数据)
    return new Observer(value);
};

//data:data数据
function Observer(data) {
    //this:Observer的实例对象
    this.data = data;
    this.walk(data);
}

Observer.prototype = {
    walk: function(data) {
        //me:Observer的实例对象
        var me = this;
        Object.keys(data).forEach(function(key) {
            //key : data中一个个可枚举属性的名称
            me.convert(key, data[key]);
        });
    },

    //key, val : data中一组组键值对
    convert: function(key, val) {
        //this:Observer的实例对象
        this.defineReactive(this.data, key, val);
    },
    //data:data数据
    //key, val : data中一组组键值对
    defineReactive: function(data, key, val) {
        //分配dep
        var dep = new Dep();
        //递归!!!!!
        var childObj = observe(val);//是对象,结束递归,进入下面
        // 改变属性的本质(数据描述符->访问描述符)
        Object.defineProperty(data, key, {
            enumerable: true, // 可枚举
            configurable: false, // 不能再define
            get: function() {
                //Dep.target:正解析的指令对应的watcher实例对象
                if (Dep.target) {
                    //dep : 真正访问的data中的数据所对应的dep
                    dep.depend();
                }
                return val;
            },
            set: function(newVal) {
                if (newVal === val) {
                    return;
                }
                val = newVal;
                childObj = observe(newVal);
                //dep唤醒所有watcher!!!
                dep.notify();
            }
        });
    }
};


var uid = 0;

function Dep() {
    this.id = uid++;
    this.subs = [];
}

Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },

    depend: function() {
        //this : 真正访问的data中的数据所对应的dep
        //Dep.target : 正解析的指令对应的watcher实例对象
        Dep.target.addDep(this);
    },

    removeSub: function(sub) {
        var index = this.subs.indexOf(sub);
        if (index != -1) {
            this.subs.splice(index, 1);
        }
    },

    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};

Dep.target = null;

模板解析(插值表达式)&&(指令)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div id="app">
        {{msg}}
    </div>
</body>
<script src="./MVVM/mvvm.js"></script>
<script src="./MVVM/observer.js"></script>
<script src="./MVVM/compile.js"></script>
<script src="./MVVM/watcher.js"></script>
<script>
    var vm = new MVVM({
        el:"#app",
        data:{
            msg:"hello world"
        }
    })


    console.log(app.childNodes);
    console.log(app.children);
</script>
</html>





compile.js:

//el:   el配置
//vm:   MVVM的实例对象
function Compile(el, vm) {
    //this:Compile实例对象
    this.$vm = vm;
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);

    if (this.$el) {
        //将模板拆入到文档碎片中
        this.$fragment = this.node2Fragment(this.$el);
        //真正的模板解析
        this.init();
        //解析完模板之后的挂载过程
        this.$el.appendChild(this.$fragment);
    }
}

Compile.prototype = {
    //将指定节点中子元素全部剪切到文档碎片中
    node2Fragment: function(el) {
        var fragment = document.createDocumentFragment(),
            child ;

        while (child = el.firstChild) {
            fragment.appendChild(child);
        }

        return fragment;
    },

    //模板解析的入口方法
    init: function() {
        //this.$fragment : 存放模板的文档碎片
        this.compileElement(this.$fragment);
    },

    //el : 存放模板的文档碎片
    compileElement: function(el) {
        //childNodes是会获取到文本节点的
        var childNodes = el.childNodes,
            me = this;

        // [].slice.call(childNodes) : 将childNodes转为真数组
        [].slice.call(childNodes).forEach(function(node) {
            //node : 模板中的每一个节点
            var text = node.textContent;
            //正则表达式  {{.*}}  ===>  "{{}}"  "   {{msg}}    "
            var reg = /\{\{(.*)\}\}/;

            if (me.isElementNode(node)) {
                me.compile(node);
            } else if (me.isTextNode(node) && reg.test(text)) {
                //vue只解析 包含插值表达式的文本节点
                //RegExp.$1 : 拿到的是第一个分组匹配到的内容!!!  就是插值表达式中内容!!
                //表达式!!!
                me.compileText(node, RegExp.$1);
            }

            //对嵌套的节点进行递归解析
            if (node.childNodes && node.childNodes.length) {
                me.compileElement(node);
            }
        });
    },
    //插值表达式的解析  node:当前正在被解析的节点  exp:表达式
    compileText: function(node, exp) {
        compileUtil.text(node, this.$vm, exp);
    },
    //指令的解析
    compile: function(node) {
        //拿到元素节点的所有属性
        var nodeAttrs = node.attributes,
            me = this;

        //将元素节点的所有属性遍历一遍 如果满足是vue的指令的话 则要解析
        [].slice.call(nodeAttrs).forEach(function(attr) {
            //当前解析的属性的名称
            var attrName = attr.name;
            //判断解析的属性是否为vue指令
            if (me.isDirective(attrName)) {
                //拿到指令对应的表达式
                var exp = attr.value;
                //将指令名称去掉v-
                var dir = attrName.substring(2);
                //区分普通指令 和 事件指令
                if (me.isEventDirective(dir)) {
                    //事件指令的解析规则
                    compileUtil.eventHandler(node, me.$vm, exp, dir);
                } else {
                    //普通指令的解析规则
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
                }

                //将vue指令从节点上移除!
                node.removeAttribute(attrName);
            }
        });
    },



    //判断属性是否为vue指令
    isDirective: function(attr) {
        return attr.indexOf('v-') == 0;
    },
    //判断属性是否为事件指令
    isEventDirective: function(dir) {
        return dir.indexOf('on') === 0;
    },
    //判断node是否是一个元素节点
    isElementNode: function(node) {
        return node.nodeType == 1;
    },
    //判断node是否是一个文本节点
    isTextNode: function(node) {
        return node.nodeType == 3;
    }
};

// 指令处理的工具类 ({{}} v-text v-html v-model v-class v-on)
var compileUtil = {
    //node:当前正在被解析的节点
    //vm : vm实例对象
    //exp:表达式
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },

    html: function(node, vm, exp) {
        this.bind(node, vm, exp, 'html');
    },

    model: function(node, vm, exp) {
        this.bind(node, vm, exp, 'model');

        //实现数据双向绑定
        var me = this,
            val = this._getVMVal(vm, exp);
        node.addEventListener('input', function(e) {
            var newValue = e.target.value;
            if (val === newValue) {
                return;
            }

            me._setVMVal(vm, exp, newValue);
            val = newValue;
        });
    },

    class: function(node, vm, exp) {
        this.bind(node, vm, exp, 'class');
    },

    eventHandler: function(node, vm, exp, dir) {
        var eventType = dir.split(':')[1],
            fn = vm.$options.methods && vm.$options.methods[exp];

        if (eventType && fn) {
            node.addEventListener(eventType, fn.bind(vm), false);
        }
    },

    //找对应指令的更新器
    //根据exp 找到exp对应的值
    bind: function(node, vm, exp, dir) {
        //找更新器
        var updaterFn = updater[dir + 'Updater'];
        //确认更新器是否存在  如果存在则调用!!!
        //this._getVMVal(vm, exp):根据表达式exp 去vm.data中找exp对应的值
        updaterFn && updaterFn(node, this._getVMVal(vm, exp));

        //每一个指令都对应着一个watcher(拿着指令最重要的两个内容:更新器 和 节点)
        new Watcher(vm, exp, function(value, oldValue) {
            updaterFn && updaterFn(node, value, oldValue);
        });
    },



    //根据表达式去vm实例对象中 找表达式对应的值!
    _getVMVal: function(vm, exp) {
        //val : data配置!!!
        var val = vm._data;
        //[obj,msg,text]
        exp = exp.split('.');
        exp.forEach(function(k) {
            // k:obj / msg / text
            val = val[k];
        });
        return val;
    },

    //数据双向绑定有关  exp:obj.msg.text2
    _setVMVal: function(vm, exp, value) {
        var val = vm._data;
        exp = exp.split('.');
        // damu.age  0 1
        exp.forEach(function(k, i) {
            if (i < exp.length - 1) {
                val = val[k];
            } else {
                //调用k对应的set方法
                val[k] = value;
            }
        });
    }
};


//更新器集合
var updater = {
    textUpdater: function(node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    },

    htmlUpdater: function(node, value) {
        node.innerHTML = typeof value == 'undefined' ? '' : value;
    },

    classUpdater: function(node, value, oldValue) {
        var className = node.className;
        className = className.replace(oldValue, '').replace(/\s$/, '');

        var space = className && String(value) ? ' ' : '';

        node.className = className + space + value;
    },

    modelUpdater: function(node, value, oldValue) {
        node.value = typeof value == 'undefined' ? '' : value;
    }
};
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div id="app">
        <span v-text="obj.msg.text" damu="damu"></span>
        {{obj.msg.text2}}
    </div>
</body>
<script src="./MVVM/mvvm.js"></script>
<script src="./MVVM/observer.js"></script>
<script src="./MVVM/compile.js"></script>
<script src="./MVVM/watcher.js"></script>
<script>
    var vm = new MVVM({
        el:"#app",
        data:{
            obj:{
                msg:{
                    text:"hello mvvm",
                    text2:"hello mvvm2"
                }
            }
        }
    })
</script>
</html>





数据响应式

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div id="app">
        <span v-html="obj.msg.text" damu="damu"></span> <br>
        {{obj.msg.text2}}
    </div>
</body>
<script src="./MVVM/mvvm.js"></script>
<script src="./MVVM/observer.js"></script>
<script src="./MVVM/compile.js"></script>
<script src="./MVVM/watcher.js"></script>
<script>
    /*
        data中的每一个key都对应着一个dep
        模板上的每一个指令都对应着一个watcher

        watcher 和 dep之间应该构建什么样的关系?
            一个指令有没有可能引用多个data中的数据?  一个watcher对应多个dep
            一个data中的数据有没有可能出现在多个指令内? 一个dep对应多个watcher

       总结:
         dep和watcher之间是多对多的关系!!!
   */

    /*
        v-html.watcher.depIds={
            1:obj的dep,
            2:msg的dep,
            3:text的dep
        }
        {{}}.watcher.depIds={
            1:obj的dep,
            2:msg的dep,
            4:text2的dep
        }

        obj.dep.subs=[v-html.watcher,{{}}.watcher]
        msg.dep.subs=[v-html.watcher,{{}}.watcher]
        text.dep.subs=[v-html.watcher]
        text2.dep.subs=[{{}}.watcher]
    */
    var vm = new MVVM({
        el:"#app",
        data:{
            obj:{
                msg:{
                    text:"hello mvvm",
                    text2:"hello mvvm2"
                }
            }
        }
    })

    setTimeout(()=>{
        vm.obj.msg.text = "xxx"
    },2000)
</script>
</html>





watcher.js:

//vm : MVVM的实例对象
//exp : 表达式
//cb : 回调函数(两个闭包,updaterFn,node)
function Watcher(vm, exp, cb) {
    this.cb = cb;
    this.vm = vm;
    this.exp = exp;
    //this: Watcher的实例对象
    this.depIds = {};
    this.value = this.get(); //拿老值!!!
}

Watcher.prototype = {
    update: function() {
        this.run();
    },
    run: function() {
        var value = this.get(); //拿新值
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },
    addDep: function(dep) {
        if (!this.depIds.hasOwnProperty(dep.id)) {
            //dep:真正访问的data中的数据所对应的dep
            dep.addSub(this);
            //this : Dep.target(watcher) 正解析的指令对应的watcher实例对象
            this.depIds[dep.id] = dep;
        }
    },
    get: function() {
        Dep.target = this; //真正解析的指令对应的watcher实例对象
        var value = this.getVMVal();
        Dep.target = null;
        return value;
    },

    //根据表达式exp 去 data中找exp对应的值
    //exp : obj.msg.text (调三次get方法)
    getVMVal: function() {
        var exp = this.exp.split('.');
        var val = this.vm._data;
        exp.forEach(function(k) {
            val = val[k];
        });
        return val;
    }
};

数据双向绑定

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="obj.msg.text2">
        {{obj.msg.text2}}
    </div>
</body>
<script src="./MVVM/mvvm.js"></script>
<script src="./MVVM/observer.js"></script>
<script src="./MVVM/compile.js"></script>
<script src="./MVVM/watcher.js"></script>
<script>
    var vm = new MVVM({
        el:"#app",
        data:{
            obj:{
                msg:{
                    text:"hello mvvm",
                    text2:"hello mvvm2"
                }
            }
        }
    })
</script>
</html>





compile.js:

//el:   el配置
//vm:   MVVM的实例对象
function Compile(el, vm) {
    //this:Compile实例对象
    this.$vm = vm;
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);

    if (this.$el) {
        //将模板拆入到文档碎片中
        this.$fragment = this.node2Fragment(this.$el);
        //真正的模板解析
        this.init();
        //解析完模板之后的挂载过程
        this.$el.appendChild(this.$fragment);
    }
}

Compile.prototype = {
    //将指定节点中子元素全部剪切到文档碎片中
    node2Fragment: function(el) {
        var fragment = document.createDocumentFragment(),
            child ;

        while (child = el.firstChild) {
            fragment.appendChild(child);
        }

        return fragment;
    },

    //模板解析的入口方法
    init: function() {
        //this.$fragment : 存放模板的文档碎片
        this.compileElement(this.$fragment);
    },

    //el : 存放模板的文档碎片
    compileElement: function(el) {
        //childNodes是会获取到文本节点的
        var childNodes = el.childNodes,
            me = this;

        // [].slice.call(childNodes) : 将childNodes转为真数组
        [].slice.call(childNodes).forEach(function(node) {
            //node : 模板中的每一个节点
            var text = node.textContent;
            //正则表达式  {{.*}}  ===>  "{{}}"  "   {{msg}}    "
            var reg = /\{\{(.*)\}\}/;

            if (me.isElementNode(node)) {
                me.compile(node);
            } else if (me.isTextNode(node) && reg.test(text)) {
                //vue只解析 包含插值表达式的文本节点
                //RegExp.$1 : 拿到的是第一个分组匹配到的内容!!!  就是插值表达式中内容!!
                //表达式!!!
                me.compileText(node, RegExp.$1);
            }

            //对嵌套的节点进行递归解析
            if (node.childNodes && node.childNodes.length) {
                me.compileElement(node);
            }
        });
    },
    //插值表达式的解析  node:当前正在被解析的节点  exp:表达式
    compileText: function(node, exp) {
        compileUtil.text(node, this.$vm, exp);
    },
    //指令的解析
    compile: function(node) {
        //拿到元素节点的所有属性
        var nodeAttrs = node.attributes,
            me = this;

        //将元素节点的所有属性遍历一遍 如果满足是vue的指令的话 则要解析
        [].slice.call(nodeAttrs).forEach(function(attr) {
            //当前解析的属性的名称
            var attrName = attr.name;
            //判断解析的属性是否为vue指令
            if (me.isDirective(attrName)) {
                //拿到指令对应的表达式
                var exp = attr.value;
                //将指令名称去掉v-
                var dir = attrName.substring(2);
                //区分普通指令 和 事件指令
                if (me.isEventDirective(dir)) {
                    //事件指令的解析规则
                    compileUtil.eventHandler(node, me.$vm, exp, dir);
                } else {
                    //普通指令的解析规则
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
                }

                //将vue指令从节点上移除!
                node.removeAttribute(attrName);
            }
        });
    },



    //判断属性是否为vue指令
    isDirective: function(attr) {
        return attr.indexOf('v-') == 0;
    },
    //判断属性是否为事件指令
    isEventDirective: function(dir) {
        return dir.indexOf('on') === 0;
    },
    //判断node是否是一个元素节点
    isElementNode: function(node) {
        return node.nodeType == 1;
    },
    //判断node是否是一个文本节点
    isTextNode: function(node) {
        return node.nodeType == 3;
    }
};

// 指令处理的工具类 ({{}} v-text v-html v-model v-class v-on)
var compileUtil = {
    //node:当前正在被解析的节点
    //vm : vm实例对象
    //exp:表达式
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },

    html: function(node, vm, exp) {
        this.bind(node, vm, exp, 'html');
    },

    model: function(node, vm, exp) {
        this.bind(node, vm, exp, 'model');

        //实现数据双向绑定
        var me = this,
            val = this._getVMVal(vm, exp);
        node.addEventListener('input', function(e) {
            var newValue = e.target.value;
            if (val === newValue) {
                return;
            }

            me._setVMVal(vm, exp, newValue);
            val = newValue;
        });
    },

    class: function(node, vm, exp) {
        this.bind(node, vm, exp, 'class');
    },

    eventHandler: function(node, vm, exp, dir) {
        var eventType = dir.split(':')[1],
            fn = vm.$options.methods && vm.$options.methods[exp];

        if (eventType && fn) {
            node.addEventListener(eventType, fn.bind(vm), false);
        }
    },

    //找对应指令的更新器
    //根据exp 找到exp对应的值
    bind: function(node, vm, exp, dir) {
        //找更新器
        var updaterFn = updater[dir + 'Updater'];
        //确认更新器是否存在  如果存在则调用!!!
        //this._getVMVal(vm, exp):根据表达式exp 去vm.data中找exp对应的值
        updaterFn && updaterFn(node, this._getVMVal(vm, exp));

        //每一个指令都对应着一个watcher(拿着指令最重要的两个内容:更新器 和 节点)
        new Watcher(vm, exp, function(value, oldValue) {
            updaterFn && updaterFn(node, value, oldValue);
        });
    },



    //根据表达式去vm实例对象中 找表达式对应的值!
    _getVMVal: function(vm, exp) {
        //val : data配置!!!
        var val = vm._data;
        //[obj,msg,text]
        exp = exp.split('.');
        exp.forEach(function(k) {
            // k:obj / msg / text
            val = val[k];
        });
        return val;
    },

    //数据双向绑定有关  exp:obj.msg.text2
    _setVMVal: function(vm, exp, value) {
        var val = vm._data;
        exp = exp.split('.');
        // damu.age  0 1
        exp.forEach(function(k, i) {
            if (i < exp.length - 1) {
                val = val[k];
            } else {
                //调用k对应的set方法
                val[k] = value;
            }
        });
    }
};


//更新器集合
var updater = {
    textUpdater: function(node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    },

    htmlUpdater: function(node, value) {
        node.innerHTML = typeof value == 'undefined' ? '' : value;
    },

    classUpdater: function(node, value, oldValue) {
        var className = node.className;
        className = className.replace(oldValue, '').replace(/\s$/, '');

        var space = className && String(value) ? ' ' : '';

        node.className = className + space + value;
    },

    modelUpdater: function(node, value, oldValue) {
        node.value = typeof value == 'undefined' ? '' : value;
    }
};

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值