1.环境搭建
安装&构建
git clone git@github.com:vuejs/core.git
pnpm i
pnpm build
构建后,在packages/vue/dist 会多出vue.global.js和vue.global.js.map 。我们可以基于这两个文件去调试源码。
官方例子
我们可以看到很多经典的demo:如todolist (这里 classic文件夹用的是传统的option模式,composition文件夹则是用compositionAPI实现。可以对比两种的差异。)
我们可以看到所有demo使用的vue路径就是刚才编译好的packages/vue/distvue.global.js
新增测试页面
1.初次渲染
在packages/vue/examples/composition 新增一个test.html
<script src="../../dist/vue.global.js"></script>
<div id="demo">
--{{val}}--
</div>
<script>
debugger
const app = Vue.createApp({
setup() {
const val = ref("oooo")
return {
val,
}
}
})
app.mount('#demo')
</script>
<style>
</style>
- 这里我们定义一个响应式对象val,
- 绑定到页面显示
调试
-
使用live server插件,右键运行这个test.html 页面
-
打开控制台,刷新开发debugger
在源码,我们通过单步执行,慢慢断点进入具体的逻辑
我们可以全局搜索processText(n1, n2, container, anchor) 找到最终实例化挂载替换文本的地方的代码,加上断点。
通过堆栈,我们可以看出第一次运行的结果
- app.mount (index.ts)
- mount 挂载 (apiCreateApp.ts)
- render 渲染 (renderer.ts)
- patch 对比更新 (renderer.ts)
- processComponent 对比更新 (renderer.ts)
- mountComponent 对比更新 (renderer.ts)
- setupRenderEffect (renderer.ts)
- instance.update (renderer.ts)
- ReactiveEffect.run (effect.ts)
- componentUpdateFn (renderer.ts)
- patch (renderer.ts) 这里执行processText挂载text内容
由此我们可以看出,需要学习的代码包括 apiCreateApp.ts和 renderer.ts
核心逻辑
- mountComponent最终只是执行 自定义setup方法,和组件状态初始化
- 通过effect 触发 update方法
- update方法里面执行patch
- patch的时候才真正渲染dom ,执行processText渲染text内容。
2.初次渲染 + 子组件渲染
在packages/vue/examples/composition 新增一个test2.html
<script src="../../dist/vue.global.js"></script>
<script type="text/x-template" id="app-template">
<p>子组件:{{message}} - {{state.a}}</p>
</script>
<script>
const { reactive, computed,ref,h } = Vue
const App = {
template: '#app-template',
props: {
message: String,
},
setup(props) {
const state = reactive({
a:"ggg"
})
return {
state,
}
}
}
</script>
<div id="demo">
--{{val}}--
<app :message="val" >
</app>
</div>
<script>
debugger
const app = Vue.createApp({
components: {
App
},
setup() {
const val = ref("oooo")
return {
val,
}
}
})
app.mount('#demo')
</script>
<style>
</style>
这里我们多加了一个子组件 App
使用上面方法开始看到
第二次,父节点+子节点
- app.mount (index.ts)
- mount (apiCreateApp.ts)
- render (renderer.ts)
- patch (renderer.ts)
- processComponent (renderer.ts)
- mountComponent (renderer.ts)
- setupRenderEffect (renderer.ts)
- instance.update (renderer.ts)
- ReactiveEffect.run (effect.ts)
- componentUpdateFn (renderer.ts)
- patch (renderer.ts)
加上
- processFragment (renderer.ts)
- mountChildren (renderer.ts) 这里循环patch2次,第一次:修改--{{val}}-- , 第二次:触发创建新的组件
- patch (第二次patch)
- processComponent
- mountComponent
- setupRenderEffect
- instance.update
- ReactiveEffect.run
- componentUpdateFn
- patch (这里处理最终挂载app组件对应的html到页面)
从代码中可以看出,多了processFragment + mountChildren的逻辑,当组件有子组件 会包裹一个Fragment
核心逻辑
- mountComponent最终只是执行 自定义setup方法,和组件状态初始化
- 通过effect 触发 update方法
- update方法里面执行patch
- patch的时候发现下面是app组件
- 执行render渲染组件
- 渲染一个Fragment
- 根据所有子元素,执行mountChildren,循环patch
- 循环第一次 执行processText 渲染修改--{{val}}--
- 循环第二次 触发创建新子组件,
- 新子组件又关联一个新的effect 触发 update方法
- update方法里面执行patch 渲染子组件创建
2.运行逻辑
例子1:完整源码逻辑
代码
<script src="../../dist/vue.global.js"></script>
<div id="demo">
--{{val}}--
</div>
<script>
debugger
const app = Vue.createApp({
setup() {
const val = ref("oooo")
return {
val,
}
}
})
app.mount('#demo')
</script>
<style>
</style>
流程图
关键的文件
- packages/runtime-dom/src/index.ts 入口 全局render对象 ,所有实际dom操作接口
- packages/runtime-core/src/renderer.ts 渲染 patch
- packages/reactivity/src/effect.ts 依赖收集与触发update方法
- packages/runtime-core/src/component.ts 组件setup实例化
关键方法
createApp
调用Vue.createApp实例化一个app , 使用app.mount('#demo') 进行挂载
先看createApp的实现,
- ensureRenderer().createApp(...args) 获取app
- 使用重写mount方法,让mount前先获取dom容器 packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
const { mount } = app // 这里重写了原来mount方法,让mount 延迟执行
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => { // 重写先执行的mount 主要做dom获取容器,做跨平台区分
const container = normalizeContainer(containerOrSelector) // normalizeContainer针对dom层 做元素访问
if (!container) return
const component = app._component
// clear content before mounting
container.innerHTML = ''
const proxy = mount(container, false, resolveRootNamespace(container)) // 这里是延迟mount后,在这里再被执行
if (container instanceof Element) {
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
return proxy
}
return app
}) as CreateAppFunction<Element>
//获取容器
function normalizeContainer(
container: Element | ShadowRoot | string,
): Element | null {
if (isString(container)) {
const res = document.querySelector(container) //主要做dom获取
return res
}
return container as any
}
- createApp 调用的是ensureRenderer().createApp(...args)
- ensureRenderer() 负责确认跨平台的渲染器,里面又调用了createRenderer 返回renderer,并且通过外部变量保存,通过闭包方式缓存起来。
packages/runtime-dom/src/index.ts
import { nodeOps } from './nodeOps'
import { patchProp } from './patchProp'
const rendererOptions = extend({ patchProp }, nodeOps) //这里的 extend = Object.assign
let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer
function ensureRenderer() {
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
)
}
- 传入重写nodeOps节点操作
- 传入重写pathProp属性操作 这两个属性用于跨平台实现节点的操作。
packages/runtime-dom/src/nodeOps.ts
import type { RendererOptions } from '@vue/runtime-core'
export const svgNS = 'http://www.w3.org/2000/svg'
export const mathmlNS = 'http://www.w3.org/1998/Math/MathML'
const doc = (typeof document !== 'undefined' ? document : null) as Document
const templateContainer = doc && /*#__PURE__*/ doc.createElement('template')
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)
}
},
createElement: (tag, namespace, is, props): Element => {
const el =
namespace === 'svg'
? doc.createElementNS(svgNS, tag)
: namespace === 'mathml'
? doc.createElementNS(mathmlNS, tag)
: is
? doc.createElement(tag, { is })
: doc.createElement(tag)
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
},
setElementText: (el, text) => {
el.textContent = text
},
parentNode: node => node.parentNode as Element | null,
nextSibling: node => node.nextSibling,
querySelector: selector => doc.querySelector(selector),
}
packages/runtime-dom/src/patchProp.ts
import type { RendererOptions } from '@vue/runtime-core'
type DOMRendererOptions = RendererOptions<Node, Element>
export const patchProp: DOMRendererOptions['patchProp'] = (
el,
key,
prevValue,
nextValue,
namespace,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren,
) => {
const isSVG = namespace === 'svg'
if (key === 'class') {
patchClass(el, nextValue, isSVG)
} else if (key === 'style') {
patchStyle(el, prevValue, nextValue)
} else if (isOn(key)) {
// ignore v-model listeners
if (!isModelListener(key)) {
patchEvent(el, key, prevValue, nextValue, parentComponent)
}
} else if (
key[0] === '.'
? ((key = key.slice(1)), true)
: key[0] === '^'
? ((key = key.slice(1)), false)
: shouldSetAsProp(el, key, nextValue, isSVG)
) {
patchDOMProp(
el,
key,
nextValue,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren,
)
if (
!el.tagName.includes('-') &&
(key === 'value' || key === 'checked' || key === 'selected')
) {
patchAttr(el, key, nextValue, isSVG, parentComponent, key !== 'value')
}
} else {
if (key === 'true-value') {
;(el as any)._trueValue = nextValue
} else if (key === 'false-value') {
;(el as any)._falseValue = nextValue
}
patchAttr(el, key, nextValue, isSVG, parentComponent)
}
}
createRenderer
createRenderer内部又调用baseCreateRenderer方法(两者都在一个文件),内部传入的RendererOptions就是nodeOps和patchProp实现的内容。
packages/runtime-core/src/renderer.ts
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement,
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
export interface RendererOptions<
HostNode = RendererNode,
HostElement = RendererElement,
> {
patchProp(
el: HostElement,
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
prevChildren?: VNode<HostNode, HostElement>[],
parentComponent?: ComponentInternalInstance | null,
parentSuspense?: SuspenseBoundary | null,
unmountChildren?: UnmountChildrenFn,
): void
insert(el: HostNode, parent: HostElement, anchor?: HostNode | null): void
remove(el: HostNode): void
createElement(
type: string,
namespace?: ElementNamespace,
isCustomizedBuiltIn?: string,
vnodeProps?: (VNodeProps & { [key: string]: any }) | null,
): HostElement
createText(text: string): HostNode
createComment(text: string): HostNode
setText(node: HostNode, text: string): void
setElementText(node: HostElement, text: string): void
parentNode(node: HostNode): HostElement | null
nextSibling(node: HostNode): HostNode | null
querySelector?(selector: string): HostElement | null
setScopeId?(el: HostElement, id: string): void
cloneNode?(node: HostNode): HostNode
insertStaticContent?(
content: string,
parent: HostElement,
anchor: HostNode | null,
namespace: ElementNamespace,
start?: HostNode | null,
end?: HostNode | null,
): [HostNode, HostNode]
}
baseCreateRenderer
我们可以看到baseCreateRenderer的代码真的长的吓死人 2k多行代码
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions,
): any {
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
createComment: hostCreateComment,
setText: hostSetText,
setElementText: hostSetElementText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
setScopeId: hostSetScopeId = NOOP,
insertStaticContent: hostInsertStaticContent,
} = options
// Note: functions inside this closure should use `const xxx = () => {}`
// style in order to prevent being inlined by minifiers.
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
namespace = undefined,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren,
) => {
}
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
}
const processCommentNode: ProcessTextOrCommentFn = (
n1,
n2,
container,
anchor,
) => {
}
const mountStaticNode = (
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
namespace: ElementNamespace,
) => {
// static nodes are only present when used with compiler-dom/runtime-dom
// which guarantees presence of hostInsertStaticContent.
;[n2.el, n2.anchor] = hostInsertStaticContent!(
n2.children as string,
container,
anchor,
namespace,
n2.el,
n2.anchor,
)
}
/**
* Dev / HMR only
*/
const patchStaticNode = (
n1: VNode,
n2: VNode,
container: RendererElement,
namespace: ElementNamespace,
) => {
// static nodes are only patched during dev for HMR
if (n2.children !== n1.children) {
const anchor = hostNextSibling(n1.anchor!)
// remove existing
removeStaticNode(n1)
// insert new
;[n2.el, n2.anchor] = hostInsertStaticContent!(
n2.children as string,
container,
anchor,
namespace,
)
} else {
n2.el = n1.el
n2.anchor = n1.anchor
}
}
const moveStaticNode = (
{ el, anchor }: VNode,
container: RendererElement,
nextSibling: RendererNode | null,
) => {
}
const removeStaticNode = ({ el, anchor }: VNode) => {
}
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
}
const mountElement = (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
}
const setScopeId = (
el: RendererElement,
vnode: VNode,
scopeId: string | null,
slotScopeIds: string[] | null,
parentComponent: ComponentInternalInstance | null,
) => {
}
const mountChildren: MountChildrenFn = (
children,
container,
anchor,
parentComponent,
parentSuspense,
namespace: ElementNamespace,
slotScopeIds,
optimized,
start = 0,
) => {
for (let i = start; i < children.length; i++) {
const child = (children[i] = optimized
? cloneIfMounted(children[i] as VNode)
: normalizeVNode(children[i]))
patch(
null,
child,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
}
}
const patchElement = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
}
// The fast path for blocks.
const patchBlockChildren: PatchBlockChildrenFn = (
oldChildren,
newChildren,
fallbackContainer,
parentComponent,
parentSuspense,
namespace: ElementNamespace,
slotScopeIds,
) => {
}
const processFragment = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
}
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
}
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
namespace: ElementNamespace,
optimized,
) => {
}
const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
const instance = (n2.component = n1.component)!
}
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
namespace: ElementNamespace,
optimized,
) => {
}
const updateComponentPreRender = (
instance: ComponentInternalInstance,
nextVNode: VNode,
optimized: boolean,
) => {
}
const patchChildren: PatchChildrenFn = (
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace: ElementNamespace,
slotScopeIds,
optimized = false,
) => {
}
const patchUnkeyedChildren = (
c1: VNode[],
c2: VNodeArrayChildren,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
}
// can be all-keyed or mixed
const patchKeyedChildren = (
c1: VNode[],
c2: VNodeArrayChildren,
container: RendererElement,
parentAnchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
}
const move: MoveFn = (
vnode,
container,
anchor,
moveType,
parentSuspense = null,
) => {
const { el, type, transition, children, shapeFlag } = vnode
}
const unmount: UnmountFn = (
vnode,
parentComponent,
parentSuspense,
doRemove = false,
optimized = false,
) => {
}
const remove: RemoveFn = vnode => {
}
const removeFragment = (cur: RendererNode, end: RendererNode) => {
}
const unmountComponent = (
instance: ComponentInternalInstance,
parentSuspense: SuspenseBoundary | null,
doRemove?: boolean,
) => {
}
const unmountChildren: UnmountChildrenFn = (
children,
parentComponent,
parentSuspense,
doRemove = false,
optimized = false,
start = 0,
) => {
}
const getNextHostNode: NextFn = vnode => {
}
let isFlushing = false
const render: RootRenderFunction = (vnode, container, namespace) => {
}
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate),
}
}
可以看到 该方法返回createApp由createAppAPI(render, hydrate)产生
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate),
}
createAppAPI
createAppAPI在
packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
render: RootRenderFunction<HostElement>,
hydrate?: RootHydrateFunction,
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
if (!isFunction(rootComponent)) {
rootComponent = extend({}, rootComponent)
}
if (rootProps != null && !isObject(rootProps)) {
rootProps = null
}
const context = createAppContext()
const installedPlugins = new WeakSet()
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) {
},
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) {
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(
rootContainer: HostElement,
isHydrate?: boolean,
namespace?: boolean | ElementNamespace,
): any {
if (!isMounted) {
const vnode = createVNode(rootComponent, rootProps) // 这里创建了一个根root的vnode
vnode.appContext = context
if (namespace === true) {
namespace = 'svg'
} else if (namespace === false) {
namespace = undefined
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer, namespace) // 关键渲染的地方
}
isMounted = true
app._container = rootContainer
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
return getComponentPublicInstance(vnode.component!)
}
},
unmount() {
if (isMounted) {
render(null, app._container)
},
provide(key, value) {
context.provides[key as string | symbol] = value
return app
},
runWithContext(fn) {
const lastApp = currentApp
currentApp = app
try {
return fn()
} finally {
currentApp = lastApp
}
},
})
return app
}
}
该代码包含了app里面常见的component,mount,directive,use,的初始化。
关键代码 render(vnode, rootContainer, namespace) ,rootContainer就是 #demo。
这里,当执行mount方法就会执行render开始渲染。也就是2k多行代码里面的render
render
代码如下
const render: RootRenderFunction = (vnode, container, namespace) => {
if (vnode == null) {//这里不为空 用的是上面创建的root vnode
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
//关键逻辑 执行了patch
patch(
container._vnode || null,
vnode,
container,
null,
null,
null,
namespace,
)
}
if (!isFlushing) {
isFlushing = true
flushPreFlushCbs()
flushPostFlushCbs()
isFlushing = false
}
container._vnode = vnode
}
const internals: RendererInternals = {
p: patch,
um: unmount,
m: move,
r: remove,
mt: mountComponent,
mc: mountChildren,
pc: patchChildren,
pbc: patchBlockChildren,
n: getNextHostNode,
o: options,
}
let hydrate: ReturnType<typeof createHydrationFunctions>[0] | undefined
let hydrateNode: ReturnType<typeof createHydrationFunctions>[1] | undefined
if (createHydrationFns) {
;[hydrate, hydrateNode] = createHydrationFns(
internals as RendererInternals<Node, Element>,
)
}
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate),
}
}
patch
下面是patch的内容
const patch: PatchFn = (
n1, //上一次vnode
n2, //这次vnode
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
namespace = undefined,
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, namespace)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, namespace)
}
break
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
//首次会进入渲染组件方法
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
internals,
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
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)
}
}
processComponent
首次渲染,进入processComponent,
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
n2.slotScopeIds = slotScopeIds
if (n1 == null) {//n1为空
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { // 没有keepalive
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
namespace,
optimized,
)
} else {
// 关键代码 执行首次挂载
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
optimized,
)
}
} else {
updateComponent(n1, n2, optimized)
}
}
mountComponent代码,关键是执行setupRenderEffect代码,针对单个组件进行设置依赖收集
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
namespace: ElementNamespace,
optimized,
) => {
// resolve props and slots for setup context
if (!(__COMPAT__ && compatMountInstance)) {
if (__DEV__) {
startMeasure(instance, `init`)
}
setupComponent(instance) //关键代码 执行自定义setup方法
if (__DEV__) {
endMeasure(instance, `init`)
}
}
if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
parentSuspense &&
parentSuspense.registerDep(instance, setupRenderEffect, optimized)
if (!initialVNode.el) {
const placeholder = (instance.subTree = createVNode(Comment))
processCommentNode(null, placeholder, container!, anchor)
}
} else {
//关键代码,进行设置依赖收集
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
namespace,
optimized,
)
}
if (__DEV__) {
popWarningContext()
endMeasure(instance, `mount`)
}
}
packages/runtime-core/src/component.ts
setupComponent方法 初始化 prop 和插槽等,并调用 setupStatefulComponent
setupStatefulComponent 主要执行自定义setup方法
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false,
) {
isSSR && setInSSRSetupState(isSSR)
const { props, children } = instance.vnode
const isStateful = isStatefulComponent(instance)
initProps(instance, props, isStateful, isSSR) // 初始化prop
initSlots(instance, children) // 初始化插槽
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR) // 关键代码
: undefined
isSSR && setInSSRSetupState(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
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
if (__DEV__) {
exposePropsOnRenderContext(instance)
}
// 2. call setup()
const { setup } = Component
if (setup) {
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
const reset = setCurrentInstance(instance)
pauseTracking()
const setupResult = callWithErrorHandling( //关键代码 执行自定义setup fn(),并保存返回值
setup, // 自定义setup方法
instance,
ErrorCodes.SETUP_FUNCTION,
[
__DEV__ ? shallowReadonly(instance.props) : instance.props,
setupContext,
],
)
resetTracking()
reset()
//处理异步情况
if (isPromise(setupResult)) {
...
} else {
handleSetupResult(instance, setupResult, isSSR)
}
} else {
finishComponentSetup(instance, isSSR)
}
}
setupRenderEffect
packages/runtime-core/src/renderer.ts
setupRenderEffect代码也很长,关键是new了一个ReactiveEffect方法,用于组件的依赖收集,每单响应数据变化时,触发调用定义的componentUpdateFn,也同时等价于 调用update() , effect.run() 的执行
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
namespace: ElementNamespace,
optimized,
) => {
const componentUpdateFn = () => {
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
const { bm, m, parent } = instance
const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)
instance.isMounted = true
// #2458: deference mount-only object parameters to prevent memleaks
initialVNode = container = anchor = null as any
let { next, bu, u, parent, vnode } = instance
if (__FEATURE_SUSPENSE__) {
const nonHydratedAsyncRoot = locateNonHydratedAsyncRoot(instance)
// we are trying to update some async comp before hydration
// this will cause crash because we don't know the root node yet
if (nonHydratedAsyncRoot) {
// only sync the properties and abort the rest of operations
if (next) {
next.el = vnode.el
updateComponentPreRender(instance, next, optimized)
}
// and continue the rest of operations once the deps are resolved
nonHydratedAsyncRoot.asyncDep!.then(() => {
// the instance may be destroyed during the time period
if (!instance.isUnmounted) {
componentUpdateFn() //关键代码
}
})
return
}
}
// updateComponent
// This is triggered by mutation of component's own state (next: null)
// OR parent calling processComponent (next: VNode)
patch( // 关键代码,这里有执行了一次patch,即每次effect触发时候都会执行一次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,
namespace,
)
next.el = nextTree.el
}
// create reactive effect for rendering 关键代码
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
NOOP,
() => queueJob(update),
instance.scope, // track it in component's effect scope
))
const update: SchedulerJob = (instance.update = () => {
if (effect.dirty) {
effect.run() // effect(fn)传入的fn
}
})
update.id = instance.uid
// allowRecurse
// #1801, #2043 component render effects should allow recursive updates
toggleRecurse(instance, true)
update()
}
}
流程:
- 第一次根组件调用mountComponent实例化,会触发一次setupRenderEffect,把根组件对应一个effect的update。并且会立即执行update方法
- update方法执行时候触发patch
- patch的时候查找当前组件下对应新所有子节点和 新属性
- 如果有子节点,并且是html直接渲染html,如果是组件继续递归
- 判断新属性 进行更新
例子2逻辑
代码
<script src="../../dist/vue.global.js"></script>
<script type="text/x-template" id="app-template">
<p>子组件:{{message}} - {{state.a}}</p>
</script>
<script>
const { reactive, computed,ref,h } = Vue
const App = {
template: '#app-template',
props: {
message: String,
},
setup(props) {
const state = reactive({
a:"ggg"
})
return {
state,
}
}
}
</script>
<div id="demo">
--{{val}}--
<app :message="val" >
</app>
</div>
<script>
debugger
const app = Vue.createApp({
components: {
App
},
setup() {
const val = ref("oooo")
return {
val,
}
}
})
app.mount('#demo')
</script>
<style>
</style>
流程图
执行逻辑
在处理第一次patch时候,由于该html没有根节点,会自动包裹一个fragment。
--{{val}}--
<app :message="val" >
</app>
所有第一次patch多处理了 processFragment的逻辑处理,代码如下
const processFragment = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!
let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2
if (
__DEV__ &&
// #5523 dev root fragment may inherit directives
(isHmrUpdating || patchFlag & PatchFlags.DEV_ROOT_FRAGMENT)
) {
// HMR updated / Dev root fragment (w/ comments), force full diff
patchFlag = 0
optimized = false
dynamicChildren = null
}
// check if this is a slot fragment with :slotted scope ids
if (fragmentSlotScopeIds) {
slotScopeIds = slotScopeIds
? slotScopeIds.concat(fragmentSlotScopeIds)
: fragmentSlotScopeIds
}
if (n1 == null) {
hostInsert(fragmentStartAnchor, container, anchor)
hostInsert(fragmentEndAnchor, container, anchor)
// a fragment can only have array children
// since they are either generated by the compiler, or implicitly created
// from arrays.
mountChildren(
// #10007
// such fragment like `<></>` will be compiled into
// a fragment which doesn't have a children.
// In this case fallback to an empty array
(n2.children || []) as VNodeArrayChildren,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
} else {
if (
patchFlag > 0 &&
patchFlag & PatchFlags.STABLE_FRAGMENT &&
dynamicChildren &&
// #2715 the previous fragment could've been a BAILed one as a result
// of renderSlot() with no valid children
n1.dynamicChildren
) {
// a stable fragment (template root or <template v-for>) doesn't need to
// patch children order, but it may contain dynamicChildren.
patchBlockChildren(
n1.dynamicChildren,
dynamicChildren,
container,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
)
if (__DEV__) {
// necessary for HMR
traverseStaticChildren(n1, n2)
} else if (
// #2080 if the stable fragment has a key, it's a <template v-for> that may
// get moved around. Make sure all root level vnodes inherit el.
// #2134 or if it's a component root, it may also get moved around
// as the component is being moved.
n2.key != null ||
(parentComponent && n2 === parentComponent.subTree)
) {
traverseStaticChildren(n1, n2, true /* shallow */)
}
} else {
// keyed / unkeyed, or manual fragments.
// for keyed & unkeyed, since they are compiler generated from v-for,
// each child is guaranteed to be a block so the fragment will never
// have dynamicChildren.
patchChildren(
n1,
n2,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
}
}
}
根据n1是否为空 走不同逻辑
- 第一次都是刚创建,n1为空,走hostInsert + mountChildren
hostInsert(fragmentStartAnchor, container, anchor)
hostInsert(fragmentEndAnchor, container, anchor)
// a fragment can only have array children
// since they are either generated by the compiler, or implicitly created
// from arrays.
mountChildren(
// #10007
// such fragment like `<></>` will be compiled into
// a fragment which doesn't have a children.
// In this case fallback to an empty array
(n2.children || []) as VNodeArrayChildren,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
其中 mountChildren会递归遍历所有子组件并批量执行patch
const mountChildren: MountChildrenFn = (
children,
container,
anchor,
parentComponent,
parentSuspense,
namespace: ElementNamespace,
slotScopeIds,
optimized,
start = 0,
) => {
for (let i = start; i < children.length; i++) {
const child = (children[i] = optimized
? cloneIfMounted(children[i] as VNode)
: normalizeVNode(children[i]))
patch(
null,
child,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
}
}
这时候
- 第一次patch执行的是processText,把
--{{val}}--插入到<div id="demo"> - 第二次patch执行processCompoment,并调用mountComponent 最后执行processElement输出p标签到
<div id="demo">
源码
参考
[玩转vue3](time.geekbang.org/column/intr…