我的源码学习之路(16)--- vue3(3)

214 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 11 天,点击查看活动详情

前言

不甘于平庸又不努力

2023继续!!!


vue3源码走起

首先 git clone https://github.com/vuejs/vue-next.git,然后安装依赖npm run dev

Vue3的核心源码在packages这里,基于RollUp构建,且V3整体源码采用的是 Monorepo 规范.每个子模块都可以独立编译和打包的从而独立对外提供服务

image.png

├── packages              
│   ├── compiler-core    // 核心编译器(平台无关)
│   ├── compiler-dom     // dom编译器
│   ├── compiler-sfc     // vue单文件编译器
│   ├── compiler-ssr     // 服务端渲染编译
│   ├── global.d.ts      // typescript声明文件
│   ├── reactivity       // 响应式模块,可以与任何框架配合使用【很重要】
│   ├── runtime-core     // 运行时核心实例相关代码(平台无关)
│   ├── runtime-dom      // 运行时dom 关api,属性,事件处理
│   ├── runtime-test     // 运行时测试相关代码
│   ├── server-renderer   // 服务端渲染
│   ├── sfc-playground    // 单文件组件在线调试器
│   ├── shared             // 内部工具库,不对外暴露API
│   ├── size-check          // 简单应用,用来测试代码体积
│   ├── template-explorer  // 用于调试编译器输出的开发工具
│   └── vue                 // 面向公众的完整版本, 包含运行时和编译器

image.png

入口文件[packages/vue/src/index.ts]

const compileCache: Record<string, RenderFunction> = Object.create(null)

function compileToFunction(
  template: string | HTMLElement,
  options?: CompilerOptions
): RenderFunction {
  if (!isString(template)) { // 如果模板不是字符串
    if (template.nodeType) { // 判断是否为dom的节点
      template = template.innerHTML // 取innerHtml
    } else {
      __DEV__ && warn(`invalid template option: `, template)
      return NOOP
    }
  }

  const key = template
  const cached = compileCache[key]
  if (cached) { // 如果有缓存函数,返回缓存
    return cached
  }

  if (template[0] === '#') { // 如果模板以#开头,表明要找对应的id元素
    const el = document.querySelector(template)
    if (__DEV__ && !el) {
      warn(`Template element not found or is empty: ${template}`)
    }
    template = el ? el.innerHTML : ``
  }

  const opts = extend(
    {
      hoistStatic: true,
      onError: __DEV__ ? onError : undefined,
      onWarn: __DEV__ ? e => onError(e, true) : NOOP
    } as CompilerOptions,
    options
  )


  const { code } = compile(template, opts) // 返回code,下面着重讲解下

  function onError(err: CompilerError, asWarning = false) {
   ...
  }

  const render = (
    __GLOBAL__ ? new Function(code)() : new Function('Vue', code)(runtimeDom) //将code作为参数构建匿名函数并调用,返回结果为渲染函数
  ) as RenderFunction

  // mark the function as runtime compiled
  ;(render as InternalRenderFunction)._rc = true // 将函数标记为运行时编译

  return (compileCache[key] = render) // 返回渲染函数并缓存 ,与31行相对应
}

registerRuntimeCompiler(compileToFunction) // 将编译函数注册到运行时
// 导出
export { compileToFunction as compile }
export * from '@vue/runtime-dom'

AST

V2中也有这一块,但是写到这时候我已经忘了。再学一遍吧!

定义:抽象语法树,是对源代码的结构抽象。因此我们对该树进行语法分析,通过变换该抽象结构,而不改变原来的语义,达到优化的目的。AST在线解析器

Eg:

function add(a, b) { return a+b }

image.png

image.png

那V3中生成ast的位置就是:compile(template, opts)这个函数中返回了一个baseCompile()[packages/compiler-core/src/compile.ts]的函数,这个函数中利用baseParse生成ast,接着transform对ast进行变换,利用generate根据变换后的ast生成code 并返回

compile方法

export function baseCompile(
  template: string | RootNode,
  options: CompilerOptions = {}
): CodegenResult {
  const onError = options.onError || defaultOnError
  const isModuleMode = options.mode === 'module'

  const ast = isString(template) ? baseParse(template, options) : template // 生成ast.最重要的方法
  const [nodeTransforms, directiveTransforms] =
    getBaseTransformPreset(prefixIdentifiers) //根据前缀标识,获取预设转换函数
...
    // 对ast进行变换
  transform(
    ast,
    extend({}, options, {
      prefixIdentifiers,
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []) // user transforms
      ],
      directiveTransforms: extend(
        {},
        directiveTransforms,
        options.directiveTransforms || {} // user transforms
      )
    })
  )
// 根据ast生成vue入口需要的编译代码code
  return generate(
    ast,
    extend({}, options, {
      prefixIdentifiers
    })
  )
}

v3在这个过程中的新特性

  1. transform()[packages/compiler-core/src/transform.ts]方法中出现了一段代码

image.png

  1. parseTag()[packages/compiler-core/src/parse.ts]方法中v-if v-for

image.png

运行入口creatApp

从入口文件中可以看到调用方法来看runtime-dom,


export const createApp = ((...args) => {
//获取匿名单例的createApp函数,其中匿名单例可以返回render、hydrate、createApp三种渲染函数
  const app = ensureRenderer().createApp(...args)

  const { mount } = app // 对原有的mount方法进行包装
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    const container = normalizeContainer(containerOrSelector)
    if (!container) return

    const component = app._component
  ...

    // clear content before mounting 挂载前清空内容
    container.innerHTML = ''
    const proxy = mount(container, false, container instanceof SVGElement) 挂载并获得代理对象
    if (container instanceof Element) {
      container.removeAttribute('v-cloak') 清除v-cloak
      container.setAttribute('data-v-app', '') 容器增加data-v-app属性
    }
    return proxy
  }

  return app
}) as CreateAppFunction<Element>

看到这我发现V3的代码梳理起来有点难得样子。我现在歇一歇 准备整体看完一遍再来写

后记

本文仅作为自己一个阅读记录,具体还是要看大佬们的文章

下一篇:我的源码学习之路(16)--- vue3(3)