2023.24 vue3 渲染系统
大家好,我是wo不是黄蓉,今年学习目标从源码共读开始,希望能跟着若川大佬学习源码的思路学到更多的东西。
5月份事情比较多,没有持续学习,6月份继续学习打卡。上一篇文章结尾写的看下写浏览器插件,这个已经写出来了,但是大多数是api相关的操作,后面就没有分享,只是放在git项目里当作是一个学习吧。
为什么要学习源码?之前我看源码是为了应付面试,现在觉得看源码学习的是一种撸代码思路和一些值得借鉴的代码编写方式,毕竟我们不是大佬,大部分人都是从ctrl+c、ctrl+v过来的。把学到的东西应用项目中也是一种能力。
长话短说,开始学习~
学习调试
-
clone
代码到本地,源码链接 - 本地安装依赖,我是以
todomvc.html
来调试的,调试前,需要执行pnpm run build
打包源码到dist
目录,然后在浏览器运行html页面,就可以调试了
vue
运行包含两个阶段:1编译时(源码不可调试,是由其他类库操作的),2运行时(可进行调试)
createApp 创建应用
createApp
只是准备vue
实例对象,在其对象上挂载了一个mount
方法,执行自身的mount
方法,挂载时需要提供一个挂载的容器
createApp
和mount
之间还有一个baseCreateRenderer
方法,baseCreateRenderer
操作dom
相关操作
mount
方法,创建vnode
执行渲染函数,执行副作用函数进行挂载
渲染函数里面进行patch
操作
借用网上的一张图,整体流程如下,推荐看的一篇vue渲染相关的文章如下
createApp({}).mount('#app') createApp
也就是createApp({})
前面这一步,返回app
对象,等待挂载,看下createApp
做了哪些准备工作
var createApp = (...args) => {
//获取实例,调用的实际是baseCreateRenderer里面的createApp方法
const app = ensureRenderer().createApp(...args);
if (true) {
injectNativeTagCheck(app);
injectCompilerOptionsCheck(app);
}
//从实例中结构出Mount方法
const { mount } = app;
app.mount = (containerOrSelector) => {
const container = normalizeContainer(containerOrSelector);
if (!container)
return;
const component = app._component;
if (!isFunction(component) && !component.render && !component.template) {
//创建模板渲染对象
component.template = container.innerHTML;
}
container.innerHTML = "";
//挂载对象
const proxy = mount(container, false, container instanceof SVGElement);
return proxy;
};
return app;
};
ensureRenderer
创建reder的入口函数,返回的是createRenderer
方法,createRenderer
方法又返回的baseCreateRenderer
方法,baseCreateRenderer
最终返回的是一个对象,返回的对象是调用ensureRenderer
方法最终的结果。
ensureRenderer
调用完后,紧接着调用了createApp
方法,其实也就是baseCreateRenderer
返回对象中的一个方法,实际上是createAppAPI
//ensureRenderer
function ensureRenderer() {
return renderer || (renderer = createRenderer(rendererOptions));
}
function createRenderer(options) {
return baseCreateRenderer(options);
}
//createApp实际返回的内容
function baseCreateRenderer(){
//...相关核心操作dom的方法
const target = getGlobalThis();
target.__VUE__ = true;
return {
render: render2,
hydrate: hydrate2,
createApp: createAppAPI(render2, hydrate2)
};
}
baseCreateRenderer
dom操作相关核心方法,都是一些对节点操作的方法,这些函数分析diff
算法时以及渲染流程时会看到。例如:
createAppAPI
发生了什么?
可以看到这里创建了app对象,并将其返回了,在此期间,关联了插件、指令、mixin、组件、挂载组件的方法等,将app对象和渲染进行关联,实际上渲染操作是在调用mount
方法时进行的
这里会初始化上下文,以及初始化app
,最后返回app
function createAppAPI(render2, hydrate2) {
return function createApp2(rootComponent, rootProps = null) {
if (!isFunction(rootComponent)) {
rootComponent = extend({}, rootComponent);
}
if (rootProps != null && !isObject(rootProps)) {
warn2(`root props passed to app.mount() must be an object.`);
rootProps = null;
}
//初始化上下文
const context = createAppContext();
const installedPlugins = /* @__PURE__ */ new Set();
let isMounted = false;
const app = context.app = {
_uid: uid++,
_component: rootComponent,
_props: rootProps,
_container: null,
_context: context,
_instance: null,
version,
use(plugin, ...options) {},
mixin(mixin) { },
component(name, component) { },
directive(name, directive) {},
mount(rootContainer, isHydrate, isSVG) { },
unmount() { },
provide(key, value) {},
runWithContext(fn) {
currentApp = app;
}
};
return app;
};
}
直到执行.mount("#app")
都是在准备环境,到执行mount
方法才开始渲染模板和解析模板
最主要的方法mount
方法,创建vnode
执行渲染函数,执行副作用函数进行挂载
进入mount
方法,进入第二阶段挂载阶段
createApp
返回的内容
mount(rootContainer, isHydrate, isSVG) {
//挂载函数,创建vnode
const vnode = createVNode(
rootComponent,
rootProps
);
//将vnode和app上下文关联起来
vnode.appContext = context;
if (true) {
//添加reload函数
context.reload = () => {
render2(cloneVNode(vnode), rootContainer, isSVG);
};
}
//执行render函数进行渲染,将vnode渲染成dom节点并进行挂载,render过程会检查组件是否更新,继续调用patch方法对比前后节点
render2(vnode, rootContainer, isSVG);
isMounted = true;
app._container = rootContainer;
rootContainer.__vue_app__ = app;
if (true) {
app._instance = vnode.component;
devtoolsInitApp(app, version);
}
return getExposeProxy(vnode.component) || vnode.component.proxy;
}
},
挂载阶段又分为几个小阶段
挂载阶段
2.1 createVNode创建vnode
createVNode
返回的是createBaseVNode
,调用createBaseVNode
返回vnode
,mount
中const vnode = createVNode( rootComponent, rootProps );
返回的结果就是createBaseVNode
调用的结果
创建虚拟节点,获取到虚拟节点后进行渲染操作render2(vnode, rootContainer, isSVG);
接下来到执行render
方法
createBaseVNode
时为什么要在顶部创建一个空白的vnode
?
到最后面才看出为什么要创建一个空的vnode
,这个vnode
因为没有对#app
根元素进行解析,所以创建的这个空的vnode
节点就相当于是根节点的占位符,解析完所有子元素后,render
函数最后会执行这样一行代码container._vnode = vnode;
而最后vnode
内容就是#app
下解析的第一个元素的容器
function createVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, isBlockNode = false) {
//type可能是一个组件或者是一个html元素,是vnode的话,直接复制,不用重复解析
if (isVNode(type)) {
const cloned = cloneVNode(
type,
props,
true
/* mergeRef: true */
);
}
//创建vnode
return createBaseVNode(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag, //shapeFlag:代表的元素类型
isBlockNode,
true
);
}
//为什么要在顶部创建一个空白的vnode?
function createBaseVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, shapeFlag = type === Fragment ? 0 : 1 /* 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
};
return vnode;
}
render
函数,执行第一次的patch
操作 。挂载vnode
进行渲染
const render2 = (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);
}
flushPreFlushCbs();
flushPostFlushCbs();
container._vnode = vnode;
};
2.2 检查更新阶段 patch
patch
确定元素类型,挂载组件。第一次patch
是从render2
开始的,第一次判定为组件,执行:processComponent
方法
这里不知道为什么经常用到位运算?
在网上搜到的答案,使用位运算是为了提升性能
学习位运算推荐看下这篇文章,其中包括实战中权限方案的设计等,可以通过位运算来添加、校验、删除权限,很是神奇
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = isHmrUpdating ? false : !!n2.dynamicChildren) => {
const { type, ref: ref2, 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 (true) {
patchStaticNode(n1, n2, container, isSVG);
}
break;
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
);
break;
default:
if (shapeFlag & 1 /* ELEMENT */) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
);
} else if (shapeFlag & 6 /* COMPONENT */) {
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
);
} else if (shapeFlag & 64 /* TELEPORT */) {
;
type.process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
);
} else if (shapeFlag & 128 /* SUSPENSE */) {
;
type.process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
);
} else if (true) {
warn2("Invalid VNode type:", type, `(${typeof type})`);
}
}
if (ref2 != null && parentComponent) {
setRef(ref2, n1 && n1.ref, parentSuspense, n2 || n1, !n2);
}
};
2.3. 组件挂载阶段
mountComponent
,设置render
副作用
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
const compatMountInstance = false;
const instance = compatMountInstance || (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
));
//...前面这些都不重要,最重要是触发setupRenderEffect
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
);
if (true) {
popWarningContext();
endMeasure(instance, `mount`);
}
};
2.4 设置副作用 setupRenderEffect
setupRenderEffect
主要部分代码,创建了副作用对象,立即执行其update
方法,即执行effect2.run()
,而run
里面传的可执行函数是componentUpdateFn
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
//重要部分代码2
const effect2 = instance.effect = new ReactiveEffect2(
componentUpdateFn,
() => queueJob(update),
instance.scope
// track it in component's effect scope
);
const update = instance.update = () => effect2.run();
update.id = instance.uid;
update();
};
componentUpdateFn
这里进行了第二次patch
操作,此时对比的对象是子树,第二次时遍历的对象是模板里面#app下的元素,会执行到processElement
方法,然后执行mountElement
方法,
const componentUpdateFn = () => {
if (!instance.isMounted) {
let vnodeHook;
const { el, props } = initialVNode;
const { bm, m, parent } = instance;
const isAsyncWrapperVNode = isAsyncWrapper(initialVNode);
toggleRecurse(instance, true);
if (el && hydrateNode) {
} else {
const subTree = instance.subTree = renderComponentRoot(instance);
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
);
initialVNode.el = subTree.el;
}
if (m) {
queuePostRenderEffect(m, parentSuspense);
}
if (!isAsyncWrapperVNode && (vnodeHook = props && props.onVnodeMounted)) {
const scopedInitialVNode = initialVNode;
queuePostRenderEffect(
() => invokeVNodeHook(vnodeHook, parent, scopedInitialVNode),
parentSuspense
);
}
if (false) {
queuePostRenderEffect(
() => instance.emit("hook:mounted"),
parentSuspense
);
}
if (initialVNode.shapeFlag & 256 /* COMPONENT_SHOULD_KEEP_ALIVE */ || parent && isAsyncWrapper(parent.vnode) && parent.vnode.shapeFlag & 256 /* COMPONENT_SHOULD_KEEP_ALIVE */) {
instance.a && queuePostRenderEffect(instance.a, parentSuspense);
if (false) {
queuePostRenderEffect(
() => instance.emit("hook:activated"),
parentSuspense
);
}
}
instance.isMounted = true;
if (true) {
devtoolsComponentAdded(instance);
}
initialVNode = container = anchor = null;
} else {
let { next, bu, u, parent, vnode } = instance;
let originNext = next;
let vnodeHook;
if (true) {
pushWarningContext(next || instance.vnode);
}
toggleRecurse(instance, false);
if (next) {
next.el = vnode.el;
updateComponentPreRender(instance, next, optimized);
} else {
next = vnode;
}
if (bu) {
invokeArrayFns(bu);
}
if (vnodeHook = next.props && next.props.onVnodeBeforeUpdate) {
invokeVNodeHook(vnodeHook, parent, next, vnode);
}
if (false) {
instance.emit("hook:beforeUpdate");
}
toggleRecurse(instance, true);
if (true) {
startMeasure(instance, `render`);
}
const nextTree = renderComponentRoot(instance);
if (true) {
endMeasure(instance, `render`);
}
const prevTree = instance.subTree;
instance.subTree = nextTree;
if (true) {
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 (true) {
endMeasure(instance, `patch`);
}
next.el = nextTree.el;
if (originNext === null) {
updateHOCHostEl(instance, nextTree.el);
}
if (u) {
queuePostRenderEffect(u, parentSuspense);
}
if (vnodeHook = next.props && next.props.onVnodeUpdated) {
queuePostRenderEffect(
() => invokeVNodeHook(vnodeHook, parent, next, vnode),
parentSuspense
);
}
if (false) {
queuePostRenderEffect(
() => instance.emit("hook:updated"),
parentSuspense
);
}
if (true) {
devtoolsComponentUpdated(instance);
}
if (true) {
popWarningContext();
}
}
};
mountElement
,挂载元素操作,将vnode转成dom并进行挂载,最后一次执行hostInsert
即将所有解析到的元素插入到#app
,执行完就可以看到页面已经挂载到页面上了。vue3子节点挂载到父元素的过程不是一次性的,而是每次解析完子元素就进行挂载,最后一次将所有#app
下的元素都挂载上去,页面就渲染完成了
const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
let el;
let vnodeHook;
const { type, props, shapeFlag, transition, dirs } = vnode;
//从这里可以看出来,vue对每次操作vnode以及将vnode转为dom的记录都是有保存的,等到下次需要时可以直接拿出来使用,不用重复进行解析,其他地方mountChildren const child = children[i] = optimized ? cloneIfMounted(children[i]) : normalizeVNode(children[i]);判断如果时vnode直接进行复制
el = vnode.el = hostCreateElement(
vnode.type,
isSVG,
props && props.is,
props
);
if (shapeFlag & 8 /* TEXT_CHILDREN */) {
hostSetElementText(el, vnode.children);
} else if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
mountChildren(
vnode.children,
el,
null,
parentComponent,
parentSuspense,
isSVG && type !== "foreignObject",
slotScopeIds,
optimized
);
}
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, "created");
}
setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent);
if (props) {
for (const key in props) {
if (key !== "value" && !isReservedProp(key)) {
hostPatchProp(
el,
key,
null,
props[key],
isSVG,
vnode.children,
parentComponent,
parentSuspense,
unmountChildren
);
}
}
if ("value" in props) {
hostPatchProp(el, "value", null, props.value);
}
if (vnodeHook = props.onVnodeBeforeMount) {
invokeVNodeHook(vnodeHook, parentComponent, vnode);
}
}
if (true) {
Object.defineProperty(el, "__vnode", {
value: vnode,
enumerable: false
});
Object.defineProperty(el, "__vueParentComponent", {
value: parentComponent,
enumerable: false
});
}
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);
}
};
hostCreateElement
,根据vnode创建dom元素
createElement: (tag, isSVG, is, props) => {
const el = isSVG ? doc.createElementNS(svgNS, tag) : doc.createElement(tag, is ? { is } : void 0);
if (tag === "select" && props && props.multiple != null) {
;
el.setAttribute("multiple", props.multiple);
}
return el;
},
hostInsert
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null);
},
vue3为什么要每次先创建一个对象然后再在某个时刻触发对象里面的方法呢?
例如,在createApp阶段,使用ensureRenderer().createApp(...args)
来创建app
,又例如,在mount
阶段执行副作用函数,是将组将更新的操作挂载到effect
上
盲猜,其实将操作对象的方法放在外面也是可以的,不过这样和该对象关联的相关操作就比较分散,将所有操作都挂载到对象上其实更方便理解代码逻辑比较清晰。
这次大多数走的是第一次渲染的流程,代码太多,文章太长,下节看下更新渲染的流程。