Vue2双向绑定,Object.defineProperty、Observe、Compile、Watcher、Dep各显神通,相辅相成

这个问题真的可以说是一个好问题,毕竟基本上面试时也总会被问到,一问双向绑定原理怎么回事儿啊?就说,用了Object.defineProperty()做得数据劫持,劫持了get()和set(),然后巴拉巴拉......

首先,有个Observe(obj) 用来对Vue中data的数据进行劫持(监视)

其次,有个Compile(el,vm) 对HTML进行模板编译(借助编译这个过程进行数据的提取和修改)

然后,有个Watcher类 用于对Compile提取出来的数据进行订阅——订阅者(每个数据对应一个)

最后,有个Dep类 用于收集Watcher订阅者们,目的是有改动时操作所有watcher实例一同更新


其实用代码表述下可以是下面这样:

Vue类

为了后续操作vm.name而不是vm.$data.name,我们做属性代理,这样获取时就会返回data中的对应数据,设置时也会将值给到data的对应属性,只是我们更简单的使用vm就可以操作data的属性

class Vue {
  constructor(options) {
    this.$data = options.data

    // 调用数据劫持的方法
    Observe(this.$data)

    // 属性代理
    Object.keys(this.$data).forEach((key) => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          return this.$data[key]
        },
        set(newValue) {
          this.$data[key] = newValue
        },
      })
    })

    // 调用模板编译的函数
    Compile(options.el, this)
  }
}

Observe 

对数据进行劫持,为了深度劫持所以需要递归

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

  // 通过 Object.keys(obj) 获取到当前 obj 上的每个属性
  Object.keys(obj).forEach((key) => {
    // 当前被循环的 key 所对应的属性值
    let value = obj[key]
    // 把 value 这个子节点,进行递归
    Observe(value)
    // 需要为当前的 key 所对应的属性,添加 getter 和 setter
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 只要执行了下面这一行,那么刚才 new 的 Watcher 实例,
        // 就被放到了 dep.subs 这个数组中了
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      set(newVal) {
        value = newVal
        Observe(value)
        // 通知每一个订阅者更新自己的文本
        dep.notify()
      },
    })
  })
}

Compile中用到了文档碎片,咱得知道,页面元素在页面结构里就不能在文档碎片里,也就是说页面元素不能脚踏两只船,所以当我们把元素加到文档碎片中,页面上就没东西了。

借助文档碎片fragment来暂存页面文档,操作文档碎片对其中引用的数据进行提取更新等操作,避免了直接操作DOM进行数据更新引起多次的重绘,节约浏览器性能。等在文档碎片里都倒饬(数据赋值/更新)好了再追加到body页面中进行渲染。

找到一个双大括号包裹的数据就创建一个Watcher实例,用于进行监听,方便后续Dep批量更新

// 对 HTML 结构进行模板编译的方法
function Compile(el, vm) {
  // 获取 el 对应的 DOM 元素
  vm.$el = document.querySelector(el)

  // 创建文档碎片,提高 DOM 操作的性能
  const fragment = document.createDocumentFragment()

  while ((childNode = vm.$el.firstChild)) {
    fragment.appendChild(childNode)
  }

  // 进行模板编译
  replace(fragment)

  vm.$el.appendChild(fragment)

  // 负责对 DOM 模板进行编译的方法
  function replace(node) {
    // 定义匹配插值表达式的正则
    const regMustache = /\{\{\s*(\S+)\s*\}\}/

    // 证明当前的 node 节点是一个文本子节点,需要进行正则的替换
    if (node.nodeType === 3) {
      // 注意:文本子节点,也是一个 DOM 对象,如果要获取文本子节点的字符串内容,需要调用 textContent 属性获取
      const text = node.textContent
      // 进行字符串的正则匹配与提取
      const execResult = regMustache.exec(text)
      console.log(execResult)
      if (execResult) {
        const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm)
        node.textContent = text.replace(regMustache, value)
        // 在这个时候,创建 Watcher 类的实例
        new Watcher(vm, execResult[1], (newValue) => {
          node.textContent = text.replace(regMustache, newValue)
        })
      }
      // 终止递归的条件
      return
    }

    // 判断当前的 node 节点是否为 input 输入框
    if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
      // 得到当前元素的所有属性节点
      const attrs = Array.from(node.attributes)
      const findResult = attrs.find((x) => x.name === 'v-model')
      if (findResult) {
        // 获取到当前 v-model 属性的值   v-model="name"    v-model="info.a"
        const expStr = findResult.value
        const value = expStr.split('.').reduce((newObj, k) => newObj[k], vm)
        node.value = value

        // 创建 Watcher 的实例
        new Watcher(vm, expStr, (newValue) => {
          node.value = newValue
        })

        // 监听文本框的 input 输入事件,拿到文本框最新的值,把最新的值,更新到 vm 上即可
        // ——当然实际Vue处理的不止text类型的input,还有checkbox、selection这些,这里以text类型的举例
        node.addEventListener('input', (e) => {
          const keyArr = expStr.split('.')
          const obj = keyArr.slice(0, keyArr.length - 1).reduce((newObj, k) => newObj[k], vm)
          const leafKey = keyArr[keyArr.length - 1]
          obj[leafKey] = e.target.value
        })
      }
    }

    // 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
    node.childNodes.forEach((child) => replace(child))
  }
}
// 订阅者的类
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 实例存到 Dep 实例的 subs 数组中 ↓↓↓↓↓↓
    Dep.target = this
    key.split('.').reduce((newObj, k) => newObj[k], vm)
    Dep.target = null
  }

  // watcher 的实例,需要有 update 函数,从而让发布者能够通知我们进行更新!
  update() {
    const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)
    this.cb(value)
  }
}
// 依赖收集的类/收集 watcher 订阅者的类
class Dep {
  constructor() {
    // 今后,所有的 watcher 都要存到这个数组中
    this.subs = []
  }

  // 向 subs 数组中,添加 watcher 的方法
  addSub(watcher) {
    this.subs.push(watcher)
  }

  // 负责通知每个 watcher 的方法
  notify() {
    this.subs.forEach((watcher) => watcher.update())
  }
}

一定注意Watcher构造函数中后三行代码,很有趣,尤其是执行时机的巧妙 :

        构造函数在new Watcher()时执行,而此时将这个Watcher的实例也就是this给到了Dep的target属性来存储。

        下面紧接着来了个获取对应值的过程,但并没有对这个值做接收,为啥?

        因为这里我们借用这个时机,什么时机?获取值的时机,必然触发Observe的get()

        我们在Observe中创建的Dev实例dev,也就得在Observe中来将Watcher实例加到Dev中

        Observe的get()被触发就会执行,此时就可以来调用Dev实例dev的的addSub

        而已经将Watcher给到了Dev的属性target上,所以就借机addSub(Dev.target)收入囊中

        一旦被Dev收入囊中,后续有数据更新,就触发Dev的notify()方法来挨个更新Watcher

        一旦有数据更新就调用Observe的set(),所以notify()在set()中调用那是最好不过了

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值