【Vue系列】深入探究vue框架的双向绑定底层原理

1. 实现一个极简的双向绑定效果

1.1 访问器属性

访问器属性是对象中的一种特殊属性,它不能直接在对象中设置,而必须通过defineProperty()方法单独定义。

var obj = { };

// 为obj定义一个名为hello的访问器属性

Object.defineProperty(obj, "hello", {

get: function () {return sth},

set: function (val) {/* do sth */}

})

obj.hello  // 可以像普通属性一样读取访问器属性

访问器属性的"值"比较特殊,读取或设置访问器属性的值,实际上是调用其内部特性:getset函数。

obj.hello // 读取属性,就是调用get函数并返回get函数的返回值

obj.hello = "abc" // 为属性赋值,就是调用set函数,赋值其实是传参

getset方法内部的this都指向obj,这意味着getset函数可以操作对象内部的值。另外,访问器属性的会"覆盖"同名的普通属性,因为访问器属性会被优先访问,与其同名的普通属性则会被忽略(也就是所谓的被"劫持"了)。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>vueTest</title>
<script>
        var obj ={};
        //关键是object对象的defineProperty方法
        Object.defineProperty(obj, 'hello',{
            set: function (newVal) {
                document.getElementById('a').value = newVal; //获取输入数据
                document.getElementById('b').innerHTML = newVal; //同步显示输入数据
            }
            });
            //添加监听事件
            document.addEventListener('keyup',function (e) {
                obj.hello = e.target.value;
            });

    </script>
</head>
<body>
    <div>
        <input type="text" id="a">
        <span id="b"> </span>
    </div>
    
</body>
</html>

效果:

此例实现的效果是:随文本框输入文字的变化,span中会同步显示相同的文字内容;在js或控制台显式的修改obj.name的值,视图会相应更新。这样就实现了model =>view以及view => model的双向绑定,并且是响应式的。

扫描二维码关注公众号,回复: 13614554 查看本文章

以上就是Vue实现双向绑定的基本原理。

2.  复现Vue框架的双向绑定效果

上面一个例子只是一个简单的例子,我们要完全实现vue的双向绑定功能(如下代码)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>VueTest2</title>
     <script src="https://cdn.bootcss.com/vue/2.5.16/vue.min.js"></script>
    <script>
        var vue = new Vue({
            el:'#app',
            data:{
                text:'hello world'
            }
        })
    </script>
</head>

<body>
    <div id = "app">
        <input type="text" v-model = "text">{
   
   {text}}}
    </div>
</body>
</html>

要实现Vue双向数据绑定的效果,需要三步:

 1、输入框以及文本节点与data中的数据绑定

 2、输入框内容变化时,data中的数据同步变化。即view => model的变化。

 3、data中的数据变化时,文本节点的内容同步变化。即model => view的变化。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>VueTest2</title>

</head>

<body>
    <div id ="app">
        <input type="text">
    </div>
    <script>
        //劫持id = “app” 节点的子节点、放到节点容器dom中
        var dom = nodeToFragment(document.getElementById('app'));
        console.log(dom)
        function nodeToFragment(node) {
            var flag = document.createDocumentFragment();
            var child;
            while(child = node.firstChild){
                flag.append(child);//劫持node的所有子节点
            }
            return flag;
        }
        //把劫持到的子节点添加回app节点去。
        document.getElementById('app').appendChild(dom);

        //数据初始化绑定
        function compile(node,vue) {
            var reg = /\{\{(.*)\}\}/;
            //节点类型为元素
            if (node.nodeType === 1){
                var attr = node.attributes;
                //解析属性
                for (var i =0; i < attr.length;i++){
                    if(attr[i].nodeName == 'v-model'){
                        var name = attr[i].nodeValue;//获取v-model绑定的属性名
                        node.addEventListener('input',function (e) {
                            vue[name] = e.target.value;
                        });
                        node.value = vue[name];//将data的值赋给该node
                        node.removeAttribute('v-model');
                    }
                };
            }
            //节点类型为text
            if (node.nodeType === 3){
                if (reg.test(node.nodeValue)){
                    var name = RegExp.$1;//获取匹配到的字符串
                    name = name.trim();
                    // node.nodeValue = vue[name];//将值赋给该node
                    new Watcher(vm,node,name);
                }
            }
        }
        function nodeToFragment(node, vue) {
            var flag = document.createDocumentFragment();
            var child;

            while(child = node.firstChild){
                compile(child,vue);
                flag.append(child);
            }
            return flag;
        }
        function Vue(options) {
            this.data = options.data;
            var data = this.data;
            observe(data, this);

            var id = options.el;
            var dom = nodeToFragment(document.getElementById(id),this);
            //编译完成后,将dom返回到app中
            document.getElementById(id).appendChild(dom);
        }


        //发布者
        function defineReactive(obj, key, val) {
            var dep = new Dep();

            Object.defineProperty(obj,key,{
                get: function () {
                    if (Dep.target) dep.addSub(Dep.target);
                    return val;
                },
                set:function (newVal) {
                    if (newVal == val ) return
                    val = newVal;
                    console.log(val);//
                    dep.notify();
                }
            });
        }

        function observe(obj, vm) {
            Object.keys(obj).forEach(function (key) {
                defineReactive(vm, key, obj[key]);
            })
        }
        //主题对象
        function Dep() {
            this.subs = [];
        }
        Dep.prototype={
            addSub:function (sub) {
                this.subs.push(sub);
            },
            notify:function () {
                this.subs.forEach(function (sub) {
                    sub.update();
                })
            }
        }
        //观察者
        function Watcher(vm, node, name){
            Dep.target = this;
            this.name = name;
            this.node = node;
            this.vm = vm;
            this.update();
            Dep.target =null;
        }
        Watcher.prototype = {
            update:function () {
                this.get();
                this.node.nodeValue = this.value();
            },
            //获取data中的属性值
            get:function () {
                this.value = this.vm[this.name];//触发相应属性的get
            }
        }

        var vue = new Vue({
            el:'app',
            data:{
                txt:'hello world'
            }
        });

    </script>
</body>
</html>

发布者-订阅者逻辑: 

        //发布者-订阅者示例:
        //发布者
        var pub ={
            publish :function () {
                dep.notify();
            }
        }

        //订阅者
        var sub1 = {update:function (){console.log(1)}};
        var sub2 = {update:function (){console.log(2)}};
        //主题对象
        function Dep() {
            this.subs = [sub1,sub2];
        }
        Dep.prototype={
            notify:function () {
                this.subs.forEach(function (sub) {
                    sub.update();//notify方法通知观察者update
                })
            }
        }
        //发布者发布消息,主题对象执行notify方法,进而触发订阅者执行update方法
        var dep = new Dep();
        pub.publish();

 

猜你喜欢

转载自blog.csdn.net/weixin_41950078/article/details/115254347