关于前端:Vue3-源码系列项目调试和项目架构分析一

5次阅读

共计 6723 个字符,预计需要花费 17 分钟才能阅读完成。

为什么浏览源码

在公司大多数开发人员是在开发和保护一个绝对稳固成熟的零碎,每天搬砖写业务代码,很少有机会在我的项目里捣鼓利用一些新的技术。那么,怎么晋升本人的技术能力呢?浏览优良的开源我的项目是值得举荐的学习形式,特地是在我的项目里常常应用的框架源码,这样既加深本人对框架的了解,知其然而知其所以然,也能学习它优良的代码设计,标准等,学习开源大牛们的思维结晶,排汇他们优良的教训。

vue3 在 vue2 根底上做了很大的改良,重写了响应式零碎、编译原理,设计 composition 组合式 api,应用 TypeScript 等等,之前浏览过 vue2 外围源码,受害良多,也让我更加有能源去浏览钻研 vue3 底层设计带来了哪些变动,从中学习新的技术和思维。

源码学习办法

浏览源码,并不是一上来就啃源码,那样会被绕晕,在宏大的代码量背后被劝退。依照以往的教训。首先是搭建好开发调试环境,让我的项目跑起来,这时候心里就有底了。接着理解我的项目的架构和技术栈,晓得大体的目录构造性能,做的是什么事;最初带着目标和问题去浏览源码:例如

  • 我的项目初始化和挂载流程是什么?
  • 响应式零碎是怎么实现的?
  • nextTick 异步更新策略是怎么实现的?
  • ……

带着这些问题一步一步的去看,一步一步的调试,单点冲破,沿着这条主线,急躁寻找到想要的答案。

搭建源码调试环境

  1. 克隆源码,下载到本地
git clone [email protected]:vuejs/core.git
  1. 装置依赖,应用的是 pnpm
cd core && pnpm install
  1. 打包 vue 文件
pnpm run dev

运行 pnpm run dev 创立 packages/vue/dist 目录,生成两个文件 vue.global.jsvue.global.js.map,是打包后的 vue 文件

  1. 启动本地服务

ctrl + c 完结过程,执行 pnpm run serve

pnpm run serve

下面命令启动一个服务,默认启动一个端口,关上 localhost,显示我的项目的目录构造界面

进入界面事例,点击 packages =》vue =》examples,抉择一个 classic/todomvc,进入 todomvc.html 界面

也能够在 packages/vue/example 目录下新建一个 html 文件,引入 dist/vue.global.js 文件,装置 http 服务插件 VSCode Live Server,在本地启动服务关上 html 文件

  1. 关上浏览器开发者工具

source 面板,快捷键 cmd + p,输出 todomvc,进入 todomvc 文件源码

cmd + f 搜寻 Vue.createApp,大略在 85 行的地位打上断点单步调试,进入 createApp 函数;

查看源码文件的门路构造,右键鼠标,抉择 Reveal in sidebar 在左侧边栏关上

这时能够看到 createApp 函数的真面目,在 /packages/runtime-dom/src/index.ts

  1. 开启源码调试模式

如果想在源码中打上 debuggerconsole 调试,能够在 dev 命令后增加调试参数 --sourcemap

"scripts": {"dev": "node scripts/dev.js --sourcemap",}

rollup 在打包 vue 的时候,依据环境参数配置打包

我的项目架构

开始浏览源码前,先从全局的角度对框架的设计有整体的认知,否则在浏览过程中,容易被细节困住,迷失方向。

我的项目构造

先从目录构造开始剖析,理解每个模块做哪些性能,模块与模块之间是如何划分和关联的。

Vue 3 从代码构造上进行了梳理,采纳 monorepo 单体仓库模式治理我的项目代码,应用 pnpm workspace 形式实现。它将外部实现的局部形象成了一个个模块放在 packages 下,每个 packages 子目录都有本人的类型申明、单元测试。package 能够独立公布,这样设计便于保护、发版和浏览。例如咱们能够独自援用 reactivity 这个模块,在导入这些软件包时,须要 @vue/ 前缀。

// 目录构造剖析
├── .github              // github 工作流,issue 模版,代码奉献指南
├── .vscode              // vscode 编辑器的配置
├── packages             // vue 源码外围包,应用 pnpm workspace 工作区治理
│   ├── compiler-core    // 编译器(平台无关),例如根底的 baseCompile 编译模版文件, baseParse 生成 AST
│   ├── compiler-dom     // 基于 compiler-core,专为浏览器的编译模块,能够看到它基于 baseCompile,baseParse,重写了 complie、parse
│   ├── compiler-sfc     // 编译 vue 单文件组件
│   ├── compiler-ssr     // 服务端渲染编译
│   ├── reactivity       // vue 独立的响应式模块,能够与任何框架配合, 应用 proxy
│   ├── reactivity-transform  // 响应式试验性能,目前仅用于测试
│   ├── runtime-core     // 与平台无关的运行时。有虚构 DOM 渲染器,vue 组件和各种 API。可针对某个具体平台实现高阶 runtime,比方自定义渲染器
│   ├── runtime-dom      // 针对浏览器的 runtime。蕴含解决原生 DOM API 
│   ├── runtime-test     // 一个专门为了测试而写的轻量级 runtime。因为这个 rumtime「渲染」出的 DOM 树其实是一个 JS 对象,所以这个 runtime 能够用在所有 JS 环境里。你能够用它来测试渲染是否正确。│   ├── server-renderer     // 服务端渲染
│   ├── sfc-playground
│   ├── shared             // 外部工具库, 不裸露 API
│   ├── size-check          // 简略利用,用来测试代码体积
│   ├── template-explorer  // 用于调试编译器输入的开发工具
│   └── vue                 // 面向公众的残缺版本, 蕴含运行时和编译器
│   └── vue-compat          // 用于兼容 vue2
│   ├── global.d.ts      // 申明文件
├── scripts              // vue3 脚本文件,蕴含配置文件,进行编译和打包等
│   ├── bootstrap.js
│   ├── build.js
│   ├── checkYarn.js
│   ├── dev.js
│   ├── release.js
│   ├── setupJestEnv.ts
│   ├── utils.js
│   └── verifyCommit.js
├── test-dts             // 测试文件
│   ├── README.md
│   ├── component.test-d.ts
│   ├── componentTypeExtensions.test-d.tsx
│   ├── defineComponent.test-d.tsx
│   ├── functionalComponent.test-d.tsx
│   ├── h.test-d.ts
│   ├── index.d.ts
│   ├── inject.test-d.ts
│   ├── reactivity.test-d.ts
│   ├── ref.test-d.ts
│   ├── setupHelpers.test-d.ts
│   ├── tsconfig.build.json
│   ├── tsconfig.json
│   ├── tsx.test-d.tsx
│   └── watch.test-d.ts
├── CHANGELOG.md    // 多个版本提交记录、工夫和内容
├── LICENSE         // MIT 协定是所有开源许可中最宽松的一个,除了必须蕴含许可申明外,再无任何限度。├── README.md       // 我的项目阐明
├── api-extractor.json   // 这是所有包的共享根本配置文件
├── jest.config.js       // 测试配置文件
├── package.json         // 我的项目依赖
├── rollup.config.js     // rollup 打包配置文件
├── tsconfig.json        // 定了用来编译这个我的项目的根文件和编译选项
├── pnpm-lock.yaml       // 锁定依赖版本
└── pnpm-workspace.yaml  // pnpm 工作区 
  • compiler-core: 编译器(平台无关),例如根底的 baseCompile 编译模版文件, baseParse 生成 AST
  • compiler-dom: 基于 compiler-core,专为浏览器的编译模块,能够看到它基于 baseCompilebaseParse,重写了 complie、parse
  • compiler-sfc: 编译 vue 单文件组件
  • compiler-ssr: 服务端渲染相干的
  • reactivity: vue 独立的响应式模块
  • runtime-core: 也是与平台无关的根底模块,有 vue 的各类 API,虚构 dom 的渲染器
  • runtime-dom: 针对浏览器的 runtime。蕴含解决原生 DOM API
  • runtime-test:一个专门为了测试而写的轻量级 runtime。因为这个 rumtime「渲染」出的 DOM 树其实是一个 JS 对象,所以这个 runtime 能够用在所有 JS 环境里。你能够用它来测试渲染是否正确。
  • shared:外部工具库, 不裸露 API
  • size-check:简略利用,用来测试代码体积
  • template-explorer:用于调试编译器输入的开发工具
  • vue:面向公众的残缺版本, 蕴含运行时和编译器
  • api-extractor.json —— 所有包共享的配置文件。当咱们 src 下有多个文件时,打包后会生成多个申明文件。应用 @microsoft/api-extractor 这个库是为了把所有的 .d.ts 合成一个,并且,还是能够依据写的正文主动生成文档。
  • template-explorer: 用于调试编译器输入的开发工具。您能够运行 npm run dev dev template-explorer 并关上它 index.html 以获取基于以后源代码的模板编译的正本。在线编译网址:vue-next-template-explorer.netlify.app/#

Vue 源码入口

首先浏览 官网 理解性能应用,Vue3 实例化利用不在采纳 new 形式,而是应用 createApp

import {createApp} from 'vue'

createApp({data() {
    return {count: 0}
  }
}).mount('#app')

从下面调试在 createApp 打上 debug,进入这个函数,函数地位在文件 packages/runtime-dom/src/index.ts

export const createApp = ((...args) => {const app = ensureRenderer().createApp(...args)

  if (__DEV__) {injectNativeTagCheck(app)
    injectCompilerOptionsCheck(app)
  }

  const {mount} = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {const container = normalizeContainer(containerOrSelector)
    if (!container) return

    const component = app._component
    if (!isFunction(component) && !component.render && !component.template) {if (__COMPAT__ && __DEV__) {for (let i = 0; i < container.attributes.length; i++) {const attr = container.attributes[i]
          if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
            compatUtils.warnDeprecation(
              DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
              null
            )
            break
          }
        }
      }
    }

    // clear content before mounting
    container.innerHTML = ''
    const proxy = mount(container, false, container instanceof SVGElement)
    if (container instanceof Element) {container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }

  return app
}) as CreateAppFunction<Element>

下面代码 createApp 创立利用时调用的办法实现流程

  • ensureRenderer 是一个单例模式的函数,会返回一个 renderer,如果无 renderer 则会调用 createRenderer 进行获取 renderer,取得了一个 app 实例;
  • dev 环境下注册一个办法:isNativeTag,挂载到 app.config 上面;
  • 获取到实例的 mount 办法,并保留下来;
  • 重写实例的 mount 办法;

    • 调用 normalizeContainer 获取根元素容器;
    • 判断 template,获取须要渲染的模板;
    • 把容器的 innerHTML 置空;
    • 调用下面实例的 mount 办法;
    • 删除 v-cloak 属性,增加 data-v-app 属性;
  • 返回 mount 后的代理;

从下面来看,createApp 次要做的事是调用 `ensureRenderer().createApp(…args)
创立一个 app 实例,而后重写 mount` 办法挂载,返回这个实例,整个实例化和挂载的流程很清晰,细节在前面深入研究剖析。

其次,当代码逻辑比较复杂难读懂的时候,能够从测试用例动手。比方某个办法应用在官网上形容不是很清晰,想晓得的更多些,比起间接深刻到源码中,测试用例是一个绝对快捷省时的形式。

测试文件后缀是 spec,例如 createApp 测试文件 createApp.spec.ts,有两条测试用例

  • 能够挂载到 svg 元素上
  • 不应该扭转原来的根组件选项对象

而且也给出对应修复的 issue,还能够去到 issue 上理解这个性能的背景

describe('createApp for dom', () => {
  // #2926
  test('mount to SVG container', () => {const root = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
    createApp({render() {return h('g')
      }
    }).mount(root)
    expect(root.children.length).toBe(1)
    expect(root.children[0] instanceof SVGElement).toBe(true)
  })

  // #4398
  test('should not mutate original root component options object', () => {
    const originalObj = {data() {
        return {counter: 0}
      }
    }

    const handler = jest.fn(msg => {expect(msg).toMatch(`Component is missing template or render function`)
    })

    const Root = {...originalObj}

    const app = createApp(Root)
    app.config.warnHandler = handler
    app.mount(document.createElement('div'))

    // ensure mount is based on a copy of Root object rather than Root object itself
    expect(app._component).not.toBe(Root)

    // ensure no mutation happened to Root object
    expect(originalObj).toMatchObject(Root)
  })
})

总结思考

浏览源码,抉择一个适合的工具让代码先跑起来,可能在源码上调试,而后理分明代码组织关系及用处,单点冲破,带着问题和目标去浏览,利用好单元测试。同时也要权衡利弊,该跳就跳,长期处于蒙的状态很容易走进死胡同,能够标记回头再看;利用搜索引擎,联合网上的源码剖析材料了解。

正文完
 0