vue 数据双向绑定的实现方法

1. 前言

本文适合于学习Vue源码的初级学者,阅读后,你将对Vue的数据双向绑定原理有一个大致的了解,认识Observer、Compile、Wathcer三大角色(如下图所示)以及它们所发挥的功能。

本文将一步步带你实现简易版的数据双向绑定,每一步都会详细分析这一步要解决的问题以及代码为何如此写,因此,在阅读完本文后,希望你能自己动手实现一个简易版数据双向绑定。

2. 代码实现

2.1 目的分析

本文要实现的效果如下图所示:

本文用到的HTML和JS主体代码如下:

<div id="app">
  <h1 v-text="msg"></h1>
  <input type="text" v-model="msg">
  <div>
    <h1 v-text="msg2"></h1>
    <input type="text" v-model="msg2">
  </div>
</div>
let vm = new Vue({
    el: "#app",
    data: {
      msg: "hello world",
      msg2: "hello xiaofei"
    }
  })

我们将按照下面三个步骤来实现:

  • 第一步:将data中的数据同步到页面上,实现 M ==> V 的初始化;
  • 第二步:当input框中输入值时,将新值同步到data中,实现 V ==> M 的绑定;
  • 第三步:当data数据发生更新的时候,触发页面发生变化,实现 M ==> V 的绑定。

2.2 实现过程

2.2.1 入口代码

首先,我们要创造一个Vue类,这个类接收一个 options 对象,同时,我们要对 options 对象中的有效信息进行保存;

然后,我们有三个主要模块:Observer、Compile、Wathcer,其中,Observer用来数据劫持的,Compile用来解析元素,Wathcer是观察者。可以写出如下代码:(Observer、Compile、Wathcer这三个概念,不用细究,后面会详解讲解)。

class Vue {
    // 接收传进来的对象
    constructor(options) {
      // 保存有效信息
      this.$el = document.querySelector(options.el);
      this.$data = options.data;

      // 容器: {属性1: [wathcer1, wathcer2...], 属性2: [...]},用来存放每个属性观察者
      this.$watcher = {};

      // 解析元素: 实现Compile
      this.compile(this.$el); // 要解析元素, 就得把元素传进去

      // 劫持数据: 实现 Observer
      this.observe(this.$data); // 要劫持数据, 就得把数据传入
    }
    compile() {}
    observe() {}
  }

2.2.2 页面初始化

在这一步,我们要实现页面的初始化,即解析出v-text和v-model指令,并将data中的数据渲染到页面中。

这一步的关键在于实现compile方法,那么该如何解析el元素呢?思路如下:

  • 首先要获取到el下面的所有子节点,然后遍历这些子节点,如果子节点还有子节点,那我们就需要用到递归的思想;
  • 遍历子节点找到所有有指令的元素,并将对应的数据渲染到页面中。

代码如下:(主要看compile那部分)

class Vue {
    // 接收传进来的对象
    constructor(options) {
      // 获取有用信息
      this.$el = document.querySelector(options.el);
      this.$data = options.data;

      // 容器: {属性1: [wathcer1, wathcer2...], 属性2: [...]}
      this.$watcher = {};

      // 2. 解析元素: 实现Compile
      this.compile(this.$el); // 要解析元素, 就得把元素传进去

      // 3. 劫持数据: 实现 Observer
      this.observe(this.$data); // 要劫持数据, 就得把数据传入
    }
    compile(el) {
      // 解析元素下的每一个子节点, 所以要获取el.children
      // 备注: children 返回元素集合, childNodes返回节点集合
      let nodes = el.children;

      // 解析每个子节点的指令
      for (var i = 0, length = nodes.length; i < length; i++) {
        let node = nodes[i];
        // 如果当前节点还有子元素, 递归解析该节点
        if(node.children){
          this.compile(node);
        }
        // 解析带有v-text指令的元素
        if (node.hasAttribute("v-text")) {
          let attrVal = node.getAttribute("v-text");
          node.textContent = this.$data[attrVal]; // 渲染页面
        }
        // 解析带有v-model指令的元素
        if (node.hasAttribute("v-model")) {
          let attrVal = node.getAttribute("v-model");
          node.value = this.$data[attrVal];
        }
      }
    }
    observe(data) {}
  }

这样,我们就实现页面的初始化了。

2.2.3 视图影响数据

因为input带有v-model指令,因此我们要实现这样一个功能:在input框中输入字符,data中绑定的数据发生相应的改变。

我们可以在input这个元素上绑定一个input事件,事件的效果就是:将data中的相应数据修改为input中的值。

这一部分的实现代码比较简单,只要看标注那个地方就明白了,代码如下:

class Vue {
    constructor(options) {
      this.$el = document.querySelector(options.el);
      this.$data = options.data;

      this.$watcher = {};  

      this.compile(this.$el);

      this.observe(this.$data);
    }
    compile(el) {
      let nodes = el.children;

      for (var i = 0, length = nodes.length; i < length; i++) {
        let node = nodes[i];
        if(node.children){
          this.compile(node);
        }
        if (node.hasAttribute("v-text")) {
          let attrVal = node.getAttribute("v-text");
          node.textContent = this.$data[attrVal];
        }
        if (node.hasAttribute("v-model")) {
          let attrVal = node.getAttribute("v-model");
          node.value = this.$data[attrVal];
          // 看这里!!只多了三行代码!!
          node.addEventListener("input", (ev)=>{
            this.$data[attrVal] = ev.target.value;
            // 可以试着在这里执行:console.log(this.$data),
            // 就可以看到每次在输入框输入文字的时候,data中的msg值也发生了变化
          })
        }
      }
    }
    observe(data) {}
  }

2.2.4 数据影响视图

至此,我们已经实现了:当我们在input框中输入字符的时候,data中的数据会自动发生更新;

本小节的主要任务是:当data中的数据发生更新的时候,绑定了该数据的元素会在页面上自动更新视图。具体思路如下:

1) 我们将要实现一个 Wathcer 类,它有一个update方法,用来更新页面。观察者的代码如下:

class Watcher{
    constructor(node, updatedAttr, vm, expression){
      // 将传进来的值保存起来,这些数据都是渲染页面时要用到的数据
      this.node = node;
      this.updatedAttr = updatedAttr;
      this.vm = vm;
      this.expression = expression;
      this.update();
    }
    update(){
      this.node[this.updatedAttr] = this.vm.$data[this.expression];
    }
  }

2) 试想,我们该给哪些数据添加观察者?何时给数据添加观察者?

在解析元素的时候,当解析到v-text和v-model指令的时候,说明这个元素是需要和数据双向绑定的,因此我们在这时往容器中添加观察者。我们需用到这样一个数据结构:{属性1: [wathcer1, wathcer2...], 属性2: [...]},如果不是很清晰,可以看下图:

可以看到:vue实例中有一个$wathcer对象,$wathcer的每个属性对应每个需要绑定的数据,值是一个数组,用来存放观察了该数据的观察者。(备注:Vue源码中专门创造了Dep这么一个类,对应这里所说的数组,本文属于简易版本,就不过多介绍了)

3) 劫持数据:利用对象的访问器属性getter和setter做到当数据更新的时候,触发一个动作,这个动作的主要目的就是让所有观察了该数据的观察者执行update方法。

总结一下,在本小节我们需要做的工作:

  1. 实现一个Wathcer类;
  2. 在解析指令的时候(即在compile方法中)添加观察者;
  3. 实现数据劫持(实现observe方法)。

完整代码如下:

  class Vue {
    // 接收传进来的对象
    constructor(options) {
      // 获取有用信息
      this.$el = document.querySelector(options.el);
      this.$data = options.data;

      // 容器: {属性1: [wathcer1, wathcer2...], 属性2: [...]}
      this.$watcher = {};

      // 解析元素: 实现Compile
      this.compile(this.$el); // 要解析元素, 就得把元素传进去

      // 劫持数据: 实现 Observer
      this.observe(this.$data); // 要劫持数据, 就得把数据传入
    }
    compile(el) {
      // 解析元素下的每一个子节点, 所以要获取el.children
      // 拓展: children 返回元素集合, childNodes返回节点集合
      let nodes = el.children;

      // 解析每个子节点的指令
      for (var i = 0, length = nodes.length; i < length; i++) {
        let node = nodes[i];
        // 如果当前节点还有子元素, 递归解析该节点
        if (node.children) {
          this.compile(node);
        }
        if (node.hasAttribute("v-text")) {
          let attrVal = node.getAttribute("v-text");
          // node.textContent = this.$data[attrVal];
          // Watcher在实例化时调用update, 替代了这行代码

          /**
           * 试想Wathcer要更新节点数据的时候要用到哪些数据?
           * e.g.   p.innerHTML = vm.$data[msg]
           * 所以要传入的参数依次是: 当前节点node, 需要更新的节点属性, vue实例, 绑定的数据属性
          */
          // 往容器中添加观察者: {msg1: [Watcher, Watcher...], msg2: [...]}
          if (!this.$watcher[attrVal]) {
            this.$watcher[attrVal] = [];
          }
          this.$watcher[attrVal].push(new Watcher(node, "innerHTML", this, attrVal))
        }
        if (node.hasAttribute("v-model")) {
          let attrVal = node.getAttribute("v-model");
          node.value = this.$data[attrVal];

          node.addEventListener("input", (ev) => {
            this.$data[attrVal] = ev.target.value;
          })

          if (!this.$watcher[attrVal]) {
            this.$watcher[attrVal] = [];
          }
          // 不同于上处用的innerHTML, 这里input用的是vaule属性
          this.$watcher[attrVal].push(new Watcher(node, "value", this, attrVal))
        }
      }
    }
    observe(data) {
      Object.keys(data).forEach((key) => {
        let val = data[key];  // 这个val将一直保存在内存中,每次访问data[key],都是在访问这个val
        Object.defineProperty(data, key, {
          get() {
            return val;  // 这里不能直接返回data[key],不然会陷入无限死循环
          },
          set(newVal) {
            if (val !== newVal) {
              val = newVal;// 同理,这里不能直接对data[key]进行设置,会陷入死循环
              this.$watcher[key].forEach((w) => {
                w.update();
              })
            }
          }
        })
      })
    }
  }

  class Watcher {
    constructor(node, updatedAttr, vm, expression) {
      // 将传进来的值保存起来
      this.node = node;
      this.updatedAttr = updatedAttr;
      this.vm = vm;
      this.expression = expression;
      this.update();
    }
    update() {
      this.node[this.updatedAttr] = this.vm.$data[this.expression];
    }
  }

  let vm = new Vue({
    el: "#app",
    data: {
      msg: "hello world",
      msg2: "hello xiaofei"
    }
  })

至此,代码就完成了。

3. 未来的计划

用设计模式的知识,分析上面这份源码存在的问题,并和Vue源码进行比对,算是对Vue源码的解析

以上就是vue 数据双向绑定的实现方法的详细内容,更多关于vue 数据双向绑定的资料请关注我们其它相关文章!

(0)

相关推荐

  • 解决vue项目input输入框双向绑定数据不实时生效问题

    我就废话不多说了,大家还是直接看代码吧~ <input type="text" maxlength="11" placeholder="请输入联系人电话" v-model="form.phone" /> 这样的输入框,绑定的是data中的form对象上的phone字段. 在mounted钩子函数里边写: this.form.phone = '1888888888'; 这样在页面上时候不会随着输入框值改变而改变. 解

  • 理解Proxy及使用Proxy实现vue数据双向绑定操作

    1.什么是Proxy?它的作用是? 据阮一峰文章介绍:Proxy可以理解成,在目标对象之前架设一层 "拦截",当外界对该对象访问的时候,都必须经过这层拦截,而Proxy就充当了这种机制,类似于代理的含义,它可以对外界访问对象之前进行过滤和改写该对象. 如果对vue2.xx了解或看过源码的人都知道,vue2.xx中使用 Object.defineProperty()方法对该对象通过 递归+遍历的方式来实现对数据的监控的,具体了解 Object.defineProperty可以看我上一篇文

  • vue子组件改变父组件传递的prop值通过sync实现数据双向绑定(DEMO)

    最近开始在用elementUI做一个后台管理系统项目,遇到一个问题,需求是这样,在父组件有一个按钮,点击按钮会显示弹窗(子组件),在子组件中用的是elementUI 的el-diolog弹窗组件,在关闭弹窗时(elementUI自带事件)便会报错.话不多说直接上代码. DEMO 这是父组件的代码: <template> <div> <app-refund :dialogVisible="refundVisible"></app-refund&g

  • vue自定v-model实现表单数据双向绑定问题

    vue.js的一大功能便是实现数据的双向绑定,本文给大家介绍vue自定v-model实现表单数据双向绑定,具体内容如下所示: 子组件代码如下 v-model语法糖 v-model实现了表单输入的双向绑定,我们一般是这么写的: <div id="app"> <input v-model="price"> </div> new Vue({ el: '#app', data: { price: '' } }); 通过该语句实现price

  • Vue数据双向绑定底层实现原理

    简介: Vue 最独特的特性之一,是其非侵入性的响应式系统.数据模型仅仅是普通的 JavaScript 对象.而当你修改它们时,视图会进行更新.简单的说,就是数据变视图变. 当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter.Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不

  • Vue数据双向绑定原理实例解析

    Vue数据双向绑定原理是通过数据劫持结合发布者-订阅者模式的方式来实现的,首先是对数据进行监听,然后当监听的属性发生变化时则告诉订阅者是否要更新,若更新就会执行对应的更新函数从而更新视图 MVC模式 以往的MVC模式是单向绑定,即Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新 MVVM模式 MVVM模式就是Model–View–ViewModel模式.它实现了View的变动,自动反映在 ViewModel,反之亦然.对于双向绑定的理解,就是用户更

  • Vue使用.sync 实现父子组件的双向绑定数据问题

    1.前言 最近在vue 项目中有一个需求, 就是我需要根据不同的类型在页面中放不同的组件, 组件需要跟当前页面的数据进行双向绑定,如果都写在同一个页面 代码会显得比较多,毕竟我当前页面已经7-800行代码了 所以我需要把一些元素定义成组件 ,封装起来,所以就会遇到 数据的传值绑定问题 2.父组件 首先我们来看看官方文档 [ https://cn.vuejs.org/v2/guide/components.html#sync-修饰符 ] .sync 修饰符所提供的功能.当一个子组件改变了一个 pr

  • 详解vue中的父子传值双向绑定及数据更新问题

    在进行父子组件传值时,用到子组件直接控制父组件中的变量值以及在vue中直接更改对象或者数组的值,视图未发生变化的解决办法,当时完成项目时,一直未找到原因,修改了好久. 1.父子组件传值双向绑定 在传递给子组件中的变量上使用.sync修饰符,就能够实现父子传值的双向绑定 <!-- 父组件 --> <template> <div class="audioCate"> <child :show.sync="showModel" @

  • Vuejs学习笔记之使用指令v-model完成表单的数据双向绑定

    表单类控件承载了一个网页数据的录入与交互,本章将介绍如何使用指令v-model完成表单的数据双向绑定. 6.1 基本用法 表单控件在实际业务较为常见,比如单选.多选.下拉选择.输入框等,用它们可以完成数据的录入.校验.提交等. Vue.js提供了v-model指令,用于在表单类元素上双向绑定数据,例如在输入框上使用时,输入的内容会实时映射到绑定的数据上. 例如下面的例子: <div id="app"> <input type="text" v-mo

  • vue响应式更新机制及不使用框架实现简单的数据双向绑定问题

    最近看到有些人说vue是双向数据绑定的,有些人说vue是单向数据流的,我认为这两种说法都是错误的,vue是一款具有响应式更新机制的框架,既可以实现单向数据流也可以实现数据的双向绑定. 2 单向数据流与数据双向绑定 单向数据流是指model中的数据发生改变时引起view的改变. 双向数据绑定是指model中的数据发生改变时view的改变,view的改变也会引起model的改变. //这个是单向数据流,改变这个input的value值并不能是data中的text属性发生改变. <input type

  • vue双向绑定数据限制长度的方法

    vue双向绑定数据如何限制长度?具体方法请阅读文章 问题描述 vue中输入框v-modle 双向绑定的数据:在需要的业务场景下需要对其进行字数长度限制: 解决方案 可以使用以下方法: 方法一: //方法一:输入框添加keypress方法:然后函数为: maxLength(attr,length){ let keyCode = event.keyCode; console.log("keyCode"); let len=0; console.log(this[attr].length);

  • 详解基于Vue的支持数据双向绑定的select组件

    今天用Vue做一个小项目时需要用到多个select筛选功能,但是原生的太丑,如果直接写在当前页多个select处理起来又太繁琐,第三方ui又太大,所以就自己写了一个,并上传了GitHub仓库,仓库地址:https://github.com/tuohuang/vue-select 使用方法: 引入组件: import VueSelect from '../components/VueSelect' 注册组件 export default { components: { VueSelect } }

随机推荐