vue数据双向绑定

最近面试官经常问vue数据双向绑定,老是说不清楚,在这里来记录一下:

(1)定义:

vue通过object.defindPrototype方法劫持数据加上发布订阅模式的方式来实现的,数据发生变化,视图跟着变化,视图变化,数据也随之发生改变,数据和视图同步。

(2)表现:

v-model指令:是v-bind和v-on的语法糖,通过v-bind绑定input标签的value属性,通过v-on绑定input标签的input方法,输入数据,触发input方法,改变value值,改变绑定的值,同时其他页面用到这个数据的地方也会更新。代码如下:

<input v-model="sth" />
// 等同于上面
<input v-bind:value="sth" v-on:input="sth = $event.target.value" />

我们仔细观察上面两行代码,可以得出一个结论:

在给<input />元素添加v-model属性时,默认会把value作为元素的属性,然后把'input'事件作为实时传递value的触发事件,因此,我们可以在vue父子组件里面使用v-model。

<my-checkbox v-model="foo"></my-checkbox>
 
Vue.component('my-checkbox', {
 tempalte: `<input 
        @input="$emit('input', $event.target.value)"
        :value="value"
       />`
 props: ['value'],
})

上面代码的关键在于子组件,不一定要是input标签才行,主要是props接受到值必须是value,还有$emit的函数必须为input,参数为value,才可以实现v-model的效果。

(3)原理:

vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的,通过控制台输出一个定义在vue初始化数据上的对象里面有什么东西。

var vm = new Vue({
    data: {
        obj: {
            a: 1
        }
    },
    created: function () {
        console.log(this.obj);
    }
});

通过object.defindPrototype方法里面的get和set对数据进行劫持,使它变成了一个响应式数据。bject.defineProperty( )是用来做什么的?它可以来控制一个对象属性的一些特有操作,比如读写权、是否可以枚举。我们先来看Object.defineProperty()这个方法:

Object.defineProperty(obj, key, {
    get: function(val) {
        //获取时候触发
        console.log('我被获取了')
        return val;
    },
    set: function (newVal) {
        //修改时候触发
        console.log('我被设置了')
    }
})

已经了解到vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,那么在设置或者获取的时候我们就可以在get或者set方法里假如其他的触发函数,达到监听数据变动的目的,无疑这个方法是本文中最重要、最基础的内容之一。

 (4)vue实现:

先看原理图:

 

 在vue中v-model,v-name,{{}}等都可以对数据进行显示,也就是说假如一个属性都通过这三个指令了,那么每当这个属性改变的时候,相应的这个三个指令的html视图也必须改变,于是vue中就是每当有这样的可能用到双向绑定的指令,就在一个Dep中增加一个订阅者,其订阅者只是更新自己的指令对应的数据,也就是v-model='name'和{{name}}有两个对应的订阅者,各自管理自己的地方。每当属性的set方法触发,就循环更新Dep中的订阅者。

(5)vue 代码实现:

a.observer实现,主要是给每个vue的属性用Object.defineProperty(),代码如下:

function defineReactive (obj, key, val) {
    var dep = new Dep();
        Object.defineProperty(obj, key, {
             get: function() {
                    //添加订阅者watcher到主题对象Dep
                    if(Dep.target) {
                        // JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用
                        dep.addSub(Dep.target);
                    }
                    return val;
             },
             set: function (newVal) {
                    if(newVal === val) return;
                    val = newVal;
                    console.log(val);
                    // 作为发布者发出通知
                    dep.notify();//通知后dep会循环调用各自的update方法更新视图
             }
       })
}
        function observe(obj, vm) {
            Object.keys(obj).forEach(function(key) {
                defineReactive(vm, key, obj[key]);
            })
        }

b.实现compile:

function Compile(node, vm) {
    if(node) {
        this.$frag = this.nodeToFragment(node, vm);
        return this.$frag;
    }
}
Compile.prototype = {
    nodeToFragment: function(node, vm) {
        var self = this;
        var frag = document.createDocumentFragment();
        var child;
        while(child = node.firstChild) {
            console.log([child])
            self.compileElement(child, vm);
            frag.append(child); // 将所有子节点添加到fragment中
        }
        return frag;
    },
    compileElement: function(node, vm) {
        var reg = /\{\{(.*)\}\}/;
        //节点类型为元素(input元素这里)
        if(node.nodeType === 1) {
            var attr = node.attributes;
            // 解析属性
            for(var i = 0; i < attr.length; i++ ) {
                if(attr[i].nodeName == 'v-model') {//遍历属性节点找到v-model的属性
                    var name = attr[i].nodeValue; // 获取v-model绑定的属性名
                    node.addEventListener('input', function(e) {
                        // 给相应的data属性赋值,进而触发该属性的set方法
                        vm[name]= e.target.value;
                    });
                    new Watcher(vm, node, name, 'value');//创建新的watcher,会触发函数向对应属性的dep数组中添加订阅者,
                }
            };
        }
        //节点类型为text
        if(node.nodeType === 3) {
            if(reg.test(node.nodeValue)) {
                var name = RegExp.$1; // 获取匹配到的字符串
                name = name.trim();
                new Watcher(vm, node, name, 'nodeValue');
            }
        }
    }
}

c.watcher实现

function Watcher(vm, node, name, type) {
    Dep.target = this;
    this.name = name;
    this.node = node;
    this.vm = vm;
    this.type = type;
    this.update();
    Dep.target = null;
}

Watcher.prototype = {
    update: function() {
        this.get();
        this.node[this.type] = this.value; // 订阅者执行相应操作
    },
    // 获取data的属性值
    get: function() {
        console.log(1)
        this.value = this.vm[this.name]; //触发相应属性的get
    }
}

d.Dep添加订阅者

function Dep() {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
        sub.update();
        })
    }
}

 

posted @ 2021-08-09 16:39  鸡腿太小  阅读(355)  评论(0编辑  收藏  举报