007-双向绑定原理

时间:2024-03-17 16:59:39

双向绑定原理

  • 双向绑定
  • 思考:
  • 一句话描述原理
  • DocuemntFragment(碎片化文档)
  • Object.defineProperty(数据劫持)
  • 发布订阅者模式
  • Vue 双向绑定图示
  • Vue 双向绑定完整实现代码

双向绑定

vue中 data定义的数据会添加双向绑定的功能,即数据更新后,页面内容会同步更新;页面内容更新后,数据也会同步更新。

思考:

  1. 初始化,如何将 data 中的数据更新到DOM模板中? - 碎片化文档
  2. 页面更新,如何更新数据? - input事件监听
  3. 数据变了更新页面,那如何知道数据变了呢? - 数据劫持,Object.defineProperty()
  4. 已知数据变了(发布者),如何更新跟这个数据相关的页面内容 {{}}、属性绑定、v-model(订阅者)呢? - 发布订阅者模式

一句话描述原理

Vue 数据双向绑定是通过数据劫持结合发布订阅者模式的方式来实现的。使用 DocuemntFragment(碎片化文档)获取所有子节点,将 v-model {{}} 类似语法的值进行填充,监听页面元素的 input 事件,当 val 变更时,更新 data 中的数据,给 data 通过 object.defineProperty 添加响应监听,当 val 变化时,会触发 set 方法,通过发布订阅模式,触发订阅者的更新方法,更新视图。

DocuemntFragment(碎片化文档)

在这里插入图片描述

function nodeToFragment(node){
    var fragment = document.createDocumentFragment();
    var child = null;
    while(child = node.firstChild){
        fragment.appendChild(child)
        
    }
    return fragment
}

Object.defineProperty(数据劫持)

在这里插入图片描述

var obj = {};  // 定义一个空对象
Object.defineProperty(obj, 'val', { // 定义要修改对象的属性
    get: function () {
        console.log('获取对象的值')
    },
    set: function (newVal) { 
        console.log('设置对象的值:最新的值是'+newVal);
    }
});
obj.hello = 'hello world'

js通过Object.defineProperty方法简单的实现双向绑定:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <input type="text" id="app">
    <span id="childSpan"></span>
</body>
<script>
    var obj = {}
    var initValue='初始值'
    Object.defineProperty(obj,'initValue',{
        get(){
            console.log('获取obj最新的值');
            return initValue
        },
        set(newVal){
            initValue = newVal
            console.log('设置最新的值');
            // 获取到最新的值  然后将最新的值赋值给我们的span
            document.getElementById('childSpan').innerHTML = initValue
            console.log(obj.initValue);
        }
    })
    document.addEventListener('keyup', function (e) {
        obj.initValue = e.target.value; //监听文本框里面的值 获取最新的值 然后赋值给obj 
    })
    
</script>
</html>

发布订阅者模式

发布订阅者模式又叫 观察者模式,他定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得将得到通知。

// 实现发布订阅模式

// 事件容器:
let handlers = {}

// 添加事件:
handlers['onmsg'].push(fn1)

// 触发事件:
this.handlers['onmsg'].forEach(handler => { handler(...params) })

学习参考:JavaScript设计模式 -发布订阅者模式

Vue 双向绑定图示

在这里插入图片描述

Vue 双向绑定完整实现代码

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
        <div id="app">
            测试双向绑定demo
            <input type="text" v-model="text" /> {{text}}
        </div>
    </body>
    <script type="text/javascript">
        //编译函数
        function compile(node, vm) {
            var reg = /\{\{(.*)\}\}/; // 来匹配{{xxx}}中的xxx
            //如果是元素节点
            if(node.nodeType === 1) {
                var attr = node.attributes;
                //解析元素节点的所有属性
                for(let i = 0; i < attr.length; i++) {
                    if(attr[i].nodeName == 'v-model') {
                        var name = attr[i].nodeValue //看看是与哪一个数据相关
                        node.addEventListener('input', function(e) { //将与其相关的数据改为最新值
                            vm[name] = e.target.value
                        })
                        node.value = vm.data[name]; //将data中的值赋予给该node
                        node.removeAttribute('v-model')
                    }
                }
            }

            //如果是文本节点
            if(node.nodeType === 3) {
                if(reg.test(node.nodeValue)) {
                    var name = RegExp.$1; //获取到匹配的字符串
                    name = name.trim();
                    node.nodeValue = vm[name]; //将data中的值赋予给该node
                    new Watcher(vm, node, name) //绑定一个订阅者
                }
            }
        }

        // 在向碎片化文档中添加节点时,每个节点都处理一下
        function nodeToFragment(node, vm) {
            var fragment = document.createDocumentFragment();
            var child;
            while(child = node.firstChild) {
                compile(child, vm);
                fragment.appendChild(child);
            }
            return fragment
        }

        //  Vue构造函数     
        //   观察data中的所有属性值,注意增添了observe
        function Vue(options) {
            this.data = options.data;
            observe(this.data, this)
            var id = options.el;
            var dom = nodeToFragment(document.getElementById(id), this)
            //处理完所有节点后,重新把内容添加回去
            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) {
            for(let key of Object.keys(obj)) {
                defineReactive(vm, key, obj[key]);
            }
        }

        // Watcher监听者
        function Watcher(vm, node, name) {
            Dep.target = this;
            this.vm = vm;
            this.node = node;
            this.name = name;
            this.update();
            Dep.target = null;
        }

        Watcher.prototype = {
            update() {
                this.get();
                this.node.nodeValue = this.value //更改节点内容的关键
            },
            get() {
                this.value = this.vm[this.name] //触发相应的get
            }
        }

        //dep构造函数
        function Dep() {
            this.subs = [] // 观察主题添加订阅者
        }
        Dep.prototype = {
            // 添加订阅者
            addSub(sub) {
                this.subs.push(sub)
            },
            // 发布通知
            notify() {
                this.subs.forEach(function(sub) {
                    sub.update();
                })
            }
        }

        var vm = new Vue({
            el: 'app',
            data: {
                text: '赵刚'
            }
        })
    </script>

</html>