手写响应式: 60行代码实现一个迷你响应式系统

701 阅读6分钟

hello, 这里是link, 前半年基本都把时间花在了看 Vue 的源码上, 一直觉得自己对响应式的代码还是比较了解的. 但是前段时间看到有道面试题, 居然要手写一个响应式系统, 一时间无从下手. 查阅了一番资料, 决定把它梳理一下, 根据尤大自己发布的教程, 我们手把手写一个迷你的响应式系统. 你面试的时候按这么写, 如果别人说你写的不专业, 你就说是尤雨溪教的.😋, 看是面试官专业还是尤雨溪专业.

如果你特别了解响应式, 并且希望看尤大的课, 你可以直接看这个迷你响应式. 但是我会在除了原有代码的基础上, 阐述一些我个人觉得尤大没有细讲的地方, 如果你不是很了解响应式, 那我推荐你看完我的文章

image.png

何为响应式?

简单的来说就是我们某个数据发生了改变, 可以使得用到这个数据的其他数据也发生改变. 它意图就是: 帮开发者省去手动改变其他数据的这一过程

let a = 1       // 1
let b = a * 10  // 10

改变 a

  • 正常情况下
a = 2       // 2
b = a * 10  // 20 😿
  • 响应式
a = 2       // 2
b = 20      // 20 b 会根据 a 的值变成新的值, 而不需要我们自己动手去做

贴近Vue

上面这个例子好像不够形象, 我们来看看, 比较贴近vue的用法. 如果你是vue2的用户可能还是很匪夷所思, 这贴近vue吗? 从直观感受来讲, 它更贴近vue3的使用方式. 但是这段代码同样可以解释vue2的响应式系统. 至于为何vue2/3如此相像, 那是因为它们的原理及思想是一样的.

const states = {
    count: 0
}
autorun(() => {
    console.log("count:" + states.count)
})
// log 0
states.count++
// log 1

这段代码做的事情就是, 只要count改变了, 就会自动执行一次 autorun 函数. 这其实就是vue的响应式做的事情. autorun传进去的函数, 它可能是计算属性, 监听属性, 也可以是一个组件. 因为这些实例, 本质上都是一个函数.你可以理解为:

  • 值改变了, 就更新页面

  • 或者改变计算属性

  • 亦或者触发监听属性


好了, 我们现在将上面代码中隐藏的功能拿出来逐一分析一下, 具体需要做哪些的事情, 才能让上面的代码实现

  1. 代码首次执行的时候, 就应该将目标对象的值变成响应式的(数据劫持)

  2. 我们要实现autorun让目标函数能够在恰当的时候被执行(依赖收集)

  3. 我们改变count值的时候, 要能够触发目标函数(派发更新)

我们分模块来实现上述的功能

数据劫持

通过Object.defineProperty()完成, 这个网上都讲烂了, 咱就不提了. 具体看实现吧

一步一步来, 在这个case中我们只考虑object类型, 并且是一个扁平的对象, 如果你了解一些边缘情况的处理(数组, 多层对象等), 推荐你看看vue的源码vue-core-analyse

function isObject(obj) {
    return (
        typeof obj === 'object'
        && !Array.isArray(obj)
        && obj !== null
        && obj !== undefined
    )
}

function observe(obj) {
    if (!isObject(obj)) return
    defineReactivity(obj)
}

好了. 参数的校验就做到这里, 我们现在要批量地将对象内的值通过defineReactivity变成响应式的, 这里定义activeValue是为了赋值后能够通过get方法获取到

function defineReactivity(obj) {
    Object.keys(obj).forEach((key) => {
        let activeValue = obj[key]
        Object.defineProperty(obj, key, {
            get() {
                return activeValue
            },
            set(newValue) {
                activeValue = newValue
            }
        })
    })
}

发布者

数据劫持就完成了, 现在数据被访问以及被设置成新值的过程, 我们都可以操控了. 现在我们需要考虑的时候, 数据变化等过程, 怎样才能找到对应的函数去执行? 简单, 找个地方存起来就好了. 而一般这个实例我们称之为 发布者. 为什么需要使一个实例? 因为它不仅要存函数, 还要用来通知函数执行

class Dep {
    constructor() {
        this.subscriber = new Set()
    }
    // 收集
    depend() {
        this.subscriber.add(xxxx)
    }
    // 通知更新
    notify() {
        this.subscriber.forEach((sub) => sub())
    }
}

我们先不考虑具体怎么收集, 而是在什么时候收集, 在什么时候更新?, 简单, 数据被访问的时候收集, 数据被改变的时候通知更新, 我们来改造一下defineReactivity函数

function defineReactivity(obj) {
    Object.keys(obj).forEach((key) => {
        let activeValue = obj[key]
+       const dep = new Dep()
        Object.defineProperty(obj, key, {
            get() {
                // 数据被访问的时候收集
+               dep.depend()
                return activeValue
            },
            set(newValue) {
                // 数据被改变的时候通知更新
                activeValue = newValue
+               dep.notify()
            }
        })
    })
}

订阅者

Dep中, depend的方法我们还没有完成, 我们现在把注意力集中到autorun函数, 它的参数, 就是我们需要派发更新的对象. 在vue真正的代码中, 实际上 watcher 是一个类的实例, 但是这里为了方便我们理解, 我们简化代码, 用一个函数代替, 因为实际上, watcher实例也是会默认执行一个getter函数, 可以认为就是我们传进去的目标函数.

let activeWatcher = null
function autorun(update) {
    function watcher() {
        activeWatcher = watcher
        update()
        activeWatcher = null
    }
    watcher()
}
  • 这里保存一份全局的 activeWatcher 是用来给 Dep实例 保存用的, 我们修改一下的depend方法
// 收集
depend() {
+   if (activeWatcher) {
+       this.subscriber.add(activeWatcher)
+   }
}

到这里我相信你能理解, 保存一份全局watcher 方便给dep收集, 但是为什么需要在 watcher 执行的时候, 额外存一份activeWatcher而不是直接存watcher函数就好了呢? 好似有点脱裤子放屁🤔.

实际上作为一个迷你的响应式, 这么做确实没什么必要. 但是我们把它当做Vue的真正响应式系统来看的话, 你就明白为什么了. 因为watcher实例肯定不只有一个, 我们数据收集依赖的时候, 要确保收集的是当前被激活的watcher.

全部代码

// 发布者模块
class Dep {
  constructor() {
      this.subscriber = new Set()
  }
  // 收集
  depend() {
      if (activeWatcher) {
          this.subscriber.add(activeWatcher)
      }
  }
  // 通知更新
  notify() {
      this.subscriber.forEach((sub) => sub())
  }
}
// 数据劫持模块
function isObject(obj) {
  return (
      typeof obj === 'object'
      && !Array.isArray(obj)
      && obj !== null
      && obj !== undefined
  )
}

function observe(obj) {
  if (!isObject(obj)) return
  defineReactivity(obj)
}

function defineReactivity(obj) {
  Object.keys(obj).forEach((key) => {
      let activeValue = obj[key]
      const dep = new Dep()
      Object.defineProperty(obj, key, {
          get() {
              // 数据被访问的时候收集
              dep.depend()
              return activeValue
          },
          set(newValue) {
              // 数据被改变的时候通知更新
              activeValue = newValue
              dep.notify()
          }
      })
  })
}
// 订阅者模块
let activeWatcher = null
function autorun(update) {
  function watcher() {
      activeWatcher = watcher
      update()
      activeWatcher = null
  }
  watcher()
}


const states = {
  count: 0
}

observe(states)

autorun(() => {
  console.log("count:" + states.count)
})

// log 0
states.count++
// log 1

感谢😘


如果觉得文章内容对你有帮助: