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项目如何引入bootstrap、elementUI、echarts
Nov 26 Vue.js
vue实现两个区域滚动条同步滚动
Dec 13 Vue.js
vue el-upload上传文件的示例代码
Dec 21 Vue.js
vue 实现基础组件的自动化全局注册
Dec 25 Vue.js
Vue实现简易购物车页面
Dec 30 Vue.js
vue 页面跳转的实现方式
Jan 12 Vue.js
vue中h5端打开app(判断是安卓还是苹果)
Feb 26 Vue.js
vue3.0封装轮播图组件的步骤
Mar 04 Vue.js
vue前端工程的搭建
Mar 31 Vue.js
Vue接口封装的完整步骤记录
May 14 Vue.js
Vue项目打包、合并及压缩优化网页响应速度
Jul 07 Vue.js
vue里使用create, mounted调用方法
Apr 26 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多个版本的分析解释
2011/07/21 PHP
浅析php header 跳转
2013/06/17 PHP
php图片处理函数获取类型及扩展名实例
2014/11/19 PHP
PHP函数实现从一个文本字符串中提取关键字的方法
2015/07/01 PHP
php集成动态口令认证
2016/07/21 PHP
详解php与ethereum客户端交互
2018/04/28 PHP
微博@符号的用户名提示效果。(想@到谁?)
2010/11/05 Javascript
可自定义速度的js图片无缝滚动示例分享
2014/01/20 Javascript
js实现编辑div节点名称的方法
2014/12/17 Javascript
JavaScript实现给定时间相加天数的方法
2016/01/25 Javascript
在Vue.js中使用Mixins的方法
2017/09/12 Javascript
基于jQuery解决ios10以上版本缩放问题
2017/11/03 jQuery
详解React 在服务端渲染的实现
2017/11/16 Javascript
Vue条件循环判断+计算属性+绑定样式v-bind的实例
2018/09/18 Javascript
[42:04]DOTA2上海特级锦标赛主赛事日 - 2 胜者组第一轮#3Secret VS OG第一局
2016/03/03 DOTA
python 提取文件的小程序
2009/07/29 Python
Python实现把json格式转换成文本或sql文件
2015/07/10 Python
Python批量创建迅雷任务及创建多个文件
2016/02/13 Python
python之Character string(实例讲解)
2017/09/25 Python
python指定写入文件时的编码格式方法
2018/06/07 Python
python变量赋值方法(可变与不可变)
2019/01/12 Python
seek引发的python文件读写的问题及解决
2019/07/26 Python
Python完成哈夫曼树编码过程及原理详解
2019/07/29 Python
django ajax发送post请求的两种方法
2020/01/05 Python
Python3 hashlib密码散列算法原理详解
2020/03/30 Python
Python xml、字典、json、类四种数据类型如何实现互相转换
2020/05/27 Python
Python制作数据预测集成工具(值得收藏)
2020/08/21 Python
俄罗斯连接商品和买家的在线平台:goods.ru
2020/11/30 全球购物
校园摄影活动策划方案
2014/02/05 职场文书
观看《周恩来的四个昼夜》思想汇报
2014/09/12 职场文书
感恩母亲节活动总结
2015/02/10 职场文书
戒赌保证书
2015/05/11 职场文书
2015公司年度工作总结
2015/05/14 职场文书
详解Redis实现限流的三种方式
2021/04/27 Redis
Windows11插耳机没反应怎么办? win11耳机没声音的多种解决办法
2021/11/21 数码科技
Vue如何清空对象
2022/03/03 Vue.js