简介
说明
本文用示例介绍Vue的双向绑定的原理(实现方式)。
什么是双向绑定
双向绑定:数据和视图任意一个变化,则另一个也跟着变化。也就是:数据变化更新视图,视图变化更新数据。
Vue是MVVM模型。数据属于Model,视图属于View。本文要介绍的双向绑定就是:VM(ViewModel)。
MVVM表示的是 Model-View-ViewModel
- Model:模型层。负责处理业务逻辑以及和服务器端进行交互
- View:视图层:负责将数据模型转化为UI展示出来,可以简单理解为HTML页面
- ViewModel:视图模型层。用来连接Model和View,是Model和View之间的通信桥梁
Vue双向绑定的实现方案
官网
深入响应式原理 — Vue.js
简介
Vue是通过数据劫持和观察者模式(订阅者模式)来实现双向绑定的。
| 项 | 数据劫持 | 观察者模式 | 
| 目的 | 监视数据的变化。 用于数据变化时更新视图。 | 监视视图的变化。 用于视图变化时更新数据。 | 
| 实现方案 | Vue2: Object.defineProperty() Vue3: Proxy(ES6的新特性) | 对每个元素节点进行扫描和解析并绑定更新函数。 | 
Object.defineProperty
优点
- 兼容性好。(ES5就已支持)
缺点
- 不能监听数组;
- 因为数组没有getter和setter。(数组长度不确定,如果太长性能负担太大)
- 只能监听属性,而不是整个对象;需要遍历属性;
- 只能监听属性变化(set、get),不能监听属性的删减;
Proxy
优点
- 可以直接监听数组的变化
- 可以直接监听整个对象,,而非是对象的某个属性
- 拦截方法丰富: 多达13种, 不限于 get、set、deleteProperty、has等。
缺点
- 兼容性差。(ES6的新特性)
详述
双向绑定需要三个部分:监听器(Observer),解析器(Compile),订阅者(Watcher)。
- 监听器(Observer)
- 监听数据的变化。
- 对数据的属性进行递归遍历,都加上setter和getter。给数据赋值时,会触发setter;读数据时,会触发getter。
- 解析器(Compile)
- 作用1:初始化页面。
- 对每个元素节点进行扫描和解析,利用正则将页面中的"{{xxx}}"替换成data中对应的数据
- 作用2:监听视图的变动。
- 将每个指令对应的节点绑定更新函数,添加订阅者。一旦订阅者发出通知,就执行相应的更新函数。
- 订阅者(Watcher)
- 连接Observer和Compile 之间的桥梁。
- 通过Observer监听数据变化,更新视图。
- 通过Compile监听视图变化,更新数据。
手写双向绑定(简单实现)
需求:将输入的内容(“视图”内容)保存到对象(“数据”内容)中,对象保存时,同时更新<div>的内容(“视图”内容)。
代码
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>This is title</title>
</head>
<body>
<input type="text" id="id-input"><br/>
<div id="id-div"></div>
<script>
    let user = {};
    let input = document.querySelector("#id-input");
    let text = document.querySelector("#id-div");
    // 数据到视图 model => view
    Object.defineProperty(user,"name",{
        get:function(){
            console.log('获取user的name');
            return this._name;
        },
        set:function(val){
            console.log('修改user的name为:' + val);
            this._name = val;
            text.textContent = this._name;
        }
    })
    // 视图到数据 view => model
    input.addEventListener('input',function(e){
        user.name = e.target.value;
        console.log("-----------------------")
        console.log("监听器中将user的name设置为:" + user.name);
    })
</script>
</body>
</html>测试
手写双向绑定(模仿vue2)
代码
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>This is title</title>
</head>
<body>
<!-- 实现vue -->
<div id="app">
    <input type="text" v-model="text">
    {{ text }}
</div>
<script type="text/javascript">
    function defineReactive(obj, key, value) {
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            get: function () {
                if (Dep.target) {
                    dep.addSub(Dep.target);
                }
                return value
            },
            set: function (newVal) {
                // 数据没有改变则不需要通知
                if (newVal === value) {
                    return
                }
                value = newVal;
                console.log('新值:' + value);
                // 数据改变,通知订阅者
                dep.notify();
            }
        })
    }
    // 观察者函数
    function observe(obj, vm) {
        for (let key of Object.keys(obj)) {
            defineReactive(vm, key, obj[key]);
        }
    }
    /*编译函数*/
    function compile(node, vm) {
        let reg = /\{\{(.*)\}\}/; // 来匹配 {{ xxx }} 中的xxx
        // 如果是元素节点
        if (node.nodeType === 1) {
            let attr = node.attributes;
            // 解析元素节点的所有属性
            for (let i = 0; i < attr.length; i++) {
                if (attr[i].nodeName === 'v-model') {
                    let name = attr[i].nodeValue; // 看看是与哪一个数据相关
                    node.addEventListener('input', function (e) {
                        vm[name] = e.target.value; // 将实例的text 修改为最新值
                    });
                    node.value = vm[name]; // 将data的值赋给该node
                    node.removeAttribute('v-model');
                }
            }
        }
        // 如果是文本节点
        if (node.nodeType === 3) {
            if (reg.test(node.nodeValue)) {
                let name = RegExp.$1; // 获取到匹配的字符串
                name = name.trim();
                // node.nodeValue = vm[name];  // 将data的值赋给该node
                new Watcher(vm, node, name); // 不直接通过赋值的操作,而是通过绑定一个订阅者
            }
        }
    }
    // Watcher构造函数
    function Watcher(vm, node, name) {
        Dep.target = this; // Dep.target 是一个全局变量
        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();
            })
        }
    }
    function nodeToFragment(node, vm) {
        let fragment = document.createDocumentFragment();
        let child;
        while (child = node.firstChild) {
            compile(child, vm);
            fragment.appendChild(child);
        }
        return fragment
    }
    // Vue构造函数
    function MyVue(options) {
        this.data = options.data;
        let data = this.data;
        observe(data, this);
        let id = options.el;
        let dom = nodeToFragment(document.getElementById(id), this);
        // 处理完所有dom节点后,重新将内容添加回去
        document.getElementById(id).appendChild(dom);
    }
    let vm = new MyVue({
        el: 'app',
        data: {
            text: 'hello world'
        }
    });
</script>
</body>
</html>测试
其他网址
通俗易懂了解Vue双向绑定原理及实现 - osc_2x36yftz的个人空间 - OSCHINA - 中文开源技术交流社区
手写JS(七)--实现VUE的双向绑定 - 掘金










