博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
vue双向数据绑定原理
阅读量:5780 次
发布时间:2019-06-18

本文共 7680 字,大约阅读时间需要 25 分钟。

什么是双向数据绑定?Vue是一个MVVM框架,数据绑定简单来说,就是当数据发生变化时,相应的视图会进行更新,当视图更新时,数据也会跟着变化。

实现数据绑定的方式大致有以下几种:

- 1、发布者-订阅者模式(backbone.js)- 2、脏值检查(angular.js)- 3、数据劫持(vue.js)

发布者-订阅者模式

一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是 vm.set('property', value),有兴趣可参考

我们更希望可以通过 vm.property = value 这种方式进行数据更新,同时自动更新视图。

脏值检查

angular是通过脏值检查方式来对比数据是否变化,来决定是否更新视图,最常见的方式是通过setInterval()来监测数据变化,当然,只会在某些指定事件触发时下才进行脏值检查。大致如下:

- DOM事件,譬如用户输入文本,点击按钮等。( ng-click )- XHR响应事件 ( $http )- 浏览器Location变更事件 ( $location )- Timer事件( $timeout , $interval )- 执行 $digest() 或 $apply()

数据劫持

Vue.js则是通过数据劫持以及结合发布者-订阅者来实现的,数据劫持是利用ES5的Object.defineProperty(obj, key, val)来劫持各个属性的的setter以及getter,在数据变动时发布消息给订阅者,从而触发相应的回调来更新视图。

一、实现最基础的数据绑定

    输入的值为:    

二、双向数据绑定实现(此处用MVue替代)

上面的只是简单的使用了Object.defineProperty(),并不是我们最终想要的效果,最终想要的效果如下:

输入的值为:{
{text}}

实现思路:

1、输入框以及文本节点和data中的数据进行绑定
2、输入框内容变化时,data中的对应数据同步变化,即 view => model
3、data中数据变化时,对应的文本节点内容同步变化 即 model => view

上述流程如图所示:

vue双向数据绑定原理图

1、实现一个数据监听器Obverser,对data中的数据进行监听,若有变化,通知相应的订阅者。

2、实现一个指令解析器Compile,对于每个元素上的指令进行解析,根据指令替换数据,更新视图。
3、实现一个Watcher,用来连接Obverser和Compile, 并为每个属性绑定相应的订阅者,当数据发生变化时,执行相应的回调函数,从而更新视图。
4、构造函数 (new MVue({}))

MVue构造函数

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

function MVue (options) {        this.$el = options.el;        this.$data = options.data;        // 初始化操作,后面会说        // ...    }

1、实现 view => model

DocumentFragment(文档片段)

vue进行编译时,将挂载目标的所有子节点劫持到DocumentFragment中,经过一份解析等处理后,再将DocumentFragment整体挂载到目标节点上。

function nodeToFragment (node, vm) {        var flag = document.createDocumentFragment();        var child;        while (child = node.firstChild) {            compile(child, vm);             if (child.firstChild) {                var dom = nodeToFragment(child, vm);                child.appendChild(dom);            }            flag.appendChild(child);        }        return flag;    }

模板编译(指令解析,事件绑定、初始化数据绑定)

编译过程图

模板编译

代码如下:

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];                    // 移除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]);            }        }    }

现在,我们修改下MVue构造函数,增加模板编译,如下:

function MVue (options) {        this.$el = options.el;        this.$data = options.data;        // 模板编译        let elem = document.querySelector(this.$el);        elem.appendChild(nodeToFragment(elem, this))    }

那么,我们的view => model 已经实现了,包括初始化绑定默认值,只要修改了input中的值,data中对应的值相应变化,并触发了setter, 更新属性值等(可以自行在set方法中打印看效果,或者在控制台手动输入vm.$data.text也会看到效果)。

2、实现 model => view
上面可以看出,虽然我们实现了初始化数据绑定,以及输入框变化时,data中text也会变化,但是文本节点仍然没有任何变化,那么如果做到文本节点也同步变化呢,这里用的是发布者-订阅者模式。

发布者-订阅者模式

发布者-订阅者模式又称为观察者模式,让多个观察者同时监听某个主题对象,当主题对象发生变化时,会通知所有的观察者对象,即:发布者发出通知给主题对象 => 主题对象接收到通知后推送给所有订阅者 => 订阅者执行相应的操作。

1)首先,定义一个主题对象,用来收集所有的订阅者,并提供notify方法,用来调用订阅者的update方法,从而执行相应的操作。

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

不难看出,当text属性变化时,会触发set方法,作为发布者,将数据更新消息通过主题对象发送给订阅者, 那么该如何通知呢?

我们知道,在new一个vue时,会执行两个操作,一个事编译模板,一个监听data数据,在监听data时,vue为data的每个属性都生成一个主题对象Dep,而在编译模板时,会为每个与数据绑定的节点生成一个Watcher,那么只要关联了Dep与Watcher,是不是就实现了消息通知呢,关键逻辑是实现二者关联。

已实现:输入框变化 => 触发相应的事件,修改值 => 触发set方法

需要实现:发出通知dep.notify() => 触发订阅者update方法 => 更新视图

我们修改下compile中文本节点内容(只修改部分)

// 文本节点    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);        }    }

2)其次、实现订阅者Watcher

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();            this.node.nodeValue = this.value;        },        get () {            this.value = this.vm.$data[this.name]        }    }

首先,定义了一个全局的Dep.target,然后执行了update方法,进而执行了get方法,都去了this.vm的访问器属性, 从而将订阅的消息保存在该属性的主题对象中,并最终将Dep.target设置为空,全局变量,是watcher和dep之间的唯一桥梁,必须保证Dep.target只有一个值。

3)接着、实现一个obverser给data中每个属性添加一个主题对象

遍历data中的所有属性,包括子属性对象的属性

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

使用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            }        })    }

最后,我们需要再次修改构造函数MVue

function MVue (options) {        this.$el = options.el;        this.$data = options.data;        // 数据监听        obverser(this.$data);        // 模板编译        let elem = document.querySelector(this.$el);        elem.appendChild(nodeToFragment(elem, this))    }

现在,已经实现了model => view的变化

当输入框值变化时 => text也会变化 => 文本节点值变化

但如果细心的话,会发现还有一个问题,当我们手动改变text的值时(如在控制台上输入vm.$data.text = 'xxx'),会发现,文本节点值已经变化了,但是输入框的值没有变化。

如果给输入框也添加一个Watcher,是不是也就和文本节点一样实现了呢,但需要注意的是,输入框、文本框、下拉框等,是通过value改变值的,而不是nodeValuefa,因为可以做如下修改:
compile中:

// 初始化数据绑定    // node.value = vm.$data[name];    new Watcher(vm, node, name);    // 移除v-model 属性    node.removeAttribute('v-model')

wather中:

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]        }    }

OK,基本上完工。

获取完整代码,猛戳

个人博客也可以获取完整代码(https://jefferye.github.io)

转载地址:http://xtuyx.baihongyu.com/

你可能感兴趣的文章
集群Cluster介绍
查看>>
艾伟_转载:学习 ASP.NET MVC (第一回)理论篇
查看>>
艾伟也谈项目管理,敏捷个人:内容框架之执行力
查看>>
一起谈.NET技术,再次分享一个多选文件上传方案
查看>>
10款对开发者有用的Android应用
查看>>
应用Visual Studio 2010辅“.NET研究”助敏捷测试(上)
查看>>
JavaScript 判断当前设备是否是移动端还是PC
查看>>
你的睡眠时间和睡眠质量达标了么?
查看>>
让 UILabel 垂直方向顶端对齐的代码
查看>>
Hibernate---->第一个例子
查看>>
Android学习之Service命令的妙用
查看>>
封装好的实用的读写XML类---增删改查XML
查看>>
和角公式与倍角公式
查看>>
在mac console下 执行c++文件
查看>>
REST构架风格介绍:状态表述转移(转)
查看>>
CSS - 背景图片拉伸的方法
查看>>
杨氏矩阵查找
查看>>
Silverlight WCF RIA服务(三十五)身份验证、角色、个性化 6
查看>>
如何改变常用编辑器(eclipse)的字号大小
查看>>
在代码里面滚动当前屏幕(原创)
查看>>