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报错 Object doesn't support this property or method的原因分析
Mar 31 Javascript
仅IE支持clearAttributes/mergeAttributes方法使用介绍
May 04 Javascript
通过jQuery源码学习javascript(一)
Dec 27 Javascript
JS使用ajax从xml文件动态获取数据显示的方法
Mar 24 Javascript
vue2.0开发实践总结之入门篇
Dec 06 Javascript
详解使用grunt完成requirejs的合并压缩和js文件的版本控制
Mar 02 Javascript
Vue表单验证插件的制作过程
Apr 01 Javascript
Angular服务Request异步请求的实例讲解
Aug 13 Javascript
使用 electron 实现类似新版 QQ 的登录界面效果(阴影、背景动画、窗体3D翻转)
Oct 23 Javascript
JS根据json数组多个字段排序及json数组常用操作
Jun 06 Javascript
JavaScript Dom 绑定事件操作实例详解
Oct 02 Javascript
vue项目在线上服务器访问失败原因分析
Aug 14 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中全局变量global和$GLOBALS[]的区别分析
2012/08/06 PHP
php基于curl主动推送最新内容给百度收录的方法
2016/10/14 PHP
PHP随机生成中文段落示例【测试网站内容时使用】
2020/04/26 PHP
JavaScript监测ActiveX控件是否已经安装过的代码
2008/09/02 Javascript
JS下高效拼装字符串的几种方法比较与测试代码
2010/04/15 Javascript
Google 静态地图API实现代码
2010/11/19 Javascript
js string 转 int 注意的问题小结
2013/08/15 Javascript
浅谈JSON中stringify 函数、toJosn函数和parse函数
2015/01/26 Javascript
JavaScript 数组中最大最小值
2016/06/05 Javascript
老生常谈jquery id选择器和class选择器的区别
2017/02/12 Javascript
JavaScript中正则表达式判断匹配规则及常用方法
2017/08/03 Javascript
JS实现的JSON数组去重算法示例
2018/04/11 Javascript
微信小程序如何自定义table组件
2019/06/29 Javascript
JavaScript队列结构Queue实现过程解析
2020/03/07 Javascript
使用原生JS实现滚轮翻页效果的示例代码
2020/05/31 Javascript
基于Python实现的百度贴吧网络爬虫实例
2015/04/17 Python
Python自动登录126邮箱的方法
2015/07/10 Python
matplotlib简介,安装和简单实例代码
2017/12/26 Python
Python实现的特征提取操作示例
2018/12/03 Python
python GUI图形化编程wxpython的使用
2019/07/19 Python
python编写计算器功能
2019/10/25 Python
python3 常见解密加密算法实例分析【base64、MD5等】
2019/12/19 Python
pandas中的ExcelWriter和ExcelFile的实现方法
2020/04/24 Python
Django限制API访问频率常用方法解析
2020/10/12 Python
CSS3教程:background-clip和background-origin
2008/10/17 HTML / CSS
在html5的Canvas上绘制椭圆的几种方法总结
2013/01/07 HTML / CSS
德国家具在线:Fashion For Home
2017/03/11 全球购物
编写函数,将一个3*3矩阵转置
2013/10/09 面试题
毕业生求职信
2014/06/10 职场文书
大学生赌博检讨书
2014/09/22 职场文书
四风自我剖析材料思想汇报
2014/10/01 职场文书
党员示范岗材料
2014/12/19 职场文书
2015年防灾减灾工作总结
2015/07/24 职场文书
《窃读记》教学反思
2016/02/18 职场文书
Python pandas求方差和标准差的方法实例
2021/08/04 Python
如何使用SQL Server语句创建表
2022/04/12 SQL Server