从0到1手写mini-vue3系列——(2)响应式(reactive)API实现

前言

为了深入理解vue3框架实现的原理,我打算抽时间学习一下vue3源码。之前一直想过要学习的,但由于项目忙、静不下心、懒等原因,慢慢也就搁置了。之前也只是简单的了解过一点vue3的核心源码,只是看了看网上的文章,也没有手写过;这一次打算只保留最核心的代码,手写实现一个极简的 mini-vue3

虽然自己在学习vue3源码过程中也是一直在参考网上一些优秀作者的技术文章一点一滴过来的。但我觉得学习一个新事物,只是去看、去了解是不够的!只有自己动手实现一遍,从0到1走过这个过程,且在这个过程中加入自己的思考,这样就算是看别人的文章、案例实现出来的也能学到很多东西。

学会吸取别人精华之处,在理解透彻的基础上自己去总结、复盘,慢慢也就会变成自己的知识。有时候学习也要学会站在巨人的肩膀上前行。

文章如果存在问题,欢迎大家评论区指点、讨论。如果对你有帮助,你的点赞、评论、收藏是我最大的动力。

谨以此篇记录自己的学习过程,同时,也希望能够帮助到其他同学。

mini-vue3源码地址 持续完善中,你的star是对我最大的支持!

专栏系列:
从0到1手写mini-vue3系列——(1)源码工程初始化 – 掘金 (juejin.cn)

遇到的问题

在上一篇中完成了源码工程的初始化。 本篇开始来分析并手写完成vue3核心api ——reactive,但在写代码调试时遇到了问题。

源码打包调试使用时报错xxx of undefined及警告

  • 在使用 rollup 打包后,在html中引入使用时,访问导出的具体方法时报错
    从0到1手写mini-vue3系列——(2)响应式(reactive)API实现

  • 终端报 Unresolved dependencies 警告

从0到1手写mini-vue3系列——(2)响应式(reactive)API实现

报错原因

webpack 不同的是 rollup 并不知道如何寻找路径以外的依赖,比如 node_module 中的依赖。
所以需要借助 @rollup/plugin-node-resolve 插件帮助程序可以在项目依赖中找到对应文件。

安装@rollup/plugin-node-resolve解决

  • @rollup/plugin-node-resolve安装报错
    从0到1手写mini-vue3系列——(2)响应式(reactive)API实现

翻译:
运行此命令会将依赖项添加到工作区根目录中,这可能不是您想要的-如果您真的这么想,请使用-w标志(或–workspaceroot)再次运行此命令,使其明确。如果您不想再看到此警告,可以将忽略工作区根目录检查设置设置为true。

加入-w再次执行即可安装成功
pnpm -w i @rollup/plugin-node-resolve -D

rollup配置文件:

import resolve from '@rollup/plugin-node-resolve'
export default {
  plugins: [
    resolve(),
  ]
}

重新打包后,在 packages/vue/examples 提供的案例中 index.html,引入,查看打印发现一切正常:

从0到1手写mini-vue3系列——(2)响应式(reactive)API实现

reactive分析

const obj = reactive({
        num1: 1,
        num2: 2,
        name: '王五',
        info: {
          age: 20,
        },
      });

在vue3中通过reactive API创建包裹的对象都是具备响应式的;这里有个重要的点 reactive对象属性发生改变时 会造成副作用

这里的副作用也就是 副作用API(effect) ,如果effect内部依赖了reactive则reactive的改变会重新触发effect

这一篇主要看看 reactive 是如何实现的,下篇来研究副作用 effect的实现。

reactive初始化

reactive实际上就是返回了一个 proxy 代理后的对象。仿照vue3源码格式,我们看看它做了些什么?

reactive核心源码:

import { mutableHandlers } from './baseHandlers'

/** 使用 weakMap 缓存 proxy */
const reactiveMap = new WeakMap()


/**
 * @description: reactive 响应式api 入口函数
 * @param {*} target 要代理的目标对象
 * @return {*}
 */
export const reactive = (target) => {
  return createReactiveObject(target, reactiveMap, mutableHandlers)
}


/**
 * @description: 创建处理代理对象
 * 核心是proxy,目的是可以监听到用户get、set操作
 * @param {*} target
 * @param {*} proxyMap
 * @param {*} baseHandlers
 * @return {*}
 */
function createReactiveObject (target, proxyMap, baseHandlers) {
  // 1、get: 如果已经代理过直接返回
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // 2、如果没有被代理,直接创建一个 proxy进行代理
  const proxy = new Proxy(target, baseHandlers)
  // 3、把创建好的 proxy 给存储到全局 reactiveMap
  proxyMap.set(target, proxy)
  return proxy

}

看上面源码发现 reactive API内部其实是 return 了一个叫 createReactiveObject的函数; createReactiveObject函数接收了3个参数:

  • target
    目标对象也就是 reactive 传入的对象
  • proxyMap
    全局变量 WeakMap,用于存储 proxy
  • baseHandlers
    proxy 的拦截器(下面单独说)

该函数主要判断目标对象是否被代理过,如果代理过直接返回;没代理过直接创建 proxy对象返回,并且将该 proxy 存储到全局变量 reactiveMap(WeakMap) 中。

使用 WeakMap 数据结构来缓存 proxy

此时,全局变量 reactiveMap 的key-value对应关系:

  • key-> target(原目标对象)
  • value->proxy(proxy代理后的对象)

使用 WeakMap 进行了一次缓存,这样同一个值再次进行reactive的时候就会读取缓存中的值。

从0到1手写mini-vue3系列——(2)响应式(reactive)API实现

baseHandlers

packages/reactivity/src/baseHandlers.js


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

export const mutableHandlers = {
 get,
 set
}
// ...

上面 createReactiveObject函数中传入的baseHandlers, 其实就是导出了 get\set 两个函数,用于 proxy 中的拦截器。

getter函数

get阶段通过Reflect返回结果,并触发  track(依赖收集)

function createGetter () {
 return function get (target, key, receiver) {
  // console.log('proxy get读取')
  // 正常访问,使用 Reflect 反射对象
  const result = Reflect.get(target,key,receiver)
  //  核心:依赖收集
  track(target, key)
  // 嵌套结构对象父级对象属于读取操作,如子类没有代理,将不会监听到子类对象的变化,
  // 就无法触发effect,所有进行递归处理嵌套对象
  if (isObject(result)) {
   return reactive(result)
  }
  return result
 }
}

setter函数

set阶段通过Reflect完成赋值,并触发  trigger(依赖触发)

function createSetter () {
 return function set (target, key, newValue, receiver) {
  // console.log('proxy set设置')
  const result = Reflect.set(target, key, newValue, receiver)
  // 核心:依赖触发
  trigger(target, key)
  return result
 }
}

总结

reactive 返回使用 proxy 代理后的对象。其中在拦截器 getter、setter中配合 Es6提供的 Reflect(反射)去读取、设置对象的返回值。

reactive 函数核心其实就是proxy + Reflect的基础使用。

下一篇将分析 getter、setter 阶段最重要的 track依赖收集、 trigger依赖触发、以及副作用 effect

原文链接:https://juejin.cn/post/7343530251152621622 作者:尖椒土豆sss

(0)
上一篇 2024年3月8日 上午10:23
下一篇 2024年3月8日 上午10:33

相关推荐

发表回复

登录后才能评论