面试官:什么是双向绑定?原理是什么?

401 阅读1分钟

1.什么是双向绑定

所谓的双向绑定就是数据驱动,数据驱动是vue.js最大的特点,在vue中,用户界面发生的变化,开发者不需要手动的去修改DOM。

比如,我们点击一个button,需要元素的文本做一个“是/否”的切换操作,在传统jQuery中,对于页面修改的流程通常是:对button绑定事件,然后获取文案对应元素的dom对象,最后根据切换来修改dom对象的文本值。

2.Vue实现数据驱动(双向绑定)原理

Vue实现数据双向绑定主要采用数据劫持,配合发布者-订阅者模式我,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应监听回调

当一个普通JavaScript对象传给Vuie实例作为他的data选项时,Vue将遍历它的属性,用Object.defineProperty将它们转为getter/setter。用户看不到getter/setter,但是在内部它们让vue追踪依赖,在属性被访问和修改时通知变化

。

vue的数据双向绑定将MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己Model的数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 => 视图更新;视图交互变化 => 数据model变更 双向绑定的效果

3.模拟vue数据驱动,实现input:v-model双向绑定

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <input type="text" v-model="num">
    {{num}}
  </div>
  <script>
    //发布者
    class Dep {
      constructor() {
        this.subs = []
      }
      //注册订阅
      addSub(sub) {
        this.subs.push(sub)
      }
      //发布通知
      notify() {
        this.subs.map(sub => {
          sub.update()
        })
      }
    }
​
    //订阅者
    class Watcher {
      constructor(vm, node, name) {
        this.name = name
        this.node = node
        this.vm = vm
        Dep.target = this
        this.update()
        Dep.target = null
      }
 
​
      //订阅者接口
      update() {
        this.node.nodeValue = this.vm[this.name]
        this.node.value = this.vm[this.name]
      }
    }
​
    class Vue {
      constructor({el, data}) {
        this.data = data
        observe(this.data, this)
        let ele = document.querySelector(el)
        let dom = nodeToFragement(ele, this)
        ele.appendChild(dom)
      }
    }
​
    const app = new Vue({
      el: '#app',
      data: {
        num: 1
      }
    })
​
    //把vm.data的属性直接挂载到vm上,并对属性值进行代理检测
    function observe(obj, vm) {
      Object.entries(obj).map(([key, val]) => {
        defineReactive(vm, key, val)
      })
    }
​
    //代理检测
    function defineReactive(obj, key, val) {
      let dep = new Dep()
      Object.defineProperty(obj, key, {
        get() {
          console.log('读取数据', val);
          //TODO添加订阅者
          if(Dep.target) {
            dep.addSub(Dep.target)
          }
          return val
        },
        set(newVal) {
          if(newVal === val) {
            return
          }
          val = newVal
          //TODO通知订阅者
          dep.notify()
          console.log('更新数据', val);
        }
      })
    }
​
​
    function nodeToFragement(node, vm) {
      let flag = document.createDocumentFragment()
      let child
      while(child = node.firstChild) {
        compile(child, vm)
        flag.append(child)
      }
      return flag
    }
​
    //处理模板,提取{{插值}} 创建观察者
    function compile(node, vm) {
      let reg = /{{(.*)}}/
      //nodeType === 1 => 元素类型
      if(node.nodeType === 1) {
        let attrs = node.attributes
        for(let i = 0, len = attrs.length; i < len; i++) {
          if(attrs[i].nodeName === 'v-model') {
            let name = attrs[i].nodeValue
            //TODO创建观察者,等待通知
            new Watcher(vm, node, name)
            node.addEventListener('input', e => {
              console.log('123');
              //触发setter,联动发布者发布通知
              vm[name] = e.target.value
            })
            node.removeAttribute('v-model')
          }
        }
      }
​
      //nodeType === 3 text类型
      if(node.nodeType === 3) {
        if(reg.test(node.nodeValue)) {
          let name = RegExp.$1
          name = name.trim()
          //TODO创建观察者
          new Watcher(vm, node, name)
          //触发getter,联动订阅者添加
          node.nodeValue = vm[name]
        }
      }
    }
  </script>
</body>
</html><!-- 
  核心思路在compile里面
   function compile(node, vm) {
      let reg = /{{(.*)}}/
      //nodeType === 1 => 元素类型
      if(node.nodeType === 1) {
        let attrs = node.attributes
        for(let i = 0, len = attrs.lengthl; i < len; i++) {
          if(attrs[i].nodeName === 'v-model') {
            let name = attrs[i].nodeValue
            //TODO创建观察者,等待通知
​
            node.addEventListener('input', e => {
              //触发setter,联动发布者发布通知
              vm[name] = e.target.value
            })
            node.removeAttribute('v-model')
          }
        }
      }
​
  首先检测
  <input type="text" v-model="num">
  提取v-model的值
  
 -->

实现效果

image.png