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


Posted in Javascript onSeptember 02, 2020

双向数据绑定无非就是,视图 => 数据,数据 => 视图的更新过程

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

以下的方案中的实现思路:

  • 定义一个Vue的构造函数并初始化这个函数(myVue.prototype._init)
  • 实现数据层的更新:数据劫持,定义一个 obverse 函数重写data的set和get(myVue.prototype._obsever)
  • 实现视图层的更新:订阅者模式,定义个 Watcher 函数实现对DOM的更新(Watcher)
  • 将数据和视图层进行绑定,解析指令v-bind、v-model、v-click(myVue.prototype._compile)
  • 创建Vue实例(new myVue)

1.object.defineproperty方式实现双向数据绑定

<!DOCTYPE html>
<html>
 
<head>
 <title>myVue</title>
 <style>
  #app{
  text-align: center;
 }
</style>
</head>
 
<body>
 <div id="app">
  <form>
   <input type="text" v-model="number" />
   <button type="button" v-click="increment">增加</button>
  </form>
  <h3 v-bind="number"></h3>
 </div>
</body>
<script>
 
 // 定义一个myVue构造函数
 function myVue(option) {
  this._init(option)
 }
 
 myVue.prototype._init = function (options) { // 传了一个配置对象
  this.$options = options // options 为上面使用时传入的结构体,包括el,data,methods
  this.$el = document.querySelector(options.el) // el是 #app, this.$el是id为app的Element元素
  this.$data = options.data // this.$data = {number: 0}
  this.$methods = options.methods // this.$methods = {increment: function(){}}
 
 
  // _binding保存着model与view的映射关系,也就是我们前面定义的Watcher的实例。当model改变时,我们会触发其中的指令类更新,保证view也能实时更新
  this._binding = {}
 
  this._obsever(this.$data)
  this._compile(this.$el)
 }
 
 // 数据劫持:更新数据
 myVue.prototype._obsever = function (obj) {
  let _this = this
  Object.keys(obj).forEach((key) => { // 遍历obj对象
   if (obj.hasOwnProperty(key)) { // 判断 obj 对象是否包含 key属性
    _this._binding[key] = [] // 按照前面的数据,_binding = {number: []} 存储 每一个 new Watcher
   }
   let value = obj[key]
   if (typeof value === 'object') { //如果值还是对象,则遍历处理
    _this._obsever(value)
   }
   Object.defineProperty(_this.$data, key, {
    enumerable: true,
    configurable: true,
    get: () => { // 获取 value 值
     return value
    },
    set: (newVal) => { // 更新 value 值
     if (value !== newVal) {
      value = newVal
      _this._binding[key].forEach((item) => { // 当number改变时,触发_binding[number] 中的绑定的Watcher类的更新
       item.update() // 调 Watcher 实例的 update 方法更新 DOM
      })
     }
    }
   })
  })
 }
 
 // 订阅者模式: 绑定更新函数,实现对 DOM 元素的更新
 function Watcher(el, data, key, attr) {
  this.el = el // 指令对应的DOM元素
  this.data = data // this.$data 数据: {number: 0, count: 0}
  this.key = key // 指令绑定的值,本例如"number"
  this.attr = attr // 绑定的属性值,本例为"innerHTML","value"
 
  this.update()
 }
 // 比如 H3.innerHTML = this.data.number; 当number改变时,会触发这个update函数,保证对应的DOM内容进行了更新
 Watcher.prototype.update = function () {
  this.el[this.attr] = this.data[this.key]
 }
 
 // 将view与model进行绑定,解析指令(v-bind,v-model,v-clickde)等
 myVue.prototype._compile = function (el) { // root 为id为app的Element元素,也就是我们的根元素
  let _this = this
  let nodes = Array.prototype.slice.call(el.children) // 将为数组转化为真正的数组
  nodes.map(node => {
   if (node.children.length && node.children.length > 0) { // 对所有元素进行遍历,并进行处理
    _this._compile(node)
   }
   if (node.hasAttribute('v-click')) { // 如果有v-click属性,我们监听它的onclick事件,触发increment事件,即number++
    let attrVal = node.getAttribute('v-click')
    node.onclick = _this.$methods[attrVal].bind(_this.$data) // bind是使data的作用域与method函数的作用域保持一致
   }
 
   // 如果有v-model属性,并且元素是INPUT或者TEXTAREA,我们监听它的input事件
   if (node.hasAttribute('v-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {
    let attrVal = node.getAttribute('v-model')
 
    _this._binding[attrVal].push(new Watcher(
     node, // 对应的 DOM 节点
     _this.$data,
     attrVal, // v-model 绑定的值
     'value'
    ))
    node.addEventListener('input', () => {
     _this.$data[attrVal] = node.value // 使number 的值与 node的value保持一致,已经实现了双向绑定
    })
   }
   if (node.hasAttribute('v-bind')) {
    let attrVal = node.getAttribute('v-bind')
    _this._binding[attrVal].push(new Watcher(
     node,
     _this.$data,
     attrVal, // v-bind 绑定的值
     'innerHTML'
    ))
   }
  })
 }
 
 
 window.onload = () => { // 当文档内容完全加载完成会触发该事件,避免获取不到对象的情况
  new myVue({
   el: '#app',
   data: {
    number: 0,
    count: 0
   },
   methods: {
    increment() {
     this.number++
    },
    incre() {
     this.count++
    }
   }
  })
 }
</script>
 
</html>

2.Proxy 实现双向数据绑定

<!DOCTYPE html>
<html>
 
<head>
 <title>myVue</title>
 <style>
  #app{
  text-align: center;
 }
</style>
</head>
 
<body>
 <div id="app">
  <form>
   <input type="text" v-model="number" />
   <button type="button" v-click="increment">增加</button>
  </form>
  <h3 v-bind="number"></h3>
 </div>
</body>
<script>
 
 // 定义一个myVue构造函数
 function myVue(option) {
  this._init(option)
 }
 
 myVue.prototype._init = function (options) { // 传了一个配置对象
  this.$options = options // options 为上面使用时传入的结构体,包括el,data,methods
  this.$el = document.querySelector(options.el) // el是 #app, this.$el是id为app的Element元素
  this.$data = options.data // this.$data = {number: 0}
  this.$methods = options.methods // this.$methods = {increment: function(){}}
 
  this._binding = {}
  this._obsever(this.$data)
  this._complie(this.$el)
 
 }
 
// 数据劫持:更新数据
myVue.prototype._obsever = function (data) {
  let _this = this
  let handler = {
   get(target, key) {
    return target[key]; // 获取该对象上key的值
   },
   set(target, key, newValue) {
    let res = Reflect.set(target, key, newValue); // 将新值分配给属性的函数
    _this._binding[key].map(item => {
     item.update();
    });
    return res;
   }
  };
  // 把代理器返回的对象代理到this.$data,即this.$data是代理后的对象,外部每次对this.$data进行操作时,实际上执行的是这段代码里handler对象上的方法
  this.$data = new Proxy(data, handler);
 }
 
 // 将view与model进行绑定,解析指令(v-bind,v-model,v-clickde)等
 myVue.prototype._complie = function (el) { // el 为id为app的Element元素,也就是我们的根元素
  let _this = this
  let nodes = Array.prototype.slice.call(el.children) // 将为数组转化为真正的数组
 
  nodes.map(node => {
   if (node.children.length && node.children.length > 0) this._complie(node)
   if (node.hasAttribute('v-click')) { // 如果有v-click属性,我们监听它的onclick事件,触发increment事件,即number++
    let attrVal = node.getAttribute('v-click')
    node.onclick = _this.$methods[attrVal].bind(_this.$data) // bind是使data的作用域与method函数的作用域保持一致
   }
 
   // 如果有v-model属性,并且元素是INPUT或者TEXTAREA,我们监听它的input事件
   if (node.hasAttribute('v-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {
    let attrVal = node.getAttribute('v-model')
    
    console.log(_this._binding)
    if (!_this._binding[attrVal]) _this._binding[attrVal] = []
    _this._binding[attrVal].push(new Watcher(
     node, // 对应的 DOM 节点
     _this.$data,
     attrVal, // v-model 绑定的值
     'value',
    ))
    node.addEventListener('input', () => {
     _this.$data[attrVal] = node.value // 使number 的值与 node的value保持一致,已经实现了双向绑定
    })
   }
   if (node.hasAttribute('v-bind')) {
    let attrVal = node.getAttribute('v-bind')
    if (!_this._binding[attrVal]) _this._binding[attrVal] = []
    _this._binding[attrVal].push(new Watcher(
     node,
     _this.$data,
     attrVal, // v-bind 绑定的值
     'innerHTML',
    ))
   }
 
  })
 }
 // 绑定更新函数,实现对 DOM 元素的更新
 function Watcher(el, data, key, attr) {
  this.el = el // 指令对应的DOM元素
  this.data = data // 代理的对象 this.$data 数据: {number: 0, count: 0}
  this.key = key // 指令绑定的值,本例如"num"
  this.attr = attr // 绑定的属性值,本例为"innerHTML","value"
 
  this.update()
 }
 // 比如 H3.innerHTML = this.data.number; 当number改变时,会触发这个update函数,保证对应的DOM内容进行了更新
 Watcher.prototype.update = function () {
  this.el[this.attr] = this.data[this.key]
 }
 
 window.onload = () => { // 当文档内容完全加载完成会触发该事件,避免获取不到对象的情况
  new myVue({
   el: '#app',
   data: {
    number: 0,
    count: 0
   },
   methods: {
    increment() {
     this.number++
    },
    incre() {
     this.count++
    }
   }
  })
 }
</script>
 
</html>

3.将上面代码改成class的写法

<!DOCTYPE html>
<html>
 
<head>
 <title>myVue</title>
 <style>
  #app{
  text-align: center;
 }
</style>
</head>
 
<body>
 <div id="app">
  <form>
   <input type="text" v-model="number" />
   <button type="button" v-click="increment">增加</button>
  </form>
  <h3 v-bind="number"></h3>
 </div>
</body>
<script>
 
 class MyVue {
  constructor(options) { // 接收了一个配置对象
   this.$options = options // options 为上面使用时传入的结构体,包括el,data,methods
   this.$el = document.querySelector(options.el) // el是 #app, this.$el是id为app的Element元素
   this.$data = options.data // this.$data = {number: 0}
   this.$methods = options.methods // this.$methods = {increment: function(){}}
 
   this._binding = {}
   this._obsever(this.$data)
   this._complie(this.$el)
  }
  _obsever (data) { // 数据劫持:更新数据
   let _this = this
   let handler = {
    get(target, key) {
     return target[key]; // 获取该对象上key的值
    },
    set(target, key, newValue) {
     let res = Reflect.set(target, key, newValue); // 将新值分配给属性的函数
     _this._binding[key].map(item => {
      item.update();
     });
     return res;
    }
   };
   // 把代理器返回的对象代理到this.$data,即this.$data是代理后的对象,外部每次对this.$data进行操作时,实际上执行的是这段代码里handler对象上的方法
   this.$data = new Proxy(data, handler);
  }
  _complie(el) { // el 为id为app的Element元素,也就是我们的根元素
   let _this = this
   let nodes = Array.prototype.slice.call(el.children) // 将为数组转化为真正的数组
 
   nodes.map(node => {
    if (node.children.length && node.children.length > 0) this._complie(node)
    if (node.hasAttribute('v-click')) { // 如果有v-click属性,我们监听它的onclick事件,触发increment事件,即number++
     let attrVal = node.getAttribute('v-click')
     node.onclick = _this.$methods[attrVal].bind(_this.$data) // bind是使data的作用域与method函数的作用域保持一致
    }
 
    // 如果有v-model属性,并且元素是INPUT或者TEXTAREA,我们监听它的input事件
    if (node.hasAttribute('v-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {
     let attrVal = node.getAttribute('v-model')
     if (!_this._binding[attrVal]) _this._binding[attrVal] = []
     _this._binding[attrVal].push(new Watcher(
      node, // 对应的 DOM 节点
      _this.$data,
      attrVal, // v-model 绑定的值
      'value',
     ))
     node.addEventListener('input', () => {
      _this.$data[attrVal] = node.value // 使number 的值与 node的value保持一致,已经实现了双向绑定
     })
    }
    if (node.hasAttribute('v-bind')) {
     let attrVal = node.getAttribute('v-bind')
     if (!_this._binding[attrVal]) _this._binding[attrVal] = []
     _this._binding[attrVal].push(new Watcher(
      node,
      _this.$data,
      attrVal, // v-bind 绑定的值
      'innerHTML',
     ))
    }
 
   })
  }
 }
 
 class Watcher {
  constructor (el, data, key, attr) {
   this.el = el // 指令对应的DOM元素
   this.data = data // 代理的对象 this.$data 数据: {number: 0, count: 0}
   this.key = key // 指令绑定的值,本例如"num"
   this.attr = attr // 绑定的属性值,本例为"innerHTML","value"
   this.update()
  }
 
  update () {
   this.el[this.attr] = this.data[this.key]
  }
 }
 
 
 
 window.onload = () => { // 当文档内容完全加载完成会触发该事件,避免获取不到对象的情况
  new MyVue({
   el: '#app',
   data: {
    number: 0,
    count: 0
   },
   methods: {
    increment() {
     this.number++
    },
    incre() {
     this.count++
    }
   }
  })
 }
</script>
 
</html>

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
js传值 判断
Oct 26 Javascript
js实现网页右上角滑出会自动消失大幅广告的方法
Feb 27 Javascript
JavaScript处理解析JSON数据过程详解
Sep 11 Javascript
jquery验证邮箱格式是否正确实例讲解
Nov 16 Javascript
基于slideout.js实现移动端侧边栏滑动特效
Nov 28 Javascript
详解Vue.js搭建路由报错 router.map is not a function
Jun 27 Javascript
详解plotly.js 绘图库入门使用教程
Feb 23 Javascript
JS实现的JSON序列化操作简单示例
Jul 02 Javascript
JS实现获取当前所在周的周六、周日示例分析
May 11 Javascript
JavaScript算法学习之冒泡排序和选择排序
Nov 02 Javascript
nuxt配置通过指定IP和端口访问的实现
Jan 08 Javascript
如何在selenium中使用js实现定位
Aug 18 Javascript
Vue 组件的挂载与父子组件的传值实例
Sep 02 #Javascript
vue中的.$mount('#app')手动挂载操作
Sep 02 #Javascript
vue中提示$index is not defined错误的解决方式
Sep 02 #Javascript
解决vue组件没显示,没起作用,没报错,但该显示的组件没显示问题
Sep 02 #Javascript
vue或react项目生产环境去掉console.log的操作
Sep 02 #Javascript
JS继承实现方法及优缺点详解
Sep 02 #Javascript
vue-cli3访问public文件夹静态资源报错的解决方式
Sep 02 #Javascript
You might like
PHP借助phpmailer发送邮件
2015/05/11 PHP
Thinkphp模板标签if和eq的区别和比较实例分析
2015/07/01 PHP
PHP中使用substr()截取字符串出现中文乱码问题该怎么办
2015/10/21 PHP
提交表单后 PHP获取提交内容的实现方法
2016/05/25 PHP
PHP排序算法之堆排序(Heap Sort)实例详解
2018/04/21 PHP
ThinkPHP5+Layui实现图片上传加预览功能
2018/08/17 PHP
select标记美化--JS式插件、后期加载
2013/04/01 Javascript
JS去掉第一个字符和最后一个字符的实现代码
2014/02/20 Javascript
nodejs爬虫抓取数据之编码问题
2015/07/03 NodeJs
vue之nextTick全面解析
2017/05/17 Javascript
JavaScript中使用Async实现异步控制
2017/08/15 Javascript
nodejs操作mongodb的增删改查功能实例
2017/11/09 NodeJs
JS排序算法之冒泡排序,选择排序与插入排序实例分析
2017/12/13 Javascript
Nuxt.js实战详解
2018/01/18 Javascript
浅谈React Native 传参的几种方式(小结)
2019/05/21 Javascript
JavaScript 链表定义与使用方法示例
2020/04/28 Javascript
JavaScript中使用Spread运算符的八种方法总结
2020/06/18 Javascript
Vue中 axios delete请求参数操作
2020/08/25 Javascript
Python Web框架Flask中使用七牛云存储实例
2015/02/08 Python
多个应用共存的Django配置方法
2018/05/30 Python
Python实现SQL注入检测插件实例代码
2019/02/02 Python
Python requests模块实例用法
2019/02/11 Python
python解析yaml文件过程详解
2019/08/30 Python
pycharm远程连接vagrant虚拟机中mariadb数据库
2020/06/05 Python
Python列表推导式实现代码实例
2020/09/09 Python
Pycharm如何自动生成头文件注释
2020/11/14 Python
pycharm配置QtDesigner的超详细方法
2021/01/25 Python
兰蔻加拿大官方网站:Lancome加拿大
2016/08/05 全球购物
什么是数据库锁?Oracle中都有哪些类型的锁?
2015/08/21 面试题
优秀毕业生找工作自荐信
2014/06/23 职场文书
爱心助学感谢信
2015/01/21 职场文书
闪闪的红星观后感
2015/06/08 职场文书
2015年“我们的节日·中秋节”活动总结
2015/07/30 职场文书
详解Python常用的魔法方法
2021/06/03 Python
pandas数值排序的实现实例
2021/07/25 Python
oracle删除超过N天数据脚本的方法
2022/02/28 Oracle