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 相关文章推荐
jquery 快速回到页首的方法
Dec 05 Javascript
JavaScript代码应该放在HTML代码哪个位置比较好?
Oct 16 Javascript
《JavaScript DOM 编程艺术》读书笔记之JavaScript 简史
Jan 09 Javascript
jquery实现页面常用的返回顶部效果
Mar 04 Javascript
Javascript的表单与验证-非空验证
Mar 18 Javascript
浅谈javascript中的三种弹窗
Oct 21 Javascript
JS传播事件、取消事件默认行为、阻止事件传播详解
Aug 14 Javascript
jQuery实现html双向绑定功能示例
Oct 09 jQuery
vue router-link传参以及参数的使用实例
Nov 10 Javascript
解决angularjs WdatePicker ng-model的问题
Sep 13 Javascript
详解JavaScript中的坐标和距离
May 27 Javascript
【js设计模式】SOLID五大设计原则
Mar 24 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 正则表达式常用函数
2014/08/17 PHP
PHP+RabbitMQ实现消息队列的完整代码
2019/03/20 PHP
脚本安需导入(装载)的三种模式的对比
2007/06/24 Javascript
jquery 无限级联菜单案例分享
2013/03/26 Javascript
node.js中的require使用详解
2014/12/15 Javascript
js实现文本框只允许输入数字并限制数字大小的方法
2015/08/19 Javascript
极力推荐10个短小实用的JavaScript代码段
2016/08/03 Javascript
实现JavaScript高性能的数据存储
2016/12/11 Javascript
webuploader模态框ueditor显示问题解决方法
2016/12/27 Javascript
AngularJS框架中的双向数据绑定机制详解【减少需要重复的开发代码量】
2017/01/19 Javascript
原生js实现轮播图的示例代码
2017/02/20 Javascript
js实现带三角符的手风琴效果
2017/03/01 Javascript
vue 组件使用中的一些细节点
2018/04/25 Javascript
JavaScript错误处理操作实例详解
2019/01/04 Javascript
微信小程序学习笔记之函数定义、页面渲染图文详解
2019/03/28 Javascript
js事件触发操作实例分析
2019/06/21 Javascript
12 种使用Vue 的最佳做法
2020/03/30 Javascript
Vue页面跳转传递参数及接收方式
2020/09/09 Javascript
Python采集腾讯新闻实例
2014/07/10 Python
Python3 能振兴 Python的原因分析
2014/11/28 Python
在Django中创建动态视图的教程
2015/07/15 Python
Python的for和break循环结构中使用else语句的技巧
2016/05/24 Python
详解Python文本操作相关模块
2017/06/22 Python
浅谈Python peewee 使用经验
2017/10/20 Python
python使用beautifulsoup4爬取酷狗音乐代码实例
2019/12/04 Python
利用CSS3实现平移动画效果示例代码
2016/10/12 HTML / CSS
纯css实现照片墙3D效果的示例代码
2017/11/13 HTML / CSS
Hotels.com印度:酒店预订
2019/05/11 全球购物
银行会计职员个人的自我评价
2013/09/29 职场文书
前台领班岗位职责
2013/12/04 职场文书
企业统计员岗位职责
2013/12/13 职场文书
监察局领导班子四风问题整改措施思想汇报
2014/10/05 职场文书
2014年学校安全工作总结
2014/11/13 职场文书
公务员政审材料范文
2014/12/23 职场文书
Java实现多文件上传功能
2021/06/30 Java/Android
vue3中provide && inject的使用
2021/07/01 Vue.js