美文网首页
vue双向绑定

vue双向绑定

作者: forever远方 | 来源:发表于2020-12-09 21:15 被阅读0次

Vue-2.+
采用数据劫持结合发布者-订阅者模式的方式,使用Object.defineProperty()方法对象通过 递归+遍历 的方式,重写属性的setter,getter方法来实现对数据的监控的。在数据变动时发布消息给订阅者,触发相应的监听回调。
缺点:

  1. 监听数组的方法不能触发Object.defineProperty方法中的set操作(如果要监听的到话,需要重新编写数组的方法:push、pop、shift、unshift、splice、sort、reverse)。
  2. 必须遍历每个对象的每个属性,如果对象嵌套很深的话,需要使用递归调用。

Vue-3.0
Proxy
const obj = new Proxy(data, handler);
当外界每次对obj进行操作时,就会执行handler对象上的一些方法。handler中常用的对象方法如下:

  1. get(target, propKey, receiver)

  2. set(target, propKey, value, receiver)

  3. has(target, propKey)

  4. construct(target, args):

  5. apply(target, object, args)

  6. 需要实现一个数据监听器 Observer, 能够对所有数据进行监听,如果有数据变动的话,拿到最新的值并通知订阅者Watcher.

  7. 需要实现一个指令解析器Compile,它能够对每个元素的指令进行扫描和解析,根据指令模板替换数据,以及绑定相对应的函数。

  8. 需要实现一个Watcher, 它是链接Observer和Compile的桥梁,它能够订阅并收到每个属性变动的通知,然后会执行指令绑定的相对应
    的回调函数,从而更新视图。

image.png
<!DOCTYPE html>
<html>
<head>
    <title>MyVue</title>
</head>
<body>
<div id="app">
    <input id="input1" type="text" v-model="inputValue" style="margin-top: 50px"/>
    <button id="btn1" type="button" v-click="add">add</button>
    <span id="span1" v-bind="inputValue" style="margin-left: 30px"></span>

    <br/>

    <input type="text" v-model="inputValue2" style="margin-top: 50px"/>
    <span v-bind="inputValue2" style="margin-left: 30px"></span>

    <br/>

    <input type="text" v-model="inputValue3" style="margin-top: 50px"/>
    <span v-bind="inputValue3" style="margin-left: 30px"></span>
    <span style="margin-left: 30px;color:red">{{inputValue3}}</span>

</div>
</body>


<!--<script>-->
<!--  let data = {-->
<!--    inputValue: '',-->
<!--  }-->
<!--  // Vue无法监听到对象属性的添加和删除-->

<!--  // Object.defineProperty(data, 'inputValue', {-->
<!--  //   get() {-->
<!--  //     return data.inputValue-->
<!--  //   },-->
<!--  //   set(newVal) {-->
<!--  //     document.getElementById('span1').innerHTML = newVal-->
<!--  //   }-->
<!--  // })-->
<!--  const handler = {-->
<!--    get: function(target, key) {-->
<!--      console.log('handler=get===>', target, key)-->
<!--      return target[key];-->
<!--    },-->
<!--    set: function(target, key, newVal) {-->
<!--      console.log('handler=set===>', target, key, newVal)-->
<!--      target[key] = newVal;-->
<!--      document.getElementById('span1').innerHTML = newVal-->
<!--    }-->
<!--  };-->
<!--  data = new Proxy(data, handler);-->

<!--  document.getElementById('input1').addEventListener('input', (e)=>{-->
<!--    data.inputValue = e.target.value-->
<!--  })-->
<!--  document.getElementById('btn1').addEventListener('click', (e)=>{-->
<!--    data.inputValue = 'q'-->
<!--  })-->
<!--</script>-->


<script>
    let data = ['1'];

    const handler = {
        set: function(target, key, newVal) {
          const res = Reflect.set(target, key, newVal);
          console.log('handler=set===>', target)
          document.getElementById('span1').innerHTML = JSON.stringify(target)
          return res
        }
    };
    data = new Proxy(data, handler);

    document.getElementById('input1').addEventListener('input', (e)=>{
      console.log('input===>', e.target.value)
      data.push(e.target.value)
    })
</script>


<script>
  class MyVue {
    _binding

    constructor(options) {
      this._init(options)
    }

    _init(options) {
      this.$options = options  // options 为上面使用时传入的结构体,包括el,data,methods
      this.$el = document.querySelector(options.el)  // el是 #app, $el是id为app的Element元素
      this.$data = options.data
      this.$methods = options.methods
      this.$watch = options.watch || {}
      console.log('myVue=_init==>', this)

      // 初始化_binding,保存着model与view的映射关系,也就是我们前面定义的Watcher的实例。当model改变时,我们会触发其中的指令类更新,保证view也能实时更新
      this._binding = {}

      this._observer()
      // this._proxyObserver()
      console.log('myVue=_obsever==>', this)

      // this._compile()
      // console.log('myVue=_compile==>', this)

      const dom = this._nodeToFragment(this.$el)
      this.$el.appendChild(dom)
    }

    // 数据劫持:更新数据
    _observer() {
      const vm = this
      const obj = vm.$data
      const mWatch = vm.$watch
      Object.keys(obj).map(key => {
        let value = obj[key]
        // if (typeof value == 'object') {  //如果值还是对象,则继续遍历
        //   vm._observer(value)
        // }
        // 对对象已有的属性添加数据描述
        Object.defineProperty(vm.$data, key, {
          configurable: true, // 能否使用delete、能否需改属性特性、或能否修改访问器属性,false为不可重新定义,默认值为true
          enumerable: true, // 对象属性是否可通过for-in循环,false为不可循环,默认值为true
          get: () => {  // 重写get
            console.log('myVue=get===>', key, value)
            return value
          },
          set: (newVal) => {   // 重写set
            console.log('myVue=set===>', key, newVal, value)

            if (value !== newVal) {
              if (mWatch[key]) {
                mWatch[key](newVal, value)
              }
              //利用闭包的特性,修改value,get取值时也会变化
              //不能使用obj[key]=newVal
              //因为在set中继续调用set赋值,引起递归调用
              value = newVal
              // obj[key] = newVal

              // 当value更新时,出发_binding中绑定的watcher
              vm._binding[key].map(item => {
                item.update() // 顶用watcher中的update方法更新dom
              })
            }
          }
        })
      })
    }

    _proxyObserver() {
      const vm = this
      const mWatch = vm.$watch

      const handler = {
        get(target, key) {
          console.log('myVue=get===>', target, key, target[key])
          return Reflect.get(target, key)
        },
        set(target, key, newVal) {
          console.log('myVue=set===>', target, key, newVal)
          if (mWatch[key]) {
            mWatch[key](newVal, target[key])
          }
          // 当value更新时,出发_binding中绑定的watcher
          const res = Reflect.set(target, key, newVal)
          vm._binding[key].map(item => {
            item.update() // 顶用watcher中的update方法更新dom
          })
          return res
        }
      }
      // 把代理器返回的对象代理到this.$data,即this.$data是代理后的对象,外部每次对this.$data进行操作时,实际上执行的是这段代码里handler对象上的方法
      vm.$data = new Proxy(vm.$data, handler)
    }

    // 将view与model进行绑定,解析指令(v-bind,v-model,v-click)等
    _compile() {
      const vm = this
      const el = vm.$el

      let nodes = Array.prototype.slice.call(el.children) // 将伪数组转成数组
      console.log('_compile====>', el.children)
      console.log('_compile1====>', nodes)

      nodes.map(node => {
        console.log('node=====>', node)

        if (node.children && node.children.length > 0) {  // 对所有元素进行遍历,并进行处理
          vm._compile(node)
        }

        // 解析属性
        if (node.hasAttribute('v-click')) {
          // v-click绑定的attrVal为methods里的方法名
          const attrVal = node.getAttribute('v-click')
          node.onclick = vm.$methods[attrVal].bind(vm.$data)  // // bind是使data的作用域与method函数的作用域保持一致
        }

        if (node.hasAttribute('v-model') && node.tagName === 'INPUT') {
          // v-model绑定的为 data里的key
          const attrVal = node.getAttribute('v-model')
          if (!vm._binding[attrVal]) vm._binding[attrVal] = []
          vm._binding[attrVal].push(new Watcher(node, 'value', vm.$data, attrVal))

          node.addEventListener('input', () => {
            vm.$data[attrVal] = node.value
          })
        }

        if (node.hasAttribute('v-bind')) {
          const attrVal = node.getAttribute('v-bind')
          if (!vm._binding[attrVal]) vm._binding[attrVal] = []
          vm._binding[attrVal].push(new Watcher(node, 'innerHTML', vm.$data, attrVal))
        }
      })
    }

    _nodeToFragment(node, flag){
      flag = flag || document.createDocumentFragment()
      let child
      while (child = node.firstChild) {
        this._compile2(child)

        // appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除
        // 所以,第二次循环时,node.firstChild 已经不再是之前的第一个子元素了
        flag.appendChild(child)
        if (child.firstChild) {
          this._nodeToFragment(child, flag)
        }
      }
      return flag
    }

    _compile2(node) {
      console.log('node=====>', node, node.nodeType, node.nodeValue)

      const vm = this

      if (node.nodeType === 1) {
        const attrs = node.attributes

        Object.keys(attrs).map(i => {
          const attr = attrs[i]
          // console.log('node====>', attr)
          if (!attr) return

          if (attr.nodeName === 'v-model' ) {
            const key = attr.nodeValue  // 获取 v-model 绑定的属性名
            node.value = vm.$data[key]

            node.addEventListener('input', (e) => {
              vm.$data[key] = e.target.value
            })

            if (!vm._binding[key]) vm._binding[key] = []
            vm._binding[key].push(new Watcher(node, 'value', vm.$data, key))

            node.removeAttribute('v-model')
          }

          if (attr.nodeName === 'v-bind') {
            const key = attr.nodeValue  // 获取 v-model 绑定的属性名
            node.innerHTML = vm.$data[key]

            console.log('v-bind===>', node, node.innerHTML)

            if (!vm._binding[key]) vm._binding[key] = []
            vm._binding[key].push(new Watcher(node, 'innerHTML', vm.$data, key))

            node.removeAttribute('v-bind')
          }
        })
      } else if (node.nodeType === 3) {
        const reg = /\{\{(.*)\}\}/
        if (reg.test(node.nodeValue)) {
          const key = RegExp.$1.trim() // 获取匹配到的字符串

          if (!vm._binding[key]) vm._binding[key] = []
          vm._binding[key].push(new Watcher(node, 'nodeValue', vm.$data, key))
        } else {
          console.log('qqqqqqqqq====>', node)
        }
      }
    }
  }

  // 订阅者,用于监听更新dom
  class Watcher {
    el
    attr
    data
    key

    constructor(el, attr, data, key) {
      this.el = el
      this.attr = attr
      this.data = data
      this.key = key

      this.update()
    }

    update() {
      this.el[this.attr] = this.data[this.key]
    }
  }

  const myVue = new MyVue({
    el: '#app',
    data: {
      inputValue: '',
      inputValue2: '',
      inputValue3:''
    },
    watch: {
      inputValue(newV, oldV) {
        console.log('watch=inputValue===>', newV, oldV)
      }
    },
    methods: {
      add() {
        console.log('myVue=add===>')
        this.inputValue += '1'
      },
      add2() {
        console.log('myVue=add2===>')
        this.inputValue2 += '2'
      }
    }
  })
</script>


</html>

相关文章

网友评论

      本文标题:vue双向绑定

      本文链接:https://www.haomeiwen.com/subject/fujkgktx.html