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 相关文章推荐
PJ Blog修改-禁止复制的代码和方法
Oct 25 Javascript
javascript div 遮罩层封锁整个页面
Jul 10 Javascript
传智播客学习之java 反射
Nov 22 Javascript
js常用代码段收集
Oct 28 Javascript
javascript阻止scroll事件多次执行的思路及实现
Nov 08 Javascript
javascript 获取网页标题代码实例
Jan 22 Javascript
简介JavaScript中fixed()方法的使用
Jun 08 Javascript
实例讲解js验证表单项是否为空的方法
Jan 09 Javascript
AngularJS动态加载模块和依赖的方法分析
Nov 08 Javascript
jQuery实现QQ空间汉字转拼音功能示例
Jul 10 jQuery
vue2中,根据list的id进入对应的详情页并修改title方法
Aug 24 Javascript
JavaScript实现的联动菜单特效示例
Jul 08 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
当年上海收录机产品生产,进口和价格情况
2021/03/04 无线电
PHP 反射(Reflection)使用实例
2015/05/12 PHP
ASP小贴士/ASP Tips javascript tips可以当桌面
2009/12/10 Javascript
js中字符替换函数String.replace()使用技巧
2011/08/14 Javascript
JavaScript 反科里化 this [译]
2012/09/20 Javascript
JavaScript 模式之工厂模式(Factory)应用介绍
2012/11/15 Javascript
ie7+背景透明文字不透明超级简单的实现方法
2014/01/17 Javascript
js 实现浏览历史记录示例
2014/04/20 Javascript
详解JavaScript对象和数组
2015/12/03 Javascript
js控制li的隐藏和显示实例代码
2016/10/15 Javascript
深入学习nodejs中的async模块的使用方法
2017/07/12 NodeJs
基于jQuery选择器之表单对象属性筛选选择器的实例
2017/09/19 jQuery
Bootstrap modal只加载一次数据的解决办法(推荐)
2017/11/24 Javascript
jQuery实现动画、消失、显现、渐出、渐入效果示例
2018/09/06 jQuery
Javascript删除数组里的某个元素
2019/02/28 Javascript
原生js添加一个或多个类名的方法分析
2019/07/30 Javascript
Vue+Typescript中在Vue上挂载axios使用时报错问题
2019/08/07 Javascript
layui数据表格重载实现往后台传参
2019/11/15 Javascript
浅谈python内置变量-reversed(seq)
2017/06/21 Python
Python 中Pickle库的使用详解
2018/02/24 Python
pytorch: tensor类型的构建与相互转换实例
2018/07/26 Python
详解python中Numpy的属性与创建矩阵
2018/09/10 Python
python实现五子棋程序
2020/04/24 Python
python定时截屏实现
2020/11/02 Python
意大利一家专营包包和配饰的网上商店:Borse Last Minute
2019/08/26 全球购物
日本最大化妆品和美容产品的综合口碑网站:cosme shopping
2019/08/28 全球购物
室内设计实习自我鉴定
2013/09/25 职场文书
服装机修工岗位职责
2013/12/26 职场文书
机关财务管理制度
2014/01/17 职场文书
网络编辑岗位职责
2014/03/18 职场文书
公司晚会策划方案
2014/05/17 职场文书
公司员工活动策划方案
2014/08/20 职场文书
学习三严三实对照检查材料思想汇报
2014/09/22 职场文书
党的群众路线教育实践活动整改方案
2014/10/28 职场文书
培训学校2015年度工作总结
2015/07/20 职场文书
初中班主任教育随笔
2015/08/15 职场文书