vue3创建和挂载实例
这幅图完整的描述了vue3初始化时创建应用实例和挂载实例的流程,以及创建应用实例时的虚拟节点创建、render渲染、patch算法新旧节点和组件比较、虚拟dom创建、lifecycle函数的执行、diff算法等等;接下来详细描述整个执行过程。
这幅图完整的描述了vue3初始化时创建应用实例和挂载实例的流程,以及创建应用实例时的虚拟节点创建、render渲染、patch算法新旧节点和组件比较、虚拟dom创建、lifecycle函数的执行、diff算法等等;接下来详细描述整个执行过程。
一、创建应用实例
export const createApp = ((...args) => {
// 创建了 render 渲染器,和创建了 app 实例,最终返回 app 应用实例 createAppAPI 的方法包括 mount mixin provide 等方法
const app = ensureRenderer().createApp(...args)
if (__DEV__) {
injectNativeTagCheck(app)
injectCompilerOptionsCheck(app)
}
const { mount } = app
// 重写 app 的 mount 方法
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// 返回 containerOrSelector 对应的容器
const container = normalizeContainer(containerOrSelector) // 得到 dom 节点
if (!container) return
const component = app._component
// 组件不存在render函数和模板template,则使用container的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) {
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
// 返回的是个 vnode
return proxy
}
vue
中创建应用实例调用的方法是createApp
,在此方法中会做了两件事情:
1、调用ensureRenderer().createApp(...args)
创建应用的实例,此实例中包含mount
、provide
等实例方法;
2、重写实例上的mount
方法
接下来分析如何进行实例的创建:
调用ensureRenderer()
方法,确认render
渲染:
// patchProp 主要是对 class 和 style 的操作 nodeOps 主要是对节点的操作 element
const rendererOptions = /*#__PURE__*/ extend({ patchProp }, nodeOps) // extend = Object.assign 把 nodeOps 拷贝到 patchProp ==> 最终返回的是一个对 css、class、dom操作的方法
function ensureRenderer() {
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
)
}
最终返回的是createRenderer
,它接收的参数中包含的方法是对节点node
的操作和class
、style
等的操作;
继续分析createRenderer
方法:
/**
* The createRenderer function accepts two generic(通用) arguments:
* HostNode and HostElement, corresponding to Node and Element types in the
* host environment. For example, for runtime(运行时)-dom, HostNode would be the DOM
* `Node` interface and HostElement would be the DOM `Element` interface.
*
* Custom renderers can pass in the platform specific types like this: // 自定义渲染器可以传入特定于平台的类型
*
* ```js
* const { render, createApp } = createRenderer<Node, Element>({
* patchProp,
* ...nodeOps
* })
* ```
*/
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
很简单只是继续调用另一个方法baseCreateRenderer
方法;
分析baseCreateRenderer
方法:
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
const patch = () => {
}
/
*
* // 等一系列方法,在这一步不进行具体分析,在后面会进行再次分析
*
/
const render = () => {
}
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
baseCreateRenderer
方法是整个实例挂载的核心方法,在此方法中会进行render渲染、patch算法的新旧节点比较、diff算法比较、生命周期函数执行、任务队列判断等等。
我们在创建实例的时候ensureRenderer().createApp(...args)
最终调用的方法是createApp
,但是createApp
最终调用了createAppAPI(render, hydrate)
,可以看到他里面传递了render
方法和hydrate
两个参数。
分析createAppAPI
方法:
export function createAppAPI<HostElement>(
render: RootRenderFunction<HostElement>,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
if (!isFunction(rootComponent)) {
rootComponent = { ...rootComponent }
}
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
const context = createAppContext() // ==> 将 mixins 和 provides 等方法设置给 context
const installedPlugins = new Set()
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 mixin component unmount provide provide,这些方法都是创建实例的时候返回的
/
mount(
rootContainer: HostElement, // ===> 我们在 vue 项目中挂载的根组件,就是 app.mount("#app"), 是容器
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isMounted) {
// #5571
if (__DEV__ && (rootContainer as any).__vue_app__) {
warn(
`There is already an app instance mounted on the host container.\n` +
` If you want to mount another app on the same host container,` +
` you need to unmount the previous app by calling \`app.unmount()\` first.`
)
}
const vnode = createVNode( // 调用方法创建 vnode, vnode 是组件
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
// HMR root reload
if (__DEV__) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer, isSVG)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer, isSVG) // 进行了 render 渲染,调用 patch 函数进行新旧节点比较,新节点添加,最终返回新rootContainer
}
isMounted = true
app._container = rootContainer // 把新的 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
} else if (__DEV__) {
warn(
`App has already been mounted.\n` +
`If you want to remount the same app, move your app creation logic ` +
`into a factory function and create fresh app instances for each ` +
`mount - e.g. \`const createMyApp = () => createApp(App)\``
)
}
},
})
let isMounted = false
return app
}
}
该方法就是我们创建实例最终返回的方法,包含了mount
use
mixin
component
unmount
provide
provide
;对于该方法具体做了什么,请看下一节的实例挂载。
二、挂载实例
在VUE
中挂载实例调用的方法是app.mount()
,我们接下来就分析app.mount()
做了什么。
挂载应用实例的方法app.mount()
是在createAppAPI
方法中,我们来详细分析createAppAPI
方法:
export function createAppAPI<HostElement>(
render: RootRenderFunction<HostElement>,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
if (!isFunction(rootComponent)) {
rootComponent = { ...rootComponent }
}
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
const context = createAppContext() // ==> 将 mixins 和 provides 等方法设置给 context
const installedPlugins = new Set()
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 mixin component unmount provide provide,这些方法都是创建实例的时候返回的
/
mount(
rootContainer: HostElement, // ===> 我们在 vue 项目中挂载的根组件,就是 app.mount("#app"), 是容器
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isMounted) {
// #5571
if (__DEV__ && (rootContainer as any).__vue_app__) {
warn(
`There is already an app instance mounted on the host container.\n` +
` If you want to mount another app on the same host container,` +
` you need to unmount the previous app by calling \`app.unmount()\` first.`
)
}
// 1、创建虚拟节点vnode
const vnode = createVNode( // 调用方法创建 vnode, vnode 是组件
rootComponent as ConcreteComponent, // ===> 传入的组件
rootProps
)
// store(存储) app context on the root VNode.
// this will be set on the root instance(例子) on initial(最初的) mount.
// 2、为虚拟节点`vnode`挂载`appContext`
vnode.appContext = context
// HMR root reload
if (__DEV__) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer, isSVG)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
// 3、render 渲染
render(vnode, rootContainer, isSVG) // 进行了 render 渲染,调用 patch 函数进行新旧节点比较,新节点添加,最终返回新rootContainer
}
isMounted = true
app._container = rootContainer // 把新的 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
} else if (__DEV__) {
warn(
`App has already been mounted.\n` +
`If you want to remount the same app, move your app creation logic ` +
`into a factory function and create fresh app instances for each ` +
`mount - e.g. \`const createMyApp = () => createApp(App)\``
)
}
},
})
let isMounted = false
return app
}
}
分析mount
方法:
1、创建虚拟节点vnode
,调用createVNode()
方法,该方法接收两个参数,第一个参数rootComponent as ConcreteComponent
就是我们创建实例时传入的组件,将该组件转换为虚拟节点vnode,分析该方法;
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 {
if (!type || type === NULL_DYNAMIC_COMPONENT) {
if (__DEV__ && !type) {
warn(`Invalid vnode type when creating vnode: ${type}.`)
}
type = Comment
}
if (isVNode(type)) { // 判断传入的是否是 vnode
// 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)
}
if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {
if (cloned.shapeFlag & ShapeFlags.COMPONENT) {
currentBlock[currentBlock.indexOf(type)] = cloned
} else {
currentBlock.push(cloned)
}
}
cloned.patchFlag |= PatchFlags.BAIL
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(位图)
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
)
}
判断传入的组件类型,如果传入的类型本身就是vnode,就克隆并返回该vnode;判断props,如果props不为空就将class和style进行规范化,规范化时调用normalizeStyle
和normalizeClass
方法,这两个方法大同小异,我们只分析normalizeClass
方法。
分析normalizeClass
方法:
export function normalizeClass(value: unknown): string {
let res = ''
if (isString(value)) { // 字符串类型直接进行 res 赋值
res = value
} else if (isArray(value)) { // 数组类型,递归进行拼接字符串 res
for (let i = 0; i < value.length; i++) {
const normalized = normalizeClass(value[i])
if (normalized) {
res += normalized + ' '
}
}
} else if (isObject(value)) { // 字符串拼接
for (const name in value) {
if (value[name]) {
res += name + ' '
}
}
}
return res.trim() // 调用 trim 方法清除字符串两端的空格
}
再次回到 _createVNode
方法中,我们会发现它最后返回的是createBaseVNode
方法,此方法才是真正返回vnode
的方法。
分析createBaseVNode
方法:
function createBaseVNode( // 创建真正的 vnode
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,
shapeFlag,
patchFlag,
dynamicProps,
dynamicChildren: null,
appContext: null,
ctx: currentRenderingInstance
} 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
。
这幅图就是我们vue
中的虚拟节点vnode
;到这里我们就分析完了第一步的创建虚拟节点vnode
。
2、为虚拟节点vnode
挂载appContext
:
分析appContext
:
app.appContext = context
// 在createApi中可以看到,context 是调用 createAppContext() 的产物
const context = createAppContext() // ==> 将 mixins 和 provides 等方法设置给 context
//我们来看 createAppContext() 方法
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()
}
}
可以看出createAppContext
方法就是返回一些实例并挂载。
3、render 函数渲染
先来回顾一下render
函数在createApi
的形式
render(vnode, rootContainer, isSVG)
这里的vnode
就是我们第一步分析得到的组件虚拟节点;rootContainer
则是我们挂载的根组件,因为它是mount
方法传参的第一个参数。
接下来分析render
方法:
我们要知道render
方法是在创建应用实例的时候提到的baseCreateRenderer
方法中
const render: RootRenderFunction = (vnode, container, isSVG) => {
if (vnode == null) { // vnode 为空,卸载 container._vnode
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else { // 不为空的话,就进行 patch 算法比较
patch(container._vnode || null, vnode, container, null, null, null, isSVG) // 新旧节点的比较,不断的回调调用 patch 方法,有心节点 // 进行添加
}
flushPreFlushCbs()
flushPostFlushCbs()
container._vnode = vnode // 把新得到的 vnode 给我们创建的 vue 项目也就是 ("#app")
}
在render
方法中会判断vnode
是否为空,为空的话会进行卸载,不为空则会执行patch
算法,进行新旧节点的比较。
分析patch
方法:
一定要清楚一个点,我们做的所有操作最终都会挂载到 container 上
const patch: PatchFn = ( // patch 算法进行新旧 Vnode 比较
n1, // 容器
n2, // 组件
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren // 优化
) => {
if (n1 === n2) { // 比较 vnode 是否相同
return
}
// patching & not same type, unmount old tree
if (n1 && !isSameVNodeType(n1, n2)) { // vnode 类型不同将 n1 卸载,是我们挂载的那个("#app")
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
if (n2.patchFlag === PatchFlags.BAIL) { // patchFlags 的类型是 bail 类型时退出优化
optimized = false // 退出优化
n2.dynamicChildren = null // 有 dynamicChildren 的时候就进行优化
}
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:
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)
}
}
①先进行比较传入的n1
、n2
的vnode
的是否相同,相同的话直接返回;
②再比较他俩的类型是否相同,不同的话就卸载n1,因为n2是最新的vnode,所以不会卸载n2卸载的是n1;
③判断传入n2的type,分类进行执行各自的方法;patch算法中主要就是这一步,判断不同的type执行各自的方法。
我们重点分析第三步的type判断。
类型为Text:
执行processText
方法
该类型为文本
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
if (n1 == null) {
// 将创建的文本节点插入
hostInsert(
(n2.el = hostCreateText(n2.children as string)), // 创建文本节点,将节点插入到 container 中
container,
anchor
)
} else {
const el = (n2.el = n1.el!)
if (n2.children !== n1.children) {
hostSetText(el, n2.children as string) // 如果节点不同,将n2上的children节点赋值到n1上
}
}
}
类型为Text做的事情很简单,就是将文本节点进行设置。先判断有没有旧节点,没有旧节点直接进行设置插入;有旧节点就进行比较,如果不同将n2的节点设置到n1上。
类型为Comment:
该类型为注释节点
const processCommentNode: ProcessTextOrCommentFn = (
n1,
n2,
container,
anchor
) => {
if (n1 == null) {
// 插入节点
hostInsert(
(n2.el = hostCreateComment((n2.children as string) || '')), // 创建注释节点
container,
anchor
)
} else {
// there's no support for dynamic comments
n2.el = n1.el
}
}
只是将注释的节点进行一个插入,不做过多阐述。
类型为Static
该类型为静态节点
const mountStaticNode = (
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
isSVG: boolean
) => {
// 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,
isSVG,
n2.el,
n2.anchor
)
}
这段是插入静态节点的方法
insertStaticContent(content, parent, anchor, isSVG, start, end) {
// <parent> before | first ... last | anchor </parent>
const before = anchor ? anchor.previousSibling : parent.lastChild
// #5308 can only take cached path if:
// - has a single root node
// - nextSibling info is still available
if (start && (start === end || start.nextSibling)) {
// cached
while (true) {
parent.insertBefore(start!.cloneNode(true), anchor)
if (start === end || !(start = start!.nextSibling)) break
}
} else {
// fresh insert
templateContainer.innerHTML = isSVG ? `<svg>${content}</svg>` : content
const template = templateContainer.content
if (isSVG) {
// remove outer svg wrapper
const wrapper = template.firstChild!
while (wrapper.firstChild) {
template.appendChild(wrapper.firstChild)
}
template.removeChild(wrapper)
}
parent.insertBefore(template, anchor) // 将只指定节点插入到给点给节点的前面
}
return [
// first
before ? before.nextSibling! : parent.firstChild!,
// last
anchor ? anchor.previousSibling! : parent.lastChild!
]
}
类型为Fragment
该类型为片段,举个例子<div>{ name }</div>
,这种类型的就是片段也叫区块
const processFragment = ( // 进行虚拟节点的比较,透传回调 patch 方法
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
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) {
// 将 fragmentStartAnchor 和 fragmentEndAnchor 插入到 container 中,目前 fragmentStartAnchor 和 fragmentEndAnchor 为空文本element(hostCreateText(''))
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( // 挂载 children
n2.children as VNodeArrayChildren,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
isSVG,
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(动态children).
patchBlockChildren( // 动态节点挂载比较
n1.dynamicChildren,
dynamicChildren,
container,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds
)
if (__DEV__ && parentComponent && parentComponent.type.__hmrId) {
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( // key 的不同情况
n1,
n2,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
}
单文件组件里,不再需要加一个根节点,因为使用了文档片段fragment
来承载子节点,最后再一并添加到文档中。
若旧的片段节点为空,则插入起始锚点,挂载新的子节点;
旧的片段不为空:
①存在优化条件时:使用patchBlockChildren
优化diff
;
②不存在优化条件时:使用patchChildren
进行全量diff
。
进行了有无旧节点的判断,两种情况:
没有旧节点:
没有旧节点的话,会将 fragmentStartAnchor
和 fragmentEndAnchor
插入到 container
中,目前 fragmentStartAnchor
和 fragmentEndAnchor
为空文本element(hostCreateText(''))
调用 mountChildren
方法,挂载子节点,遍历每一个子节点再次进行patch
算法
const mountChildren: MountChildrenFn = (
children,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
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( // child 进行 patch 算法
null,
child,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
旧节点不为空:
不为空的情况也进行了两次的判断,有无patchFlag
且节点的children
是不是需要优化;patchFlag是这里的一个重点,它会判断当前是什么类型的动态更新,例如动态class、动态style、动态属性、动态文本节点等。
在这里我们贴出源码中关于patchFlag
的描述:
export const enum PatchFlags {
/** 动态的文本节点
* Indicates an element with dynamic textContent (children fast path)
*/
TEXT = 1,
/** 动态的 class
* Indicates an element with dynamic class binding.
*/
CLASS = 1 << 1,
/** 动态的 style
* Indicates an element with dynamic style
* The compiler pre-compiles static string styles into static objects
* + detects and hoists inline static objects
* e.g. `style="color: red"` and `:style="{ color: 'red' }"` both get hoisted
* as:
* ```js
* const style = { color: 'red' }
* render() { return e('div', { style }) }
* ```
*/
STYLE = 1 << 2,
/** 动态属性,不包括类名和样式
* Indicates an element that has non-class/style dynamic props.
* Can also be on a component that has any dynamic props (includes
* class/style). when this flag is present, the vnode also has a dynamicProps
* array that contains the keys of the props that may change so the runtime
* can diff them faster (without having to worry about removed props)
*/
PROPS = 1 << 3,
/** 动态 key,当 key 变化时需要完整的 diff 算法做比较
* Indicates an element with props with dynamic keys. When keys change, a full
* diff is always needed to remove the old key. This flag is mutually
* exclusive with CLASS, STYLE and PROPS.
*/
FULL_PROPS = 1 << 4,
/** 表示带有事件监听器的节点
* Indicates an element with event listeners (which need to be attached
* during hydration)
*/
HYDRATE_EVENTS = 1 << 5,
/** 一个不会改变子节点顺序的 Fragment
* Indicates a fragment whose children order doesn't change.
*/
STABLE_FRAGMENT = 1 << 6,
/** 带有 key 属性的 Fragment
* Indicates a fragment with keyed or partially keyed children
*/
KEYED_FRAGMENT = 1 << 7,
/** 子节点没有 key 的 Fragment
* Indicates a fragment with unkeyed children.
*/
UNKEYED_FRAGMENT = 1 << 8,
/** 表示只需要non-props修补的元素
* Indicates an element that only needs non-props patching, e.g. ref or
* directives (onVnodeXXX hooks). since every patched vnode checks for refs
* and onVnodeXXX hooks, it simply marks the vnode so that a parent block
* will track it.
*/
NEED_PATCH = 1 << 9,
/** 动态的solt
* Indicates a component with dynamic slots (e.g. slot that references a v-for
* iterated value, or dynamic slot names).
* Components with this flag are always force updated.
*/
DYNAMIC_SLOTS = 1 << 10,
/** 表示仅因为用户在模板的根级别放置注释而创建的片段。 这是一个仅用于开发的标志,因为注释在生产中被剥离
* Indicates a fragment that was created only because the user has placed
* comments at the root level of a template. This is a dev-only flag since
* comments are stripped in production.
*/
DEV_ROOT_FRAGMENT = 1 << 11,
/**
* SPECIAL FLAGS -------------------------------------------------------------
* Special flags are negative integers. They are never matched against using
* bitwise operators (bitwise matching should only happen in branches where
* patchFlag > 0), and are mutually exclusive. When checking for a special
* flag, simply check patchFlag === FLAG.
*/
//以下两个是特殊标记
/** 表示已提升的静态vnode,更新时调过整个子树
* Indicates a hoisted static vnode. This is a hint for hydration to skip
* the entire sub tree since static content never needs to be updated.
*/
HOISTED = -1,
/** 指示差异算法应该退出优化模式
* A special flag that indicates that the diffing algorithm should bail out
* of optimized mode. For example, on block fragments created by renderSlot()
* when encountering non-compiler generated slots (i.e. manually written
* render functions, which should always be fully diffed)
* OR manually cloneVNodes
*/
BAIL = -2
}
先来分析有patchFlag
且节点的children
需要优化且一个不会改变子节点顺序的 Fragment:
这块执行了patchBlockChildren
方法
const patchBlockChildren: PatchBlockChildrenFn = (
oldChildren,
newChildren,
fallbackContainer,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds
) => {
for (let i = 0; i < newChildren.length; i++) {
const oldVNode = oldChildren[i]
const newVNode = newChildren[i]
// Determine the container (parent element) for the patch.
const container =
// oldVNode may be an errored async setup() component inside Suspense
// which will not have a mounted element
oldVNode.el &&
// - In the case of a Fragment, we need to provide the actual parent
// of the Fragment itself so it can move its children.
(oldVNode.type === Fragment ||
// - In the case of different nodes, there is going to be a replacement
// which also requires the correct parent container
!isSameVNodeType(oldVNode, newVNode) ||
// - In the case of a component, it could contain anything.
oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT))
? hostParentNode(oldVNode.el)! // 返回父节点
: // In other cases, the parent container is not actually used so we
// just pass the block element here to avoid a DOM parentNode call.
fallbackContainer
patch(
oldVNode,
newVNode,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
true
)
}
}
对新旧虚拟Node的dynamicChildren
属性所代表的虚拟Node数组进行遍历,并调用patch
函数进行更新操作。
另一种情况,根据key的情况进行执行:
该条件分支是执行了patchChildren
方法。
const patchChildren: PatchChildrenFn = (
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized = false
) => {
const c1 = n1 && n1.children
const prevShapeFlag = n1 ? n1.shapeFlag : 0
const c2 = n2.children
const { patchFlag, shapeFlag } = n2
// fast path
if (patchFlag > 0) {
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
// this could be either fully-keyed or mixed (some keyed some not)
// presence of patchFlag means children are guaranteed to be arrays
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
// unkeyed
patchUnkeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
return
}
}
// children has 3 possibilities: text, array or no children.
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// text children fast path
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
}
if (c2 !== c1) {
hostSetElementText(container, c2 as string)
}
} else {
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// prev children was array
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// two arrays, cannot assume anything, do full diff
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// no new children, just unmount old
unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
}
} else {
// prev children was text OR null
// new children is array OR null
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(container, '')
}
// mount new if array
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
}
}
首先判断该patchFlag要大于0,进入条件分支后,再次判断是否是带有 key 属性的 Fragment
分析具有 key 属性的 Fragment:
这块是最重要的地方,也就是我们说的vue3的新版diff算法
const patchKeyedChildren = ( // 定义指针,以不同的指针方式创建和和卸载 vnode
c1: VNode[], // n1.children 旧的组件
c2: VNodeArrayChildren, // n2.children 新的
container: RendererElement,
parentAnchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean // false
) => {
let i = 0
const l2 = c2.length
let e1 = c1.length - 1 // prev ending index 指针
let e2 = l2 - 1 // next ending index 指针
// 1. sync from start
// (a b) c
// (a b) d e
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
break
}
i++
}
// 2. sync from end
// a (b c)
// d e (b c)
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = (c2[e2] = optimized
? cloneIfMounted(c2[e2] as VNode)
: normalizeVNode(c2[e2]))
if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
break
}
e1--
e2--
}
// 3. common sequence + mount 取新组件多出来的部分
// (a b)
// (a b) c
// i = 2, e1 = 1, e2 = 2
// (a b)
// c (a b)
// i = 0, e1 = -1, e2 = 0
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1
const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
while (i <= e2) {
patch(
null,
(c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i])),
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
i++
}
}
}
// 4. common sequence + unmount
// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
// a (b c)
// (b c)
// i = 0, e1 = 0, e2 = -1
else if (i > e2) { // i 大于新组件的 卸载 vnode,
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
// 5. unknown sequence 未知序列,存在位移的数据
// [i ... e1 + 1]: a b [c d e] f g
// [i ... e2 + 1]: a b [e d c h] f g
// i = 2, e1 = 4, e2 = 5
else {
const s1 = i // prev starting index
const s2 = i // next starting index
// 5.1 build key:index map for newChildren
const keyToNewIndexMap: Map<string | number | symbol, number> = new Map()
for (i = s2; i <= e2; i++) {
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
if (nextChild.key != null) {
if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) {
warn(
`Duplicate keys found during update:`,
JSON.stringify(nextChild.key),
`Make sure keys are unique.`
)
}
keyToNewIndexMap.set(nextChild.key, i)
}
}
// 5.2 loop through old children left to be patched and try to patch 绕过旧的 children 从左边
// matching nodes & remove nodes that are no longer present
let j
let patched = 0
const toBePatched = e2 - s2 + 1
let moved = false
// used to track(跟踪) whether any node has moved
let maxNewIndexSoFar = 0
// works as Map<newIndex, oldIndex>
// Note that oldIndex is offset by +1
// and oldIndex = 0 is a special value indicating(表示) the new node has
// no corresponding(符合) old node.
// used for determining(确定) longest stable subsequence(子序列)
const newIndexToOldIndexMap = new Array(toBePatched)
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
for (i = s1; i <= e1; i++) {
const prevChild = c1[i]
if (patched >= toBePatched) { // 新的 children 已经被 patch 完,所以旧的得被 卸载
// all new children have been patched so this can only be a removal
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
let newIndex // 作用是,作比较需不需要移动 prevChild 到新 children 上
if (prevChild.key != null) {
newIndex = keyToNewIndexMap.get(prevChild.key) // 把旧的 children 从新的里面找出来
} else {
// key-less node, try to locate a key-less node of the same type 没有 key 的情况
for (j = s2; j <= e2; j++) {
if (
newIndexToOldIndexMap[j - s2] === 0 &&
isSameVNodeType(prevChild, c2[j] as VNode)
) {
newIndex = j
break
}
}
}
if (newIndex === undefined) {
unmount(prevChild, parentComponent, parentSuspense, true)
} else {
newIndexToOldIndexMap[newIndex - s2] = i + 1
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
moved = true // 需要移动的
}
patch(
prevChild,
c2[newIndex] as VNode,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
patched++
}
}
// 5.3 move and mount 最长递增子序列
// generate longest stable subsequence only when nodes have moved
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: EMPTY_ARR
j = increasingNewIndexSequence.length - 1
// looping backwards so that we can use last patched node as anchor
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i
const nextChild = c2[nextIndex] as VNode
const anchor =
nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
if (newIndexToOldIndexMap[i] === 0) {
// mount new
patch(
null,
nextChild,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (moved) {
// move if:
// There is no stable subsequence (e.g. a reverse)
// OR current node is not among the stable sequence
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor, MoveType.REORDER)
} else {
j--
}
}
}
}
}
全局定义两个指针e1和e2,
1、从前往后;让c1和c2从头开始执行while循环,如果遇到n1和n2的类型相同执行patch更新,不同则跳出while语句。
2、从后往前;让c1和c2从尾开始向前比较,如果遇到n1和n2的类型相同执行patch更新,不同则跳出while语句。
3、比较多出来的部分;经过1、2步后,如果i大于了旧组件的长度,说明新组件是更长的,那么就比较n2多出来的这部分,这部分就比较直接,让n2自己进行patch更新。
4、卸载旧组件多出来部分;经过1、2步发现当前新组件小于旧组件,那么就卸载旧组件多出来的部分。
5、未知序列,存在位移的数据,这一部分就是最复杂的地方,由于存在位移情况要进行算法的比较;定义两个指针s1、s2
-
5.1为
newChildren
(新组件)定义一个Map集合叫keyToNewIndexMap
,将新组件的每一个命名为nextChild.key
存到该集合中 -
5.2 有一个很关键的元素 newIndex, 作用是,作比较需不需要移动 prevChild 到新 children 上;会把当前的旧 children 和5.1的集合进行匹配,如果匹配上说明需要移动元素。记录 patched 比较的次数、是否需要移动 moved
-
5.3 这一步就用到算法最长递归子序列,首先是判断5.2中的moved是否为true,为true的话就会调用getSequence方法;之后就是向后循环,看c2的元素符合哪个条件,进行新的patch更新或者移动(move)元素
-
// 这是移动元素的方法 const move: MoveFn = ( vnode, container, anchor, moveType, parentSuspense = null ) => { const { el, type, transition, children, shapeFlag } = vnode if (shapeFlag & ShapeFlags.COMPONENT) { move(vnode.component!.subTree, container, anchor, moveType) return } if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { vnode.suspense!.move(container, anchor, moveType) return } if (shapeFlag & ShapeFlags.TELEPORT) { ;(type as typeof TeleportImpl).move(vnode, container, anchor, internals) return } if (type === Fragment) { hostInsert(el!, container, anchor) for (let i = 0; i < (children as VNode[]).length; i++) { move((children as VNode[])[i], container, anchor, moveType) } hostInsert(vnode.anchor!, container, anchor) return } if (type === Static) { moveStaticNode(vnode, container, anchor) return } // single nodes const needTransition = moveType !== MoveType.REORDER && shapeFlag & ShapeFlags.ELEMENT && transition if (needTransition) { if (moveType === MoveType.ENTER) { transition!.beforeEnter(el!) hostInsert(el!, container, anchor) queuePostRenderEffect(() => transition!.enter(el!), parentSuspense) } else { const { leave, delayLeave, afterLeave } = transition! const remove = () => hostInsert(el!, container, anchor) const performLeave = () => { leave(el!, () => { remove() afterLeave && afterLeave() }) } if (delayLeave) { delayLeave(el!, remove, performLeave) } else { performLeave() } } } else { hostInsert(el!, container, anchor) } }
其他类型
其他类型包括新旧节点比较(processElement)、**新旧组件比较(processComponent)**等大同小异;我们分析这两个类型。
新旧节点比较
该比较调用的方法是processElement
,接下来分析该方法。
const processElement = ( // 新旧节点比较
n1: VNode | null,
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'
if (n1 == null) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
方法很简单,旧组件n1为空则调用mountElement
挂载节点,不为空则调用patchElement
。
1、旧组件n1为空调用mountElement
const mountElement = ( // 挂载节点
vnode: VNode, // ===> n2
container: RendererElement, // 初始为 null
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null, // null
parentSuspense: SuspenseBoundary | null, // null
isSVG: boolean, // false
slotScopeIds: string[] | null, // null
optimized: boolean // false
) => {
let el: RendererElement
let vnodeHook: VNodeHook | undefined | null
const { type, props, shapeFlag, transition, dirs } = vnode
el = vnode.el = hostCreateElement( // 创建 vnode.type 类型的 html
vnode.type as string,
isSVG,
props && props.is,
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) { // 数组类型的则需要进行 patch 更新
mountChildren(
vnode.children as VNodeArrayChildren,
el,
null,
parentComponent,
parentSuspense,
isSVG && type !== 'foreignObject',
slotScopeIds,
optimized
)
}
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'created')
}
// scopeId 设置指定元素的属性值
setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent)
// props
if (props) {
for (const key in props) {
if (key !== 'value' && !isReservedProp(key)) {
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)
}
}
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)
}
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)
}
}
该方法主要是创建和挂载html,并执行声明周期函数created、beforeMount、mounted
2、不为空则调用patchElement
const patchElement = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
const el = (n2.el = n1.el!)
let { patchFlag, dynamicChildren, dirs } = n2
// #1426 take the old vnode's patch flag into account since user may clone a
// compiler-generated vnode, which de-opts to FULL_PROPS
patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS
const oldProps = n1.props || EMPTY_OBJ // 将组件上的 props 旧属性拿出来
const newProps = n2.props || EMPTY_OBJ // 将组件上的 props 新属性拿出来
let vnodeHook: VNodeHook | undefined | null
// disable recurse(递归) in beforeUpdate hooks
parentComponent && toggleRecurse(parentComponent, false)
if ((vnodeHook = newProps.onVnodeBeforeUpdate)) {
invokeVNodeHook(vnodeHook, parentComponent, n2, n1) // vnode 的钩子函数
}
if (dirs) {
invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate') // vnode 的钩子函数
}
parentComponent && toggleRecurse(parentComponent, true)
if (__DEV__ && isHmrUpdating) {
// HMR updated, force full diff
patchFlag = 0
optimized = false
dynamicChildren = null
}
const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
if (dynamicChildren) {
patchBlockChildren(
n1.dynamicChildren!,
dynamicChildren,
el,
parentComponent,
parentSuspense,
areChildrenSVG,
slotScopeIds
)
if (__DEV__ && parentComponent && parentComponent.type.__hmrId) {
traverseStaticChildren(n1, n2)
}
} else if (!optimized) { // 非优化进行 diff 算法
// full diff
patchChildren(
n1,
n2,
el,
null,
parentComponent,
parentSuspense,
areChildrenSVG,
slotScopeIds,
false
)
}
// 进行不同的 patchProps 比较
if (patchFlag > 0) {
// the presence of a patchFlag means this element's render code was
// generated by the compiler and can take the fast path.
// in this path old node and new node are guaranteed to have the same shape
// (i.e. at the exact same position in the source template)
if (patchFlag & PatchFlags.FULL_PROPS) { // key 的比较
// element props contain dynamic keys, full diff needed
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
} else {
// class
// this flag is matched when the element has dynamic class bindings.
if (patchFlag & PatchFlags.CLASS) {
if (oldProps.class !== newProps.class) {
hostPatchProp(el, 'class', null, newProps.class, isSVG)
}
}
// style
// this flag is matched when the element has dynamic style bindings
if (patchFlag & PatchFlags.STYLE) {
hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
}
// props
// This flag is matched when the element has dynamic prop/attr bindings
// other than class and style. The keys of dynamic prop/attrs are saved for
// faster iteration.
// Note dynamic keys like :[foo]="bar" will cause this optimization to
// bail out and go through a full diff because we need to unset the old key
if (patchFlag & PatchFlags.PROPS) {
// if the flag is present then dynamicProps must be non-null
const propsToUpdate = n2.dynamicProps!
for (let i = 0; i < propsToUpdate.length; i++) {
const key = propsToUpdate[i]
const prev = oldProps[key]
const next = newProps[key]
// #1471 force patch value
if (next !== prev || key === 'value') {
hostPatchProp(
el,
key,
prev,
next,
isSVG,
n1.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
}
}
// text
// This flag is matched when the element has only dynamic text children.
if (patchFlag & PatchFlags.TEXT) {
if (n1.children !== n2.children) {
hostSetElementText(el, n2.children as string)
}
}
} else if (!optimized && dynamicChildren == null) {
// unoptimized, full diff
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
}
if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
}, parentSuspense)
}
}
该函数是将新组件的props拿出来,进行不同patchProp;具体的分析已经再代码中体现。
新旧组件比较
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
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( // 回去执行 hook 方法
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else {
updateComponent(n1, n2, optimized)
}
}
新旧组件的比较,也是比较了旧组件n1是否为空,为空的话执行各种hook方法mountComponent
;非空执行更新组件updateComponent
。在这里我们主要分析为空的情况时的依赖更新,并引出vue的API方法nextTick
1、n1为空,执行mountComponent
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 = 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`)
}
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( // 执行 hook 方法
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
if (__DEV__) {
popWarningContext()
endMeasure(instance, `mount`)
}
}
该方法的核心是调用setupRenderEffect
执行 hook 方法
const setupRenderEffect: SetupRenderEffectFn = ( // 执行各种 hook
instance, // n2 新组件的实例
initialVNode, // n2 新组件
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
const componentUpdateFn = () => {
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
const { bm, m, parent } = instance
const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)
toggleRecurse(instance, false)
// beforeMount hook
if (bm) {
invokeArrayFns(bm)
}
// onVnodeBeforeMount
if (
!isAsyncWrapperVNode &&
(vnodeHook = props && props.onVnodeBeforeMount)
) {
invokeVNodeHook(vnodeHook, parent, initialVNode)
}
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 = () => {
if (__DEV__) {
startMeasure(instance, `render`)
}
instance.subTree = renderComponentRoot(instance)
if (__DEV__) {
endMeasure(instance, `render`)
}
if (__DEV__) {
startMeasure(instance, `hydrate`)
}
hydrateNode!(
el as Node,
instance.subTree,
instance,
parentSuspense,
null
)
if (__DEV__) {
endMeasure(instance, `hydrate`)
}
}
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 {
if (__DEV__) {
startMeasure(instance, `render`)
}
const subTree = (instance.subTree = renderComponentRoot(instance))
if (__DEV__) {
endMeasure(instance, `render`)
}
if (__DEV__) {
startMeasure(instance, `patch`)
}
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
if (__DEV__) {
endMeasure(instance, `patch`)
}
initialVNode.el = subTree.el
}
// mounted hook 执行 onmounted 方法
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 ||
(parent &&
isAsyncWrapper(parent.vnode) &&
parent.vnode.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
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsComponentAdded(instance)
}
// #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父调用processComponent(下一个:VNode)
// OR parent calling processComponent (next: VNode)
let { next, bu, u, parent, vnode } = instance
let originNext = next
let vnodeHook: VNodeHook | null | undefined
if (__DEV__) {
pushWarningContext(next || instance.vnode)
}
// 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) // 组件实例中的 effect.allowRecurse 设置为 true
// render
if (__DEV__) {
startMeasure(instance, `render`)
}
const nextTree = renderComponentRoot(instance)
if (__DEV__) {
endMeasure(instance, `render`)
}
const prevTree = instance.subTree
instance.subTree = nextTree
if (__DEV__) {
startMeasure(instance, `patch`)
}
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
)
if (__DEV__) {
endMeasure(instance, `patch`)
}
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
)
}
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsComponentUpdated(instance)
}
if (__DEV__) {
popWarningContext()
}
}
}
// create reactive effect(结果) for rendering
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn, // ====> 在这里触发 intance 的 track 依赖收集和 trigger 更新
() => queueJob(update),
instance.scope // track it in component's effect scope
))
const update: SchedulerJob = (instance.update = () => effect.run())
update.id = instance.uid
// allowRecurse
// #1801, #2043 component render effects should allow recursive updates
toggleRecurse(instance, true)
if (__DEV__) {
effect.onTrack = instance.rtc
? e => invokeArrayFns(instance.rtc!, e)
: void 0
effect.onTrigger = instance.rtg
? e => invokeArrayFns(instance.rtg!, e)
: void 0
update.ownerInstance = instance
}
update()
}
这个方法中的effect是最终要的,这块也是最核心的。在这里会触发 intance 的 track 依赖收集和 trigger 更新,第二个参数就是进行更新的方法,此方法在文件夹scheduler.ts
中进行了定义,会将任务进行细化,任务入队和创建微任务,并进行循环任务队列,这里会调用三个方法分别是queueJob-> queueFlush -> flushJobs。
export function queueJob(job: SchedulerJob) { // job 是主任务队列,是通过组件的实例解构出来的
// the dedupe search uses the startIndex argument of Array.includes()
// by default the search index includes the current job that is being run
// so it cannot recursively trigger itself again.
// if the job is a watch() callback, the search will start with a +1 index to
// allow it recursively trigger itself - it is the user's responsibility to
// ensure it doesn't end up in an infinite loop.
if (
!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)
) {
if (job.id == null) {
queue.push(job) // 任务入队
} else {
queue.splice(findInsertionIndex(job.id), 0, job) // 移除
}
queueFlush() // 创建微任务
}
}
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
function flushJobs(seen?: CountMap) {
isFlushPending = false
isFlushing = true
if (__DEV__) {
seen = seen || new Map()
}
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child so its render effect will have smaller
// priority(优先级) number)
// 2. If a component is unmounted during a parent component's update,
// its update can be skipped(跳过).
queue.sort(comparator)
// conditional usage of checkRecursiveUpdate must be determined out of
// try ... catch block since Rollup by default de-optimizes treeshaking
// inside try-catch. This can leave all warning code unshaked. Although
// they would get eventually shaken by a minifier like terser, some minifiers
// would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
const check = __DEV__
? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
: NOOP
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job && job.active !== false) {
if (__DEV__ && check(job)) {
continue
}
// console.log(`running:`, job.id)
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER) // 循环返回队列的任务结果
}
}
} finally {
flushIndex = 0
queue.length = 0
flushPostFlushCbs(seen)
isFlushing = false // 主任务结束,再次设置为 false
currentFlushPromise = null // 当前没有任务执行,返回 null
// some postFlushCb queued jobs!
// keep flushing until it drains.
if (queue.length || pendingPostFlushCbs.length) {
flushJobs(seen)
}
}
}
vue中有一个API是nextTick,而nextTick也是执行的这三个方法,需要判断当前任务队列中还有没有任务,如果有的话就等执行完之后再执行,并且nextTick的执行是异步的。他的执行顺序是queueFlush -> flushJobs -> nextTick参数的 fn
let isFlushing = false // 是否正在执行
let isFlushPending = false // 是否正在等待执行
const queue: SchedulerJob[] = []
let flushIndex = 0
const pendingPostFlushCbs: SchedulerJob[] = []
let activePostFlushCbs: SchedulerJob[] | null = null
let postFlushIndex = 0
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any> // 微任务
let currentFlushPromise: Promise<void> | null = null // 当前任务
const RECURSION_LIMIT = 100
type CountMap = Map<SchedulerJob, number>
export function nextTick<T = void>( // nextTick 的执行顺序 -> queueFlush -> flushJobs -> nextTick参数的 fn/
this: T,
fn?: (this: T) => void
): Promise<void> {
const p = currentFlushPromise || resolvedPromise // 再这里进行取或,看当前是任务队列执行还是微任务执行
return fn ? p.then(this ? fn.bind(this) : fn) : p
}
完整的代码文件夹scheduler.ts
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
import { isArray, NOOP } from '@vue/shared'
import { ComponentInternalInstance, getComponentName } from './component'
import { warn } from './warning'
export interface SchedulerJob extends Function {
id?: number
pre?: boolean
active?: boolean
computed?: boolean
/**
* Indicates whether the effect is allowed to recursively trigger itself
* when managed by the scheduler.
*
* By default, a job cannot trigger itself because some built-in method calls,
* e.g. Array.prototype.push actually performs reads as well (#1740) which
* can lead to confusing infinite loops.
* The allowed cases are component update functions and watch callbacks.
* Component update functions may update child component props, which in turn
* trigger flush: "pre" watch callbacks that mutates state that the parent
* relies on (#1801). Watch callbacks doesn't track its dependencies so if it
* triggers itself again, it's likely intentional and it is the user's
* responsibility to perform recursive state mutation that eventually
* stabilizes (#1727).
*/
allowRecurse?: boolean
/**
* Attached by renderer.ts when setting up a component's render effect
* Used to obtain component information when reporting max recursive updates.
* dev only.
*/
ownerInstance?: ComponentInternalInstance
}
export type SchedulerJobs = SchedulerJob | SchedulerJob[]
let isFlushing = false // 是否正在执行
let isFlushPending = false // 是否正在等待执行
const queue: SchedulerJob[] = []
let flushIndex = 0
const pendingPostFlushCbs: SchedulerJob[] = []
let activePostFlushCbs: SchedulerJob[] | null = null
let postFlushIndex = 0
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any> // 微任务
let currentFlushPromise: Promise<void> | null = null // 当前任务
const RECURSION_LIMIT = 100
type CountMap = Map<SchedulerJob, number>
export function nextTick<T = void>( // nextTick 的执行顺序 -> queueFlush -> flushJobs -> nextTick参数的 fn/
this: T,
fn?: (this: T) => void
): Promise<void> {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}
// #2768
// Use binary-search to find a suitable position in the queue,
// so that the queue maintains the increasing order of job's id,
// which can prevent the job from being skipped and also can avoid repeated patching.
function findInsertionIndex(id: number) {
// the start index should be `flushIndex + 1`
let start = flushIndex + 1
let end = queue.length
while (start < end) {
const middle = (start + end) >>> 1
const middleJobId = getId(queue[middle])
middleJobId < id ? (start = middle + 1) : (end = middle)
}
return start
}
export function queueJob(job: SchedulerJob) { // job 是主任务队列,是通过组件的实例解构出来的
// the dedupe search uses the startIndex argument of Array.includes()
// by default the search index includes the current job that is being run
// so it cannot recursively trigger itself again.
// if the job is a watch() callback, the search will start with a +1 index to
// allow it recursively trigger itself - it is the user's responsibility to
// ensure it doesn't end up in an infinite loop.
if (
!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)
) {
if (job.id == null) {
queue.push(job) // 任务入队
} else {
queue.splice(findInsertionIndex(job.id), 0, job) // 移除
}
queueFlush() // 创建微任务
}
}
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
export function invalidateJob(job: SchedulerJob) {
const i = queue.indexOf(job)
if (i > flushIndex) {
queue.splice(i, 1)
}
}
export function queuePostFlushCb(cb: SchedulerJobs) {
if (!isArray(cb)) {
if (
!activePostFlushCbs ||
!activePostFlushCbs.includes(
cb,
cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex
)
) {
pendingPostFlushCbs.push(cb)
}
} else {
// if cb is an array, it is a component lifecycle hook which can only be
// triggered by a job, which is already deduped in the main queue, so
// we can skip duplicate check here to improve perf
pendingPostFlushCbs.push(...cb)
}
queueFlush()
}
export function flushPreFlushCbs(
seen?: CountMap,
// if currently flushing, skip the current job itself
i = isFlushing ? flushIndex + 1 : 0
) {
if (__DEV__) {
seen = seen || new Map()
}
for (; i < queue.length; i++) {
const cb = queue[i]
if (cb && cb.pre) {
if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
continue
}
queue.splice(i, 1)
i--
cb()
}
}
}
export function flushPostFlushCbs(seen?: CountMap) {
if (pendingPostFlushCbs.length) {
const deduped = [...new Set(pendingPostFlushCbs)]
pendingPostFlushCbs.length = 0
// #1947 already has active queue, nested flushPostFlushCbs call
if (activePostFlushCbs) {
activePostFlushCbs.push(...deduped)
return
}
activePostFlushCbs = deduped
if (__DEV__) {
seen = seen || new Map()
}
activePostFlushCbs.sort((a, b) => getId(a) - getId(b))
for (
postFlushIndex = 0;
postFlushIndex < activePostFlushCbs.length;
postFlushIndex++
) {
if (
__DEV__ &&
checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])
) {
continue
}
activePostFlushCbs[postFlushIndex]()
}
activePostFlushCbs = null
postFlushIndex = 0
}
}
const getId = (job: SchedulerJob): number =>
job.id == null ? Infinity : job.id
const comparator = (a: SchedulerJob, b: SchedulerJob): number => {
const diff = getId(a) - getId(b)
if (diff === 0) {
if (a.pre && !b.pre) return -1
if (b.pre && !a.pre) return 1
}
return diff
}
function flushJobs(seen?: CountMap) {
isFlushPending = false
isFlushing = true
if (__DEV__) {
seen = seen || new Map()
}
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child so its render effect will have smaller
// priority(优先级) number)
// 2. If a component is unmounted during a parent component's update,
// its update can be skipped(跳过).
queue.sort(comparator)
// conditional usage of checkRecursiveUpdate must be determined out of
// try ... catch block since Rollup by default de-optimizes treeshaking
// inside try-catch. This can leave all warning code unshaked. Although
// they would get eventually shaken by a minifier like terser, some minifiers
// would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
const check = __DEV__
? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
: NOOP
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job && job.active !== false) {
if (__DEV__ && check(job)) {
continue
}
// console.log(`running:`, job.id)
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER) // 循环返回队列的任务结果
}
}
} finally {
flushIndex = 0
queue.length = 0
flushPostFlushCbs(seen)
isFlushing = false // 主任务结束,再次设置为 false
currentFlushPromise = null // 当前没有任务执行,返回 null
// some postFlushCb queued jobs!
// keep flushing until it drains.
if (queue.length || pendingPostFlushCbs.length) {
flushJobs(seen)
}
}
}
function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob) {
if (!seen.has(fn)) {
seen.set(fn, 1)
} else {
const count = seen.get(fn)!
if (count > RECURSION_LIMIT) {
const instance = fn.ownerInstance
const componentName = instance && getComponentName(instance.type)
warn(
`Maximum recursive updates exceeded${
componentName ? ` in component <${componentName}>` : ``
}. ` +
`This means you have a reactive effect that is mutating its own ` +
`dependencies and thus recursively triggering itself. Possible sources ` +
`include component template, render function, updated hook or ` +
`watcher source function.`
)
return true
} else {
seen.set(fn, count + 1)
}
}
}
2、非空执行更新组件updateComponent
const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
const instance = (n2.component = n1.component)!
if (shouldUpdateComponent(n1, n2, optimized)) {
if (
__FEATURE_SUSPENSE__ &&
instance.asyncDep &&
!instance.asyncResolved
) {
// async & still pending - just update props and slots
// since the component's reactive effect for render isn't set-up yet
if (__DEV__) {
pushWarningContext(n2)
}
updateComponentPreRender(instance, n2, optimized)
if (__DEV__) {
popWarningContext()
}
return
} else {
// normal update
instance.next = n2
// in case the child component is also queued, remove it to avoid
// double updating the same child component in the same flush.
invalidateJob(instance.update)
// instance.update is the reactive effect.
instance.update()
}
} else {
// no update needed. just copy over properties
n2.el = n1.el
instance.vnode = n2
}
}
这块我们主要看updateComponentPreRender
这个方法。
const updateComponentPreRender = ( // 触发 instance 依赖收集和更新
instance: ComponentInternalInstance,
nextVNode: VNode,
optimized: boolean
) => {
nextVNode.component = instance
const prevProps = instance.vnode.props
instance.vnode = nextVNode
instance.next = null
updateProps(instance, nextVNode.props, prevProps, optimized)
updateSlots(instance, nextVNode.children, optimized)
// 进行依赖收集和更新的三个方法
pauseTracking() // shouldTrack = true
// props update may have triggered pre-flush watchers.
// flush them before the render update.
flushPreFlushCbs() // 执行任务队列里的任务
resetTracking()
}
render
函数结束
到这里整个的patch过程就结束了,我们再回顾一下render
函数
const render: RootRenderFunction = (vnode, container, isSVG) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container, null, null, null, isSVG) // 新旧节点的比较,不断的回调调用 patch 方法,有心节点进行添加
}
// 执行任务队列
flushPreFlushCbs()
flushPostFlushCbs()
container._vnode = vnode // 把新得到的 vnode 给我们创建的 vue 项目也就是 ("#app")
}
patch 算法执行完之后,render 函数还会再此执行一次任务队列,flushPreFlushCbs
方法直接执行任务队列的任务,但是 flushPostFlushCbs
方法他会判断当前有没有正在执行的任务,如果有正在执行的任务会等待中的任务推到队列中,如果没有正在执行的任务则会直接执行目前需要等待的任务队列。这两个方法也是同样再schedule.ts
文件中。
export function flushPreFlushCbs(
seen?: CountMap,
// if currently flushing, skip the current job itself
i = isFlushing ? flushIndex + 1 : 0
) {
if (__DEV__) {
seen = seen || new Map()
}
for (; i < queue.length; i++) {
const cb = queue[i]
if (cb && cb.pre) {
if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
continue
}
queue.splice(i, 1)
i--
cb() // 按顺序执行任务队列的任务
}
}
}
// pendingPostFlushCbs: 表示等待中的任务队列 activePostFlushCbs: 表示激活的任务队列
export function flushPostFlushCbs(seen?: CountMap) {
if (pendingPostFlushCbs.length) { // 等待执行的任务数组
const deduped = [...new Set(pendingPostFlushCbs)]
pendingPostFlushCbs.length = 0
// #1947 already has active queue, nested(嵌套) flushPostFlushCbs call
if (activePostFlushCbs) { // 有正在执行的任务,就把他推他正在执行的队列中,并返回
activePostFlushCbs.push(...deduped)
return
}
// 没有正在执行的任务,就进行执行该任务队列
activePostFlushCbs = deduped
if (__DEV__) {
seen = seen || new Map()
}
activePostFlushCbs.sort((a, b) => getId(a) - getId(b))
for (
postFlushIndex = 0;
postFlushIndex < activePostFlushCbs.length;
postFlushIndex++
) {
if (
__DEV__ &&
checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])
) {
continue
}
activePostFlushCbs[postFlushIndex]()
}
activePostFlushCbs = null
postFlushIndex = 0
}
}
render
方法最后就会把处理后的vnode
挂载到容器(container
)的虚拟节点(_vnode
)上。
4、mount方法结束
到这一步我们就把render
函数的执行也分析完了,最后就是看看mount
方法最终做了什么。这里只放了mount
的结尾代码,详细代码可参考第二节的分析mount方法
mount(
rootContainer: HostElement, // ===> 我们在 vue 项目中挂载的根组件,就是 app.mount("#app"), 是容器
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isMounted) {
// 省略上方代码
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer, isSVG) // 进行了 render 渲染,调用 patch 函数进行新旧节点比较,新节点添加,最终返回新的 rootContainer
}
isMounted = true
app._container = rootContainer // 把新的 rootContainer 赋值给挂载的容器
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
// 省略下方代码
}
}
在这里我们可以看到,render
方法执行完之后,他会把处理之后的新的 rootContainer
挂载在到 app
实例上的容器(_container
)上;这里的container 其实是一个重点,我们patch算法后会把节点都挂在到container上,最后我们再把container 挂载到app实例上的_container上。
三、应用实例创建完成
到这里整个vue实例就创建完成了,我们再来看看实例创建成功之后做了什么。
export const createApp = ((...args) => {
// 创建了 render 渲染器,和创建了 app 实例,最终返回 app 应用实例 createAppAPI 的方法包括 mount mixin provide 等方法
const app = ensureRenderer().createApp(...args)
if (__DEV__) {
injectNativeTagCheck(app)
injectCompilerOptionsCheck(app)
}
const { mount } = app
// 重写 app 的 mount 方法
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// 返回 containerOrSelector 对应的容器
const container = normalizeContainer(containerOrSelector) // 得到 dom 节点
if (!container) return
const component = app._component
// 组件不存在render函数和模板template,则使用container的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) {
container.removeAttribute('v-cloak') // 删除指定的属性
container.setAttribute('data-v-app', '') // 设置元素的属性值
}
// 返回的是个 vnode
return proxy
}
return app
}) as CreateAppFunction<Element>
提一下的是,我们在创建实例的时候重写了mount方法,我们最终调用mount方法后返回的是proxy,并且也会看到他会判断类型是否是element。到这里整个vue的实例创建和挂载就完成了。
更多推荐
所有评论(0)