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使用Element实现增删改查+打包的步骤
Nov 25 Vue.js
vue动态合并单元格并添加小计合计功能示例
Nov 26 Vue.js
Vue实现简单购物车功能
Dec 13 Vue.js
浅析vue中的nextTick
Dec 28 Vue.js
如何在vue-cli中使用css-loader实现css module
Jan 07 Vue.js
Vue实现一种简单的无限循环滚动动画的示例
Jan 10 Vue.js
vue二选一tab栏切换新做法实现
Jan 19 Vue.js
深入了解Vue动态组件和异步组件
Jan 26 Vue.js
vue实现可移动的悬浮按钮
Mar 04 Vue.js
vue+element ui实现锚点定位
Jun 29 Vue.js
一定要知道的 25 个 Vue 技巧
Nov 02 Vue.js
vue 自定义组件添加原生事件
Apr 21 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
ajax取消挂起请求的处理方法
2013/03/18 PHP
PHP字符串的编码问题的详细介绍
2013/04/27 PHP
使用Composer安装Yii框架的方法
2016/03/15 PHP
[原创]IE view-source 无法查看看源码 JavaScript看网页源码
2009/07/19 Javascript
javascript 闭包疑问
2010/12/30 Javascript
兼容ie、firefox的图片自动缩放的css跟js代码分享
2012/01/21 Javascript
跨域传值即主页面与iframe之间互相传值
2013/12/09 Javascript
jquery选择符快速提取web表单数据示例
2014/03/27 Javascript
IE6 hack for js 集锦
2014/09/23 Javascript
JavaScript数据结构学习之数组、栈与队列
2017/05/02 Javascript
Vue header组件开发详解
2018/01/26 Javascript
微信小程序五子棋游戏的棋盘,重置,对弈实现方法【附demo源码下载】
2019/02/20 Javascript
vue实现codemirror代码编辑器中的SQL代码格式化功能
2019/08/27 Javascript
Vue3项目打包后部署到服务器 请求不到后台接口解决方法
2020/02/06 Javascript
[01:43]倾听DOTA2英雄之声 魅惑魔女国服配音鉴赏
2013/06/06 DOTA
[01:14]DOTA2亚洲邀请赛 ShowOpen
2015/02/07 DOTA
对于Python装饰器使用的一些建议
2015/06/03 Python
python连接mysql实例分享
2016/10/09 Python
使用python3+xlrd解析Excel的实例
2018/05/04 Python
Python3.7基于hashlib和Crypto实现加签验签功能(实例代码)
2019/12/04 Python
pytorch实现focal loss的两种方式小结
2020/01/02 Python
python中列表的含义及用法
2020/05/26 Python
Python文件操作模拟用户登陆代码实例
2020/06/09 Python
python Cartopy的基础使用详解
2020/11/01 Python
HTML5中实现拖放效果无须借助javascript
2012/12/26 HTML / CSS
很酷的小工具和电子产品商城:GearBest
2016/11/19 全球购物
印度在线内衣和时尚目的地:Zivame
2017/09/28 全球购物
Osklen官方在线商店:巴西服装品牌
2019/04/25 全球购物
说出你对remoting 和webservice的理解和应用
2014/06/08 面试题
幼儿教师培训感言
2014/03/08 职场文书
党员对照检查剖析材料
2014/10/13 职场文书
志愿者事迹材料
2014/12/26 职场文书
个人廉洁自律总结
2015/03/06 职场文书
使用 Apache 反向代理的设置技巧
2022/01/18 Servers
JavaWeb实现显示mysql数据库数据
2022/03/19 Java/Android
Go中使用gjson来操作JSON数据的实现
2022/08/14 Golang