Vue 数据双向绑定原理

1,283 阅读4分钟

一、双向绑定基本概念

  • vue 是一个 mvvm 框架,即数据双向绑定,即当数据发生变化的时候,视图也就发生变化,当视图发生变化的时候,数据也会跟着同步变化。数据双向绑定一定是对于UI控件来说的,非UI控件不会涉及到数据双向绑定

  • 在 vue 中,如果使用 vuex,实际上数据还是单向的,之所以说是数据双向绑定,这是对于用的UI控件来说,对于处理表单,vue 的双向数据绑定用起来就特别舒服了,但即两者并不互斥,在全局性数据流使用单项,方便跟踪,局部性数据流使用双向,简单易操作

二、简单的数据双向绑定实现方法

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>forvue</title>
</head>
<body>
  <input type="text" id="textInput">
  输入:<span id="textSpan"></span>
  <script>
    var obj = {},
    textInput = document.querySelector('#textInput')
    textSpan = document.querySelector('#textSpan')

    Object.defineProperty(obj, 'text', {
      // 当该属性被赋值的时候触发
      set: function (newValue) { 
        textInput.value = newValue
        textSpan.innerHTML = newValue
      }
    });

    textInput.addEventListener('keyup', function (e) {
        obj.text = e.target.value;
    });

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

代码解析:使用 Object.defineProperty() 来对 obj 中的 text 属性做劫持,也就是为这个属性添加 set 方法,当该属性被赋值的时候,就会触发 set 方法,来修改 Input 的 value 值以及 span 中的 innerHTML ,这样就可以做到数据发生改变,视图也就发生变化;然后监听 input 的keyup 事件,从而修改对象的属性值,这样就做到了当视图发生变化的时候,数据也会跟着同步变化

三、vue2双向绑定原理

3-1 实现基本流程

image.png

  • 双向绑定的三个组成部分:

    1. 数据层(Model):应用的数据及业务逻辑
    2. 视图层(View):应用的展示效果,各类UI组件
    3. 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来
  • 这里的 ViewModel 主要职责就是,数据变化后更新视图,视图变化后更新数据;并且它有两个重要的组成部分

    1. 监听器(Observer):对所有数据的属性进行监听
    2. 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
  • vue 中双向绑定是采用数据劫持结合观察者模式的方式,过程大概分为三步:

    1. new Vue() 首先执行初始化,对 data 执行响应式处理,这个过程发生 Observe 中,也就是将 data 中每个属性用 Object.defineProperty() 实现数据劫持,并且为每个属性分配一个 dep,这个 dep 有一个 subs 属性,是一个数组,专门是用来记录该属性的订阅者的
    2. 同时对模板执行编译,找到其中动态绑定的数据,从 data 中获取并初始化视图,这个过程发生在 Compile 中,编译过程中如果模版使用到了该属性,那么就会就会触发该属性的 get 方法,里面会调用该属性 dep 的 addSub 方法,给 dep 数组添加 watcher,遇到 v-model 会添加一个订阅者,{{}} 也会添加一个订阅者,v-bind 也会添加一个订阅者;如果是模版中的元素绑定了 v-model 就会为它添加监听事件
    3. 当我们在绑定 v-model 的元素上修改了值就等于为该修改了属性的值,则会触发该属性的 set 方法,在 set 方法内通知属性对应的 dep,然后循环调用他所有 watcher 的 update 方法更新视图
  • 观察者模式,让多个观察者同时监听某个主题对象,当主题对象发生变化时,会通知所有的观察者对象,即:发布者发出通知给主题对象 => 主题对象接收到通知后推送给所有订阅者 => 订阅者执行相应的操作

3-2 代码实现

     <div id="app">
        <input type="text" v-model="text">
        输入的值为:{{text}}
        <div>
            <input type="text" v-model="text">
        </div>
    </div>
    <script>
        var vm = new MVue({
            el: '#app',
            data: {
                text: 'hello world'
            }
        })
    </script>

现在的目的就是将上述代码的 input 和 data 中的数据进行绑定,当输入框内容变化时,data中的对应数据同步变化,即 view => model,当data中数据变化时,对应的文本节点内容同步变化 即 model => view

在初始化 vue 实例的时候对 data 中每个属性劫持监听,同时进行模板编译,指令解析,最后挂载到相应的 DOM 中

Vue 构造函数


function Vue (options) {
    this.$el = options.el;
    this.$data = options.data;

    // 数据监听
    obverser(this.$data);

    // 模板编译( 指令解析,事件绑定、初始化数据绑定 )
    let elem = document.querySelector(this.$el);
    elem.appendChild(nodeToFragment(elem, this))
}

Dep 构造函数,主要用来生成 Dep 实例,用来收集所有的订阅者,并提供 notify 方法,用来调用订阅者的 update 方法,从而执行相应的操作。在数据监听的时候,vue 会为 data 中的每个属性都生成一个 dep,在编译模板时,会为每个与数据绑定的节点生成一个 watcher ,就实现了 dep 与 watcher的关联

    function Dep () {
        this.subs = [];
    }
    Dep.prototype = {
        addSub (sub) {
            this.subs.push(sub);
        },
        notify () {
            this.subs.forEach(sub => {
                // 执行订阅者的update方法
                sub.update();
            })
        }
    }

订阅者 Watcher,默认组件渲染的时候,会创建一个 watcher,并且赋值给了 Dep.target,当渲染视图的时候,会取 data 中的数据, 会走每个属性的 get 方法,然后判断 Dep.target 是否有值,如果有则调用了 dep.depend(),让 dep 记录到该订阅者,并最终将 Dep.target 设置为空

    function Watcher (vm, node, name) {
        // 全局的、唯一
        Dep.target = this;
        this.node = node;
        this.name = name;
        this.vm = vm;
        this.index = index;
        this.update();
        Dep.target = null;
    }

    Watcher.prototype = {
        update () {
            this.get();
            let _name;
            if (this.index === 1) {
                _name = this.name;
            } else {
                _name = this.value;
            }
            if (this.node.nodeName === 'INPUT') {
                // 可以添加TEXTAREA、SELECT等
                this.node.value = this.value;
            } else {
               // this.node.nodeValue = this.value;
               this.node.nodeValue = this.node.nodeValue.replace(new RegExp('\\{?\\{?\\s*(' + _name + ')\\s*\\}?\\}?'), this.value);
            } 
            ++this.index;
        },
        get () {
            this.value = this.vm.$data[this.name]
        }
    }

obverser 方法,给 data 中每个属性添加一个 dep 遍历 data 中的所有属性,包括子属性对象的属性

     function obverser (obj) {
        Object.keys(obj).forEach(key => {
            if (obj.hasOwnProperty(key)) {
                if (obj[key].constructor === 'Object') {
                    obverser(obj[key])
                }
                defineReactive(obj, key);
            }
        })
    }

在 obverser 方法中调用了 defineReactive ,目的是使用了 Object.definePeoperty() 来监听属性变动,给属性添加 setter 和getter

    function defineReactive (obj, key) {
        var _value= obj[key];
        // new一个主题对象
        var dep = new Dep();
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            set (newVal) {
                if (_value= newVal) {
                    return;
                }
                _value= newVal;
                console.log(value)
                // 作为发布者发出通知给主题对象
                dep.notify();
            },
            get () {
                // 如果订阅者存在,添加到主题对象中
                if (Dep.target) {
                    dep.addSub(Dep.target);
                }
                return _value
            }
        })
    }

compile 方法主要做的是模版编译,包括了指令解析,事件绑定、初始化数据绑定

    function compile (node, vm) {
        let reg = /\{\{(.*)\}\}/;
        // 元素节点
        if (node.nodeType === 1) {
            var attrs = node.attributes;
            for (let attr of attrs) {
                if (attr.nodeName === 'v-model') {
                    // 获取v-model指令绑定的data属性
                    var name = attr.nodeValue;
                    // 绑定事件
                    node.addEventListener('input', function(e) {
                        vm.$data[name] = e.target.value;
                    })
             
                    // 初始化数据绑定
                    // node.value = vm.$data[name];
                    new Watcher(vm, node, name);
                    // 移除v-model 属性
                    node.removeAttribute('v-model')

                }
            }
        }
        
            // 文本节点
        if (node.nodeType === 3) {
            if (reg.test(node.nodeValue)) {
                var name = RegExp.$1 && (RegExp.$1.trim());
                // 绑定数据到文本节点中
               //  node.nodeValue = node.nodeValue.replace(new RegExp('\\{\\{\\s*(' + name + ')\\s*\\}\\}'), vm.$data[name]);
               new Watcher(vm, node, name);
            }
        }
    }