通俗理解之Vue-----双向绑定

252 阅读4分钟

一、什么是双向绑定?

1、前景知识:

(1)单向绑定(MVC模式 Model View Controller):

主要由

  • Model(模型):用于封装与应用程序业务逻辑相关的数据以及对数据的处理方法。Model 有对数据直接访问的权力,例如对数据库的访问。
  • View(视图) :视图代表模型包含的数据的可视化。
  • Controller(控制器):是用户与系统之间的纽带,它接受用户输入,并指示模型和视图基于用户输入执行操作(处理数据、展示数据)

主要实现过程:

image.png 控制器接收到视图层的用户请求和操作,然后调用相应的模型针对用户需求进行业务处理,并返回数据给控制器。控制器再调用相应的视图来显示处理的结果,并通过视图呈现给用户。

(2)MVVM模式
  • 数据层(Model):应用的数据及业务逻辑
  • 视图层(View):应用的展示效果,各类UI组件
  • 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来

View 和 Model 之间其实并没有直接的联系,而是通过ViewModel进行交互,它能够监听到数据的变化,然后通知视图进行自动更新,而当用户操作视图时,VM也能监听到视图的变化,然后通知数据做相应改动,这实际上就实现了数据的双向绑定。

2、双向绑定实现原理

Vue 框架是一个典型的 MVVM模式的框架,即数据的双向绑定。主要是实现以下两个过程:

  • 视图变化如何更新数据
  • 数据变化如何更新视图
(1) 视图变化如何更新数据

通过事件监听的方式实现对视图变化的监测

(2) 数据变化如何更新视图
  1. 监听器 Observer 用来实现对数据进行监测 new Vue()首先执行初始化,对data执行响应化处理,通过Object.defineProperty方法中的get() 和 set() 实现对数据的监测
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]);
            })
        }

2.同时指令解析器 Compile扫描和解析每个节点的相关指令,初始化模板数据

Compile对每个节点元素进行扫描和解析,将相关指令通过节点类型对应初始化成一个订阅者 Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者 Watcher 接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。

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');
            }
        }
    }
}

3.订阅者 Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图

class Watcher {
        constructor(vm,exp,cb){
            this.vm = vm;
            this.exp = exp;
            this.cb = cb;
            this.value = this.get();  // 将自己添加到订阅器的操作
        },

        update(){
            let value = this.vm.data[this.exp];
            let oldVal = this.value;
            if (value !== oldVal) {
                this.value = value;
                this.cb.call(this.vm, value, oldVal);
            },
        get(){
            Dep.target = this;  // 缓存自己
            let value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
            Dep.target = null;  // 释放自己
            return value;
        }
    }
  • vm:一个Vue的实例对象;
  • exp:是node节点的v-model或v-on:click等指令的属性值。如v-model="name",exp就是name;
  • cb:是Watcher绑定的更新函数

这里有个疑问就是wacther如何装入到对应的dep中?

当对应的wacther初始化时会触发wacther中的get(),然后再Dep.target = this缓存当前的数据key通过获取值的方式强行触发对应数据的get()方法触发dep.depend()将自己装入dep中。

4.使用Dep收集对应数据属性的所有依赖watcher

class Dep {
        constructor(){
            this.subs = []
        },
        //增加订阅者
        addSub(sub){
            this.subs.push(sub);
        },
        //判断是否增加订阅者
        depend () {
            if (Dep.target) {
                this.addSub(Dep.target)
            }
        },

        //通知订阅者更新
        notify(){
            this.subs.forEach((sub) =>{
                sub.update()
            })
        }
    }
Dep.target = null;
3、总结
  • new Vue() 初始化 在监听器 Observer 中通过Object.defineProperty方法,实现对data中数据的可监测化。
  • 定义更新函数和Watcher
  • 同时使用解析器Compiler对模板执行编译,通过对每个元素节点的指令扫描和解析,将指令模版置换成对应的数据和初始化对应的watcher与更新函数
  • 使用Dep依赖收集器,将同个视图中对应某个数据属性所有依赖watcher收集在对应某个数据属性的Dep中
  • 当数据发生变化后会触发该数据的set()以及里面的dep.notify(),通过dep.notify()遍历触发dep里的所有watcher的update()实现数据的更新。

差不多了解的就这些啦,如果理解有错欢迎纠正!!!

参考文献:

[1]((31条消息) 通俗易懂了解Vue双向绑定原理及实现_liuyuanyan的博客-CSDN博客_vue数据双向绑定原理 通俗易懂)

[2](面试官:双向数据绑定是什么 | web前端面试 - 面试官系列 (vue3js.cn))

[3]((31条消息) 前端技术栈:Vue 双向绑定_SongXJ--的博客-CSDN博客)