【vue源码02】Vue项目入口,启程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会把这个函数注册成运行时用来编译的函数。
这个函数干了这些事情:
- 检查 template 是否为字符串,如果不是,尝试将其 innerHTML 作为模板内容。
- 从缓存中查找已编译的模板,如果找到则直接返回。
- 处理以 # 开头的模板,尝试将其解析为 DOM 元素的内容。
- 初始化编译选项 opts,并根据用户提供的 options 进行合并。
- 根据环境判断是否需要自定义元素处理。
- 调用 compile 函数,传入模板和编译选项,得到编译后的 JavaScript 代码(code)。
- 定义错误处理函数 onError,用于在编译过程中捕获和处理错误。
- 根据环境(全局构建或模块化构建)创建渲染函数 render,并执行编译后的代码,生成一个执行函数。
- 标记生成的 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。下面是函数的详细解释:
- 参数:
- src: 输入的模板字符串或已经解析的根节点。
- options: 编译选项,用于定制编译行为,例如自定义指令转换、节点转换等。
- 函数内部:
- 使用 baseCompile 函数进行实际的编译工作,baseCompile 是 Vue.js 编译器的核心部分。
- extend 函数用于合并多个对象,创建新的配置对象,用于 baseCompile 函数:
-- parserOptions 是 Vue.js 内置的解析选项。
-- options 是用户自定义的编译选项。
-- nodeTransforms 是节点转换数组,包括忽略副作用标签(如 <script> 和自定义标签)的转换,以及默认的 DOM 节点转换和用户自定义的转换。
-- directiveTransforms 是指令转换对象,包含默认的 DOM 指令转换和用户自定义的转换。
-- transformHoist 用于静态提升优化,但在浏览器环境中设为 null,在非浏览器环境中(如 Node.js)使用 stringifyStatic 函数进行静态值的序列化。
- 返回值:
- baseCompile 函数执行后返回的 CodegenResult,包含生成的 JavaScript 代码和其他编译信息。
这个 compile 函数是 Vue.js 模板编译过程中的重要一环,它将模板字符串转换为可执行的渲染函数,这个函数在运行时用于根据组件的状态生成和更新虚拟 DOM,进而更新实际的 DOM。
所以,这就是将模板编译成渲染函数的具体方法啦,从这里就可以进入Vue编译相关的内容了,以后再研究吧