v-model双向绑定原理

v-model双向数据绑定原理

话不多说直接上代码
我是看了 黑马的彬哥讲解的 超级详细 保姆级别教学 记录下来 方便自己查阅
视频地址:https://www.bilibili.com/video/BV1Dr4y1c7xS?p=24&t=473.1

这是封装的js

class Vue {
  constructor(options) {
    this.$data = options.data
    // 数据劫持
    Observer(this.$data)

    // 属性代理  为了方便用户 直接this.xxx就可操作数据 不用this.$data.xxx
    Object.keys(this.$data).forEach(key => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          return this.$data[key]
        },
        set(newVal) {
          this.$data[key] = newVal
        }
      })
    })

    // 编译模板
    Compile(options.el, this)
  }

}

// 定义一个数据劫持的方法
function Observer(obj) {
  // 这是递归的终止条件
  if (!obj || typeof obj !== 'object') return

  // 创建一个Dep实例
  const dep = new Dep()

  // 通过Object.keys获取该对象的每个键以数组方式存储
  Object.keys(obj).forEach(key => {

    // 当前被循环的key所对应的值  get中不能直接return obj[key]会报错
    let value = obj[key]
    // 再次调用该函数 防止对象中还有对象
    Observer(value)
    // 为当前的key添加getter和setter
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,

      get() {
        // 只要执行了下面这一行代码 那么刚才new的watcher实列
        // 就被放到了dep.sub这个数组中了
        Dep.target && dep.addSub(Dep.target)
        return value
      },

      set(newVal) {
        value = newVal
        // 添加新值的时候递归一次 为其添加setter和getter
        // 这里不能用arguments.callee 因为这是一个新函数
        Observer(value)

        // 通知每个订阅者更新自己的文本
        dep.notify()
      }
    })
  })
}

// 对HTML结构经行模板编译
function Compile(el, vm) {
  // 获取el对应的元素
  vm.$el = document.querySelector(el)
  // 创建文档碎片 防止页面过度重排重绘
  // 将页面的元素都先暂存到fragment中(页面元素会消失,页面元素被放进fragment了)
  const fragment = document.createDocumentFragment()

  // 如果vm.$el有第一个直接子元素就追加
  while (childNode = vm.$el.firstChild) {
    fragment.appendChild(childNode)
  }
  // 
  replace(fragment)

  // 将文档碎片的内容重新加载到页面
  vm.$el.appendChild(fragment)

  // 负责对dom模板进行编译的方法
  function replace(node) {
    // 定义匹配插值的reg
    var regMustache = /\{\{\s*(\S+)\s*\}\}/

    // 判断当前node节点是否是文本节点 是的话经行替换
    // 3 表示文本节点
    if (node.nodeType === 3) {
      // 文本子节点也是一个dom对象,获取其内容用textContent属性
      const text = node.textContent
      // 经行字符串的正则提取与匹配
      // execRes第二项是跟当前名字一样的值 可作为key
      const execRes = regMustache.exec(text)
      console.log(execRes);
      // 匹配成功的操作
      if (execRes) {
        // 拿到vm中当前key的值
        const value = execRes[1].split('.').reduce((acc, k) => acc[k], vm)
        node.textContent = text.replace(regMustache, value)
        // --------------------------
        // 在这个时候创建watcher类的实例
        new Watcher(vm, execRes[1], (newVal) => {
          node.textContent = text.replace(regMustache, newVal)
        })
        // -----------------------------
      }

      // 终止递归的条件
      return
    }
    // 判断当前节点是否是input输入框
    // 值 1 表示元素节点
    if (node.nodeType === 1 && node.nodeName.toUpperCase() === 'INPUT') {
      const attrs = Array.from(node.attributes)
      const findRes = attrs.find(elem => elem.name === 'v-model')
      // console.log(findRes);
      if (findRes) {
        // 获取当前v-model的属性值
        const expStr = findRes.value
        const value = expStr.split('.').reduce((acc, key) => acc[key], vm)
        node.value = value
        // ================
        // 创建订阅者实例
        new Watcher(vm, expStr, (newVal) => {
          node.value = newVal
        })
        // =================

        // 监听文本框的input事件,拿到输入的最新值 把最新值 更新到vm上即可
        node.addEventListener('input', (e) => {
          const keyArr = expStr.split('.')
          // 为了防止 info.a.b 这样的数据  我们只需要获取a 给a[b] 赋值
          // 如果keyArr只要一个值 那么截取的时候不就是空数组了嘛
          // 答: [].reduce(()=>{},obj123) 会返回obj123
          const obj = keyArr.slice(0, keyArr.length - 1).reduce((acc, key) => acc[key], vm)
          obj[keyArr[keyArr.length - 1]] = e.target.value
        })
      }
    }
    // 如果不是文本节点 经行递归处理
    // 虽然childNodes返回的是类数组对象但是 nodeList也有forEach方法
    node.childNodes.forEach(child => replace(child))
  }
}


// 创建信息搜集者、发布者的类
class Dep {
  constructor() {
    this.sub = []
  }

  // 添加订阅者
  addSub(watcher) {
    this.sub.push(watcher)
  }

  // 发布
  notify() {
    this.sub.forEach(watcher => watcher.update())
  }
}

// 创建订阅者类
class Watcher {
  // cb回调函数中 记录着当前Watcher如何更新自己的文本内容
  // 但是,只知道如何更新自己还不行,还必须拿到最新的数据
  // 因此,还需要再new Watcher期间 把vm也传递进来(因为vm中保存着最新的数据)
  // 除此之外 还需要知道 在vm众多数据中 哪一个才是当前所需要的数据
  // 因此 必须在 new Watcher期间 指定watcher对应的数据名字
  constructor(vm, key, cb) {
    this.vm = vm
    this.key = key
    this.cb = cb

    // 下面三行代码 负责把创建的Watcher实例存到subs数组中
    // 创建一个类的自定义属性target 值是当前Watcher实例
    Dep.target = this
    // 获取vm[k]的值 目的是为了触发vm[k]的get方法 
    key.split('.').reduce((acc, k) => acc[k], vm)
    // 最后再值清空
    Dep.target = null
  }

  // 让发布者调用此函数 通知自己进行更新
  update() {
    var newVal = this.key.split('.').reduce((acc, k) => acc[k], this.vm)
    this.cb(newVal)
  }
}

这是HTML片段

<body>
  <div id="app">
    <h1>a的值是:{{a}}</h1>
    <h1>b的值是:{{b}}</h1>
    <h1>info.c的值是:{{info.c}}</h1>
    <div>
      a的值是:
      <input type="text" v-model="a">
    </div>
    <div>
      info.c的值是:
      <input type="text" v-model="info.c">
    </div>
  </div>

  <script src="./vue.js"></script>
  <script>
    var vm = new Vue({
      el: '#app',
      data: {
        a: 'a1',
        b: 'b1',
        info: {
          c: 'c1',
          d: 'd1'
        }
      }
    })

    console.log(vm);
  </script>
</body>
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值