Jeffery

人无远虑、必有近忧

  • 主页
  • 随笔
  • 技术
  • 相册
  • 关于
所有文章 友链 关于我

Jeffery

人无远虑、必有近忧

  • 主页
  • 随笔
  • 技术
  • 相册
  • 关于

Vue的双向数据绑定

阅读数:0次 2018-04-24

文章导航

× 文章目录
  1. 1. 发布者-订阅者模式
  2. 2. 脏值检查
  3. 3. 数据劫持
  • 一、实现最基础的数据绑定
  • 二、双向数据绑定实现(此处用MVue替代)
    1. MVue构造函数
    2. DocumentFragment(文档片段)
    3. 模板编译(指令解析,事件绑定、初始化数据绑定)
    4. 发布者-订阅者模式
  • 什么是双向数据绑定?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,在数据变动时发布消息给订阅者,从而触发相应的回调来更新视图。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <input type="text" id="in"/>
    输入的值为:<span id="out"></span>

    <script>
    var int = document.getElementById('in');
    var out = document.getElementById('out');
    var obj = {};

    Object.defineProperty(obj, 'msg', {
    enumerable: true,
    configurable: true,
    set (newVal) {
    out.innerHTML = newVal;
    }
    })

    int.addEventListener('input', function(e) {
    obj.msg = e.target.value;
    })
    </script>

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <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>

    实现思路:
    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中。

    1
    2
    3
    4
    5
    6
    7
    function MVue (options) {
    this.$el = options.el;
    this.$data = options.data;

    // 初始化操作,后面会说
    // ...
    }

    1、实现 view => model

    DocumentFragment(文档片段)

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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;
    }

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

    编译过程图
    模板编译
    代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    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构造函数,增加模板编译,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    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方法,从而执行相应的操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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中文本节点内容(只修改部分)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 文本节点
    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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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中的所有属性,包括子属性对象的属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     function obverser (obj) {
    Object.keys(obj).forEach(key => {
    if (obj.hasOwnProperty(key)) {
    if (Object.prototype.toString.call(obj[key]) === '[object Object]') {
    obverser(obj[key])
    }
    defineReactive(obj, key);
    }
    })
    }

    使用Object.definePeoperty()来监听属性变动,给属性添加setter和getter

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    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(_data)
    // 作为发布者发出通知给主题对象
    dep.notify();
    },
    get () {
    // 如果订阅者存在,添加到主题对象中
    if (Dep.target) {
    dep.addSub(Dep.target);
    }
    return _value
    }
    })
    }

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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中:

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

    wather中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    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,基本上完工。

    获取完整代码,猛戳这里

    赏

    谢谢你请我吃糖果

    • javascript
    • vue

    扫一扫,分享到微信

    微信分享二维码
    Hi, Welcome to my blog
    1. 1. 发布者-订阅者模式
    2. 2. 脏值检查
    3. 3. 数据劫持
  • 一、实现最基础的数据绑定
  • 二、双向数据绑定实现(此处用MVue替代)
    1. MVue构造函数
    2. DocumentFragment(文档片段)
    3. 模板编译(指令解析,事件绑定、初始化数据绑定)
    4. 发布者-订阅者模式
  • © 2019 Jeffery
    Hexo Theme Yilia by Litten
    • 所有文章
    • 友链
    • 关于我

    tag:

    • cli
    • javascript
    • nodejs
    • design
    • 设计模式
    • readline
    • vue
    • path
    • template
    • AST
    • 源码
    • 插件
    • 轮播
    • 滚动
    • welcome
    • intoduction
    • web
    • web安全
    • vnode
    • patch
    • diff
    • 正则表达式
    • 量词
    • 贪婪模式
    • 惰性模式
    • 苏州
    • 旅行
    • 风景
    • segmentfault
    很惭愧<br><br>只做了一点微小的工作<br>谢谢大家