探索reactive源码底层逻辑——手撕源码

前言

在使用vue时,每当我们需要响应式数据时,总是会用到reactive和ref。如果在面试过程中,面试官问你有关vue中reactive的知识,或者让你手搓一个reactive,这篇文章会对你有所帮助。欢迎交流补充。

reactive

我们都知道,reactive是来帮我们实现响应式 的,但是只能将引用类型代理成响应式。因为js提供的proxy方法只接收引用类型 ,而vue是基于 JavaScript 的 Object.defineProperty 方法或者 ES6 的 Proxy 对象来监听数据的变化。

  1. Object.defineProperty

    • 在早期版本的 Vue 中,Vue 使用了 Object.defineProperty 方法来拦截对象的属性,实现数据的响应式。
    • 通过这种方式,当数据发生变化时,Vue 能够检测到并触发相应的视图更新。
  2. ES6 Proxy

    • 在 Vue 3 中,为了解决 Object.defineProperty 的一些限制,比如无法监测数组下标的变化等,Vue 引入了 ES6 的 Proxy 对象作为数据监听的机制。

第一点的版本有点古早,这里我们主要对Proxy展开解释(在es6的后续更新中,又加入了Reflect API)。

Proxy

Proxy用于拦截对目标对象的操作。这意味着你可以在目标对象上进行操作之前或之后,定义自定义的行为。我们来看看阮一峰es6对proxy的说明。来到目录第15条,可以发现,我们能将Proxy理解为一种"代理器",因此我们可以通过代理器来对被拦截下来的对象进行一些操作。

Proxy一共提供了13种拦截操作:

  • get(target, propKey, receiver) :拦截对象属性的读取,比如proxy.fooproxy['foo']
  • set(target, propKey, value, receiver) :拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。
  • has(target, propKey) :拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey) :拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target) :拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey) :拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc) :拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target) :拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target) :拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target) :拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto) :拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args) :拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
  • construct(target, args) :拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)

等会我们主要用到get 和 set。注意到这里有句话

这也就是为什么前面说Proxy只接受原始类型,vue用的就是ES6原生提供的Proxy,而打造之初规定如此。

在后续开发时,又打造了一个 Reflect。也就是目录第16。我们看看Reflect 又是何物。

其他内容大家感兴趣可以自己去看。根据第一点我们可以知道的是,reflect的打造就是把ES6之前的版本里,Object对象里的一些方法放到Reflect中(Object的方法依然可以使用),仅仅只是这样肯定是不够的,不然就没有打造的必要了。后续的一些新的方法也被放到了Reflect中。

Reflect对象一共有 13 个静态方法。

  • Reflect.apply(target, thisArg, args)
  • Reflect.construct(target, args)
  • Reflect.get(target, name, receiver)
  • Reflect.set(target, name, value, receiver)
  • Reflect.defineProperty(target, name, desc)
  • Reflect.deleteProperty(target, name)
  • Reflect.has(target, name)
  • Reflect.ownKeys(target)
  • Reflect.isExtensible(target)
  • Reflect.preventExtensions(target)
  • Reflect.getOwnPropertyDescriptor(target, name)
  • Reflect.getPrototypeOf(target)
  • Reflect.setPrototypeOf(target, prototype)

是不是有点眼熟?这跟Proxy也太像了。

那我们就简单粗暴的将它理解为Object的克隆版,更新!更合理!

effect ------(副作用)函数收集和触发

  1. 依赖收集:当一个 Vue 组件的模板(或者计算属性、侦听器等)读取响应式数据时,Vue 会在内部进行依赖收集。它会将当前正在执行的 Watcher(观察者)实例添加到该数据的依赖列表中。
  2. 触发更新:当响应式数据发生变化时,它会通知依赖列表中的 Watcher 实例,以便它们可以重新计算并更新相关的视图。

函数的收集

在 Vue 中,函数的收集发生在模板编译的过程中,以及计算属性、侦听器的定义中。当 Vue 编译模板时,它会分析模板中的数据引用,以便知道哪些数据属性需要被监听,从而在数据变化时更新视图。

  • 模板编译:Vue 的模板编译器会分析模板中的数据绑定和指令,并在内部生成相应的渲染函数。在这个过程中,它会收集模板中使用的数据属性,并在 Watcher 中建立响应式数据和视图之间的关联。
  • 计算属性和侦听器:在组件中定义的计算属性和侦听器中,当访问响应式数据时,Vue 会自动进行依赖收集。这意味着当计算属性或侦听器中使用的数据发生变化时,它们会被自动重新计算或触发相应的回调函数。

为什么需要函数的收集?

函数的收集是 Vue 响应式系统的核心机制之一,它确保了当数据发生变化时,与之相关的视图能够自动更新。通过收集函数对数据的访问,Vue 可以建立起数据和视图之间的依赖关系,从而实现了响应式的数据绑定。

有了以上这些知识的铺垫,我们正式的开始手撕代码吧!

手撕代码

文件名:reject.js

javascript 复制代码
import { mutableHandlers } from './baseHandlers.js'

// 保存被代理过的对象
export const reactiveMap = new WeakMap() // new Map() // new WeakMap 对内存的回收更加友好


export function reactive(target) { // 将target变成响应式
  return createReactiveObject(target, reactiveMap, mutableHandlers)
}


export function createReactiveObject(target, proxyMap, proxyHandlers) { // 创建响应式的函数
  // 判断target是不是一个引用类型
  if (typeof target !== 'object' || target === null) {  // 不是对象就不给操作
    return target
  }

  // 该对象是否已经被代理过(已经是响应式对象)
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }

  // 执行代理操作(将target处理成响应式)
  const proxy = new Proxy(target, proxyHandlers) // 第二个参数的作用:当target被读取值,设置值,判断值等等操作时会触发的函数

  // 往 proxyMap 增加 proxy, 把已经代理过的对象缓存起来
  proxyMap.set(target, proxy)
  
  return proxy
}

文件名:baseHandlers.js

vbnet 复制代码
import { track, trigger } from './effect.js'

const get = createGetter()
const set = createSetter()

function createGetter() {
  return function get(target, key, receiver) {
    // console.log('target对象被读取值了');
    const res = Reflect.get(target, key, receiver)  // target[key]
    // 这个属性究竟还有哪些地方用到了(副作用函数的收集, computed, watch,....)  依赖收集
    track(target, key)

    return res
  } 
}

function createSetter() {
  return function set(target, key, value, receiver) {
    // console.log('target对象被修改值了', key, value);
    const res = Reflect.set(target, key, value, receiver)  // target[key] = value;
    // 需要记录下来此时是哪一个key的值变更了,再去通知其他依赖该值的函数生效, 更新浏览器的视图(响应式)
    // 触发被修改的属性身上的副作用函数   触发依赖(被修改的key再哪些地方被用到了)发布订阅
    trigger(target, key)

    return res
    
  }
}


export const mutableHandlers = {
  get,
  set,
}
  • tips

    createGetter 函数用于创建获取器。当对象的属性被读取时,它会调用 Reflect.get 方法获取属性的值,并通过 track 函数来跟踪依赖关系,即记录哪些地方使用了这个属性的值。 createSetter 函数用于创建设置器。当对象的属性被修改时,它会调用 Reflect.set 方法设置属性的新值,并通过 trigger 函数来触发副作用,即通知依赖这个属性的地方进行相应的更新。

文件名:effect.js

javascript 复制代码
const targetMap = new WeakMap()
let activeEffect = null  // 得是一个副作用函数

export function effect(fn, options={}) { // 也是watch,computed 的核心逻辑
  const effectFn = () => {
    try {
      activeEffect = effectFn
      return fn()
    } finally {
      activeEffect = null
    }
  }
  if (!options.lazy) {
    effectFn()
  }
  return effectFn
}


// 为某个属性添加 effect
export function track(target, key) {
  // targetMap = {  // 存成这样
  //   target: {
  //     key: [effect1, effect2, effect3,...]
  //   },
  //   target2: {
  //     key: [effect1, effect2, effect3,...]
  //   }
  // }

  let depsMap = targetMap.get(target)
  if (!depsMap) { // 初次读取到值 收集effect
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  let deps = depsMap.get(key)
  if (!deps) { // 该属性还未添加过effect
    deps = new Set()
  }
  if (!deps.has(activeEffect) && activeEffect) {
    // 存入一个effect函数
    deps.add(activeEffect)
  }
  depsMap.set(key, deps)

}

// 触发某个属性 effect
export function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) { // 当前对象中所有的key都没有副作用函数(从来没有被使用过)
    return
  }

  const deps = depsMap.get(key)
  if (!deps) { // 这个属性没有依赖
    return
  }

  deps.forEach(effectFn => {
    effectFn() // 将该属性上的所有的副作用函数全部触发
  });
}

让我们来试试自己写的reactive有没有用。创建一个:text.html 文件

xml 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script type="module">
    import { reactive } from './reactive.js';
    import { effect } from './effect.js'

    const state = reactive({
      name: '喜仔',
      age: 18
    })

    effect(
      () => {
        console.log(`${state.name}今年${state.age}岁了`);
      },
      {lazy: false}
    )

    // console.log(state.name); 
    // console.log(state.age); // 18
    
    setInterval(() => {
      state.age = state.age + 1
    }, 2000)
  </script>
</body>
</html>

我们去控制台看看效果,发现确实实现了!你也快去试试吧!

看一遍不懂很正常,大家可以多多看几遍文档。一起加油吧!!!

相关推荐
小五Five几秒前
TypeScript项目中Axios的封装
开发语言·前端·javascript
小曲程序几秒前
vue3 封装request请求
java·前端·typescript·vue
临枫5411 分钟前
Nuxt3封装网络请求 useFetch & $fetch
前端·javascript·vue.js·typescript
RAY_CHEN.2 分钟前
vue3 pinia 中actions修改状态不生效
vue.js·typescript·npm
酷酷的威朗普2 分钟前
医院绩效考核系统
javascript·css·vue.js·typescript·node.js·echarts·html5
前端每日三省2 分钟前
面试题-TS(八):什么是装饰器(decorators)?如何在 TypeScript 中使用它们?
开发语言·前端·javascript
小刺猬_9853 分钟前
(超详细)数组方法 ——— splice( )
前端·javascript·typescript
渊兮兮4 分钟前
Vue3 + TypeScript +动画,实现动态登陆页面
前端·javascript·css·typescript·动画
鑫宝Code4 分钟前
【TS】TypeScript中的接口(Interface):对象类型的强大工具
前端·javascript·typescript
_Legend_King11 分钟前
vue3 + elementPlus 日期时间选择器禁用未来及过去时间
javascript·vue.js·elementui