Vue双向绑定原理代码实现

1、代码实现Vue双向绑定与事件绑定,v-bind v-model v-on

DOM结构准备
<body>
  <div id="app">
    <form>
      <input type="text" v-model="userInfo.name" />

      <button type="button" v-click="increment">增加button>
    form>

    <h3 v-bind="number">h3>
    <h4 v-bind="userInfo.name">h4>
  div>
body>
myVue创建实例,最终使用示例
window.onload = function () {
    var app = new myVue({
      el: '#app',
      data: {
        number: 0,
        userInfo: {
          name: 0
        }
      },
      methods: {
        increment: function () {
          console.log(this)
          console.log()
          this.userInfo.name += 1
          console.log(this.userInfo.name)
          this.number++
        }
      },
      directives: {}
    })
    console.log(app._binding, 'bind')
  }
创建myVue构造函数,

并初始化,将配置数据挂载到构造函数的原型对象上

  function myVue(options) {
    this._init(options)
  }

  myVue.prototype._init = function (options) {
    this.$options = options
    // 存储跟节点DOM对象
    this.$el = document.querySelector(options.el)
    // 存储data数据
    this.$data = options.data
    // 存储methods所有的方法
    this.$methods = options.methods
    // 用于存储data中数据绑定到dom的元素
    this._binding = {}
    // 形式如下
    // {
    //   number: {
    //     _directives: [watcher1, watcher2]
    //   },
    //   'userInfo.name': {
    //     _directives: [watcher1, watcher2]
    //   }
    // }

    // 绑定发布者,在set属性里面遍历_binding[key]._directives
    this._obverse(this.$data)
    // 递归根节点,通过识别节点的属性,识别出不同指令,不同指令进行不同操作,
    // 如v-bind指令:创建watcher并将其push进_binding
    // _binding[key]._directives.push(watcher)
    this._complie(this.$el)
  }
创建监听类watcher的构造函数,

并在其原型对象上实现update方法,该类将会把DOM中使用了指令的节点与data数据绑定

  function Watcher(name, el, vm, exp, attr) {
    this.name = name //指令名称,例如文本节点,该值设为"text"

    this.el = el //指令对应的DOM元素

    this.vm = vm //指令所属myVue实例

    this.exp = exp //指令对应的值,本例如"number"

    this.attr = attr //绑定的属性值,本例为"innerHTML"

    this.update()
  }

  Watcher.prototype.update = function () {
    // let exps = this.exp.split('.')
    // let res = this.vm.$data
    // exps.forEach(item => (res = res[item]))
    // console.log(res, 'res')
    this.el[this.attr] = getObjValueByStr(this.vm.$data, this.exp)
  }
在myVue原型上实现方法_obverse(),

在data数据的 set属性中实现页面数据更新的方法调用

  myVue.prototype._obverse = function (obj, preKey) {
    for (key in obj) {
      if (obj.hasOwnProperty(key)) {
        let curKey
        if (preKey && preKey != '') {
          curKey = preKey + '.' + key
        } else {
          curKey = key
        }
        console.log(curKey)
        this._binding[curKey] = {
          _directives: []
        }

        let value = obj[key]
        console.log(value, '----')

        let binding = this._binding[curKey]

        Object.defineProperty(obj, key, {
          enumerable: true,

          configurable: true,

          get: function () {
            console.log(`获取${value}`)

            return value
          },

          set: function (newVal) {
            if (value !== newVal) {
              console.log(`更新${newVal}`)
              value = newVal
              console.log(binding, 'binding')
              binding._directives.forEach(function (item) {
                item.update()
              })
            }
          }
        })

        if (typeof value === 'object') {
          this._obverse(value, curKey)
        }
      }
    }
  }

在myVue原型上实现方法_complie(),

遍历根节点中的所有节点,根据节点中设置的属性(即指令),对不同指令实现具体的功能如属性绑定指令 v-bind ,就创建一个监听实例,并放入myVue的binding中,binding[绑定的data中的数据名称]._directives.push(watcher实例)

 myVue.prototype._complie = function (root) {
    var _this = this

    var nodes = root.children

    for (var i = 0; i < nodes.length; i++) {
      var node = nodes[i]

      if (node.children.length) {
        this._complie(node)
      }

      if (node.hasAttribute('v-click')) {
        node.onclick = (function () {
          var attrVal = nodes[i].getAttribute('v-click')

          return _this.$methods[attrVal].bind(_this.$data)
        })()
      }

      if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) {
        node.addEventListener(
          'input',
          (function (key) {
            var attrVal = node.getAttribute('v-model')

            _this._binding[attrVal]._directives.push(
              new Watcher(
                'input',

                node,

                _this,

                attrVal,

                'value'
              )
            )

            return function () {
              // let arr = attrVal.split('.')
              // let resAttr = _this.$data
              // for (let j = 0; j < arr.length - 1; j++) {
              //   resAttr = resAttr[arr[j]]
              // }
              // resAttr[arr[arr.length - 1]] = nodes[key].value
              objValueByStr(_this.$data, attrVal, nodes[key].value)
              // _this.$data[attrVal] = nodes[key].value
            }
          })(i)
        )
      }

      if (node.hasAttribute('v-bind')) {
        var attrVal = node.getAttribute('v-bind')
        _this._binding[attrVal]._directives.push(
          new Watcher(
            'text',

            node,

            _this,

            attrVal,

            'innerHTML'
          )
        )
      }
    }
  }

2、实现原理说明

  1. obj.hasOwnProperty(key)方法,判断对象中是否具有key属性
  2. Object.defineProperty(对象,属性名,配置项)
  3. Object.defineProperty中配置项说明,属性绑定:data数据修改,动态更新DOM中的数据,主要是通过set属性,在set中更新DOM中数据
属性名 功能说明
configurable: 表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性,默认值为false。
enumerable 表示能否通过for in循环访问属性,默认值为false
writable: 表示能否修改属性的值。默认值为false。
value: 包含这个属性的数据值。默认值为undefined。
get: 在读取属性时调用的函数,默认值是undefined
set: 在写入属性的时候调用的函数,默认值是undefined
  1. _obverse()方法,在data所有数据中设置set,数据改变就把绑定的DOM节点值改变
  2. _complie方法,遍历DOM节点树,并通过识别出节点中的指令,为其实现不同功能

3、完整代码

DOCTYPE html>

<head>
  <title>myVuetitle>
head>

<style>
  #app {
    text-align: center;
  }
style>

<body>
  <div id="app">
    <form>
      <input type="text" v-model="userInfo.name" />

      <button type="button" v-click="increment">增加button>
    form>

    <h3 v-bind="number">h3>
    <h4 v-bind="userInfo.name">h4>
  div>
body>

<script>
  function myVue(options) {
    this._init(options)
  }

  myVue.prototype._init = function (options) {
    this.$options = options
    // 存储跟节点DOM对象
    this.$el = document.querySelector(options.el)
    // 存储data数据
    this.$data = options.data
    // 存储methods所有的方法
    this.$methods = options.methods
    // 用于存储data中数据绑定到dom的元素
    this._binding = {}
    // 形式如下
    // {
    //   number: {
    //     _directives: [watcher1, watcher2]
    //   },
    //   'userInfo.name': {
    //     _directives: [watcher1, watcher2]
    //   }
    // }

    // 绑定发布者,在set属性里面遍历_binding[key]._directives
    this._obverse(this.$data)
    // 递归根节点,通过识别节点的属性,识别出不同指令,不同指令进行不同操作,
    // 如v-bind指令:创建watcher并将其push进_binding
    // _binding[key]._directives.push(watcher)
    this._complie(this.$el)
  }

  myVue.prototype._obverse = function (obj, preKey) {
    for (key in obj) {
      if (obj.hasOwnProperty(key)) {
        let curKey
        if (preKey && preKey != '') {
          curKey = preKey + '.' + key
        } else {
          curKey = key
        }
        console.log(curKey)
        this._binding[curKey] = {
          _directives: []
        }

        let value = obj[key]
        console.log(value, '----')

        let binding = this._binding[curKey]

        Object.defineProperty(obj, key, {
          enumerable: true,

          configurable: true,

          get: function () {
            console.log(`获取${value}`)

            return value
          },

          set: function (newVal) {
            if (value !== newVal) {
              console.log(`更新${newVal}`)
              value = newVal
              console.log(binding, 'binding')
              binding._directives.forEach(function (item) {
                item.update()
              })
            }
          }
        })

        if (typeof value === 'object') {
          this._obverse(value, curKey)
        }
      }
    }
  }

  myVue.prototype._complie = function (root) {
    var _this = this

    var nodes = root.children

    for (var i = 0; i < nodes.length; i++) {
      var node = nodes[i]

      if (node.children.length) {
        this._complie(node)
      }

      if (node.hasAttribute('v-click')) {
        node.onclick = (function () {
          var attrVal = nodes[i].getAttribute('v-click')

          return _this.$methods[attrVal].bind(_this.$data)
        })()
      }

      if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) {
        node.addEventListener(
          'input',
          (function (key) {
            var attrVal = node.getAttribute('v-model')

            _this._binding[attrVal]._directives.push(
              new Watcher(
                'input',

                node,

                _this,

                attrVal,

                'value'
              )
            )

            return function () {
              // let arr = attrVal.split('.')
              // let resAttr = _this.$data
              // for (let j = 0; j < arr.length - 1; j++) {
              //   resAttr = resAttr[arr[j]]
              // }
              // resAttr[arr[arr.length - 1]] = nodes[key].value
              objValueByStr(_this.$data, attrVal, nodes[key].value)
              // _this.$data[attrVal] = nodes[key].value
            }
          })(i)
        )
      }

      if (node.hasAttribute('v-bind')) {
        var attrVal = node.getAttribute('v-bind')
        _this._binding[attrVal]._directives.push(
          new Watcher(
            'text',

            node,

            _this,

            attrVal,

            'innerHTML'
          )
        )
      }
    }
  }

  function Watcher(name, el, vm, exp, attr) {
    this.name = name //指令名称,例如文本节点,该值设为"text"

    this.el = el //指令对应的DOM元素

    this.vm = vm //指令所属myVue实例

    this.exp = exp //指令对应的值,本例如"number"

    this.attr = attr //绑定的属性值,本例为"innerHTML"

    this.update()
  }

  Watcher.prototype.update = function () {
    // let exps = this.exp.split('.')
    // let res = this.vm.$data
    // exps.forEach(item => (res = res[item]))
    // console.log(res, 'res')
    this.el[this.attr] = getObjValueByStr(this.vm.$data, this.exp)
  }

  // 通过字符串形式(userInfo.name),将对象中的属性修改为目标值
  // target=userInfo: {name: '12'}, objStr = 'userInfo.name', value = '1'
  function objValueByStr(targetObj, objStr, value) {
    let arr = objStr.split('.')
    let resAttr = targetObj
    for (let j = 0; j < arr.length - 1; j++) {
      resAttr = resAttr[arr[j]]
    }
    resAttr[arr[arr.length - 1]] = value
  }

  // 通过字符串形式('userInfo.name'),获取对象中该属性的值
  function getObjValueByStr(targetObj, objStr) {
    let exps = objStr.split('.')
    let res = targetObj
    exps.forEach(item => (res = res[item]))
    return res
  }

  window.onload = function () {
    var app = new myVue({
      el: '#app',
      data: {
        number: 0,
        userInfo: {
          name: 0
        }
      },
      methods: {
        increment: function () {
          console.log(this)
          console.log()
          this.userInfo.name += 1
          console.log(this.userInfo.name)
          this.number++
        }
      },
      directives: {}
    })
    console.log(app._binding, 'bind')
  }
script>

你可能感兴趣的:(Web前端知识学习,vue)