【vue源码02】Vue项目入口,启程Vue源码😎

5/21/2024 Vue源码学习

上一篇已经找到了vue项目的入口了,也就是packages/vue/src/index.ts,那就赶紧打开看看

# 全局变量

第一个比较在意的点,就是开头的这一行代码:

// packages/vue/src/index.ts
import { initDev } from './dev'

if (__DEV__) {
  initDev()
}

__DEV__是个啥?其实就是个全局变量,是从上一篇中的根目录下dev.js这个文件里面esbuild的配置项里传过来的:

// dev.js
  esbuild
    .context({
      entryPoints: [resolve(__dirname, `../packages/${target}/src/index.ts`)],
      // ...
      define: {
        __COMMIT__: `"dev"`,
        __VERSION__: `"${pkg.version}"`,
        __DEV__: prod ? `false` : `true`,
        __TEST__: `false`,
        // ...
      },
    })
    .then(ctx => ctx.watch())

就是在这个define里面定义了

我们现在是开发环境,__DEV__的值是true,进入看看initDev这个方法干了什么

initDev是从/packages/vue/src/dev.ts这里导入的,不是根目录的dev.js哦

这个方法其实也没干啥,就是一段提示:

// packages/vue/src/dev.ts
import { initCustomFormatter } from '@vue/runtime-dom'

export function initDev() {
  if (__BROWSER__) {
    /* istanbul ignore if */
    if (!__ESM_BUNDLER__) {
      console.info(
        `You are running a development build of Vue.\n` +
          `Make sure to use the production build (*.prod.js) when deploying for production.`,
      )
    }
    initCustomFormatter()
  }
}

回去翻一下BROWSER和ESM_BUNDLER的定义

// dev.js
        __BROWSER__: String(
          format !== 'cjs' && !pkg.buildOptions?.enableNonBrowserBranches,
        ),
        __ESM_BUNDLER__: String(format.includes('esm-bundler')),

BROWSER就是用来标识是不是浏览器环境的,可能后面会用于标识csr还是ssr(客户端渲染和服务端渲染),ESM_BUNDLER是标识是否是esm打包,用webpack或rollup打包的都是esm打包,所以这里会返回true

开发环境下这里就会打印一段提示。

而下面的initCustomFormatter也不是核心的代码,用来处理开发者工具的格式、美化组件实例这些,主要目的是增强 Vue 开发者工具中的对象查看体验,使其更易于理解和调试 Vue 应用中的数据和组件实例。

# 用WeakMap作缓存

接下来有一个这样的缓存容器:

// packages/vue/src/index.ts
const compileCache = new WeakMap<
  CompilerOptions,
  Record<string, RenderFunction>
>()

很明显看出来是用来作缓存的,他是一个WeakMap,跟普通的map相比呢就是对键是弱引用,所以当对象被垃圾回收的时候,这个对象就会被回收掉

想想,如果被缓存的内容实际在项目里已经不使用了(没有其他引用了),那自然我们是想把他释放掉,如果使用Map的话则会一直保持对他的引用,导致垃圾回收机制始终无法清理他,严重会造成内存泄露。而WeakMap则不会,垃圾回收的时候不会管WeakMap有没有应用,其他地方不引用了就直接被释放

当我们不想因为map的引用影响对象的回收时,就可以用weakmap

ts我也不太熟,乘这个机会一起看看吧

WeakMap的键是CompilerOptions类型,他的定义是这样的:

// packages/compiler-core/src/options.ts
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions

看的出来就是一些编译的配置选项了,涉及解析源码的配置、转换源码的配置、输出代码的配置

值是Record类型,看看他的定义:

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

K extends keyof any, T 这部分是定义了Record对象类型的键和值的类型。Record类型属性的键是任意可以作为键的类型,而值则是任意类型

注意我的表述,键是任意可以作为键的类型,也就是为什么这里键要用k extends keyof any,而不是直接any:

对象的键可以是任何字符串类型或数字类型,而不能是数组等类型,用k extends of any可以排除这些不能作为键的类型,确保k只能是有效的对象键类型

那[p in k ]: T 又是什么呢?

这是ts里的一种映射类型语法,就是循环遍历每一个键,然后生成对应类型的定义,执行之后能够生成每一个键对T类型值的映射关系

比如k有字符串、数字类型,最后就会生成这样的类:

type Record<K extends keyof any, T> = {
    String: T,
    Number: T
};

后面紧接着就是各函数的定义了,先看getCache函数

# getCache函数

// packages/vue/src/index.ts
function getCache(options?: CompilerOptions) {
  let c = compileCache.get(options ?? EMPTY_OBJ)
  if (!c) {
    c = Object.create(null) as Record<string, RenderFunction>
    compileCache.set(options ?? EMPTY_OBJ, c)
  }
  return c
}

很简单的逻辑,传入一个对象,里面可能是编译的各种选项,用选项作为Key。试图从缓存中获取一下,如果没有对应的缓存则创建一个空对象,有的话就直接返回

比较有意思的是这个空对象:

// packages/vue/src/index.ts
let c = compileCache.get(options ?? EMPTY_OBJ)

用了一个 ?? ,代表options为空时,使用 EMPTY_OBJ作为get的参数,不为空就用options

看看这个EMPTY_OBJ,是从shared包里面导入的

shared包里面放了各种公共的工具函数

// packages/vue/src/index.ts
import {
  EMPTY_OBJ,
  NOOP,
  extend,
  generateCodeFrame,
  isString,
} from '@vue/shared'

他的定义是这样的:

// packages/shared/src/general.ts
export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
  ? Object.freeze({})
  : {}

用ts标注了,readonly标识他是一个只读的类型,不能修改;键是string类型,值是any类型

后面是一个三目运算,如果当前处于开发环境,就要用Object.freeze()方法来冻结对象,防止修改;如果是生产环境直接返回就好了。这样做的原因是,开发环境要对这个对象给予更高的限制,防止出现不必要的问题,就用Object.freeze()方法来冻结对象,防止修改;而生产环境代码比较稳定了,不冻结处理直接返回对象即可,性能更好

# 渲染函数

接下来的代码是这样的

// packages/vue/src/index.ts
function compileToFunction( template: string | HTMLElement,
  options?: CompilerOptions,
): RenderFunction {
  // ...
}

registerRuntimeCompiler(compileToFunction)

export { compileToFunction as compile }
export * from '@vue/runtime-dom'

这里出现文件的执行入口:registerRuntimeCompiler了,从字面意思上来看是注册一个运行时的渲染器。先不管compileToFunction,看看registerRuntimeCompiler这个是怎么注册的

// packages/vue/src/index.ts
import {
  type RenderFunction,
  registerRuntimeCompiler,
  warn,
} from '@vue/runtime-dom'

registerRuntimeCompiler是从runtime-dom包里面导入的,进去看看

// packages/runtime-dom/src/component.ts
export function registerRuntimeCompiler(_compile: any) {
  compile = _compile
  installWithProxy = i => {
    if (i.render!._rc) {
      i.withProxy = new Proxy(i.ctx, RuntimeCompiledPublicInstanceProxyHandlers)
    }
  }
}

做的事情就是初始化函数外面的compile和installWithProxy,给外面的compile注册上传入的函数,然后注册installWithProxy方法。这个注册的方法也简单,就是创建一个代理,代理的逻辑在RuntimeCompiledPublicInstanceProxyHandlers里面。先看看变量的定义

proxy是什么就不多赘述了,可以去看看其他博客

# 注册代理

// packages/runtime-dom/src/component.ts
let compile: CompileFunction | undefined
let installWithProxy: (i: ComponentInternalInstance) => void

ComponentInternalInstance其实就是vue的实例了,里面有data、props、methods、computed等,还有一些生命周期函数等,所以installWithProxy函数就是给vue实例的ctx属性注册代理,再看看代理的函数RuntimeCompiledPublicInstanceProxyHandlers:

// packages/runtime-dom/src/component.ts
export const RuntimeCompiledPublicInstanceProxyHandlers = /*#__PURE__*/ extend(
  {},
  PublicInstanceProxyHandlers,
  {
    get(target: ComponentRenderContext, key: string) {
      // fast path for unscopables when using `with` block
      if ((key as any) === Symbol.unscopables) {
        return
      }
      return PublicInstanceProxyHandlers.get!(target, key, target)
    },
    has(_: ComponentRenderContext, key: string) {
      const has = key[0] !== '_' && !isGloballyAllowed(key)
      if (__DEV__ && !has && PublicInstanceProxyHandlers.has!(_, key)) {
        warn(
          `Property ${JSON.stringify(
            key,
          )} should not start with _ which is a reserved prefix for Vue internals.`,
        )
      }
      return has
    },
  },
)

简单看下,extend是shared包里面的方法,实际上就是执行了Object.assign合并对象。

PublicInstanceProxyHandlers这个代理对象比较复杂,里面包含了对 Vue 组件实例公共属性(如 data, props, methods 等)的基本访问控制逻辑,后面再来看。

第三个对象的get方法里面比较有意思,当读取对象上的某个属性时,会进入get中的逻辑。首先判断访问的是不是Symbol.unscopables字段,如果是则直接返回,不是则调用 PublicInstanceProxyHandlers.get方法获取实际属性值。

# Symbol.unscopables

这个Symbol.unscopables是什么?为什么要加一个这样的判断?

Symbol.unscopables 是一个 JavaScript 的内置属性,它是一个对象,用于指定在使用 with 语句时哪些数组方法不应被包含在作用域链中。with 语句允许将对象的属性直接作为变量访问,但通常不推荐使用,因为它可能导致混淆和性能问题。

举个例子:

let arr = ['a', 'b', 'c'];
arr[Symbol.unscopables] = {
  copyWithin: true,
  entries: true,
  keys: true,
  values: true
};

with (arr) {
  console.log(a); // "a"
  console.log(copyWithin); // ReferenceError: copyWithin is not defined
}

这里给arr添加了Symbol.unscopables属性,with里面可以读取到arr的其他值,但是无法读取到Symbol.unscopables里面的值。由于 with 语句的性能问题和可读性问题,现代JavaScript开发中通常避免使用它。

说白了,代理里面加这一段Symbol.unscopables的判断,就是对with的一个优化,当读取这些属性的时候直接跳过

# 公共代理对象

现在再回去看PublicInstanceProxyHandlers这个公共代理对象吧,他里面就是访问vue实例时,主要触发的一些核心代理逻辑。

export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
  get(...) {
    // ...
  },
  set(...): boolean { 
    // ...
  },
  has(...) {
    // ...
  },
  defineProperty(...) {
    // ...
  }
}

方法比较长,主要代理了vue实例的get、set、has和defineProperty。

  • 当访问Vue实例上的对象时,会触发get中的逻辑
  • 当设置Vue实例上的属性时,会触发get中的逻辑
  • 当判断Vue实例上的属性是否存在时,会触发has中的逻辑,比如 in 操作符
  • defineProperty用来拦截对象属性的增加修改操作,比如使用Object.defineProperty()时就会触发其中的逻辑

先看看get方法

# get

get({ _: instance }: ComponentRenderContext, key: string) {
    // ...

    const { ctx, setupState, data, props, accessCache, type, appContext } =
      instance

    // ...

    // data / props / ctx
    // This getter gets called for every property access on the render context
    // during render and is a major hotspot. The most expensive part of this
    // is the multiple hasOwn() calls. It's much faster to do a simple property
    // access on a plain object, so we use an accessCache object (with null
    // prototype) to memoize what access type a key corresponds to.
    let normalizedProps
    if (key[0] !== '$') {
      const n = accessCache![key]
      if (n !== undefined) {
        switch (n) {
          case AccessTypes.SETUP:
            return setupState[key]
          case AccessTypes.DATA:
            return data[key]
          case AccessTypes.CONTEXT:
            return ctx[key]
          case AccessTypes.PROPS:
            return props![key]
          // default: just fallthrough
        }
      } else if (hasSetupBinding(setupState, key)) {
        accessCache![key] = AccessTypes.SETUP
        return setupState[key]
      } else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
        accessCache![key] = AccessTypes.DATA
        return data[key]
      } else if (
        // only cache other properties when instance has declared (thus stable)
        // props
        (normalizedProps = instance.propsOptions[0]) &&
        hasOwn(normalizedProps, key)
      ) {
        accessCache![key] = AccessTypes.PROPS
        return props![key]
      } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
        accessCache![key] = AccessTypes.CONTEXT
        return ctx[key]
      } else if (!__FEATURE_OPTIONS_API__ || shouldCacheAccess) {
        accessCache![key] = AccessTypes.OTHER
      }
    }

    const publicGetter = publicPropertiesMap[key]
    let cssModule, globalProperties
    // public $xxx properties
    if (publicGetter) {
      if (key === '$attrs') {
        track(instance.attrs, TrackOpTypes.GET, '')
        __DEV__ && markAttrsAccessed()
      } else if (__DEV__ && key === '$slots') {
        // for HMR only
        track(instance, TrackOpTypes.GET, key)
      }
      return publicGetter(instance)
    } else if (
      // css module (injected by vue-loader)
      (cssModule = type.__cssModules) &&
      (cssModule = cssModule[key])
    ) {
      return cssModule
    } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
      // user may set custom properties to `this` that start with `$`
      accessCache![key] = AccessTypes.CONTEXT
      return ctx[key]
    } else if (
      // global properties
      ((globalProperties = appContext.config.globalProperties),
      hasOwn(globalProperties, key))
    ) {
      if (__COMPAT__) {
        const desc = Object.getOwnPropertyDescriptor(globalProperties, key)!
        if (desc.get) {
          return desc.get.call(instance.proxy)
        } else {
          const val = globalProperties[key]
          return isFunction(val)
            ? Object.assign(val.bind(instance.proxy), val)
            : val
        }
      } else {
        return globalProperties[key]
      }
    } else if (
      __DEV__ &&
      currentRenderingInstance &&
      (!isString(key) ||
        // #1091 avoid internal isRef/isVNode checks on component instance leading
        // to infinite warning loop
        key.indexOf('__v') !== 0)
    ) {
      if (data !== EMPTY_OBJ && isReservedPrefix(key[0]) && hasOwn(data, key)) {
        warn(
          `Property ${JSON.stringify(
            key,
          )} must be accessed via $data because it starts with a reserved ` +
            `character ("$" or "_") and is not proxied on the render context.`,
        )
      } else if (instance === currentRenderingInstance) {
        warn(
          `Property ${JSON.stringify(key)} was accessed during render ` +
            `but is not defined on instance.`,
        )
      }
    }
  },

我这里舍去了两个字段的判断,方法还是比较长的。

代码可以分为三个部分,第一是当访问vue实例上“原生”的一些属性时(不知道这样表述对不对),这样的属性有setupState、data、context和props,对他们进行处理。第二部分时访问vue实例上带$符号,也就是vue帮我平铺出来,方便外面使用的一些属性时,比如$el、$attrs、$slots这些时的处理逻辑。第三部分时访问vue实例上其他的属性时的逻辑。

# 访问vue实例上的“原生”属性

这部分的逻辑是这样的:

    const { ctx, setupState, data, props, accessCache, type, appContext } =
      instance

    let normalizedProps
    if (key[0] !== '$') {
      const n = accessCache![key]
      if (n !== undefined) {
        switch (n) {
          case AccessTypes.SETUP:
            return setupState[key]
          case AccessTypes.DATA:
            return data[key]
          case AccessTypes.CONTEXT:
            return ctx[key]
          case AccessTypes.PROPS:
            return props![key]
          // default: just fallthrough
        }
      } else if (hasSetupBinding(setupState, key)) {
        accessCache![key] = AccessTypes.SETUP
        return setupState[key]
      } else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
        accessCache![key] = AccessTypes.DATA
        return data[key]
      } else if (
        // only cache other properties when instance has declared (thus stable)
        // props
        (normalizedProps = instance.propsOptions[0]) &&
        hasOwn(normalizedProps, key)
      ) {
        accessCache![key] = AccessTypes.PROPS
        return props![key]
      } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
        accessCache![key] = AccessTypes.CONTEXT
        return ctx[key]
      } else if (!__FEATURE_OPTIONS_API__ || shouldCacheAccess) {
        accessCache![key] = AccessTypes.OTHER
      }
    }

在这上面,还有几行注释:

// data / props / ctx
// This getter gets called for every property access on the render context
// during render and is a major hotspot. The most expensive part of this
// is the multiple hasOwn() calls. It's much faster to do a simple property
// access on a plain object, so we use an accessCache object (with null
// prototype) to memoize what access type a key corresponds to.

这里的注释是说,这个getter在渲染期间,会调用很多次,而且最慢的部分是hasOwn()的调用。所以在这里,我们使用一个accessCache对象(没有原型)来缓存每个键对应的访问类型

也就是说,这里做了一个缓存的优化,目的是为了减少hasOwn的调用,从而提高性能

代码首先从vue实例中将setupState、data、context和props以及缓存容器accessCache等通过解构的方式拿出来,如果访问属性的键名不是以$开头的,则进入这段逻辑。

首先试图从缓存容器中,根据访问的属性名来获取一下缓存的字段,如果这个字段存在,则根据字段判断要访问的值在哪个对象属性上,并直接返回他。而如果不存在,则通过hasOwn方法分别去判断要访问的值存在于哪个对象属性上,找到后就返回他,并把他存入缓存容器中,用访问的属性名做key,属于的对象对应的标识做value。

举个例子,第一次访问vue实例中props.name值时,发现缓存中没有缓存以name为键的内容,就分别遍历setupState、data、context和props等去找,通过hasOwn方法判断name在不在这些属性中。最后发现name在props里面,于是将name作为key,AccessTypes.PROPS(就是一个常量标识,值是3)作为value存入缓存中。第二次再访问这个属性的时候就会发现缓存里面已经有name的缓存,并且通过值AccessTypes.PROPS知道这个字段是存在props中,直接返回props.name,就不用走hasOwn的判断了。

# 访问vue实例上的带$符号的属性

这段逻辑不复杂

const publicGetter = publicPropertiesMap[key]
    let cssModule, globalProperties
    // public $xxx properties
    if (publicGetter) {
      if (key === '$attrs') {
        track(instance.attrs, TrackOpTypes.GET, '')
        __DEV__ && markAttrsAccessed()
      } else if (__DEV__ && key === '$slots') {
        // for HMR only
        track(instance, TrackOpTypes.GET, key)
      }
      return publicGetter(instance)
    }

publicPropertiesMap是一个映射表,映射了带$符的属性和“原生”属性的关系。如果发现访问的属性是带$,并且在这个映射表中,也就是说跟“原生”的属性有映射关系,就走这段逻辑。

如果访问的是$attrs或$slots,则进行追踪,因为这两个属性分别对应父组件传给子组件的值和子组件的插槽,他们是要有响应式的,这里需要单独拿出来用track追踪一下,其他的属性直接返回“原生”属性上实际的值就好了。

track就涉及Vue的响应式系统了,后面再单独研究吧

# 访问vue实例上的其他属性

这部分就是一些杂项的处理了,在这里有对ctx(上下文对象,里面存有生命周期钩子函数之类的)的处理和缓存、全局属性的处理和缓存、css模块的处理以及警告处理。目前好像没有特别好研究的,等遇到再回来看吧

# set

里面的代码是这样的

  set(
    { _: instance }: ComponentRenderContext,
    key: string,
    value: any,
  ): boolean {
    const { data, setupState, ctx } = instance
    if (hasSetupBinding(setupState, key)) {
      setupState[key] = value
      return true
    } else if (
      __DEV__ &&
      setupState.__isScriptSetup &&
      hasOwn(setupState, key)
    ) {
      warn(`Cannot mutate <script setup> binding "${key}" from Options API.`)
      return false
    } else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
      data[key] = value
      return true
    } else if (hasOwn(instance.props, key)) {
      __DEV__ && warn(`Attempting to mutate prop "${key}". Props are readonly.`)
      return false
    }
    if (key[0] === '$' && key.slice(1) in instance) {
      __DEV__ &&
        warn(
          `Attempting to mutate public property "${key}". ` +
            `Properties starting with $ are reserved and readonly.`,
        )
      return false
    } else {
      if (__DEV__ && key in instance.appContext.config.globalProperties) {
        Object.defineProperty(ctx, key, {
          enumerable: true,
          configurable: true,
          value,
        })
      } else {
        ctx[key] = value
      }
    }
    return true
  },

也没太多研究的地方了,主要是一些警告和拦截,要符合vue实例要求的属性和值才能进行添加。

# has

代码是这样的

  has(
    {
      _: { data, setupState, accessCache, ctx, appContext, propsOptions },
    }: ComponentRenderContext,
    key: string,
  ) {
    let normalizedProps
    return (
      !!accessCache![key] ||
      (data !== EMPTY_OBJ && hasOwn(data, key)) ||
      hasSetupBinding(setupState, key) ||
      ((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) ||
      hasOwn(ctx, key) ||
      hasOwn(publicPropertiesMap, key) ||
      hasOwn(appContext.config.globalProperties, key)
    )
  },

应该就是对判断对象是否在vue实例上这个操作的一些优化,优先从缓存中判断,没有再用hasOwn方法去判断

# defineProperty

 defineProperty(
    target: ComponentRenderContext,
    key: string,
    descriptor: PropertyDescriptor,
  ) {
    if (descriptor.get != null) {
      // invalidate key cache of a getter based property #5417
      target._.accessCache![key] = 0
    } else if (hasOwn(descriptor, 'value')) {
      this.set!(target, key, descriptor.value, null)
    }
    return Reflect.defineProperty(target, key, descriptor)
  },

这个方法就是对vue实例上的属性进行设置,如果设置了getter,则将key的缓存置为0,为在 Vue 的响应式系统中,当存在 getter 时,属性的值可能会根据其他依赖关系动态计算,因此需要清空缓存以确保下次访问时重新计算。如果设置了value,则调用set方法进行设置。如果描述符有一个 value 字段,这意味着属性有一个固定值。在这种情况下,Vue 调用 this.set! 方法(注意感叹号表示这是一个非空断言操作符,表明 set 方法在这个上下文中是可用的)来设置属性值,传入 target、key、value 和 undefined

总的来说,defineProperty 方法在 Vue.js 中起到了桥接原生 Object.defineProperty 和 Vue 的响应式系统的角色,确保了属性定义遵循 Vue 的规则,同时保持了响应性。

# compileToFunction

看了这么深,再重新回去packages/vue/src/index里面的compileToFunction吧

// packages/vue/src/index.ts
function compileToFunction(
  template: string | HTMLElement,
  options?: CompilerOptions,
): RenderFunction {
  if (!isString(template)) {
    if (template.nodeType) {
      template = template.innerHTML
    } else {
      __DEV__ && warn(`invalid template option: `, template)
      return NOOP
    }
  }

  const key = template
  const cache = getCache(options)
  const cached = cache[key]
  if (cached) {
    return cached
  }

  if (template[0] === '#') {
    const el = document.querySelector(template)
    if (__DEV__ && !el) {
      warn(`Template element not found or is empty: ${template}`)
    }
    // __UNSAFE__
    // Reason: potential execution of JS expressions in in-DOM template.
    // The user must make sure the in-DOM template is trusted. If it's rendered
    // by the server, the template should not contain any user data.
    template = el ? el.innerHTML : ``
  }

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

  if (!opts.isCustomElement && typeof customElements !== 'undefined') {
    opts.isCustomElement = tag => !!customElements.get(tag)
  }

  const { code } = compile(template, opts)

  function onError(err: CompilerError, asWarning = false) {
    const message = asWarning
      ? err.message
      : `Template compilation error: ${err.message}`
    const codeFrame =
      err.loc &&
      generateCodeFrame(
        template as string,
        err.loc.start.offset,
        err.loc.end.offset,
      )
    warn(codeFrame ? `${message}\n${codeFrame}` : message)
  }

  // The wildcard import results in a huge object with every export
  // with keys that cannot be mangled, and can be quite heavy size-wise.
  // In the global build we know `Vue` is available globally so we can avoid
  // the wildcard object.
  const render = (
    __GLOBAL__ ? new Function(code)() : new Function('Vue', code)(runtimeDom)
  ) as RenderFunction

  // mark the function as runtime compiled
  ;(render as InternalRenderFunction)._rc = true

  return (cache[key] = render)
}

之前说了,registerRuntimeCompiler会把这个函数注册成运行时用来编译的函数。

这个函数干了这些事情:

  1. 检查 template 是否为字符串,如果不是,尝试将其 innerHTML 作为模板内容。
  2. 从缓存中查找已编译的模板,如果找到则直接返回。
  3. 处理以 # 开头的模板,尝试将其解析为 DOM 元素的内容。
  4. 初始化编译选项 opts,并根据用户提供的 options 进行合并。
  5. 根据环境判断是否需要自定义元素处理。
  6. 调用 compile 函数,传入模板和编译选项,得到编译后的 JavaScript 代码(code)。
  7. 定义错误处理函数 onError,用于在编译过程中捕获和处理错误。
  8. 根据环境(全局构建或模块化构建)创建渲染函数 render,并执行编译后的代码,生成一个执行函数。
  9. 标记生成的 render 函数为运行时编译的,并将其存储到缓存中,最后返回该渲染函数。

总的来说,compileToFunction 是 Vue.js 框架中用于将模板字符串转换为可执行的渲染函数的关键函数,这个函数在组件实例化时被调用,根据组件的 state 生成虚拟 DOM,然后更新实际的 DOM,实现了数据驱动视图的变化。

也就是说,这个函数是用来生成虚拟DOM的

最核心的执行在这段代码:

const { code } = compile(template, opts)

compile是从compiler-dom包中导入的

import {
  type CompilerError,
  type CompilerOptions,
  compile,
} from '@vue/compiler-dom'

过去看看

export function compile(
  src: string | RootNode,
  options: CompilerOptions = {},
): CodegenResult {
  return baseCompile(
    src,
    extend({}, parserOptions, options, {
      nodeTransforms: [
        // ignore <script> and <tag>
        // this is not put inside DOMNodeTransforms because that list is used
        // by compiler-ssr to generate vnode fallback branches
        ignoreSideEffectTags,
        ...DOMNodeTransforms,
        ...(options.nodeTransforms || []),
      ],
      directiveTransforms: extend(
        {},
        DOMDirectiveTransforms,
        options.directiveTransforms || {},
      ),
      transformHoist: __BROWSER__ ? null : stringifyStatic,
    }),
  )
}

用通义灵码解释一下这个函数干了什么

这个函数 compile 是 Vue.js 模板编译过程的一部分,其主要任务是将输入的模板字符串(src)或者已经解析的根节点(RootNode)转换成可执行的 JavaScript 代码(CodegenResult)。这个过程涉及解析、转换和优化模板,生成一个渲染函数,用于在运行时生成和更新虚拟 DOM。下面是函数的详细解释:

  1. 参数:
  • src: 输入的模板字符串或已经解析的根节点。
  • options: 编译选项,用于定制编译行为,例如自定义指令转换、节点转换等。
  1. 函数内部:
  • 使用 baseCompile 函数进行实际的编译工作,baseCompile 是 Vue.js 编译器的核心部分。
  • extend 函数用于合并多个对象,创建新的配置对象,用于 baseCompile 函数:

-- parserOptions 是 Vue.js 内置的解析选项。

-- options 是用户自定义的编译选项。

-- nodeTransforms 是节点转换数组,包括忽略副作用标签(如 <script> 和自定义标签)的转换,以及默认的 DOM 节点转换和用户自定义的转换。

-- directiveTransforms 是指令转换对象,包含默认的 DOM 指令转换和用户自定义的转换。

-- transformHoist 用于静态提升优化,但在浏览器环境中设为 null,在非浏览器环境中(如 Node.js)使用 stringifyStatic 函数进行静态值的序列化。

  1. 返回值:
  • baseCompile 函数执行后返回的 CodegenResult,包含生成的 JavaScript 代码和其他编译信息。

这个 compile 函数是 Vue.js 模板编译过程中的重要一环,它将模板字符串转换为可执行的渲染函数,这个函数在运行时用于根据组件的状态生成和更新虚拟 DOM,进而更新实际的 DOM。

所以,这就是将模板编译成渲染函数的具体方法啦,从这里就可以进入Vue编译相关的内容了,以后再研究吧