likes
comments
collection
share

vue3源码学习指南(一) - 如何调试源码

作者站长头像
站长
· 阅读数 7

前言

迫于就业压力,从事多年前端的我开始学起了vue源码。

初看vue源码的我理所当然般的无从下手,看了下论坛相关文章大部分是让打断点,这未尝不是一个好办法,但是打断点调试没法记录相关代码,这就意味着如果我不去记录笔记,那么记性如鱼的我等于没有调试过!显然这是我不能接受的。

所以有什么办法能让我一边调试,一边记录下相关代码呢?

本文将介绍我学习查看源码的野路子,顺便也会超级详细的介绍下怎么打断点看源码。

思路

先把源码下载下来。

下载源码

1. 直接下载源码

点击 源码官网地址 然后全选复制,最后新建一个vue.global.js文件粘贴进去。vue 源码 get!

ps:正经看源码应该是去git仓库里面clone源码,然后自己打包出vue.global.js再去操作源码,但是这样好麻烦,所以嫌麻烦先从这个看起吧。(下面补充了如何自己打包源码的方法)

2. 自己打包源码

先从 git 上将源码仓库 clone 下来

git clone https://github.com/vuejs/core.git

执行打包命令:

cd core
npm install -g pnpm
pnpm install
pnpm run dev

执行完毕后会发现在 core ---> packages ---> vue 文件夹下多了个 dist 文件夹,dist里面就有 vue.global.js 源码文件。

查看源码

新建一个 test.html 文件,先写上最简单的vue代码:

<div id="app"></div>
<script src="../vue.global.js"></script>
<script>
const app = Vue.createApp({
  template: '<h1>Hello Vue 3</h1>'
}).mount('#app')
</script>

运行程序,成功加载: vue3源码学习指南(一) - 如何调试源码

然后问题就来了,我想知道 createApp 到底干了些什么事,为什么能将 Hello Vue3 渲染到页面,我要如何下手去看源码呢?

既然下载了源码,当然是直接去源码里看!

在 vue.global.js 中搜索找到 createApp 的位置

vue3源码学习指南(一) - 如何调试源码

很好,还是看不懂,遇事不决问gpt,让gpt帮忙加上注释

vue3源码学习指南(一) - 如何调试源码

  const createApp = (...args) => {
    // 创建应用实例,使用确保渲染器的 createApp 方法并传递参数
    const app = ensureRenderer().createApp(...args);
    {
      // 注入原生标签检查
      injectNativeTagCheck(app);
      // 注入编译器选项检查
      injectCompilerOptionsCheck(app);
    }
    // 获取 app 的 mount 方法
    const { mount } = app;
    // 重写 app 的 mount 方法
    app.mount = (containerOrSelector) => {
      // 标准化容器或选择器
      const container = normalizeContainer(containerOrSelector);
      if (!container) return;
      // 获取应用的根组件
      const component = app._component;
      // 如果根组件没有渲染函数和模板,并且不是函数组件,则将容器的内部 HTML 作为模板
      if (!isFunction(component) && !component.render && !component.template) {
        component.template = container.innerHTML;
      }
      // 清空容器的内部 HTML
      container.innerHTML = "";
      // 调用原始的 mount 方法进行挂载
      const proxy = mount(container, false, resolveRootNamespace(container));
      // 如果容器是一个元素实例,移�� v-cloak 属性并设置 data-v-app 属性
      if (container instanceof Element) {
        container.removeAttribute("v-cloak");
        container.setAttribute("data-v-app", "");
      }
      // 返回代理对象
      return proxy;
    };
    // 返回应用实例
    return app;
  };

代码瞬间清晰多了,但是gpt这玩意老爱糊人,我怎么知道他说的对不对呢?所以自己调试测试是必不可少的,那么怎么调试源码呢?先介绍传统的打断点调试法。

1. 打断点调试源码

1.1 debugger

打断点的方式很简单,直接在想要加断点的地方加上 debugger 即可:

在test.html最开始处加上debugger

vue3源码学习指南(一) - 如何调试源码

运行html文件 ---> 打开F12 ---> F5刷新网页: 成功进入断点界面:

vue3源码学习指南(一) - 如何调试源码

右上角的6个箭头可以控制代码的运行和中断

vue3源码学习指南(一) - 如何调试源码

作用分别是:

  • Resume script execution (继续执行脚本)

    • 图标:绿色的播放按钮(▶️)
    • 功能:继续执行代码,直到遇到下一个断点或脚本结束。如果代码在断点处暂停,点击这个按钮将继续执行代码。
  • Step over next function call (跳过下一函数调用)

    • 图标:曲折的箭头(⤵️)
    • 功能:执行当前代码行,但如果遇到函数调用,直接跳过函数内部的执行,只是跳到下一行代码。用于快速跳过不关心的函数内部细节。
  • Step into next function call (进入下一函数调用)

    • 图标:向下的箭头(⬇️)
    • 功能:执行当前代码行,并且如果遇到函数调用,进入到函数内部,逐步执行函数内部的代码。用于详细检查函数内部的执行流程。
  • Step out of current function (跳出当前函数)

    • 图标:向上的箭头(⬆️)
    • 功能:继续执行代码直到当前函数执行完毕,并返回到调用该函数的代码行。用于快速退出当前函数的调试。
  • Step (单步执行)

    • 图标:向右的箭头(➡️)
    • 功能:逐行执行代码,不论代码是否在函数内部。每点击一次,执行一行代码。用于逐步检查每一行代码的执行情况。
  • Deactivate breakpoints (禁用所有断点)

    • 图标:一个竖线斜杠的圆圈(⭕)
    • 功能:暂时禁用所有设置的断点,继续执行代码而不会在断点处暂停。再次点击可以重新启用断点。

vue3源码学习指南(一) - 如何调试源码

如上所示当鼠标滑到变量上时,会显示变量当前的值,我们可以根据值来判断代码操作了什么。

1.2 定位问题

回到之前的问题,我想知道 Hello Vue 3 是在何处渲染到页面的,根据gpt的注释,大概率是在这里渲染的:

vue3源码学习指南(一) - 如何调试源码 所以可以直接在 vue.globl.js 文件中的此处上面加上 debugger:

vue3源码学习指南(一) - 如何调试源码 再运行 test.html 代码

vue3源码学习指南(一) - 如何调试源码

成功定位到此处,然后我们的目的是看 container.innerHTML 的值有没有改变,但是显然的 container 变量过于庞大,要找到 container.innerHTML 可不容易,因此我们可以在右侧的 watch 加上想要观察的变量方便查看值的变化:

vue3源码学习指南(一) - 如何调试源码

vue3源码学习指南(一) - 如何调试源码

从上图可知,当程序运行完 const proxy = mount(container, false, resolveRootNamespace(container)); 后,container.innerHTML 的值改为了 Hello Vue3所以接下来要调试mount代码。

vue3源码学习指南(一) - 如何调试源码

vue.global.js 中有很多 mount 代码,为了快速定位可以按照上图所示,点击进去再复制相关代码查找

vue3源码学习指南(一) - 如何调试源码

再次询问gpt得知大概作用:

      mount(rootContainer, isHydrate, namespace) {
        // 检查应用是否已经挂载
        if (!isMounted) {
          // 检查容器是否已经有其他 Vue 应用实例挂载
          if (rootContainer.__vue_app__) {
            warn$1(
              `There is already an app instance mounted on the host container.
      If you want to mount another app on the same host container, you need to unmount the previous app by calling \`app.unmount()\` first.`
            );
          }
          // 创建根组件的虚拟节点(VNode)
          const vnode = createVNode(rootComponent, rootProps);
          // 将应用上下文(context)附加到 VNode 上
          vnode.appContext = context;
          // 根据命名空间的值进行处理
          if (namespace === true) {
            namespace = "svg";
          } else if (namespace === false) {
            namespace = void 0;
          }
          // 开发模式下增加重新加载功能
          {
            context.reload = () => {
              render(cloneVNode(vnode), rootContainer, namespace);
            };
          }
          // 根据是否是 Hydrate 模式进行不同的渲染
          if (isHydrate && hydrate) {
            hydrate(vnode, rootContainer);
          } else {
            render(vnode, rootContainer, namespace);
          }
          // 标记应用已经挂载
          isMounted = true;
          // 将根容器存储在应用实例上
          app._container = rootContainer;
          // 在容器上标记当前应用实例
          rootContainer.__vue_app__ = app;
          // 开发模式下的额外处理
          {
            app._instance = vnode.component;
            devtoolsInitApp(app, version);
          }
          // 返回组件的公共实例
          return getComponentPublicInstance(vnode.component);
        } else {
          // 如果应用已经挂载,发出警告
          warn$1(
            `App has already been mounted.
      If you want to remount the same app, move your app creation logic into a factory function and create fresh app instances for each mount - e.g. \`const createMyApp = () => createApp(App)\``
          );
        }
      },

看注释觉得可疑的地方有两处,分别打上4个debugger:

vue3源码学习指南(一) - 如何调试源码

vue3源码学习指南(一) - 如何调试源码

发现在调整到第3个debugger的时候文字已经显示出来了,所以应该是在第2个debugger和第3个debugger之间有渲染到浏览器的代码,再进一步缩小debugger范围:

vue3源码学习指南(一) - 如何调试源码

vue3源码学习指南(一) - 如何调试源码

成功定位到是 render(vnode, rootContainer, namespace); 这一句渲染了代码!

再次重复上面的步骤,找到 render 函数 ---> 问gpt大概意思 ---> debugger调试定位

vue3源码学习指南(一) - 如何调试源码 定位到是 patch 函数渲染了代码!

再再次重复操作: 找到 patch 函数 ---> 问gpt大概意思 ---> debugger调试定位

vue3源码学习指南(一) - 如何调试源码

定位到是 processElement 函数渲染了代码!

再再再次重复操作:找到 processElement 函数 ---> 问gpt大概意思 ---> debugger调试定位

vue3源码学习指南(一) - 如何调试源码 定位到 mountElement 函数渲染了代码! 再再再再次重复操作!定位到 hostInsert 函数

vue3源码学习指南(一) - 如何调试源码

vue3源码学习指南(一) - 如何调试源码 这句的意思相信熟悉 html 的同学都能知道,是在DOM节点插入元素的代码,所以最终我们得到了vue渲染出节点的完整链条!!!

虽然过程很艰辛,但是相信在一次次探索查看源码的过程中,源码会填满 gpt 的注释,我们也会越来越熟悉 vue3 源码。

但是!我一开始提到的问题还没解决,我打这么多断点,每次学习的时候虽然在 vue.global.js 里面有注释,但是后续查看的时候不可能每次都从快 20000 行的代码里找吧,要么就是每次学习都像我这样写一篇浪费时间还没人点赞收藏的水文???

既然在 vue.global.js 里改不好复习回顾,那么就在html里面改吧!(都是个人的野路子,下面的不看也行)

2. 在html内修改源码

先粗略讲下修改源码的思路:

  1. 找到对应的源码,比如 createApp,复制过来。
  2. 在 html 中强行赋值覆盖,然后在此进行操作。

先将 vue.global.js 改个名,改为vue.global.my.js,以便后续区分哪个是改过的源码。

然后修改 test.html 文件:

<div id="app"></div>
<script src="../vue.global.my.js"></script>
<script>

Vue.createApp = (...args) => {
  // 创建应用实例,使用确保渲染器的 createApp 方法并传递参数
  const app = ensureRenderer().createApp(...args);
  {
    // 注入原生标签检查
    injectNativeTagCheck(app);
    // 注入编译器选项检查
    injectCompilerOptionsCheck(app);
  }
  // 获取 app 的 mount 方法
  const { mount } = app;
  // 重写 app 的 mount 方法
  app.mount = (containerOrSelector) => {
    // 标准化容器或选择器
    const container = normalizeContainer(containerOrSelector);
    if (!container) return;
    // 获取应用的根组件
    const component = app._component;
    // 如果根组件没有渲染函数和模板,并且不是函数组件,则将容器的内部 HTML 作为模板
    if (!isFunction(component) && !component.render && !component.template) {
      component.template = container.innerHTML;
    }
    debugger;
    // 清空容器的内部 HTML
    container.innerHTML = "";
    // 调用原始的 mount 方法进行挂载
    const proxy = mount(container, false, resolveRootNamespace(container));
    // 如果容器是一个元素实例,移除 v-cloak 属性并设置 data-v-app 属性
    if (container instanceof Element) {
      container.removeAttribute("v-cloak");
      container.setAttribute("data-v-app", "");
    }
    // 返回代理对象
    return proxy;
  };
  // 返回应用实例
  return app;
};

const app = Vue.createApp({
  template: '<h1>Hello Vue 3</h1>'
}).mount('#app')
</script>

代码报错了,提示有方法没定义

vue3源码学习指南(一) - 如何调试源码

经过排查发现, vue.global.js 中声明了一个名为 Vue 的自执行方法,方法的返回值为exports,exports里面传了很多内部方法,但是没有全传,我们可以简单的改下源码,达到能够暴露出所有方法的目的。

vue3源码学习指南(一) - 如何调试源码

vue3源码学习指南(一) - 如何调试源码

vue3源码学习指南(一) - 如何调试源码

vue3源码学习指南(一) - 如何调试源码

主要改动的点有4处,即删除原来的Vue方法,不让他闭包没 exports 的方法,新建一个 Vue 变量等于原来返回的 exports 值。

再次运行 test.html 文件

vue3源码学习指南(一) - 如何调试源码

程序不再报错且在createApp处成功断点!

这样改代码最大的好处是后续复习的时候可以简单直观的看到你改了什么代码,除此之外我们也可以直接在此 console 输出调试,而不是用 debugger 调试。

比如定位在何处渲染时 console 输出看值:

vue3源码学习指南(一) - 如何调试源码

vue3源码学习指南(一) - 如何调试源码

ps: 控制台打印方法,然后直接双击即可进入源码对应的位置。

此法缺点也挺多,因为vue代码很多不是独立存在的,比如 mount 函数 是一个 createAppAPI 的 返回值中的一个属性。

vue3源码学习指南(一) - 如何调试源码 所以如果我们想直接在html中改 mount 函数 得把一整个 createAppAPI 复制过来

<div id="app"></div>
<script src="../vue.global.my.js"></script>
<script>
  function createAppAPI(render, hydrate) {
    return function createApp(rootComponent, rootProps = null) {
      if (!isFunction(rootComponent)) {
        rootComponent = extend({}, rootComponent);
      }
      if (rootProps != null && !isObject(rootProps)) {
        warn$1(`root props passed to app.mount() must be an object.`);
        rootProps = null;
      }
      const context = createAppContext();
      const installedPlugins = /* @__PURE__ */ new WeakSet();
      let isMounted = false;
      const app = (context.app = {
        _uid: uid$1++,
        _component: rootComponent,
        _props: rootProps,
        _container: null,
        _context: context,
        _instance: null,
        version,
        get config() {
          return context.config;
        },
        set config(v) {
          {
            warn$1(
              `app.config cannot be replaced. Modify individual options instead.`
            );
          }
        },
        use(plugin, ...options) {
          if (installedPlugins.has(plugin)) {
            warn$1(`Plugin has already been applied to target app.`);
          } else if (plugin && isFunction(plugin.install)) {
            installedPlugins.add(plugin);
            plugin.install(app, ...options);
          } else if (isFunction(plugin)) {
            installedPlugins.add(plugin);
            plugin(app, ...options);
          } else {
            warn$1(
              `A plugin must either be a function or an object with an "install" function.`
            );
          }
          return app;
        },
        mixin(mixin) {
          {
            if (!context.mixins.includes(mixin)) {
              context.mixins.push(mixin);
            } else {
              warn$1(
                "Mixin has already been applied to target app" +
                (mixin.name ? `: ${mixin.name}` : "")
              );
            }
          }
          return app;
        },
        component(name, component) {
          {
            validateComponentName(name, context.config);
          }
          if (!component) {
            return context.components[name];
          }
          if (context.components[name]) {
            warn$1(
              `Component "${name}" has already been registered in target app.`
            );
          }
          context.components[name] = component;
          return app;
        },
        directive(name, directive) {
          {
            validateDirectiveName(name);
          }
          if (!directive) {
            return context.directives[name];
          }
          if (context.directives[name]) {
            warn$1(
              `Directive "${name}" has already been registered in target app.`
            );
          }
          context.directives[name] = directive;
          return app;
        },
        mount(rootContainer, isHydrate, namespace) {
          debugger;
          // 检查应用是否已经挂载
          if (!isMounted) {
            // 检查容器是否已经有其他 Vue 应用实例挂载
            if (rootContainer.__vue_app__) {
              warn$1(
                `There is already an app instance mounted on the host container.
      If you want to mount another app on the same host container, you need to unmount the previous app by calling \`app.unmount()\` first.`
              );
            }
            // 创建根组件的虚拟节点(VNode)
            const vnode = createVNode(rootComponent, rootProps);
            // 将应用上下文(context)附加到 VNode 上
            vnode.appContext = context;

            // 根据命名空间的值进行处理
            if (namespace === true) {
              namespace = "svg";
            } else if (namespace === false) {
              namespace = void 0;
            }
            // 开发模式下增加重新加载功能
            {
              context.reload = () => {
                render(cloneVNode(vnode), rootContainer, namespace);
              };
            }

            // 根据是否是 Hydrate 模式进行不同的渲染
            if (isHydrate && hydrate) {

              hydrate(vnode, rootContainer);
            } else {

              render(vnode, rootContainer, namespace);
            }

            // 标记应用已经挂载
            isMounted = true;
            // 将根容器存储在应用实例上

            app._container = rootContainer;
            // 在容器上标记当前应用实例
            rootContainer.__vue_app__ = app;
            // 开发模式下的额外处理
            {
              app._instance = vnode.component;
              devtoolsInitApp(app, version);
            }
            // 返回组件的公共实例
            return getComponentPublicInstance(vnode.component);
          } else {
            // 如果应用已经挂载,发出警告
            warn$1(
              `App has already been mounted.
      If you want to remount the same app, move your app creation logic into a factory function and create fresh app instances for each mount - e.g. \`const createMyApp = () => createApp(App)\``
            );
          }
        },
        unmount() {
          if (isMounted) {
            render(null, app._container); {
              app._instance = null;
              devtoolsUnmountApp(app);
            }
            delete app._container.__vue_app__;
          } else {
            warn$1(`Cannot unmount an app that is not mounted.`);
          }
        },
        provide(key, value) {
          if (key in context.provides) {
            warn$1(
              `App already provides property with key "${String(
              key
            )}". It will be overwritten with the new value.`
            );
          }
          context.provides[key] = value;
          return app;
        },
        runWithContext(fn) {
          const lastApp = currentApp;
          currentApp = app;
          try {
            return fn();
          } finally {
            currentApp = lastApp;
          }
        },
      });
      return app;
    };
  }

  Vue.createApp = (...args) => {
    // 创建应用实例,使用确保渲染器的 createApp 方法并传递参数
    const app = ensureRenderer().createApp(...args); {
      // 注入原生标签检查
      injectNativeTagCheck(app);
      // 注入编译器选项检查
      injectCompilerOptionsCheck(app);
    }
    // 获取 app 的 mount 方法
    const {
      mount
    } = app;
    // 重写 app 的 mount 方法
    app.mount = (containerOrSelector) => {
      // 标准化容器或选择器
      const container = normalizeContainer(containerOrSelector);
      if (!container) return;
      // 获取应用的根组件
      const component = app._component;
      // 如果根组件没有渲染函数和模板,并且不是函数组件,则将容器的内部 HTML 作为模板
      if (!isFunction(component) && !component.render && !component.template) {
        component.template = container.innerHTML;
      }
      // 清空容器的内部 HTML
      container.innerHTML = "";
      // 调用原始的 mount 方法进行挂载
      console.log('看看是不是在这渲染的!', container.innerHTML)
      const proxy = mount(container, false, resolveRootNamespace(container));
      console.log('看看是不是在这渲染的2!', container.innerHTML, mount)
      // 如果容器是一个元素实例,移除 v-cloak 属性并设置 data-v-app 属性
      if (container instanceof Element) {
        container.removeAttribute("v-cloak");
        container.setAttribute("data-v-app", "");
      }
      // 返回代理对象
      return proxy;
    };
    // 返回应用实例
    return app;
  };

  const app = Vue.createApp({
    template: '<h1>Hello Vue 3</h1>'
  }).mount('#app')
</script>

修改后的代码如上所示,后声明的 function 会自动把前面声明的 function 覆盖掉,所以可以在这里debugger 或者 console。

可以看出 mount 方法在 html 中修改并不优雅,所以我的建议是所有 vue 暴露出去的方法可以尝试直接在html中修改 (也就是 exports这些代码),其余的就老老实实在源码js中修改即可。

vue3源码学习指南(一) - 如何调试源码

总结

作为一个 vue3 源码的初学者,在动手之前觉得 vue3 源码如同天书,有种完全无从下手的感觉,但是在真正去学去看,发现和其他的 js 库并无太大区别,一步一步来总能搞懂一些。这篇文章只是我学习 vue3 源码的第一步,接下来也一定会坚持学习(^▽^) 努力成为一名不会有面试焦虑的菜鸟前端o(╥﹏╥)o

转载自:https://juejin.cn/post/7392788843097522202
评论
请登录