vue3源码解析 vue源码解析 github

博客 常识 2023-05-14 08:44:19 4 1

vue源码解析,Vue3源码解析 准备工作至项目结构为翻译官方贡献者指南[https:github.comvuejscoreblobmain.githubcontributi...

详情


vue3源码解析 vue源码解析 github

Vue3源码解析

准备工作项目结构为翻译官方内容,若翻译有误,尽情谅解。从入口开始以后内容为笔者阅读源码与有关博客时的心得与理解,因为小编能力有限,不会具体讲解各个指令与vue特性的实现方式。而是主要讲述vue源码的整体流程以及patch算法,若有理解不到位的地方,请联系进行理性探讨。

准备工作

需要Node.js Version 16+和,同时建议下载,ni提供的nr命令可以使npm脚本运行更简单。

$ pnpm i # 下载项目依赖包

使用了以下高阶工具:

  • 作为开发语言
  • 用于打包
  • 用于单元测试
  • 用于代码格式化

脚本

以下所有命令都使用ni包中的nr命令。当然也可以使用npm run,但是需要在命令后面添加额外的参数--,例如nr build runtime --all等价于npm run build -- runtime --all

nr build

bulid脚本可以构建所有公共包(对应包中的package.json没有private: true配置)
可以使用模糊匹配你进行包的构建

# 单独构建runtime-corenr build runtime-core# 构建所有能够匹配"runtime"的包nr build runtime --all

构建格式

默认情况下,每个包将以它的package.json文件下的buildOptions.formats指定的格式构建多版本的发行包。这些格式可以通过-f参数复写,其中支持以下格式:

  • global
  • esm-bundler
  • esm-browser
  • cjs

以下额外的格式只能应用于vue主包:

  • global-runtime
  • esm-bundler-runtime
  • esm-browser-runtime

更多关于格式的细节可以阅读这两个文件进行了解和 //TODO。

例如,使用只使用global格式构建runtime-core:

nr build runtime-core -f global

可以用逗号分隔的列表指定多种格式:

nr build runtime-core -f esm-browser,cjs

生成源映射的构建

使用--sourcemap-s参数可以带源映射构建。
PS:这会导致构建速度变慢。

带类型声明的构建

使用--types-t参数会在构建时生成类型声明

  • 每个包将类型声明集中到一个单独的.d.ts文件中。
  • /temp/.api.md中生成API报告。
  • /temp/.api.json中生成一个API模型json,这个文件可以用来生成导出api的Markdown版本。

nr dev

dev脚本在dev模式下以指定的格式(默认:global)捆绑一个目标包(默认:vue),并监视其变化。

$ nr dev> watching: packages/vue/dist/vue.global.js
  • dev脚本不支持模糊匹配-你必须指定完整的包名,例如nr dev runtime-core
  • dev脚本支持通过-f参数指定构建格式,就像build脚本一样。
  • dev脚本还支持-s参数来生成源映射,但它会使重构变慢。
  • dev脚本支持-i参数来内联所有deps。这在调试默认将deps外部化的esm-bundler构建时非常有用。//TODO

nr dev-compiler

dev-compiler脚本构建、监听和在为文件提供服务,这在调试编译器时非常有用。

nr test

test脚本只是简单地调用jest,所以几乎所有的都可以被使用。

# 运行所有测试用例$ nr test# 运行runtime-core包下的所有测试用例$ nr test runtime-core# 运行指定文件的测试用例$ nr test fileName# 运行指定文件的指定测试用例$ nr test fileName -t 'test name'

默认的test脚本包括--runInBand jest标志,以提高测试的稳定性,特别是CSS转换相关的测试。在测试特定的测试时,也可以直接运行带有标志的npx jest来加速测试(jest默认是并行运行的)。

项目结构

vue3项目是用创建的,它能够在packages目录里关联多个包,在一个项目里管理多个代码库

  • reactivity : 响应式API,例如toRefreactiveEffectcomputedwatch等,可作为与框架无关的包,独立构建。
  • runtime-core : 平台无关的运行时核心代码。包括虚拟dom渲染、组件实现和JavaScript API。可以使用这个包针对特定平台构建高价运行时(即定制渲染器)。
  • runtime-dom : 针对浏览器的运行时。包括对原生DOM API、属性(attributes)、特性(properties)、事件回调的处理。
  • runtime-test : 用于测试的轻量级运行时。可以在任何JavaScript环境使用,因为它最终只会呈现JavaScript对象形式的渲染树,其可以用来断言正确的渲染输出。另外还提供用于序列化树、触发事件和记录更新期间执行的实际节点操作的实用工具。
  • server-renderer : 服务端渲染相关。
  • compiler-core : 平台无关的编译器核心代码。包括编译器可扩展基础以及与所有平台无关的插件。
  • compiler-dom : 添加了针对浏览器的附加插件的编译器。
  • compiler-sfc : 用于编译Vue单文件组件的低阶工具。
  • compiler-ssr : 为服务端提供优化后的渲染函数的编译器。
  • template-explorer : 用于调试编译器输出的开发者工具。运行nr dev template-explorer命令后打开它的index.html文件,获取基于当前源代码的模板的编译结果。也可以使用在线版本
  • shared : 多个包共享的内部工具(特别是运行时包和编译器包所使用的与环境无关的工具)。
  • vue : 用于面向公众的完整构建,其中包含编译器和运行时。

导包

各个包可以直接使用包名导入其他包。
PS:导包时应该使用package.json下所列的包名,大多数情况下需要使用@vue/前缀:

import { h } from '@vue/runtime-core'

主要是通过一下几种方式实现导入前缀:

  • 针对TypeScript,通过tsconfig.jsoncompilerOptions.paths
  • 针对Jest,通过jest.config.jsmoduleNameMapper
  • 针对普通的Node.js,使用进行链接。

包依赖关系

 +---------------------+ | | | @vue/compiler-sfc | | | +-----+--------+------+ | | v v +---------------------+ +----------------------+ | | | | +------------>| @vue/compiler-dom +--->| @vue/compiler-core | | | | | | +----+----+ +---------------------+ +----------------------+ | | | vue | | | +----+----+ +---------------------+ +----------------------+ +-------------------+ | | | | | | | +------------>| @vue/runtime-dom +--->| @vue/runtime-core +--->| @vue/reactivity | | | | | | | +---------------------+ +----------------------+ +-------------------+

在跨包边界导入时遵循的一些规则:

  • 当从另一个包导入时,不要使用直接相对路径。应在源包进行导出并通过包层级进行导入。
  • 编译包不应该导入运行时包,反之亦然。如果需要在编译器端和运行时端共享某些内容,应该将其提取到@vue/shared中。
  • 如果A包有一个非类型导入,或者从另一个B包重新导出一个类型。那么B包应该作为A包package.json的依赖项。这是因为当一个包使用ESM/-bundler/CJS格式构建和类型声明文件会被外部化。所以当从包注册中心使用依赖包时,必须将依赖包实际安装为依赖包。

从入口开始

在源码阅读阶段,将以贴出关键性源码(所有忽略代码将以/* <功能> */方式替换),辅以注释与流程图的方式进行讲解。

阅读任何源码都应从代码的入口开始,让我们将目光落入vue/src/index.ts文件。入口文件相对简单只声明了一个编译缓存compileCachecompileToFunction编译器函数,并只运行了来自runtime-dom的函数registerRuntimeCompiler(compileToFunction)compileToFunction注册为运行时编译器,并最终导出运行时和编译器。在看源码之前,我们先看一张图来理解这个函数体的作用。

// Vue入口文件 packages/vue/src/index.ts//声明编译缓存key为HTML字符串,value为渲染函数const compileCache: Record = Object.create(null)function compileToFunction( template: string | HTMLElement, options?: CompilerOptions): RenderFunction { //如果模板不是字符串,判断是否为dom的node节点,是的话取其innerHTML作为模板 if (!isString(template)) { if (template.nodeType) { template = template.innerHTML } else { /* 错误处理 */ } } //如果有缓存渲染函数,返回缓存 const key = template const cached = compileCache[key] if (cached) { return cached } //如果模板以'#'开头,表明要找对应id元素 if (template[0] === '#') { const el = document.querySelector(template) /* 找不到el,报错 */ //不安全,因为在in-DOM模板中可能执行JS表达式。用户必须确保in-DOM模板可信。 //如果模板来自服务器,那么必须保证模板中不包含任何用户数数据 template = el ? el.innerHTML : `` } const { code } = compile( template, extend( { hoistStatic: true, //是否静态提升 /* 报错处理 */ } as CompilerOptions, options //用户添加的可选项 ) ) /* 报错处理函数 */ //将code作为参数构建匿名函数并调用,返回结果为渲染函数 const render = ( __GLOBAL__ ? new Function(code)() : new Function('Vue', code)(runtimeDom) ) as RenderFunction // 将函数标记为运行时编译 ;(render as InternalRenderFunction)._rc = true //返回渲染函数并缓存 return (compileCache[key] = render)}//将编译函数注册到运行时registerRuntimeCompiler(compileToFunction)//导出编译器函数与运行时export { compileToFunction as compile }export * from '@vue/runtime-dom'

这段代码不难,主要难点是不清楚code变量的值。我们可以打开在中提到的,我们可以看到左边为源码,右边为编译结果,打开console,可以看到抽象语法树。

Hello World
//code变量const code = 'const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { openBlock: _openBlock, createBlock: _createBlock } = _Vue return (_openBlock(), _createBlock("div", null, "Hello World")) } }'//至于render就是执行了code构建的匿名函数return的结果

初探编译-解析

在上一章中,我们不难发现入口文件的核心是调用@vue/compiler-domcompile,在这一章我们将初探编译,看看compile是如何解析源码,生成AST。

AST

在剖析源码之前,我们需要先了解什么是AST。Abstract Syntax Tree,即抽象语法树,是对源代码的结构抽象。因此我们对该树进行语义分析,通过变换该抽象结构,而不改变原来的语义,达到优化的目的等等。在前端领域,如果写一个底层框架,AST是不可或缺的技术之一,比如TypeScript、Webpack、babel、ESlint等等。
比如这样一段JS表达式function add(a, b) { return a + b},我们可以将其拆分成如下的语法树:


解析后的对象,则如下所示:

{ "type": "FunctionDeclaration", "id": { "type": "Identifier", "name": "add", "loc": {/*关于位置的信息*/} }, "params":[ { "type": "Identifier", "name": "a", "loc": {/*关于位置的信息*/} }, { "type": "Identifier", "name": "b", "loc": {/*关于位置的信息*/} } ], "body":{ "type": "BlockStatement", "body": [ { "type": "ReturnStatement", "argument":{ "type": "BinaryExpression", "operator": "+", "left": { "type": "Identifier", "name": "a", "loc": {/*关于位置的信息*/} }, "right": { "type": "Identifier", "name": "b", "loc": {/*关于位置的信息*/} }, "loc": {/*关于位置的信息*/} }, "loc": {/*关于位置的信息*/} } ], "loc": {/*关于位置的信息*/} }, "generator": false, "expression": false, "async": false, "loc": {/*关于位置的信息*/}}

如果我们将该AST结构进行改变,就能将原来的普通函数声明,变换为匿名函数赋值。使用包进行以下操作:

const recast = require("recast");const code =`function add(a, b) {return a +b}`const ast = recast.parse(code);const add = ast.program.body[0]const {variableDeclaration, variableDeclarator, functionExpression} = recast.types.buildersast.program.body[0] = variableDeclaration("const", [ variableDeclarator(add.id, functionExpression( null, add.params, add.body ))]);//将AST对象重新转回可以阅读的代码const output = recast.prettyPrint(ast, { tabWidth: 2 }).codeconsole.log(output)/*const add = function(a, b) { return a + b;};*/

compile

好的,在了解AST之后,让我们将目光重新聚焦到Vue的compile函数。compile只是简单地调用了@vue/compiler-corebaseCompile,传入符合浏览器的CompilerOptions,并将结果返回。

export function compile( template: string, options: CompilerOptions = {}): CodegenResult { return baseCompile( template, // parserOptions包含适用于浏览器的辅助函数,options为用户传入的选项 extend({}, parserOptions, options, { //nodeTransforms列表会对抽象语法树的node节点进行特定变换 nodeTransforms: [ // 忽略