刺穿Vue3.*的初始化全过程
开始
首先我们先来观察一下整体的项目结构
大致先判断一下每个文件的作用,如果暂时没办法判断就先跳过
- compiler-core AST解析相关的内容
- compiler-dom 语法树转换成生成DOM树相关的内容
- compiler-sfc 单文件组件解析相关内容
- compiler-ssr 服务端渲染编译相关
- reactivity 响应性API相关
- vue 3.*入口文件
- vue-compat 2.*与3.*的兼容版本入口
vue
接着我们跟着一次完成的初始化的过程来看下去,首先我们先来通过vue-cli来拉取项目的模版
$ vue create hello-world
// wait
$ cd hello-world
$ npm run serve
访问http://localhost:8080/就可以看到下图界面
我们打开项目文件,查看文件
- src/main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
- src/App.vue
<template>
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
- src/components/HelloWorld.vue
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
- public/index.html
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
好的,那么接下来我们先看到main.js
,主要就做了三件事情
- 引入
createApp
- 引入
App.vue
- 执行
createApp
传入App
,链式调用mount
传入#app
看到这里很多小伙伴就会问了,为什么先看main.js
,那我建议你先看看vue-cli官方文档如果还是不能理解就看看这个webpack官方文档, 回来后我们继续,在vue-next
项目中也就是3.*的项目代码中找到先去看packages/vue/src/index.ts
// 简略结构
import { compile, CompilerOptions, CompilerError } from '@vue/compiler-dom'
import { registerRuntimeCompiler, RenderFunction, warn } from '@vue/runtime-dom'
import * as runtimeDom from '@vue/runtime-dom'
function compileToFunction(
template: string | HTMLElement,
options?: CompilerOptions
): RenderFunction {
// 后续在看
}
registerRuntimeCompiler(compileToFunction)
export { compileToFunction as compile }
export * from '@vue/runtime-dom'
文件中透露两个信息
- 在这里注册了全局的运行时编译函数
createApp(...)
不在这里,可能在runtime-dom
中
runtime-dom
入口
我们看到packages/runtime-dom/src/index.ts
中的内容,在代码上我直接通过备注的方式来分析一下这块的内容
import {
createRenderer,
createHydrationRenderer,
warn,
RootRenderFunction,
CreateAppFunction,
Renderer,
HydrationRenderer,
App,
RootHydrateFunction,
isRuntimeOnly,
DeprecationTypes,
compatUtils
} from '@vue/runtime-core'
import { nodeOps } from './nodeOps'
import { patchProp } from './patchProp'
import {
isFunction,
isString,
isHTMLTag,
isSVGTag,
extend,
NOOP
} from '@vue/shared'
/*
* 渲染配置项,里面包括patchProp(...) 跟 nodeOps下的一些操作内容
* nodeOps 下主要是一些具体的节点操作方法
* patchProp 看起来更像是对 dom attrs的解析 以及 操作
*/
const rendererOptions = extend({ patchProp }, nodeOps)
let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer
/*
* renderer的获取方法,主要跟单例相关以及服务端渲染的渲染器选择相关
*/
function ensureRenderer() {
return (
renderer ||
// 如果不存在就通过createRenderer方法传入rendererOptions创建renderer
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
)
}
export const createApp = ((...args) => {
/*
* 获取renderer调用上面的createApp方法,参数为App.vue,当然经过SFC的解析, 已经变成了下图的结构
* 但是我们为了更好的了解源码,修改main文件代码如下
* args变成
* {
* name: 'App',
* template: `
* <img alt="Vue logo" src="./assets/logo.png">
* <HelloWorld></HelloWorld>
* `,
* components: {
* HelloWorld
* }
* }
*
*/
const app = ensureRenderer().createApp(...args)
// 调试代码,先行跳过
if (__DEV__) {
injectNativeTagCheck(app)
injectCompilerOptionsCheck(app)
}
// 取出mount在外重新包装一层新的mount方法
const { mount } = app
// 如下所示 containerOrSelector 为 '#app'
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// 根据#app获取对应的DOM对象
const container = normalizeContainer(containerOrSelector)
if (!container) return
// 通过调试node_modules/@vue/runtime-dom/dist/runtime-dom.esm-bundler.js 文件 发现实际上这个就是 args 传入的配置项信息,如下图
const component = app._component
// 当component参数不为函数,也不存在render函数以及template时获取dom对象的innerHTML为模版
if (!isFunction(component) && !component.render && !component.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.
component.template = container.innerHTML
// 2.x compat check
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) {
// 移除v-cloak,不了解v-cloak作用的可以看https://v3.cn.vuejs.org/api/directives.html#v-cloak
container.removeAttribute('v-cloak')
// 设置data-v-app标识
container.setAttribute('data-v-app', '')
}
return proxy
}
return app
})
-
经过loader处理后的数据结构
-
如上备注修改main文件代码
import { createApp } from 'vue/dist/vue.esm-bundler.js'
import HelloWorld from './components/HelloWorld.vue'
createApp({
name: 'App',
template: `
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld></HelloWorld>
`,
components: {
HelloWorld
}
})
.mount('#app')
- 调试看到的component 变量信息
思考:基本上createApp方法的mount只是做了一层包装,实际的挂载应该来源于createRenderer返回的mount方法中,我们需要深入进去,看具体实现
runtime-core(前半程)
实例化过程
由于当前文件中的代码量比较大,从这里开始按照函数给大家放代码
// 文件runtime-core/src/renderer.ts
import { createAppAPI, CreateAppFunction } from './apiCreateApp'
// 也是做了一层包装,实际上调用的是baseCreateRenderer方法
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
// 解构并重命名节点操作方法,之前提到过,来源于patchProp 和 nodeOps
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
createComment: hostCreateComment,
setText: hostSetText,
setElementText: hostSetElementText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
setScopeId: hostSetScopeId = NOOP,
cloneNode: hostCloneNode,
insertStaticContent: hostInsertStaticContent
} = options
// 这中间省略超多方法,后续再回来看,主要是patch
const render: RootRenderFunction = (vnode, container, isSVG) => {
// 如果vnode为空,但是我们这里不为空
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// 我们这里就直接进入patch函数
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
flushPostFlushCbs()
container._vnode = vnode
}
return {
render,
// 由于本篇不涉及,所以不提及感兴趣可以自行了解
hydrate,
// 那么这里其实还是做了render的闭包,然后将render传入createAppAPI中
createApp: createAppAPI(render, hydrate)
}
}
export function createAppContext(): AppContext {
return {
app: null as any,
config: {
isNativeTag: NO,
performance: false,
globalProperties: {},
optionMergeStrategies: {},
errorHandler: undefined,
warnHandler: undefined,
compilerOptions: {}
},
mixins: [],
components: {},
directives: {},
provides: Object.create(null),
optionsCache: new WeakMap(),
propsCache: new WeakMap(),
emitsCache: new WeakMap()
}
}
// 切换到文件runtime-core/src/apiCreateApp.ts
// 全局应用ID 为了保证ID的唯一性,全局唯一自增
let uid = 0
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
if (rootProps != null && !isObject(rootProps)) {
rootProps = null
}
// 创建当前应用的上下文对象
const context = createAppContext()
// 确保插件去重 创建插件集合
const installedPlugins = new Set()
// 挂载标识
let isMounted = false
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
_instance: null,
version,
get config() {
return context.config
},
set config(v) {
if (__DEV__) {
warn(
`app.config cannot be replaced. Modify individual options instead.`
)
}
},
use(plugin: Plugin, ...options: any[]) {
if (installedPlugins.has(plugin)) {
__DEV__ && warn(`Plugin has already been applied to target app.`)
} else if (plugin && isFunction(plugin.install)) {
installedPlugins.add(plugin)
plugin.install(app, ...options)
} else if (isFunction(plugin)) {
installedPlugins.add(plugin)
plugin(app, ...options)
}
return app
},
mixin(mixin: ComponentOptions) {
if (__FEATURE_OPTIONS_API__) {
if (!context.mixins.includes(mixin)) {
context.mixins.push(mixin)
}
}
return app
},
component(name: string, component?: Component): any {
if (!component) {
return context.components[name]
}
context.components[name] = component
return app
},
directive(name: string, directive?: Directive) {
if (!directive) {
return context.directives[name] as any
}
context.directives[name] = directive
return app
},
// 关注挂载的原mount方法
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isMounted) {
// 传入createApp的初始参数,返回Vnode如下图结构
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
vnode.appContext = context
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
// 这里又调用回了上文的render方法
/*
*
*/
render(vnode, rootContainer, isSVG)
}
isMounted = true
app._container = rootContainer
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = vnode.component
devtoolsInitApp(app, version)
}
return getExposeProxy(vnode.component!) || vnode.component!.proxy
}
},
unmount() {
if (isMounted) {
render(null, app._container)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = null
devtoolsUnmountApp(app)
}
delete app._container.__vue_app__
}
},
provide(key, value) {
// TypeScript doesn't allow symbols as index type
// https://github.com/Microsoft/TypeScript/issues/24587
context.provides[key as string] = value
return app
}
})
// 兼容2.*版本
if (__COMPAT__) {
installAppCompatProperties(app, context, render)
}
return app
}
}
// 切换到文件packages/runtime-core/src/vnode.ts
function _createVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag: number = 0,
dynamicProps: string[] | null = null,
isBlockNode = false
): VNode {
// 我们这里type依然是传入我们的构造的配置项目
if (!type || type === NULL_DYNAMIC_COMPONENT) {
if (__DEV__ && !type) {
warn(`Invalid vnode type when creating vnode: ${type}.`)
}
type = Comment
}
// 是否是Vnode节点
if (isVNode(type)) {
// createVNode receiving an existing vnode. This happens in cases like
// <component :is="vnode"/>
// #2078 make sure to merge refs during the clone instead of overwriting it
const cloned = cloneVNode(type, props, true /* mergeRef: true */)
if (children) {
normalizeChildren(cloned, children)
}
return cloned
}
// class component normalization.
if (isClassComponent(type)) {
type = type.__vccOpts
}
// 2.x async/functional component compat
if (__COMPAT__) {
type = convertLegacyComponent(type, currentRenderingInstance)
}
// class & style normalization.
if (props) {
// for reactive or proxy objects, we need to clone it to enable mutation.
props = guardReactiveProps(props)!
let { class: klass, style } = props
if (klass && !isString(klass)) {
props.class = normalizeClass(klass)
}
if (isObject(style)) {
// reactive state objects need to be cloned since they are likely to be
// mutated
if (isProxy(style) && !isArray(style)) {
style = extend({}, style)
}
props.style = normalizeStyle(style)
}
}
// encode the vnode type information into a bitmap
// 我们由于是Object 所以判断的类型是ShapeFlags.STATEFUL_COMPONEN 具体的值 是 4(方便大家在调试中看懂)
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT
: __FEATURE_SUSPENSE__ && isSuspense(type)
? ShapeFlags.SUSPENSE
: isTeleport(type)
? ShapeFlags.TELEPORT
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT
: isFunction(type)
? ShapeFlags.FUNCTIONAL_COMPONENT
: 0
if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) {
type = toRaw(type)
warn(
`Vue received a Component which was made a reactive object. This can ` +
`lead to unnecessary performance overhead, and should be avoided by ` +
`marking the component with \`markRaw\` or using \`shallowRef\` ` +
`instead of \`ref\`.`,
`\nComponent that was made reactive: `,
type
)
}
return createBaseVNode(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag,
isBlockNode,
true
)
}
function createBaseVNode(
// 实际上还是最开始的配置信息
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag = 0,
dynamicProps: string[] | null = null,
shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
isBlockNode = false,
needFullChildrenNormalization = false
) {
const vnode = {
__v_isVNode: true,
__v_skip: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
slotScopeIds: null,
children,
component: null,
suspense: null,
ssContent: null,
ssFallback: null,
dirs: null,
transition: null,
el: null,
anchor: null,
target: null,
targetAnchor: null,
staticCount: 0,
// 4
shapeFlag,
// 0
patchFlag,
dynamicProps,
dynamicChildren: null,
appContext: null
} as VNode
if (needFullChildrenNormalization) {
normalizeChildren(vnode, children)
// normalize suspense children
if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).normalize(vnode)
}
} else if (children) {
// compiled element vnode - if children is passed, only possible types are
// string or Array.
vnode.shapeFlag |= isString(children)
? ShapeFlags.TEXT_CHILDREN
: ShapeFlags.ARRAY_CHILDREN
}
// validate key
if (__DEV__ && vnode.key !== vnode.key) {
warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type)
}
// track vnode for block tree
if (
isBlockTreeEnabled > 0 &&
// avoid a block node from tracking itself
!isBlockNode &&
// has current parent block
currentBlock &&
// presence of a patch flag indicates this node needs patching on updates.
// component nodes also should always be patched, because even if the
// component doesn't need to update, it needs to persist the instance on to
// the next vnode so that it can be properly unmounted later.
(vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
// the EVENTS flag is only for hydration and if it is the only flag, the
// vnode should not be considered dynamic due to handler caching.
vnode.patchFlag !== PatchFlags.HYDRATE_EVENTS
) {
currentBlock.push(vnode)
}
if (__COMPAT__) {
convertLegacyVModelProps(vnode)
defineLegacyVNodeProperties(vnode)
}
return vnode
}
- 初始化Vnode的结构
思考:这条链路看下来后主要有以下这么几个点
- 根据 runtime-core/src/apiCreateApp.ts 中的 createAppAPI 创建 应用上下文 以及 提供应用方法
- 挂载函数中实际上做了两件事情
- 根据根组件创建一个Vnode节点
- 调用baseCreateRenderer闭包中的render方法
- render 函数中主要是调用baseCreateRenderer闭包中的patch方法,那么我们想看到实际的渲染过程,还是要跟进patch方法中查看具体过程
// 文件runtime-core/src/renderer.ts baseCreateRenderer函数的上下文中
// 这里会先补充初始化的参数因为patch本身是一个被递归函数
const patch: PatchFn = (
// null
n1,
// rootComponent 生成的 vnode节点
n2,
// DOM节点
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
if (n1 === n2) {
return
}
// patching & not same type, unmount old tree
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
// 0 !== 2
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
const { type, ref, shapeFlag } = n2
switch (type) {
case Text:
processText(n1, n2, container, anchor)
break
case Comment:
processCommentNode(n1, n2, container, anchor)
break
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
default:
// 这里的&就是按位与 00010 与 00011 的 结果就是 00010
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 首次命中进入这个逻辑,我们深入进去
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}
const processComponent = (
// null
n1: VNode | null,
// rootComponent 生成的 vnode节点
n2: VNode,
// #app的DOM节点
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
// null
n2.slotScopeIds = slotScopeIds
if (n1 == null) {
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
} else {
//命中挂在组件的函数逻辑,我们深入进去
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else {
updateComponent(n1, n2, optimized)
}
}
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 2.x compat may pre-create the component instance before actually
// mounting
const compatMountInstance =
__COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
const instance: ComponentInternalInstance =
compatMountInstance ||
// 这里我们会命中构建一个新的组件实例的逻辑 initialVNode.component 的结构如下图
(initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
if (__DEV__ && instance.type.__hmrId) {
registerHMR(instance)
}
if (__DEV__) {
pushWarningContext(initialVNode)
startMeasure(instance, `mount`)
}
// inject renderer internals for keepAlive
if (isKeepAlive(initialVNode)) {
;(instance.ctx as KeepAliveContext).renderer = internals
}
// resolve props and slots for setup context
if (!(__COMPAT__ && compatMountInstance)) {
if (__DEV__) {
startMeasure(instance, `init`)
}
// 关键组件初始化方法,包括组件中所有相关配置信息,声明周期的的初始化过程,render函数的处理过程
setupComponent(instance)
if (__DEV__) {
endMeasure(instance, `init`)
}
}
// setup() is async. This component relies on async logic to be resolved
// before proceeding
if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)
// Give it a placeholder if this is not hydration
// TODO handle self-defined fallback
if (!initialVNode.el) {
const placeholder = (instance.subTree = createVNode(Comment))
processCommentNode(null, placeholder, container!, anchor)
}
return
}
// 渲染副作用的挂载
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
if (__DEV__) {
popWarningContext()
endMeasure(instance, `mount`)
}
}
// 切换到runtime-core/src/component.ts
export function createComponentInstance(
vnode: VNode,
parent: ComponentInternalInstance | null,
suspense: SuspenseBoundary | null
) {
const type = vnode.type as ConcreteComponent
// inherit parent app context - or - if root, adopt from root vnode
const appContext =
(parent ? parent.appContext : vnode.appContext) || emptyAppContext
const instance: ComponentInternalInstance = {
uid: uid++,
vnode,
type,
parent,
appContext,
root: null!, // to be immediately set
next: null,
subTree: null!, // will be set synchronously right after creation
effect: null!,
update: null!, // will be set synchronously right after creation
scope: new EffectScope(true /* detached */),
render: null,
proxy: null,
exposed: null,
exposeProxy: null,
withProxy: null,
provides: parent ? parent.provides : Object.create(appContext.provides),
accessCache: null!,
renderCache: [],
// local resovled assets
components: null,
directives: null,
// resolved props and emits options
propsOptions: normalizePropsOptions(type, appContext),
emitsOptions: normalizeEmitsOptions(type, appContext),
// emit
emit: null!, // to be set immediately
emitted: null,
// props default value
propsDefaults: EMPTY_OBJ,
// inheritAttrs
inheritAttrs: type.inheritAttrs,
// state
ctx: EMPTY_OBJ,
data: EMPTY_OBJ,
props: EMPTY_OBJ,
attrs: EMPTY_OBJ,
slots: EMPTY_OBJ,
refs: EMPTY_OBJ,
setupState: EMPTY_OBJ,
setupContext: null,
// suspense related
suspense,
suspenseId: suspense ? suspense.pendingId : 0,
asyncDep: null,
asyncResolved: false,
// lifecycle hooks
// not using enums here because it results in computed properties
isMounted: false,
isUnmounted: false,
isDeactivated: false,
bc: null,
c: null,
bm: null,
m: null,
bu: null,
u: null,
um: null,
bum: null,
da: null,
a: null,
rtg: null,
rtc: null,
ec: null,
sp: null
}
if (__DEV__) {
instance.ctx = createDevRenderContext(instance)
} else {
instance.ctx = { _: instance }
}
instance.root = parent ? parent.root : instance
instance.emit = emit.bind(null, instance)
// apply custom element special handling
if (vnode.ce) {
vnode.ce(instance)
}
return instance
}
- initialVNode.component 的数据结构以及数据值
思考:patch方法本身更多是一个分发,针对不同的节点类型做不同的处理,那么由于我们是组件节点,我们跟到mountComponent方法中,mountComponent方法主要做了三件事情,
- createComponentInstance 创建组件根部
- setupComponent 设置组件(包括状态初始化,声明周期等)
- setupRenderEffect 设置渲染的副作用 (包括响应api的副作用挂载) 对于setupComponent 和 setupRenderEffect我们还没具体介绍,那么我们先来看一下,setupComponent的内部实现
// runtime-core/src/component.ts
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
isInSSRComponentSetup = isSSR
/*
* 当前的props 根 children都是null,这里的props主要是 给根组件的props
*/
const { props, children } = instance.vnode
// isStateful = 4
const isStateful = isStatefulComponent(instance)
// 初始化组件的props
initProps(instance, props, isStateful, isSSR)
// 初始化slots姐弟哪
initSlots(instance, children)
// 这里如果是setup周期为promise那才会有一个setupResult的放回不然就是undefined
const setupResult = isStateful
// 命中setupStatefulComponent方法继续做设置
? setupStatefulComponent(instance, isSSR)
: undefined
isInSSRComponentSetup = false
return setupResult
}
function setupStatefulComponent(
// 组件实例
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions
// 中间省略部分调试代码
// 0. create render proxy property access cache
instance.accessCache = Object.create(null)
// 1. create public instance / render proxy
// also mark it raw so it's never observed
// 这里针对组件的上下文也就是组件内部this.*的this进行代理设置
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
// 2. call setup()
// 取出setup方法,但是我们目前案例中没有setup所以我们先YY来进行结果预测
const { setup } = Component
if (setup) {
// setupContext的初始化,就是setup(props, context) 的二个参数的初始化
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
// 设置instance 到 currentInstance(模块闭包变量)
// 并且把当前组件的副作用域推入副作用域栈
setCurrentInstance(instance)
// 暂停追踪
pauseTracking()
/*
* callWithErrorHandling执行了setup方法
* setupResult为set周期中具体放回的可响应方法以及可响应数据
* 反过来说其实在这一步,setup周期内的可响应数据已经采集到了对应的依赖
* 这里可能会需要具体根据现有的全局状态去分析 响应API的具体实现,这个放在后面去看
*/
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
// 重启追踪
resetTracking()
// 推出当前副作用域
unsetCurrentInstance()
// 判断是否为Promise
if (isPromise(setupResult)) {
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
if (isSSR) {
// return the promise so server-renderer can wait on it
return setupResult
.then((resolvedResult: unknown) => {
handleSetupResult(instance, resolvedResult, isSSR)
})
.catch(e => {
handleError(e, instance, ErrorCodes.SETUP_FUNCTION)
})
} else if (__FEATURE_SUSPENSE__) {
// async setup returned Promise.
// bail here and wait for re-entry.
instance.asyncDep = setupResult
} else if (__DEV__) {
warn(
`setup() returned a Promise, but the version of Vue you are using ` +
`does not support it yet.`
)
}
} else {
// 处理同步的setup结果
handleSetupResult(instance, setupResult, isSSR)
}
} else {
finishComponentSetup(instance, isSSR)
}
}
export function handleSetupResult(
instance: ComponentInternalInstance,
setupResult: unknown,
isSSR: boolean
) {
// 如果setup执行返回一个函数,并且不是在SSR的情况下的话,那么当前函数就会作为函数的渲染函数进行使用 https://v3.cn.vuejs.org/api/composition-api.html#setup
if (isFunction(setupResult)) {
// setup returned an inline render function
if (__SSR__ && (instance.type as ComponentOptions).__ssrInlineRender) {
// when the function's name is `ssrRender` (compiled by SFC inline mode),
// set it as ssrRender instead.
instance.ssrRender = setupResult
} else {
instance.render = setupResult as InternalRenderFunction
}
// 如果是对象返回那么就给对象增加代理的数据处理能力 proxyRefs(语法糖,根据不同的数据类型给予不同的数据赋值方法罢了)
} else if (isObject(setupResult)) {
if (__DEV__ && isVNode(setupResult)) {
warn(
`setup() should not return VNodes directly - ` +
`return a render function instead.`
)
}
// setup returned bindings.
// assuming a render function compiled from template is present.
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
instance.devtoolsRawSetupState = setupResult
}
// proxyRefs 语法糖,根据不同的数据类型给予不同的数据set跟get方法
instance.setupState = proxyRefs(setupResult)
if (__DEV__) {
exposeSetupStateOnRenderContext(instance)
}
} else if (__DEV__ && setupResult !== undefined) {
warn(
`setup() should return an object. Received: ${
setupResult === null ? 'null' : typeof setupResult
}`
)
}
// 完成组件的设置
finishComponentSetup(instance, isSSR)
}
export function finishComponentSetup(
// 组件实例
instance: ComponentInternalInstance,
// false
isSSR: boolean,
skipOptions?: boolean
) {
const Component = instance.type as ComponentOptions
if (__COMPAT__) {
// 兼容之前Vue2.*的渲染模式 render(h)作为参数的场景
convertLegacyRenderFn(instance)
if (__DEV__ && Component.compatConfig) {
validateCompatConfig(Component.compatConfig)
}
}
// template / render function normalization
// 模板 / 渲染函数规范化
// could be already set when returned from setup()
// 如果没有渲染函数,也就是第一优先级为渲染函数
if (!instance.render) {
// only do on-the-fly compile if not in SSR - SSR on-the-fly compliation
// 如果不是在SSR-SSR动态编译中,则只进行动态编译
// is done by server-renderer
/*
* 不是服务端渲染
*。 而且存在全局编译器(Tips:这里的compile来源于 packages/vue/src/index.ts 的 compileToFunction)
*。 并且组件具体配置项目中不包含render函数
*/
if (!isSSR && compile && !Component.render) {
const template =
(__COMPAT__ &&
instance.vnode.props &&
instance.vnode.props['inline-template']) ||
// Vue3.*主要走下面这个,上面主要是兼容Vue2.* inline-template 模版功能
Component.template
if (template) {
if (__DEV__) {
startMeasure(instance, `compile`)
}
const {
// 用于判断模版中的自定义标签,已经废弃了,合并到compilerOptions中
isCustomElement,
// 配置运行时编译器的选项,包括标签,模版值渲染的这一类定义
compilerOptions
} = instance.appContext.config
const {
// 模版值渲染的配置项
// 估计也是废弃了应为也是合并到了compilerOptions中
delimiters,
compilerOptions: componentCompilerOptions
} =Component
// 合并编译配置项
const finalCompilerOptions: CompilerOptions = extend(
extend(
{
isCustomElement,
delimiters
},
compilerOptions
),
componentCompilerOptions
)
if (__COMPAT__) {
// pass runtime compat config into the compiler
// compatConfig 配置项目 https://v3.cn.vuejs.org/guide/migration/migration-build.html#%E5%9F%BA%E4%BA%8E%E5%8D%95%E4%B8%AA%E7%BB%84%E4%BB%B6%E7%9A%84%E9%85%8D%E7%BD%AE
finalCompilerOptions.compatConfig = Object.create(globalCompatConfig)
if (Component.compatConfig) {
extend(finalCompilerOptions.compatConfig, Component.compatConfig)
}
}
/*
* 通过模版以及finalCompilerOptions 还有 compile 输出渲染函数 具体看向 packages/compiler-dom/src/index.ts
* compile的注册以及内部流程我们放到后面去看, 根据类型,可以知道会返回一个 CompileFunction 类型的函数
*/
Component.render = compile(template, finalCompilerOptions)
if (__DEV__) {
endMeasure(instance, `compile`)
}
}
}
// 类型转换以及判断是否存在render函数
instance.render = (Component.render || NOOP) as InternalRenderFunction
// for runtime-compiled render functions using `with` blocks, the render
// proxy used needs a different `has` handler which is more performant and
// also only allows a whitelist of globals to fallthrough.
if (installWithProxy) {
installWithProxy(instance)
}
}
// support for 2.x options
// 这里主要是为了支持2.*的option的方式编写的代码
if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
// 设置自身的上下文为活跃上下文
setCurrentInstance(instance)
// 暂停追踪
pauseTracking()
// 初始化所有相关的option中的配置信息,
applyOptions(instance)
// 重启追踪
resetTracking()
// 推出上下文栈
unsetCurrentInstance()
}
}
思考:这里可以发现关键点在于compiler(...) 以及 applyOptions(...),但是compiler涉及到较多的内容,所以我们这里优先看applyOptions 然后 后面的篇幅在介绍compiler
// runtime-core/src/componentOptions.ts
/**
* 支持 2.* vue option 初始化的关键函数
* @param {ComponentInternalInstance} instance
*/
export function applyOptions(instance: ComponentInternalInstance) {
// 解析基本的component option
const options = resolveMergedOptions(instance)
// 上文有提及的上下文代理
const publicThis = instance.proxy! as any
// 上下文原对象
const ctx = instance.ctx
// do not cache property access on public proxy during state initialization
// 是否要缓存资源标识,这个主要运用于上下文代理的取值判断
shouldCacheAccess = false
// call beforeCreate first before accessing other options since
// the hook may mutate resolved options (#2791)
// 如果有beforeCreate的声明周期函数,那么调用对应的声明周期
if (options.beforeCreate) {
callHook(options.beforeCreate, instance, LifecycleHooks.BEFORE_CREATE)
}
const {
// state
data: dataOptions,
computed: computedOptions,
methods,
watch: watchOptions,
provide: provideOptions,
inject: injectOptions,
// lifecycle
created,
beforeMount,
mounted,
beforeUpdate,
updated,
activated,
deactivated,
beforeDestroy,
beforeUnmount,
destroyed,
unmounted,
render,
renderTracked,
renderTriggered,
errorCaptured,
serverPrefetch,
// public API
expose,
inheritAttrs,
// assets
components,
directives,
filters
} = options
const checkDuplicateProperties = __DEV__ ? createDuplicateChecker() : null
if (__DEV__) {
const [propsOptions] = instance.propsOptions
if (propsOptions) {
for (const key in propsOptions) {
checkDuplicateProperties!(OptionTypes.PROPS, key)
}
}
}
// options initialization order (to be consistent with Vue 2):
// - props (already done outside of this function)
// - inject
// - methods
// - data (deferred since it relies on `this` access)
// - computed
// - watch (deferred since it relies on `this` access)
// 解析 inject 方法,贴在下方
if (injectOptions) {
resolveInjections(
injectOptions,
ctx,
// null
checkDuplicateProperties,
instance.appContext.config.unwrapInjectedRef
)
}
// 解析 methods 对象
if (methods) {
for (const key in methods) {
const methodHandler = (methods as MethodOptions)[key]
if (isFunction(methodHandler)) {
ctx[key] = methodHandler.bind(publicThis)
}
}
}
if (dataOptions) {
// 首先要求option中的data必须是方法
const data = dataOptions.call(publicThis, publicThis)
if (!isObject(data)) {
warn(`data() should return an object.`)
} else {
// 通过响应API的 reactive 进行数据响应式管理
instance.data = reactive(data)
if (__DEV__) {
for (const key in data) {
checkDuplicateProperties!(OptionTypes.DATA, key)
// expose data on ctx during dev
if (key[0] !== '$' && key[0] !== '_') {
Object.defineProperty(ctx, key, {
configurable: true,
enumerable: true,
get: () => data[key],
set: NOOP
})
}
}
}
}
}
// 后续的资源需要缓存
shouldCacheAccess = true
// 如果存在自动计算配置
if (computedOptions) {
for (const key in computedOptions) {
const opt = (computedOptions as ComputedOptions)[key]
// 绑定get的执行上下文
const get = isFunction(opt)
? opt.bind(publicThis, publicThis)
: isFunction(opt.get)
? opt.get.bind(publicThis, publicThis)
: NOOP
if (__DEV__ && get === NOOP) {
warn(`Computed property "${key}" has no getter.`)
}
// 如果存在Set配置就绑定执行上下文
const set =
!isFunction(opt) && isFunction(opt.set)
? opt.set.bind(publicThis)
: __DEV__
? () => {
warn(
`Write operation failed: computed property "${key}" is readonly.`
)
}
: NOOP
// 通过响应API的computed进行处理
const c = computed({
get,
set
})
Object.defineProperty(ctx, key, {
enumerable: true,
configurable: true,
get: () => c.value,
set: v => (c.value = v)
})
if (__DEV__) {
checkDuplicateProperties!(OptionTypes.COMPUTED, key)
}
}
}
/**
* 基于option.watch进行watch的初始化
*/
if (watchOptions) {
for (const key in watchOptions) {
// 传入解析以及 通过响应API的watch进行初始化
createWatcher(watchOptions[key], ctx, publicThis, key)
}
}
/**
* 基于option.provide进行provide的初始化
*/
if (provideOptions) {
const provides = isFunction(provideOptions)
? provideOptions.call(publicThis)
: provideOptions
Reflect.ownKeys(provides).forEach(key => {
// 通过响应API的provide进行处理
provide(key, provides[key])
})
}
// created生命周期调用,完成状态的初始化后
if (created) {
callHook(created, instance, LifecycleHooks.CREATED)
}
// 声明周期调用的语法糖
function registerLifecycleHook(
register: Function,
hook?: Function | Function[]
) {
if (isArray(hook)) {
hook.forEach(_hook => register(_hook.bind(publicThis)))
} else if (hook) {
register((hook as Function).bind(publicThis))
}
}
/*
* 生命周期,目前也是通过具体的API方法进行调用,在这里先对声明周期方法进行挂载,绑定对应上下文
*/
registerLifecycleHook(onBeforeMount, beforeMount)
registerLifecycleHook(onMounted, mounted)
registerLifecycleHook(onBeforeUpdate, beforeUpdate)
registerLifecycleHook(onUpdated, updated)
registerLifecycleHook(onActivated, activated)
registerLifecycleHook(onDeactivated, deactivated)
registerLifecycleHook(onErrorCaptured, errorCaptured)
registerLifecycleHook(onRenderTracked, renderTracked)
registerLifecycleHook(onRenderTriggered, renderTriggered)
registerLifecycleHook(onBeforeUnmount, beforeUnmount)
registerLifecycleHook(onUnmounted, unmounted)
registerLifecycleHook(onServerPrefetch, serverPrefetch)
if (__COMPAT__) {
if (
beforeDestroy &&
softAssertCompatEnabled(DeprecationTypes.OPTIONS_BEFORE_DESTROY, instance)
) {
registerLifecycleHook(onBeforeUnmount, beforeDestroy)
}
if (
destroyed &&
softAssertCompatEnabled(DeprecationTypes.OPTIONS_DESTROYED, instance)
) {
registerLifecycleHook(onUnmounted, destroyed)
}
}
// vue3.2 新增的状态,在公共组件实例上可以访问的属性,其实有点类似在class中声明 public的感觉
if (isArray(expose)) {
if (expose.length) {
const exposed = instance.exposed || (instance.exposed = {})
expose.forEach(key => {
Object.defineProperty(exposed, key, {
get: () => publicThis[key],
set: val => (publicThis[key] = val)
})
})
} else if (!instance.exposed) {
instance.exposed = {}
}
}
// options that are handled when creating the instance but also need to be
// applied from mixins
// 如果配置项中有渲染方法,并且实例中的渲染方法为空函数时,那么就采用配置项中的渲染函数
if (render && instance.render === NOOP) {
instance.render = render as InternalRenderFunction
}
if (inheritAttrs != null) {
instance.inheritAttrs = inheritAttrs
}
// asset options.
if (components) instance.components = components as any
if (directives) instance.directives = directives
if (
__COMPAT__ &&
filters &&
isCompatEnabled(DeprecationTypes.FILTERS, instance)
) {
instance.filters = filters
}
}
export function resolveMergedOptions(
instance: ComponentInternalInstance
): MergedComponentOptions {
// 这里的type实际上还是rootComponent的配置项
const base = instance.type as ComponentOptions
// 取出mixins 跟 extends
const { mixins, extends: extendsOptions } = base
// 取出APP全局上下文配置项
const {
mixins: globalMixins,
optionsCache: cache,
config: { optionMergeStrategies }
} = instance.appContext
const cached = cache.get(base)
let resolved: MergedComponentOptions
// 是否有解析结果的缓存,如果没有
if (cached) {
resolved = cached
} else if (!globalMixins.length && !mixins && !extendsOptions) {
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.PRIVATE_APIS, instance)
) {
resolved = extend({}, base) as MergedComponentOptions
resolved.parent = instance.parent && instance.parent.proxy
resolved.propsData = instance.vnode.props
} else {
// 解析结果直接等于base 也就是 rootComponent的配置项
resolved = base as MergedComponentOptions
}
} else {
// 否则这里做配置项合并,下面还会针对不同的option采用不同的合并策略,感兴趣的可以看进去
resolved = {}
if (globalMixins.length) {
globalMixins.forEach(m =>
mergeOptions(resolved, m, optionMergeStrategies, true)
)
}
mergeOptions(resolved, base, optionMergeStrategies)
}
cache.set(base, resolved)
return resolved
}
/**
* 解析option配置项目中的inject内容区域
* @param {ComponentInjectOptions} injectOptions
* @param {any} ctx
* @param {} checkDuplicateProperties=NOOPasany
* @param {} unwrapRef=false
*/
export function resolveInjections(
// option的inject的配置项
injectOptions: ComponentInjectOptions,
// 组件实例的上下文
ctx: any,
// null
checkDuplicateProperties = NOOP as any,
unwrapRef = false
) {
// 如果是类似 ['name'] 定义的方式, 将会格式化成{ name: 'name' }
if (isArray(injectOptions)) {
injectOptions = normalizeInject(injectOptions)!
}
// 通过循环解析inject配置项,通过inject api方法进行初始化
for (const key in injectOptions) {
const opt = (injectOptions as ObjectInjectOptions)[key]
let injected: unknown
if (isObject(opt)) {
if ('default' in opt) {
injected = inject(
opt.from || key,
opt.default,
true /* treat default function as factory */
)
} else {
injected = inject(opt.from || key)
}
} else {
injected = inject(opt)
}
if (isRef(injected)) {
// TODO remove the check in 3.3
if (unwrapRef) {
Object.defineProperty(ctx, key, {
enumerable: true,
configurable: true,
get: () => (injected as Ref).value,
set: v => ((injected as Ref).value = v)
})
} else {
if (__DEV__) {
warn(
`injected property "${key}" is a ref and will be auto-unwrapped ` +
`and no longer needs \`.value\` in the next minor release. ` +
`To opt-in to the new behavior now, ` +
`set \`app.config.unwrapInjectedRef = true\` (this config is ` +
`temporary and will not be needed in the future.)`
)
}
ctx[key] = injected
}
} else {
ctx[key] = injected
}
if (__DEV__) {
checkDuplicateProperties!(OptionTypes.INJECT, key)
}
}
}
// 创建观察者对象
export function createWatcher(
raw: ComponentWatchOptionItem,
ctx: Data,
publicThis: ComponentPublicInstance,
key: string
) {
const getter = key.includes('.')
? createPathGetter(publicThis, key)
: () => (publicThis as any)[key]
if (isString(raw)) {
const handler = ctx[raw]
if (isFunction(handler)) {
watch(getter, handler as WatchCallback)
} else if (__DEV__) {
warn(`Invalid watch handler specified by key "${raw}"`, handler)
}
} else if (isFunction(raw)) {
watch(getter, raw.bind(publicThis))
} else if (isObject(raw)) {
if (isArray(raw)) {
raw.forEach(r => createWatcher(r, ctx, publicThis, key))
} else {
const handler = isFunction(raw.handler)
? raw.handler.bind(publicThis)
: (ctx[raw.handler] as WatchCallback)
if (isFunction(handler)) {
watch(getter, handler, raw)
} else if (__DEV__) {
warn(`Invalid watch handler specified by key "${raw.handler}"`, handler)
}
}
} else if (__DEV__) {
warn(`Invalid watch option: "${key}"`, raw)
}
}
思考:那么实际上从applyOptions看来,在Vue3.*的option实际上可以理解为语法糖,主要是为了兼容2.*的的用户习惯,在这个方法中,大量的使用了响应式API以及生命周期API,那么为了保证思维的连续性,以及保证现场的还原情况,我们先来看到在响应式api中做了什么,其中我们用reactive(...)作为我们
reactivity
reactive
这里为了方便DEBUG数据,我将main.js
文件中的配置修改如下
// main.js
import { createApp } from 'vue/dist/vue.esm-bundler.js'
import HelloWorld from './components/HelloWorld.vue'
const option = {
name: 'App',
data() {
return {
a: 1
}
},
template: `
<img alt="Vue logo" src="./assets/logo.png">
<div>{{a}}</div>
<HelloWorld></HelloWorld>
`,
components: {
HelloWorld
}
}
createApp(option).mount('#app')
回到vue-next
的源码项目中看到
// reactivity/src/reactive.ts
// 代理对象集合
export const reactiveMap = new WeakMap<Target, any>()
// 响应式api reactive 方法
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
return createReactiveObject(
// 具体代理对象
target,
// 是否只读,createReactiveObject 同时也设计 isReadonly api的构造所以提供相关配置项
false,
// 第一类拦截方法
mutableHandlers,
// 第二类拦截方法
mutableCollectionHandlers,
reactiveMap
)
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// 仅针对typeof是object的对象使用reactive
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
// 如果对象已经是代理对象并且不是只读对象,那就将它直接放回
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
//
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only a whitelist of value types can be observed.
/*
* 检查对象类型,如果类型不属于第一类或者第二类,那么就直接返回
* 为什么要区分一类二类呢? 那么这个就直接跟数据类型的劫持方式有些关系
* switch (rawType) {
* case 'Object':
* case 'Array':
* return TargetType.COMMON
* case 'Map':
* case 'Set':
* case 'WeakMap':
* case 'WeakSet':
* return TargetType.COLLECTION
* default:
* return TargetType.INVALID
* }
* 这里解释一下为什么要分两个类型区分拦截
* 正常像是Obj或者arr这种类型的可以直接拦截 get set来进行操作
* 但是Set,Map这一类的没有办法直接用Proxy拦截到add set has的方法
* 所以换了一个方式是拦截方法获取来进行拦截操作,通过get拦截实例方法获取来进行拦截
*/
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy(
target,
// 判断类型并采用对用拦截,这里由于我们配置项中配置的返回值是plain object那命中 baseHandlers
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
const get = /*#__PURE__*/ createGetter()
const set = /*#__PURE__*/ createSetter()
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// 关键字判断 isReactive
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
// 关键字判断 readonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (
// 关键字判断是否已经被代理
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target
}
const targetIsArray = isArray(target)
// 如果是数组,并且命中重写方法范围,返回绑定上下文的执行方法
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
const res = Reflect.get(target, key, receiver)
// 如果命中特殊不需要触发响应式的关键节点也直接返回
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
if (!isReadonly) {
// 设置追踪时间,也就是采集当前副作用上下文
track(target, TrackOpTypes.GET, key)
}
if (shallow) {
return res
}
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : res
}
// 如果取出的值是对象,那么也要增加响应式控制
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
// 代理taget的proxy对象
receiver: object
): boolean {
let oldValue = (target as any)[key]
if (!shallow && !isReadonly(value)) {
value = toRaw(value)
oldValue = toRaw(oldValue)
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
// 如果值是ref
oldValue.value = value
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
// 是否存在这个KEY
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
// 设置对象数据
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
// 触发事件,我理解也就是触发副作用上下文的事件
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()
function createArrayInstrumentations() {
const instrumentations: Record<string, Function> = {}
// instrument identity-sensitive Array methods to account for possible reactive
// values
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
const arr = toRaw(this) as any
for (let i = 0, l = this.length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + '')
}
// we run the method using the original args first (which may be reactive)
const res = arr[key](...args)
if (res === -1 || res === false) {
// if that didn't work, run it again using raw values.
return arr[key](...args.map(toRaw))
} else {
return res
}
}
})
// instrument length-altering mutation methods to avoid length being tracked
// which leads to infinite loops in some cases (#2137)
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
pauseTracking()
const res = (toRaw(this) as any)[key].apply(this, args)
resetTracking()
return res
}
})
return instrumentations
}
// 切换文件 reactivity/src/effect.ts
// 修改了一下文件顺序
const effectStack: ReactiveEffect[] = []
let activeEffect: ReactiveEffect | undefined
let shouldTrack = true
const trackStack: boolean[] = []
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions
): ReactiveEffectRunner {
if ((fn as ReactiveEffectRunner).effect) {
fn = (fn as ReactiveEffectRunner).effect.fn
}
const _effect = new ReactiveEffect(fn)
if (options) {
extend(_effect, options)
if (options.scope) recordEffectScope
(_effect, options.scope)
}
if (!options || !options.lazy) {
_effect.run()
}
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner
}
export function stop(runner: ReactiveEffectRunner) {
runner.effect.stop()
}
export function pauseTracking() {
trackStack.push(shouldTrack)
shouldTrack = false
}
export function enableTracking() {
trackStack.push(shouldTrack)
shouldTrack = true
}
export function resetTracking() {
const last = trackStack.pop()
shouldTrack = last === undefined ? true : last
}
// 追踪
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 如果没有可追踪对象了那就返回
if (!isTracking()) {
return
}
// 取出当前对象的depsMap 依赖图谱
// key是对象
// value是一个Map
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 取出当前Key值的依赖Set
// key是key
// value是Set 值为 ReactiveEffect
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
/*
* 当前的activeEffect 是 ReactiveEffect类型
* 其中的fn是 componentUpdateFn(...) 也就是
* 在patch中 setupRenderEffect(...)中声明的函数,我们在后面会分析
*/
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
// createDep, wasTracked, newTracked 来源于 reactivity/src/dep.ts 为了方便查看我就粘贴过来了
// trackOpBit 在effect中 ReactiveEffect run时会被赋值数据
export let trackOpBit = 1
export const createDep = (effects?: ReactiveEffect[]): Dep => {
const dep = new Set<ReactiveEffect>(effects) as Dep
dep.w = 0
dep.n = 0
return dep
}
export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0
export const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0
const initDepMarkers = ({ deps }) => {
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].w |= trackOpBit; // set was tracked
}
}
};
// 回到effect.js文件
export function isTracking() {
return shouldTrack && activeEffect !== undefined
}
export function trackEffects(
// value是Set 值为 ReactiveEffect
dep: Dep,
// undefined
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
let shouldTrack = false
1 <= 30
if (effectTrackDepth <= maxMarkerBits) {
// trackOpBit 此时运行时为 2,
if (!newTracked(dep)) {
// 按位赋值
// 比如 dep.n 为 0000
// 比如 trackOpBit 为 0010
// 结果就是为 0010 十进制转换后为 2
dep.n |= trackOpBit // set newly tracked
// true
shouldTrack = !wasTracked(dep)
}
} else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect!)
}
// 应该追踪
if (shouldTrack) {
// 在dep种增加响应效应
dep.add(activeEffect!)
// 在当前活跃的响应效应种增加当前依赖
activeEffect!.deps.push(dep)
if (__DEV__ && activeEffect!.onTrack) {
activeEffect!.onTrack(
Object.assign(
{
effect: activeEffect!
},
debuggerEventExtraInfo
)
)
}
}
}
// 触发, 这里为了调试数据变化带来的变动,我在页面上加了按钮修改a的数据 把 1 变成 2
export function trigger(
// { a: 2 }
target: object,
// set
type: TriggerOpTypes,
// a
key?: unknown,
// 2
newValue?: unknown,
// 1
oldValue?: unknown,
// null
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
deps.push(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
// 取出收集的Set<ReactiveEffect>推入队列
deps.push(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
}
const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
if (deps.length === 1) {
if (deps[0]) {
if (__DEV__) {
triggerEffects(deps[0], eventInfo)
} else {
// 调用 triggerEffects 的方法
triggerEffects(deps[0])
}
}
} else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
if (__DEV__) {
triggerEffects(createDep(effects), eventInfo)
} else {
triggerEffects(createDep(effects))
}
}
}
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
for (const effect of isArray(dep) ? dep : [...dep]) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
/*
* 如果构建时存在scheduler 那么就调用 scheduler,否则就调用 run方法
* 我们跟着当前逻辑走5-6层的函数栈后还是来到了effect.run()方法执行,于是我们看下面执行过程中发生了什么
*/
if (effect.scheduler) {
effect.scheduler()
} else {
// 不然就遍历run方法,run中实际调度的就是 也就是实际的注册方法
effect.run()
}
}
}
}
export class ReactiveEffect<T = any> {
active = true
deps: Dep[] = []
// can be attached after creation
computed?: boolean
allowRecurse?: boolean
onStop?: () => void
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope | null
) {
// 绑定scope
recordEffectScope(this, scope)
}
run() {
if (!this.active) {
return this.fn()
}
if (!effectStack.includes(this)) {
try {
// 却换当前活跃响应上下文
effectStack.push((activeEffect = this))
// 开始追踪
enableTracking()
trackOpBit = 1 << ++effectTrackDepth
1 <= 30
if (effectTrackDepth <= maxMarkerBits) {
// 把Dep中的w设置为2
initDepMarkers(this)
} else {
cleanupEffect(this)
}
// 执行具体函数
return this.fn()
} finally {
1 <= 30
if (effectTrackDepth <= maxMarkerBits) {
// 设置Dep为 n: 0, w: 0
finalizeDepMarkers(this)
}
// 还原trackOpBit
trackOpBit = 1 << --effectTrackDepth
// 还原追踪数据
resetTracking()
// 弹出副作用栈
effectStack.pop()
const n = effectStack.length
// 如果栈中还有那么调整为上一个,预计继续执行上一个站内的run方法,这个我们在后续有机会再文章
activeEffect = n > 0 ? effectStack[n - 1] : undefined
}
}
}
stop() {
if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
}
- trackEffects的函数调用栈
思考: 那么基于我们这个Effect触发的追踪以及了解,我们回到最上方开始看mountComponent的代码处,继续去了解setupRenderEffect(...),记住此时我们的instance 已经完成了状态初始化,代理的设置,但是我们刚了解的反应式api的get过程实际上还是没有跑的,所以下文中的render过程中肯定会涉及到track的过程,那么我们就可以直接了解发生了什么事情。
runtime-core(后半程)
挂载副作用以及渲染过程(不含编译)
// runtime-core/src/renderer.ts patch 函数上下文
const setupRenderEffect: SetupRenderEffectFn = (
// 组件实例
instance,
// 组件初始化的Vnode节点
initialVNode,
// 组件要挂载的上下文
container,
// 锚(暂无头绪) 默认null
anchor,
// 父级悬念(暂无头绪) 默认null
parentSuspense,
// 是否是SVG 默认false
isSVG,
// 是否开启优化 默认false
optimized
) => {
componentUpdateFn = () => {
// 放在下面同级方便查看
}
// create reactive effect for rendering
const effect = (instance.effect = new ReactiveEffect(
// run()
componentUpdateFn,
// scheduler() queueJob我个人简单理解就是做了一个任务队列,主要是通过JS的事件循环机制中的微任务来把可非阻塞任务,推入队列中,让js-core慢慢消费
() => queueJob(instance.update),
// 组件副作用域,这里的作用主要是吧,新构建的reactiveEffect推入effectScope中
instance.scope // track it in component's effect scope
))
// 组件更新方法实际上就是包裹effect的componentUpdateFn,这个地方上文也有涉及到过
const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
update.id = instance.uid
// allowRecurse
// #1801, #2043 component render effects should allow recursive updates
toggleRecurse(instance, true)
// 简化部分调试代码
...
// 我们看到 componentUpdateFn
update()
}
function toggleRecurse(
{ effect, update }: ComponentInternalInstance,
allowed: boolean
) {
effect.allowRecurse = update.allowRecurse = allowed
}
思考:那么实际上这里就是为componentUpdateFn(...)方法包裹一层effect方便reactivity在track的时候可以采集到,那么我们来看一下在componentUpdateFn(...)中发生了什么
// runtime-core/src/renderer.ts setupRenderEffect 函数上下文
const componentUpdateFn = () => {
// 挂载DOM节点标记判断
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
const { bm, m, parent } = instance
// 判断当前节点是否为异步组件包装
const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)
// 设置递归为false
toggleRecurse(instance, false)
// beforeMount hook
// invokeArrayFns 为方法执行的语法糖,主要用于执行方法数组
if (bm) {
invokeArrayFns(bm)
}
// onVnodeBeforeMount
// 执行Vnode中的bm方法
if (
!isAsyncWrapperVNode &&
(vnodeHook = props && props.onVnodeBeforeMount)
) {
invokeVNodeHook(vnodeHook, parent, initialVNode)
}
// 派发beforeMount事件
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
instance.emit('hook:beforeMount')
}
// 打开事件递归开关
toggleRecurse(instance, true)
if (el && hydrateNode) {
// vnode has adopted host node - perform hydration instead of mount.
const hydrateSubTree = () => {
// 对应子树的 vnode
instance.subTree = renderComponentRoot(instance)
hydrateNode!(
el as Node,
instance.subTree,
instance,
parentSuspense,
null
)
}
if (isAsyncWrapperVNode) {
;(initialVNode.type as ComponentOptions).__asyncLoader!().then(
// note: we are moving the render call into an async callback,
// which means it won't track dependencies - but it's ok because
// a server-rendered async wrapper is already in resolved state
// and it will never need to change.
() => !instance.isUnmounted && hydrateSubTree()
)
} else {
hydrateSubTree()
}
} else {
/*
* 对应子树的 vnode
* 同时renderComponentRoot也调用了render方法,在render的过程中,肯定也是触发track的过程
* 数据结构贴在下方
*/
const subTree = (instance.subTree = renderComponentRoot(instance))
/*
* 对于生成的vnode节点继续patch
* 生成对应的dom节点并且挂在到container中,那么实际上在这个步骤里面我们就回去渲染<img/>,<div/>,<HelloWorld/>
* 这里在下方代码中我们用img的生成过程
*/
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
initialVNode.el = subTree.el
}
// mounted hook 生命周期钩子
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
// onVnodeMounted
if (
!isAsyncWrapperVNode &&
(vnodeHook = props && props.onVnodeMounted)
) {
const scopedInitialVNode = initialVNode
queuePostRenderEffect(
() => invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode),
parentSuspense
)
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
queuePostRenderEffect(
() => instance.emit('hook:mounted'),
parentSuspense
)
}
// activated hook for keep-alive roots.
// #1742 activated hook must be accessed after first render
// since the hook may be injected by a child keep-alive
if (initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
instance.a && queuePostRenderEffect(instance.a, parentSuspense)
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
queuePostRenderEffect(
() => instance.emit('hook:activated'),
parentSuspense
)
}
}
// 完成挂载
instance.isMounted = true
// #2458: deference mount-only object parameters to prevent memleaks
initialVNode = container = anchor = null as any
} else {
// updateComponent
// This is triggered by mutation of component's own state (next: null)
// OR parent calling processComponent (next: VNode)
let { next, bu, u, parent, vnode } = instance
let originNext = next
let vnodeHook: VNodeHook | null | undefined
// Disallow component effect recursion during pre-lifecycle hooks.
toggleRecurse(instance, false)
if (next) {
next.el = vnode.el
updateComponentPreRender(instance, next, optimized)
} else {
next = vnode
}
// beforeUpdate hook
if (bu) {
invokeArrayFns(bu)
}
// onVnodeBeforeUpdate
if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
invokeVNodeHook(vnodeHook, parent, next, vnode)
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
instance.emit('hook:beforeUpdate')
}
toggleRecurse(instance, true)
// render
// 新节点
const nextTree = renderComponentRoot(instance)
// 老节点
const prevTree = instance.subTree
instance.subTree = nextTree
patch(
prevTree,
nextTree,
// parent may have changed if it's in a teleport
hostParentNode(prevTree.el!)!,
// anchor may have changed if it's in a fragment
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG
)
next.el = nextTree.el
if (originNext === null) {
// self-triggered update. In case of HOC, update parent component
// vnode el. HOC is indicated by parent instance's subTree pointing
// to child component's vnode
updateHOCHostEl(instance, nextTree.el)
}
// updated hook
if (u) {
queuePostRenderEffect(u, parentSuspense)
}
// onVnodeUpdated
if ((vnodeHook = next.props && next.props.onVnodeUpdated)) {
queuePostRenderEffect(
() => invokeVNodeHook(vnodeHook!, parent, next!, vnode),
parentSuspense
)
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
queuePostRenderEffect(
() => instance.emit('hook:updated'),
parentSuspense
)
}
}
}
- subTree 的数据结构
思考:那么整体来说首次挂载就结束了,但是目前我们还没有看到的两块东西分别是,patch处理element节点以及compiler函数的内部细节,我们先来了解patch处理img标签的过程吧,compiler我认为可以后续专门出一个专题来讲解
// runtime-core/src/renderer.ts patch
const patch: PatchFn = (
// null
n1,
// img 的 vnode 下方有图
n2,
// DOM #app
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
if (n1 === n2) {
return
}
// patching & not same type, unmount old tree
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
const { type, ref, shapeFlag } = n2
switch (type) {
case Text:
processText(n1, n2, container, anchor)
break
case Comment:
processCommentNode(n1, n2, container, anchor)
break
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
default:
// 实际上会命中ELEMENT的渲染过程
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}
const processElement = (
// null
n1: VNode | null,
// img的vnode
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
isSVG = isSVG || (n2.type as string) === 'svg'
// 我们由于是首次渲染所以命中mountElement方法
if (n1 == null) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
const mountElement = (
// img vnode
vnode: VNode,
// dom #app
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
let el: RendererElement
let vnodeHook: VNodeHook | undefined | null
const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode
if (
!__DEV__ &&
vnode.el &&
hostCloneNode !== undefined &&
patchFlag === PatchFlags.HOISTED
) {
// If a vnode has non-null el, it means it's being reused.
// Only static vnodes can be reused, so its mounted DOM nodes should be
// exactly the same, and we can simply do a clone here.
// only do this in production since cloned trees cannot be HMR updated.
el = vnode.el = hostCloneNode(vnode.el)
} else {
// 根据vnode创建元素节点, 这里的hostCreateElement就是在最开始我们设置option中的nodeOps, 下方贴出nodeOps相关代码
el = vnode.el = hostCreateElement(
// img
vnode.type as string,
// false
isSVG,
props && props.is,
/*
* { alt: "Vue logo", src: "./assets/logo.png" }
*/
props
)
// mount children first, since some props may rely on child content
// being already rendered, e.g. `<select value>`
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
vnode.children as VNodeArrayChildren,
el,
null,
parentComponent,
parentSuspense,
isSVG && type !== 'foreignObject',
slotScopeIds,
optimized
)
}
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'created')
}
// props
if (props) {
for (const key in props) {
if (key !== 'value' && !isReservedProp(key)) {
// 处理DOM元素对应的attrs
hostPatchProp(
el,
key,
null,
props[key],
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
/**
* Special case for setting value on DOM elements:
* - it can be order-sensitive (e.g. should be set *after* min/max, #2325, #4024)
* - it needs to be forced (#1471)
* #2353 proposes adding another renderer option to configure this, but
* the properties affects are so finite it is worth special casing it
* here to reduce the complexity. (Special casing it also should not
* affect non-DOM renderers)
*/
if ('value' in props) {
hostPatchProp(el, 'value', null, props.value)
}
if ((vnodeHook = props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parentComponent, vnode)
}
}
// scopeId
setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent)
}
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
Object.defineProperty(el, '__vnode', {
value: vnode,
enumerable: false
})
Object.defineProperty(el, '__vueParentComponent', {
value: parentComponent,
enumerable: false
})
}
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
}
// #1583 For inside suspense + suspense not resolved case, enter hook should call when suspense resolved
// #1689 For inside suspense + suspense resolved case, just call it
const needCallTransitionHooks =
(!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) &&
transition &&
!transition.persisted
if (needCallTransitionHooks) {
transition!.beforeEnter(el)
}
// 将渲染后的节点插入对应的上下文也就#app中,如果跟我一起debug的小伙伴实际上就回发现这里已经可以在 chrome debug的 Elements中已经有了对应的树节点了
hostInsert(el, container, anchor)
if (
(vnodeHook = props && props.onVnodeMounted) ||
needCallTransitionHooks ||
dirs
) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
needCallTransitionHooks && transition!.enter(el)
dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
}, parentSuspense)
}
}
// 切换文件到runtime-dom/src/nodeOps.ts
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
// 在指定的子节点前添加相关节点的方法
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},
// 从指定的父列表中删除一个子节点
remove: child => {
const parent = child.parentNode
if (parent) {
parent.removeChild(child)
}
},
// 创建指定的DOM节点
createElement: (tag, isSVG, is, props): Element => {
const el = isSVG
? doc.createElementNS(svgNS, tag)
: doc.createElement(tag, is ? { is } : undefined)
if (tag === 'select' && props && props.multiple != null) {
;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
}
return el
},
// 创建文本节点
createText: text => doc.createTextNode(text),
// 创建注释节点
createComment: text => doc.createComment(text),
// 设置节点中的值 一般 适用于设置文本节点跟注释节点
setText: (node, text) => {
node.nodeValue = text
},
// 设置节点中的文本内容(这里感觉与setText方法上有些雷同,但是设置的对象不同,textContent主要针对一般的DOM对象)
setElementText: (el, text) => {
el.textContent = text
},
// 返回父节点
parentNode: node => node.parentNode as Element | null,
// 返回同层级的下一个兄弟节点
nextSibling: node => node.nextSibling,
// 查询
querySelector: selector => doc.querySelector(selector),
// 设置scope-id到节点的attrs上
setScopeId(el, id) {
el.setAttribute(id, '')
},
// 拷贝DOM树
cloneNode(el) {
const cloned = el.cloneNode(true)
// #3072
// - in `patchDOMProp`, we store the actual value in the `el._value` property.
// - normally, elements using `:value` bindings will not be hoisted, but if
// the bound value is a constant, e.g. `:value="true"` - they do get
// hoisted.
// - in production, hoisted nodes are cloned when subsequent inserts, but
// cloneNode() does not copy the custom property we attached.
// - This may need to account for other custom DOM properties we attach to
// elements in addition to `_value` in the future.
if (`_value` in el) {
;(cloned as any)._value = (el as any)._value
}
return cloned
},
// __UNSAFE__
// Reason: innerHTML.
// Static content here can only come from compiled templates.
// As long as the user only uses trusted templates, this is safe.
insertStaticContent(content, parent, anchor, isSVG) {
// <parent> before | first ... last | anchor </parent>
const before = anchor ? anchor.previousSibling : parent.lastChild
let template = staticTemplateCache.get(content)
if (!template) {
const t = doc.createElement('template')
t.innerHTML = isSVG ? `<svg>${content}</svg>` : content
template = t.content
if (isSVG) {
// remove outer svg wrapper
const wrapper = template.firstChild!
while (wrapper.firstChild) {
template.appendChild(wrapper.firstChild)
}
template.removeChild(wrapper)
}
staticTemplateCache.set(content, template)
}
parent.insertBefore(template.cloneNode(true), anchor)
return [
// first
before ? before.nextSibling! : parent.firstChild!,
// last
anchor ? anchor.previousSibling! : parent.lastChild!
]
}
}
- img的vnode节点
总结: 那么就此我们大致了解了整体的构建过程,当然留下了下方的TODO,后续作者尽快补充
- complier 函数内部的运行以及实现
- reactivity的多层副作用逻辑以及scoped的作用
当然vue3.*源码中还有更多的细节需要大家一起去看去讨论,我这里只是记录自己的一些愚见,有问题也希望大家看完后给我留言