zoukankan      html  css  js  c++  java
  • 【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 的双向数据绑定吗?

  • 相关阅读:
    二分匹配
    第k短路
    2015_10
    The 15th Zhejiang University Programming Contest
    2015_8
    hdu 1565
    istringstream 用法
    floyd 闭包传递 判断两个点是否属于同一个 强连通分量
    Sicily 1866.Gene Reprogram 一种经典的hash方法
    zoj 3130 最小费用最大流 (求 s到e 的两条总花费最少的 完全没有交点的 路径)
  • 原文地址:https://www.cnblogs.com/vickylinj/p/13378034.html
Copyright © 2011-2022 走看看