vue双向绑定

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>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容

  • Vue 双向绑定 MVC模式 以往的MVC模式是单向绑定,即Model绑定到View,当我们用JavaScript...
    wdapp阅读 235评论 0 0
  • 前言 使用vue也好有一段时间了,虽然对其双向绑定原理也有了解个大概,但也没好好探究下其原理实现,所以这次特意花了...
    指尖跳动阅读 7,982评论 0 16
  • 我的github: vue双向绑定原理 MVC模式 以往的MVC模式是单向绑定,即Model绑定到View,当我们...
    KlausXu阅读 44,867评论 7 91
  • 前言 虽然知道vue双向绑定是通过Object.defineProperty方法属性拦截的方式,把 data 对象...
    景元合阅读 1,315评论 0 8
  • 渐变的面目拼图要我怎么拼? 我是疲乏了还是投降了? 不是不允许自己坠落, 我没有滴水不进的保护膜。 就是害怕变得面...
    闷热当乘凉阅读 4,241评论 0 13