探索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>

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

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

相关推荐
小悟空3 分钟前
[AI 生成] Flink 面试题
大数据·面试·flink
OpenTiny社区3 分钟前
把 SearchBox 塞进项目,搜索转化率怒涨 400%?
前端·vue.js·github
编程猪猪侠32 分钟前
Tailwind CSS 自定义工具类与主题配置指南
前端·css
qhd吴飞36 分钟前
mybatis 差异更新法
java·前端·mybatis
YGY Webgis糕手之路1 小时前
OpenLayers 快速入门(九)Extent 介绍
前端·经验分享·笔记·vue·web
患得患失9491 小时前
【前端】【vueDevTools】使用 vueDevTools 插件并修改默认打开编辑器
前端·编辑器
ReturnTrue8681 小时前
Vue路由状态持久化方案,优雅实现记住表单历史搜索记录!
前端·vue.js
UncleKyrie1 小时前
一个浏览器插件帮你查看Figma设计稿代码图片和转码
前端
遂心_1 小时前
深入解析前后端分离中的 /api 设计:从路由到代理的完整指南
前端·javascript·api
你听得到111 小时前
Flutter - 手搓一个日历组件,集成单日选择、日期范围选择、国际化、农历和节气显示
前端·flutter·架构