【Vue源码】简单实现Vue的双向数据绑定:Object.defineProperty和Proxy

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

 

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

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

参照:https://blog.csdn.net/weixin_41845146/article/details/85268973

面试题:你能写一个 Vue 的双向数据绑定吗?

posted @ 2020-07-25 20:03  vickylinj  阅读(303)  评论(0编辑  收藏  举报