vue 响应式原理模拟4 - Dep/Watcher

前面我们已经处理好了数据,并将数据都显示到了页面上,接下来要做的就是实现数据的响应式。 在 Vue 的响应式模式中我们需要用到前几篇说过的观察者模式来实现,观察者模式中就有 Dep(目标)Watcher(观察者)

观察者模式

先回顾一下 Dep 类和 Watcher 类是怎么写的:

Dep 类

js 复制代码
class Dep {
  constructor () {
    // 存储所有的观察者
    this.subs = []
  }
  // 添加观察者
  addSub (sub) {
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  // 发送通知
  notify () {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}

Watcher 类

js 复制代码
class Watcher {
  update () { 
    console.log('update') 
  }
}

在案例中使用观察者模式

我们需要给 data 中的每一个属性创建一个对应的 dep 对象,在视图初始化的时候遍历视图上的元素,将视图上用到 data 属性的地方都创建一个对应的 watcher 对象,并将这个 watcher 添加到对应属性的 dep 对象的 subs 数组中去,这一步叫收集依赖。然后在 data 中的属性发生变化时,调用 dep 对象的 notify 发送通知,调用其中 watcher 对象的 update 方法更新视图。

observer 中收集依赖

  1. data 中的每一个属性创建一个对应的 dep 对象

我们可能自然的想到在将 data 中的数据转换为响应式的时候,就遍历了整个 data 对象,那我们就可以同样在这个遍历中给每个 data 创建对应的 dep 对象:

js 复制代码
defineReactive (obj, key, val) {
    let that = this
    // 负责收集依赖,并发送通知
    + let dep = new Dep()
    this.walk(val)
    ...
  }
  1. watcher 添加到对应属性的 dep 对象的 subs 数组中去

在调用 defineReactive 的时候,视图还没有初始化,watcher 实例还没有被创建,所以我们不能直接在这个方法里调用 dep.addSub 添加观察者。但是我们其实很难把握视图初始化的时间,所以我们可以把 dep.addSub 放在 data 属性的 getter 中,当视图解析到这个属性时,就会调用这个属性的 getter 方法,此时我们再将创建的 watcher 添加到 subs 数组中。

那还有一个问题是我们怎么将 watcher 传递到 getter 方法中,最简单的就是直接在 Dep 类上添加一个 target 静态属性,在获取 data 中的属性触发 getter 方法之前,将 watcher 实例赋值给 Dep.target,然后在 getter 方法之后重置 Dep.targetnull

js 复制代码
defineReactive (obj, key, val) {
    ...
    Object.defineProperty(obj, key, {
      ...
      get () {
        // 收集依赖
        + Dep.target && dep.addSub(Dep.target)
        return val
      },
      ...
    })
  }

同步更新 watcher 类:

js 复制代码
class Watcher {
  constructor (vm, key) {
    Dep.target = this     // 将 watcher 实例赋值给 Dep.target
    vm[key]   // 触发属性的 getter,调用addSub
    Dep.target = null
  }
  // 当数据发生变化的时候更新视图
  update () {
    console.log('update') 
  }
}

observer 中发送通知

  1. data 属性改变时,调用 dep 对象的 notify 发送通知

修改 data 属性时,会触发其 setter,所以我们需要在属性的 set 方法中调用 dep.notify 发送通知:

js 复制代码
defineReactive (obj, key, val) {
    ...
    Object.defineProperty(obj, key, {
      ...
      set (newValue) {
        ...
        that.walk(newValue)
        // 发送通知
        + dep.notify()
      }
    })
  }

compiler 中更新视图

  1. 遍历视图上的元素,将视图上用到 data 属性的地方都创建一个对应的 watcher 对象

以插值表达式为例,当 data 中的 msg 发生改变时,视图上的 {{msg}} 也要发生改变。

这是对于 Dom 的操作,所以写在 compiler 类中最为合适。而处理插值表达式、各种指令所做的 Dom 操作也不同,所以要写在不同的处理方法中。对于插值表达式,我们需要在 compilerText 方法中创建相关属性的 watcher 实例。

在创建 watcher 实例的时候还要传入一个 cb 回调函数,表示数据改变后视图对应要执行的方法。这个 cb 方法还需要接收改变后的值 newValue,以做出响应。

js 复制代码
compileText(node) {
      ...
      if (reg.test(value)) {
      + let key = ''
      node.textContent = value.replace(reg, (match, p1) => {
        + key = p1.trim()
        return this.vm[key]
      })

      // 创建watcher对象,当数据改变更新视图
      + new Watcher(this.vm, key, (newValue) => {
        + node.textContent = newValue
      + })
    }
  }

完善 Watcher 类

为了获取到属性最新的值,我们要通过 vm[key] 获取,所以在构造函数中要存储 vmkey 两个属性。同时在更新视图前,需要判断修改后的 data 属性是否和以前不同,如果是一样的不做更新。 同步更新 watcher 类:

js 复制代码
class Watcher {
  constructor (vm, key, cb) {
    this.vm = vm
    this.key = key
    this.cb = cb

    Dep.target = this
    this.oldValue = vm[key]
    Dep.target = null
  }
  update () {
    let newValue = this.vm[this.key]
    if (this.oldValue === newValue) return
    this.cb(newValue)
  }
}

代码测试

以上已经实现了数据的响应式,接下来我们将 dep 文件和 watcher 文件导入到 html 中,做一个测试。

当我们在控制台修改 vm.msg = 'test' 后发现视图上的对应的插值表达式也做出了相应改变。

但是当我们想要设置 vm.msg = 'Hello Vue' 时,视图却并不改变,这是因为我们在视图初始化的时候存储了 oldValue 的值为 'Hello Vue',在 update 中判断新旧值相等就不会执行视图的更新,这个问题我们后续再处理,暂时不管。

添加指令的 watcher

我们已经完成了插值表达式的响应式,按照这个逻辑再去添加 v-textv-model 指令的响应式很简单,只需要在 textUpdatermodelUpdater 方法里创建 watcher 实例传入不同的回调即可:

js 复制代码
update (node, key, attrName) {
    let updateFn = this[attrName + 'Updater']
    updateFn && updateFn.call(this, node, this.vm[key], key)
}
textUpdater (node, value, key) {
    node.textContent = value
    + new Watcher(this.vm, key, (newValue) => {
      + node.textContent = newValue
    + })
}
modelUpdater (node, value, key) {
    node.value = value
    + new Watcher(this.vm, key, (newValue) => {
      + node.value = newValue
    + })
}

这里可以解释一下为什么在 update 方法中我们不直接调用 updateFn 而是使用 call 来调用,就是为了绑定 this 的值,这样才可以在 textUpdatermodelUpdater 方法里直接访问 this.vm 来获取 vue 实例,当然直接使用 this[attrName + 'Updater'](node, this.vm[key], key) 也可以。

到这里我们的响应式代码就差不多写完了,接下来再处理一些问题就可以了。

相关推荐
余生H17 分钟前
深入理解HTML页面加载解析和渲染过程(一)
前端·html·渲染
吴敬悦1 小时前
领导:按规范提交代码conventionalcommit
前端·程序员·前端工程化
ganlanA1 小时前
uniapp+vue 前端防多次点击表单,防误触多次请求方法。
前端·vue.js·uni-app
卓大胖_1 小时前
Next.js 新手容易犯的错误 _ 性能优化与安全实践(6)
前端·javascript·安全
m0_748246351 小时前
Spring Web MVC:功能端点(Functional Endpoints)
前端·spring·mvc
SomeB1oody1 小时前
【Rust自学】6.4. 简单的控制流-if let
开发语言·前端·rust
云只上1 小时前
前端项目 node_modules依赖报错解决记录
前端·npm·node.js
程序员_三木1 小时前
在 Vue3 项目中安装和配置 Three.js
前端·javascript·vue.js·webgl·three.js
lxw18449125141 小时前
vue 基础学习
前端·vue.js·学习
徐_三岁1 小时前
Vue3 Suspense:处理异步渲染过程
前端·javascript·vue.js