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

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

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

组成说明

  • 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 实现双向数据绑定的资料请关注我们其它相关文章!

(0)

相关推荐

  • vue解决花括号数据绑定不成功的问题

    如下所示: <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title></title> </head> <script src="js/vue.js" type="text/javascript" charset="utf-8"></script><

  • vue中的双向数据绑定原理与常见操作技巧详解

    本文实例讲述了vue中的双向数据绑定原理与常见操作技巧.分享给大家供大家参考,具体如下: 什么是双向数据绑定? vue是一个mvvm框架,即数据双向绑定,即当数据发生变化的时候,视图也就发生变化,当视图发生变化的时候,数据也会跟着同步变化.这也是算是vue的精髓之处了.值得注意的是,我们所说的数据双向绑定,一定是对于UI控件来说的,非UI控件不会涉及到数据双向绑定.单向数据绑定是使用状态管理工具的前提,如果我们使用vuex,那么数据流也是单向的,这时就会和双向数据绑定有冲突,我们可以这么解决.

  • vue.js使用v-model实现表单元素(input) 双向数据绑定功能示例

    本文实例讲述了vue.js使用v-model实现表单元素(input) 双向数据绑定功能.分享给大家供大家参考,具体如下: v-model 一般表单元素(input) 双向数据绑定 el:'#box',//这里放的是选择器. 不然会不生效 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>www.jb51.net vu

  • 一文读懂vue动态属性数据绑定(v-bind指令)

    v-bind的基本用法 一.本节说明 前面的章节我们学习了如何向页面html标签进行插值操作,那么如果我们想动态改变html标签的属性,该怎么办呢? 这就是我们这节开始要讲的内容v-bind. 二. 怎么做 ":"为v-bind的简写形式,也可称为语法糖 三. 效果 四. 深入 在上图中将a标签的href属性值设置为toutiao,VUE实例将自动去data里面寻找toutiao属性进行值绑定. 不只是a标签,所有的html标签属性都可以通过v-bind进行值绑定,然后通过改变数据动态

  • vue3.0中的双向数据绑定方法及优缺点

    熟悉vue的人都知道在vue2.x之前都是使用object.defineProperty来实现双向数据绑定的 而在vue3.0中这个方法被取代了 1. 为什么要替换Object.defineProperty 替换不是因为不好,是因为有更好的方法使用效率更高 Object.defineProperty的缺点: 1. 在Vue中,Object.defineProperty无法监控到数组下标的变化, 导致直接通过数组的下标给数组设置值,不能实时响应. push() pop() shift() unsh

  • Vue 数据绑定的原理分析

    原理 其实原理很简单,就是拦截了Object的get/set方法,在对数据进行set(obj.aget=18)时去重现渲染视图 实现方式有两种 方式1 定义了同名的get/set就相当于定义了age var test = { _age: 18, get age() { console.log('触发get'); //直接会this.age会进入死递归的 return this._age; }, set age(age) { console.log('触发set'); this._age = ag

  • Vue Object.defineProperty及ProxyVue实现双向数据绑定

    双向数据绑定无非就是,视图 => 数据,数据 => 视图的更新过程 以下的方案中的实现思路: 定义一个Vue的构造函数并初始化这个函数(myVue.prototype._init) 实现数据层的更新:数据劫持,定义一个 obverse 函数重写data的set和get(myVue.prototype._obsever) 实现视图层的更新:订阅者模式,定义个 Watcher 函数实现对DOM的更新(Watcher) 将数据和视图层进行绑定,解析指令v-bind.v-model.v-click(m

  • vue在自定义组件中使用v-model进行数据绑定的方法

    本文介绍了vue v-model进行数据绑定,分享给大家,具体如下 官方例子https://vuefe.cn/v2/api/#model 有这么一句话: 默认情况下,一个组件上的 v-model 会把 value 用作 prop 且把 input 用作 event. 示例: 先来一个组件,不用vue-model,正常父子通信 <!-- parent --> <template> <p class="parent"> <p>我是父亲, 对儿

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

    主要是通过数据劫持和发布订阅一起实现的 双向数据绑定 数据更新时,可以更新视图 视图的数据更新是,可以反向更新模型 组成说明 Observe监听器 劫持数据, 感知数据变化, 发出通知给订阅者, 在get中将订阅者添加到订阅器中 Dep消息订阅器 存储订阅者, 通知订阅者调用更新函数 订阅者Wather取出模型值,更新视图 解析器Compile 解析指令, 更新模板数据, 初始化视图, 实例化一个订阅者, 将更新函数绑定到订阅者上, 可以在接收通知二次更新视图, 对于v-model还需要监听in

  • 使用Vue如何写一个双向数据绑定(面试常见)

    1.原理 Vue的双向数据绑定的原理相信大家也都十分了解了,主要是通过 Object对象的defineProperty属性,重写data的set和get函数来实现的,这里对原理不做过多描述,主要还是来实现一个实例.为了使代码更加的清晰,这里只会实现最基本的内容,主要实现v-model,v-bind 和v-click三个命令,其他命令也可以自行补充. 添加网上的一张图 2.实现 页面结构很简单,如下 <div id="app"> <form> <input

  • vue双向数据绑定指令v-model的用法

    目录 基本使用 响应式 v-model数据双向绑定步骤 响应式 v-model简易实现原理 v-model,被称为双向数据绑定指令,就是Vue实例对数据进行修改,页面会立即感知,相反页面对数据进行修改,Vue内部也会立即感知. v-model 是vue中唯一实现双向数据绑定的指令. v-bind(单向)数据绑定,Vue实例修改数据,页面会感知到,相反页面修改数据Vue实例不能感知. v-model(双向)数据绑定,页面修改数据,Vue实例会感知到,Vue实例修改数据,页面也会感知到. 基本使用

  • Vue实现双向数据绑定

    Vue实现双向数据绑定的方式,具体内容如下 Vue是如何实现双向数据绑定的呢?答案是前端数据劫持.其通过Object.defineProperty()方法,这个方法可以设置getter和setter函数,在setter函数中,就可以监听到数据的变化,从而更新绑定的元素的值. 实现对象属性变化绑定到UI 大概的思路是: 1. 确定绑定的数据,使用Object.defineProperty()对其数据变化进行监听(对应defineGetAndSet) 2. 一旦数据发生改动,会触发setter函数.

  • vue双向数据绑定知识点总结

    1.原理 vue的双向数据绑定的原理相信大家都十分了解:主要是通过ES5的Object对象的defineProperty属性:重写data的set和get函数来实现的 所以接下来不使用ES6进行实际的代码开发:过程中如果函数使用父级this的情况:还是使用显示缓存中间变量和闭包来处理:原因是箭头函数没有独立的执行上下文this:所以箭头函数内部出现this对象会直接访问父级:所以也能看出箭头函数是无法完全替代function的使用场景的:比如我们需要独立的this或者argument的时候 1.

  • Vue响应式原理及双向数据绑定示例分析

    目录 前言 响应式原理 双向数据绑定 前言 之前公司招人,面试了一些的前端同学,因为公司使用的前端技术是Vue,所以免不了问到其响应式原理和Vue的双向数据绑定.但是这边面试到的80%的同学会把两者搞混,通常我要是先问响应式原理再问双向数据绑定原理,来面试的同学大都会认为是一回事,那么这里我们就说一下二者的区别. 响应式原理 是Vue的核心特性之一,数据驱动视图,我们修改数据视图随之响应更新,就很优雅~ Vue2.x是借助Object.defineProperty()实现的,而Vue3.x是借助

  • Angular和Vue双向数据绑定的实现原理(重点是vue的双向绑定)

    我在整理javascript高级程序设计的笔记的时候看到面向对象设计那章,讲到对象属性分为数据属性和访问器属性,我们平时用的js对象90%以上都只是用到数据属性;我们向来讲解下数据属性和访问器属性到底是什么? 数据属性:数据属性包含一个数据值的位置,在这个位置可以读取和写入值. 访问器属性:访问器属性不包含数据值;他们包含一对getter和setter函数在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值,在写入访问器属性时,会调用setter函数并传入新值. 这里介绍的重点是

  • vue双向数据绑定原理探究(附demo)

    昨天被导师叫去研究了一下vue的双向数据绑定原理...本来以为原理的东西都非常高深,没想到vue的双向绑定真的很好理解啊...自己动手写了一个. 传送门 双向绑定的思想 双向数据绑定的思想就是数据层与UI层的同步,数据再两者之间的任一者发生变化时都会同步更新到另一者. 双向绑定的一些方法 目前,前端实现数据双向数据绑定的方法大致有以下三种: 1.发布者-订阅者模式(backbone.js) 思路:使用自定义的data属性在HTML代码中指明绑定.所有绑定起来的JavaScript对象以及DOM元

  • 通过源码分析Vue的双向数据绑定详解

    前言 虽然工作中一直使用Vue作为基础库,但是对于其实现机理仅限于道听途说,这样对长期的技术发展很不利.所以最近攻读了其源码的一部分,先把双向数据绑定这一块的内容给整理一下,也算是一种学习的反刍. 本篇文章的Vue源码版本为v2.2.0开发版. Vue源码的整体架构无非是初始化Vue对象,挂载数据data/props等,在不同的时期触发不同的事件钩子,如created() / mounted() / update()等,后面专门整理各个模块的文章.这里先讲双向数据绑定的部分,也是最主要的部分.

  • 轻松理解vue的双向数据绑定问题

    Vue介绍 Vue是当前很火的一款MVVM的轻量级框架,它是以数据驱动和组件化的思想构建的.因为它提供了简洁易于理解的api,使得我们很容易上手. Vue与MVVM 如果你之前已经习惯了用jQuery操作DOM,学习Vue.js时请先抛开手动操作DOM的思维,因为Vue.js是数据驱动的,你无需手动操作DOM.Vue以数据为驱动,将自身的Dom元素与数据进行绑定,一旦创建绑定,Dom和数据保持同步. 双向绑定 主流双向数据绑定实现原理 脏值检测 : 这是AngularJS实现双向数据绑定的方式.

随机推荐