vue实现简易的双向数据绑定


Posted in Vue.js onDecember 29, 2020

主要是通过数据劫持和发布订阅一起实现的

  • 双向数据绑定 数据更新时,可以更新视图 视图的数据更新是,可以反向更新模型

组成说明

  • Observe监听器 劫持数据, 感知数据变化, 发出通知给订阅者, 在get中将订阅者添加到订阅器中
  • Dep消息订阅器 存储订阅者, 通知订阅者调用更新函数
  • 订阅者Wather取出模型值,更新视图
  • 解析器Compile 解析指令, 更新模板数据, 初始化视图, 实例化一个订阅者, 将更新函数绑定到订阅者上, 可以在接收通知二次更新视图, 对于v-model还需要监听input事件,实现视图到模型的数据流动

基本结构

HTML模板

  <div id="app">
    <form>
      <input type="text" v-model="username">
    </form>
    <p v-bind="username"></p>
  </div>
  • 一个根节点#app
  • 表单元素,里面包含input, 使用v-model指令绑定数据username
  • p元素上使用v-bind绑定数username

MyVue类

简单的模拟Vue类

将实例化时的选项options, 数据options.data进行保存 此外,通过options.el获取dom元素,存储到$el上

    class MyVue {
      constructor(options) {
        this.$options = options
        this.$el = document.querySelector(this.$options.el)
        this.$data = options.data
      }
    }

实例化MyVue

实例化一个MyVue,传递选项进去,选项中指定绑定的元素el和数据对象data

    const myVm = new MyVue({
      el: '#app',
      data: {
        username: 'LastStarDust'
      }
    })

Observe监听器实现

劫持数据是为了修改数据的时候可以感知, 发出通知, 执行更新视图操作

    class MyVue {
      constructor(options) {
        // ...
        // 监视数据的属性
        this.observable(this.$data)
      }
      // 递归遍历数据对象的所有属性, 进行数据属性的劫持 { username: 'LastStarDust' }
      observable(obj) {
        // obj为空或者不是对象, 不做任何操作
        const isEmpty = !obj || typeof obj !== 'object'
        if(isEmpty) {
          return
        }

        // ['username']
        const keys = Object.keys(obj)
        keys.forEach(key => {
          // 如果属性值是对象,递归调用
          let val = obj[key]
          if(typeof val === 'object') {
            this.observable(val)
          }
          // this.defineReactive(this.$data, 'username', 'LastStarDust')
          this.defineReactive(obj, key, val)
        })

        return obj
      }

      // 数据劫持,修改属性的get和set方法
      defineReactive(obj, key, val) {
        Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get() {
            console.log(`取出${key}属性值: 值为${val}`)
            return val
          },
          set(newVal) {
            // 没有发生变化, 不做更新
            if(newVal === val) {
              return
            }
            console.log(`更新属性${key}的值为: ${newVal}`)
            val = newVal
          }
        })
      }
    }

Dep消息订阅器

存储订阅者, 收到通知时,取出订阅者,调用订阅者的update方法

    // 定义消息订阅器
    class Dep {
      // 静态属性 Dep.target,这是一个全局唯一 的Watcher,因为在同一时间只能有一个全局的 Watcher
      static target = null
      constructor() {
        // 存储订阅者
        this.subs = []
      }
      // 添加订阅者
      add(sub) {
        this.subs.push(sub)
      }
      // 通知
      notify() {
        this.subs.forEach(sub => {
          // 调用订阅者的update方法
          sub.update()
        })
      }
    }

将消息订阅器添加到数据劫持过程中

为每一个属性添加订阅者

      defineReactive(obj, key, val) {
        const dep = new Dep()
        Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get() {
            // 会在初始化时, 触发属性get()方法,来到这里Dep.target有值,将其作为订阅者存储起来,在触发属性的set()方法时,调用notify方法
            if(Dep.target) {
              dep.add(Dep.target)
            }
            console.log(`取出${key}属性值: 值为${val}`)
            return val
          },
          set(newVal) {
            // 没有发生变化, 不做更新
            if(newVal === val) {
              return
            }
            console.log(`更新属性${key}的值为: ${newVal}`)

            val = newVal
            dep.notify()
          }
        })
      }

订阅者Wather

从模型中取出数据并更新视图

    // 定义订阅者类
    class Wather {
      constructor(vm, exp, cb) {
        this.vm = vm // vm实例
        this.exp = exp // 指令对应的字符串值, 如v-model="username", exp相当于"username"
        this.cb = cb // 回到函数 更新视图时调用
        this.value = this.get() // 将自己添加到消息订阅器Dep中
      }

      get() {
        // 将当前订阅者作为全局唯一的Wather,添加到Dep.target上
        Dep.target = this
        // 获取数据,触发属性的getter方法
        const value = this.vm.$data[this.exp]
        // 在执行添加到消息订阅Dep后, 重置Dep.target
        Dep.target = null
        return value
      }

      // 执行更新
      update() {
        this.run()
      }

      run() {
        // 从Model模型中取出属性值
        const newVal = this.vm.$data[this.exp]
        const oldVal = this.value
        if(newVal === oldVal) {
          return false
        }
        // 执行回调函数, 将vm实例,新值,旧值传递过去
        this.cb.call(this.vm, newVal, oldVal)
      }
    }

解析器Compile

  • 解析模板指令,并替换模板数据,初始化视图;
  • 将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器;
  • 初始化编译器, 存储el对应的dom元素, 存储vm实例, 调用初始化方法
  • 在初始化方法中, 从根节点开始, 取出根节点的所有子节点, 逐个对节点进行解析
  • 解析节点过程中
  • 解析指令存在, 取出绑定值, 替换模板数据, 完成首次视图的初始化
  • 给指令对应的节点绑定更新函数, 并实例化一个订阅器Wather
  • 对于v-model指令, 监听'input'事件,实现视图更新是,去更新模型的数据
    // 定义解析器
    // 解析指令,替换模板数据,初始视图
    // 模板的指令绑定更新函数, 数据更新时, 更新视图
    class Compile {
      constructor(el, vm) {
        this.el = el
        this.vm = vm
        this.init(this.el)
      }

      init(el) {
        this.compileEle(el)
      }
      compileEle(ele) {
        const nodes = ele.children
		// 遍历节点进行解析
        for(const node of nodes) {
		 // 如果有子节点,递归调用
          if(node.children && node.children.length !== 0) {
            this.compileEle(node)
          }

          // 指令时v-model并且是标签是输入标签
          const hasVmodel = node.hasAttribute('v-model')
          const isInputTag = ['INPUT', 'TEXTAREA'].indexOf(node.tagName) !== -1
          if(hasVmodel && isInputTag) {
            const exp = node.getAttribute('v-model')
            const val = this.vm.$data[exp]
            const attr = 'value'
            // 初次模型值推到视图层,初始化视图
            this.modelToView(node, val, attr)
            // 实例化一个订阅者, 将更新函数绑定到订阅者上, 未来数据更新,可以更新视图
            new Wather(this.vm, exp, (newVal)=> {
              this.modelToView(node, newVal, attr)
            })

            // 监听视图的改变
            node.addEventListener('input', (e) => {
              this.viewToModel(exp, e.target.value)
            })
          }
		 
		 // 指令时v-bind
          if(node.hasAttribute('v-bind')) {
            const exp = node.getAttribute('v-bind')
            const val = this.vm.$data[exp]
            const attr = 'innerHTML'
            // 初次模型值推到视图层,初始化视图
            this.modelToView(node, val, attr)
            // 实例化一个订阅者, 将更新函数绑定到订阅者上, 未来数据更新,可以更新视图
            new Wather(this.vm, exp, (newVal)=> {
              this.modelToView(node, newVal, attr)
            })
          }
        }
      }
      // 将模型值更新到视图
      modelToView(node, val, attr) {
        node[attr] = val
      }
      // 将视图值更新到模型上
      viewToModel(exp, val) {
        this.vm.$data[exp] = val
      }
    }

完整代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <form>
      <input type="text" v-model="username">
    </form>
    <div>
      <span v-bind="username"></span>
    </div>
    <p v-bind="username"></p>
  </div>
  <script>

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

        // 监视数据的属性
        this.observable(this.$data)

        // 编译节点
        new Compile(this.$el, this)
      }
      // 递归遍历数据对象的所有属性, 进行数据属性的劫持 { username: 'LastStarDust' }
      observable(obj) {
        // obj为空或者不是对象, 不做任何操作
        const isEmpty = !obj || typeof obj !== 'object'
        if(isEmpty) {
          return
        }

        // ['username']
        const keys = Object.keys(obj)
        keys.forEach(key => {
          // 如果属性值是对象,递归调用
          let val = obj[key]
          if(typeof val === 'object') {
            this.observable(val)
          }
          // this.defineReactive(this.$data, 'username', 'LastStarDust')
          this.defineReactive(obj, key, val)
        })

        return obj
      }

      // 数据劫持,修改属性的get和set方法
      defineReactive(obj, key, val) {
        const dep = new Dep()
        Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get() {
            // 会在初始化时, 触发属性get()方法,来到这里Dep.target有值,将其作为订阅者存储起来,在触发属性的set()方法时,调用notify方法
            if(Dep.target) {
              dep.add(Dep.target)
            }
            console.log(`取出${key}属性值: 值为${val}`)
            return val
          },
          set(newVal) {
            // 没有发生变化, 不做更新
            if(newVal === val) {
              return
            }
            console.log(`更新属性${key}的值为: ${newVal}`)

            val = newVal
            dep.notify()
          }
        })
      }
    }

    // 定义消息订阅器
    class Dep {
      // 静态属性 Dep.target,这是一个全局唯一 的Watcher,因为在同一时间只能有一个全局的 Watcher
      static target = null
      constructor() {
        // 存储订阅者
        this.subs = []
      }
      // 添加订阅者
      add(sub) {
        this.subs.push(sub)
      }
      // 通知
      notify() {
        this.subs.forEach(sub => {
          // 调用订阅者的update方法
          sub.update()
        })
      }
    }

    // 定义订阅者类
    class Wather {
      constructor(vm, exp, cb) {
        this.vm = vm // vm实例
        this.exp = exp // 指令对应的字符串值, 如v-model="username", exp相当于"username"
        this.cb = cb // 回到函数 更新视图时调用
        this.value = this.get() // 将自己添加到消息订阅器Dep中
      }

      get() {
        // 将当前订阅者作为全局唯一的Wather,添加到Dep.target上
        Dep.target = this
        // 获取数据,触发属性的getter方法
        const value = this.vm.$data[this.exp]
        // 在执行添加到消息订阅Dep后, 重置Dep.target
        Dep.target = null
        return value
      }

      // 执行更新
      update() {
        this.run()
      }

      run() {
        // 从Model模型中取出属性值
        const newVal = this.vm.$data[this.exp]
        const oldVal = this.value
        if(newVal === oldVal) {
          return false
        }
        // 执行回调函数, 将vm实例,新值,旧值传递过去
        this.cb.call(this.vm, newVal, oldVal)
      }
    }

    // 定义解析器
    // 解析指令,替换模板数据,初始视图
    // 模板的指令绑定更新函数, 数据更新时, 更新视图
    class Compile {
      constructor(el, vm) {
        this.el = el
        this.vm = vm
        this.init(this.el)
      }

      init(el) {
        this.compileEle(el)
      }
      compileEle(ele) {
        const nodes = ele.children
        for(const node of nodes) {
          if(node.children && node.children.length !== 0) {
            // 递归调用, 编译子节点
            this.compileEle(node)
          }

          // 指令时v-model并且是标签是输入标签
          const hasVmodel = node.hasAttribute('v-model')
          const isInputTag = ['INPUT', 'TEXTAREA'].indexOf(node.tagName) !== -1
          if(hasVmodel && isInputTag) {
            const exp = node.getAttribute('v-model')
            const val = this.vm.$data[exp]
            const attr = 'value'
            // 初次模型值推到视图层,初始化视图
            this.modelToView(node, val, attr)
            // 实例化一个订阅者, 将更新函数绑定到订阅者上, 未来数据更新,可以更新视图
            new Wather(this.vm, exp, (newVal)=> {
              this.modelToView(node, newVal, attr)
            })

            // 监听视图的改变
            node.addEventListener('input', (e) => {
              this.viewToModel(exp, e.target.value)
            })
          }

          if(node.hasAttribute('v-bind')) {
            const exp = node.getAttribute('v-bind')
            const val = this.vm.$data[exp]
            const attr = 'innerHTML'
            // 初次模型值推到视图层,初始化视图
            this.modelToView(node, val, attr)
            // 实例化一个订阅者, 将更新函数绑定到订阅者上, 未来数据更新,可以更新视图
            new Wather(this.vm, exp, (newVal)=> {
              this.modelToView(node, newVal, attr)
            })
          }
        }
      }
      // 将模型值更新到视图
      modelToView(node, val, attr) {
        node[attr] = val
      }
      // 将视图值更新到模型上
      viewToModel(exp, val) {
        this.vm.$data[exp] = val
      }
    }

    const myVm = new MyVue({
      el: '#app',
      data: {
        username: 'LastStarDust'
      }
    })

    // console.log(Dep.target)
  </script>
</body>
</html>

以上就是vue实现简易的双向数据绑定的详细内容,更多关于vue 实现双向数据绑定的资料请关注三水点靠木其它相关文章!

Vue.js 相关文章推荐
实用的 vue tags 创建缓存导航的过程实现
Dec 03 Vue.js
Vue.extend 登录注册模态框的实现
Dec 29 Vue.js
vue中使用echarts的示例
Jan 03 Vue.js
vue中父子组件的参数传递和应用示例
Jan 04 Vue.js
jenkins自动构建发布vue项目的方法步骤
Jan 04 Vue.js
Vue过滤器,生命周期函数和vue-resource简单介绍
Jan 12 Vue.js
vue实现简易计算器功能
Jan 20 Vue.js
vue实现轮播图帧率播放
Jan 26 Vue.js
Vue单页面应用中实现Markdown渲染
Feb 14 Vue.js
详细聊聊vue中组件的props属性
Nov 02 Vue.js
详解Vue slot插槽
Nov 20 Vue.js
Vue2项目中对百度地图的封装使用详解
Jun 16 Vue.js
vue中配置scss全局变量的步骤
Dec 28 #Vue.js
为什么推荐使用JSX开发Vue3
Dec 28 #Vue.js
Vue仿百度搜索功能
Dec 28 #Vue.js
vue中watch的用法汇总
Dec 28 #Vue.js
浅析vue中的nextTick
Dec 28 #Vue.js
Vue实现省市区三级联动
Dec 27 #Vue.js
vue3.0自定义指令(drectives)知识点总结
Dec 27 #Vue.js
You might like
PHP如何得到当前页和上一页的地址?
2006/11/27 PHP
PHP第一季视频教程(李炎恢+php100 不断更新)
2011/05/29 PHP
PHP大转盘中奖概率算法实例
2014/10/21 PHP
Jsonp 跨域的原理以及Jquery的解决方案
2010/05/18 Javascript
基于jquery的给文章加入关键字链接
2010/10/26 Javascript
jQuery操作input值的各种方法总结
2013/11/21 Javascript
Jquery动态添加及删除页面节点元素示例代码
2014/06/16 Javascript
JavaScript及jquey实现多个数组的合并操作
2014/09/06 Javascript
js实现兼容IE和FF的上下层的移动
2015/05/04 Javascript
jquery实现点击变换导航样式的方法
2015/08/31 Javascript
JS图片定时翻滚效果实现方法
2016/06/21 Javascript
JS中如何比较两个Json对象是否相等实例代码
2016/07/13 Javascript
jquery根据一个值来选中select下的option实例代码
2016/08/29 Javascript
基于原生js运动方式关键点的总结(推荐)
2017/10/01 Javascript
如何制作一个Node命令行图像识别工具
2018/12/12 Javascript
详解Vue路由自动注入实践
2019/04/17 Javascript
利用原生JavaScript实现造日历轮子实例代码
2019/05/08 Javascript
微信小程序与公众号实现数据互通的方法
2019/07/25 Javascript
python3爬取各类天气信息
2018/02/24 Python
Python学习笔记之open()函数打开文件路径报错问题
2018/04/28 Python
Python中应该使用%还是format来格式化字符串
2018/09/25 Python
Python django搭建layui提交表单,表格,图标的实例
2019/11/18 Python
如何使用Python抓取网页tag操作
2020/02/14 Python
django model object序列化实例
2020/03/13 Python
django中related_name的用法说明
2020/05/20 Python
python 实现关联规则算法Apriori的示例
2020/09/30 Python
Python SQLAlchemy库的使用方法
2020/10/13 Python
PyQt5中QSpinBox计数器的实现
2021/01/18 Python
鼠标滚轮事件和Mac触控板双指事件
2019/12/23 HTML / CSS
班班通校本培训方案
2014/03/12 职场文书
2014年实习班主任工作总结
2014/11/08 职场文书
2014保险公司个人工作总结
2014/12/09 职场文书
教师岗位职责
2015/02/03 职场文书
工程质量保证书
2015/05/09 职场文书
Vue h函数的使用详解
2022/02/18 Vue.js
Win10多屏显示如何设置?Win10电脑多屏显示设置操作方法
2022/07/07 数码科技