赞
踩
【前端】Typescript入门
【Vue3】源码解析-前置
【Vue3】源码解析-响应式原理
【Vue3】源码解析-虚拟DOM
【Vue3】源码解析-编绎模块
【Vue3】源码解析-Runtime
vue主入口文件中,引入导出了runtime-dom和compiler,而createApp就是来自runtime-dom
// packages/runtime-dom/src/index.ts export const createApp = ((...args) => { const app = ensureRenderer().createApp(...args) if (__DEV__) { injectNativeTagCheck(app) // 在dev环境***册一个方法isNativeTag,挂载到app.config下面 } const { mount } = app app.mount = (containerOrSelector: Element | ShadowRoot | string): any => { // ... } return app }) as CreateAppFunction<Element>
在该函数内部中通过调用ensureRenderer()和createApp(…args)创建了app实例并把实例返回出去,因此我们可以在app实例中安装插件,设置全局指令等等。这其中又是怎么实现的呢?
ensureRenderer()函数的用途是什么呢?
// packages/runtime-dom/src/index.ts
function ensureRenderer() {
return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
我们可以看到调用该函数后返回一个renderer,若没有renderer则调用createRenderer来进行创建。
而这边的createRenderer则是来自runtime-core
// packages/runtime-core/src/index.ts
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
该函数接收一个RendererOptions作为参数,其实际是调用了baseCreateRenderer并将options传入
传入的RendererOptions是什么?为什么在runtime-dom传入,又在runtime-core拆解。
// packages/runtime-dom/src/index.ts
const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps)
当创建完app实例后,现在让我们开始进行mount(‘#app’),让我们重新进入createApp
// packages/runtime-dom/src/index.ts export const createApp = ((...args) => { const app = ensureRenderer().createApp(...args) if (__DEV__) { injectNativeTagCheck(app) } const { mount } = app // 保存app实例上原本的mount // 重写mount app.mount = (containerOrSelector: Element | ShadowRoot | string): any => { const container = normalizeContainer(containerOrSelector) // 获取根元素容器 if (!container) return const component = app._component // 获取根组件,即App if (!isFunction(component) && !component.render && !component.template) { component.template = container.innerHTML // 使用根元素来作为模板 } // clear content before mounting container.innerHTML = '' const proxy = mount(container) // 调用实例中的mount方法 if (container instanceof Element) { container.removeAttribute('v-cloak') // 删除v-cloak属性 container.setAttribute('data-v-app', '') // 添加data-v-app属性 } return proxy } return app }) as CreateAppFunction<Element>
我们可以看到在上面的代码中,在创建完app之后,先对app实例中的mount方法进行了保存,接着又对mount进行了重写。
重写的mount方法中,先是调用了normalizeContainer(containerOrSelector)来获取根元素容器,containerOrSelector即我们传入的#app
// packages/runtime-dom/src/index.ts
function normalizeContainer(
container: Element | ShadowRoot | string
): Element | null {
if (isString(container)) {
const res = document.querySelector(container) // 进行dom操作选中容器
if (__DEV__ && !res) {
// ...
}
return res
}
// ...
return container as any
}
在获取到根元素的容器之后,进行判断,将容器原本的html作为根组件的模板,然后清除了容器中原本的html
// 创建组件实例 export function createComponentInstance(vnode) { const component = { vnode, type:vnode.type } return component } // 初始化组件 export function setupComponent(instance) { // TODO // initProps() // initSlots() setupStatefulComponent(instance) } // 设置组件状态 function setupStatefulComponent(instance) { const Component = instance.type const { setup } = Component if (setup) { const setupResult = setup() handleSetupResult(instance, setupResult) } } // 处理setup的结果 function handleSetupResult(instance, setupResult) { // function object // TODO funciton if (typeof setupResult === "object") { instance.setupState = setupResult } finishComponentSetup(instance) } // 完成组件设置 function finishComponentSetup(instance) { const Component = instance.render if(!Component.render) { instance.render = Component.render } }
完成源码初始化组件的逻辑,可以对照着这张流程图看一看代码的运行过程:
/* // type only h('div') // type + props h('div', {}) // type + omit props + children // Omit props does NOT support named slots h('div', []) // array h('div', 'foo') // text h('div', h('br')) // vnode h(Component, () => {}) // default slot // type + props + children h('div', {}, []) // array h('div', {}, 'foo') // text h('div', {}, h('br')) // vnode h(Component, {}, () => {}) // default slot h(Component, {}, {}) // named slots // named slots without props requires explicit `null` to avoid ambiguity h(Component, null, {}) **/ import { isArray, isObject } from '@vue/shared'; import { createVNode, isVNode } from './vnode'; export function h(type, propsOrChildren?, children?) { const l = arguments.length; if (l === 2) { if (isObject(propsOrChildren) && !isArray(propsOrChildren)) { if (isVNode(propsOrChildren)) { return createVNode(type, null, [propsOrChildren]); } return createVNode(type, propsOrChildren); } else { return createVNode(type, null, propsOrChildren); } } else { if (l > 3) { children = Array.prototype.slice.call(arguments, 2); } else if (l === 3 && isVNode(children)) { children = [children]; } return createVNode(type, propsOrChildren, children); } }
h方法对创建虚拟节点操作进行了二次封装,使用法变得多种多样
import { isString, ShapeFlags } from '@vue/shared'; import { createVNode, isSameVNode, Text } from './vnode'; export function createRenderer(renderOptions) { let { insert: hostInsert, createElement: hostCreateElement, createText: hostCreateText, remove: hostRemove, setElementText: hostSetElementText, setText: hostSetText, querySelector: hostQuerySelector, parentNode: hostParentNode, nextSibling: hostNextSibling, patchProp: hostPatchProp, } = renderOptions; const normalize = (child, i) => { if (isString(child[i])) { let vnode = createVNode(Text, null, child[i]); child[i] = vnode; return child[i]; } return child[i]; }; // 递归挂载子节点 const mountChildren = (children, container) => { for (let i = 0; i < children.length; i++) { let child = normalize(children, i); patch(null, child, container); } }; const mountElement = (vnode, container) => { let { type, props, children, shapeFlag } = vnode; // 挂载真实dom到vnode上 let el = (vnode.el = hostCreateElement(type)); // 属性 if (props) { for (const key in props) { hostPatchProp(el, key, null, props[key]); } } // 子节点处理,& 预算判断是否为某一个类型 if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { // 文本 hostSetElementText(el, children); } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { mountChildren(children, el); } // 插入真实dom到容器中 hostInsert(el, container); }; const processText = (n1, n2, container) => { if (n1 === null) { hostInsert((n2.el = hostCreateText(n2.children)), container); } else { // 文本内容变化,节点复用 const el = (n2.el = n1.el); if (n1.children !== n2.children) { // 更新文本 hostSetText(el, n2.children); } } }; const patchProps = (oldProps, newProps, el) => { for (let key in newProps) { hostPatchProp(el, key, oldProps[key], newProps[key]); } for (let key in oldProps) { if (!newProps[key]) { hostPatchProp(el, key, oldProps[key], undefined); } } }; const unmountChildren = (children) => { for (let i = 0; i < children.length; i++) { unmount(children[i]); } }; // 比较两个节点的差异 const patchKeyChildren = (c1, c2, el) => { }; // 比较两个节点的子节点,el为当前父节点 const patchChildren = (n1, n2, el) => { const c1 = n1.children; const c2 = n2.children; const prevShapeFlag = n1.shapeFlag; const shapeFlag = n2.shapeFlag; // 新值为文本 if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { // 旧值为数组 if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { // 文本 数组 unmountChildren(c1); } if (c1 !== c2) { // 文本 文本 hostSetElementText(el, c2); } } else { // 旧值为数组 if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { // 新值为数组 if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { // 数组 数组 diff patchKeyChildren(c1, c2, el); // 全量更新,同级比较 } else { // 空 数组 unmountChildren(c1); } } else { if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) { // 空 文本 // 数组 文本 hostSetElementText(el, ''); } if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { // 数组 空 // 数组 文本 mountChildren(c2, el); } } } }; // 先复用节点,然后比较属性,再比较子节点 const patchElement = (n1, n2) => { // 复用节点 let el = (n2.el = n1.el); let oldProps = n1.props || {}; let newProps = n2.props || {}; patchProps(oldProps, newProps, el); patchChildren(n1, n2, el); }; const processElement = (n1, n2, container) => { if (n1 === null) { mountElement(n2, container); } else { // 对比元素 patchElement(n1, n2); } }; const patch = (n1, n2, container) => { if (n1 === n2) { return; } // 如果新值与老值完全没有可比性,删除老值,创建新值 if (n1 && !isSameVNode(n1, n2)) { unmount(n1); n1 = null; } const { type, shapeFlag } = n2; switch (type) { case Text: // 文本 processText(n1, n2, container); break; default: if (shapeFlag & ShapeFlags.ELEMENT) { // 元素 processElement(n1, n2, container); } break; } }; const unmount = (vnode) => { hostRemove(vnode.el); }; const render = (vnode, container) => { if (vnode === null) { // 卸载dom if (container._vnode) { unmount(container._vnode); } } else { // 初始化及更新 patch(container._vnode || null, vnode, container); } // 缓存下次直接更新 container._vnode = vnode; }; return { render }; }
// packages/runtime-core/src/renderer.ts function baseCreateRenderer( options: RendererOptions, createHydrationFns?: typeof createHydrationFunctions ): any { const { insert: hostInsert, remove: hostRemove, patchProp: hostPatchProp, forcePatchProp: hostForcePatchProp, createElement: hostCreateElement, createText: hostCreateText, createComment: hostCreateComment, setText: hostSetText, setElementText: hostSetElementText, parentNode: hostParentNode, nextSibling: hostNextSibling, setScopeId: hostSetScopeId = NOOP, cloneNode: hostCloneNode, insertStaticContent: hostInsertStaticContent } = options // 声明了许多操作函数,约2000行 return { render, hydrate, createApp: createAppAPI(render, hydrate) } }
在调用完baseCreateRenderer后主要返回了三个函数:render,hydrate,createApp。
此时renderer便创建完成了
当我们拥有这个vnode后,就开始进入渲染阶段了。render(vnode, rootContainer),可以看到传入的参数为vnode以及根元素的容器,接下来让我们继续深入。
不知道你是否还记得,这个render函数是在调用createAPI时传入的第一个参数,因此这个函数来源于runtime-core中的baseCreateRenderer
// packages/runtime-core/src/renderer.ts
const render: RootRenderFunction = (vnode, container) => {
// (判断进行卸载还是渲染
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true) // 卸载
}
} else {
patch(container._vnode || null, vnode, container) // 创建或更新组件,进行dom diff和渲染
}
flushPostFlushCbs() // 回调调度器,使用Promise实现,与Vue2的区别是Vue2是宏任务或微任务来处理的
container._vnode = vnode // 缓存vnode节点,证明已经渲染完成,方便后续diff操作
}
在render函数中,对vnode的存在进行了判断,如果为空,则对组件进行销毁,否则将调用patch,创建组件。
接下来让我们继续进入patch函数
// packages/runtime-core/src/renderer.ts const patch: PatchFn = ( n1, // 旧 n2, // 新 container, // 容器 anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false ) => { // 如果两者类型不同,则直接卸载n1 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 // 根据不同的节点类型来进行不同的process方法 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: // 片段(dom数组) processFragment( // ... ) break default: if (shapeFlag & ShapeFlags.ELEMENT) { // 原生节点(div) processElement( // ... ) } else if (shapeFlag & ShapeFlags.COMPONENT) { // 组件,根组件即通过processComponent处理 processComponent( // ... ) } else if (shapeFlag & ShapeFlags.TELEPORT) { // 传送组件 ;(type as typeof TeleportImpl).process( // ... ) } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { // 挂起组件-异步渲染 ;(type as typeof SuspenseImpl).process( // ... ) } else if (__DEV__) { warn('Invalid VNode type:', type, `(${typeof type})`) } } // 处理节点之后处理ref if (ref != null && parentComponent) { setRef(ref, n1 && n1.ref, parentSuspense, n2) } }
我们可以看到在创建vnode时定义的shapeFlag,在这里发挥了作用。根组件经过逻辑流程之后也进入了processComponent之中。
// packages/runtime-dom/src/nodeOps.ts
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},
remove: child => {
const parent = child.parentNode
if (parent) {
parent.removeChild(child)
}
},
// ...
}
其实就是对于dom操作的封装。那为什么要在runtime-dom中传入,runtime-core拆解?
其实是因为在Vue3中runtime-core和runtime-dom的拆分,runtime-core不应该关心实际的操作,这样当新平台要接入时(比如weex)就可以只实现属于自己平台的nodeOps。
总结:创建renderer的函数调用顺序为
当创建完renderer后返回了3个函数,我们可以看到其中createApp实际上是引用了createAppAPI(render, hydrate),所以其实const app = ensureRenderer().createApp(…args)创建app实例时,调用的是createAppAPI的返回值(运用柯里化,返回的是一个函数)
// packages/runtime-core/src/apiCreateApp.ts export function createAppContext(): AppContext { return { app: null as any, // 刚创建时为空 config: { isNativeTag: NO, performance: false, globalProperties: {}, optionMergeStrategies: {}, isCustomElement: NO, errorHandler: undefined, warnHandler: undefined }, mixins: [], components: {}, directives: {}, provides: Object.create(null) } } export function createAppAPI<HostElement>( render: RootRenderFunction, hydrate?: RootHydrateFunction ): CreateAppFunction<HostElement> { return function createApp(rootComponent, rootProps = null) { // 检验root props if (rootProps != null && !isObject(rootProps)) { __DEV__ && warn(`root props passed to app.mount() must be an object.`) rootProps = null } const context = createAppContext(); // 创建context const installedPlugins = new Set(); // 创建插件列表集合,储存已安装的插件 let isMounted = false; const app: App = (context.app = { _component: rootComponent as Component, _props: rootProps, _container: null, _context: context, version, get config() {}, set config() {} use() {}, mixin() {}, component() {}, mount() {} // ... }) return app // 返回创建的app实例 }; }
看完上面的代码后结果就很清楚了,当我们调用createApp时,返回的app上有着许多函数方法和属性,相信你对这些函数方法并不陌生,这些就是vue2.x中在Vue上的那些API:use、mixin、component,在vue3则是被挂载到了app实例上
需要注意的是:我们在应用中调用的createApp(App),其中的APP就是第一个参数,作为根组件
// packages/runtime-core/src/apiCreateApp.ts mount(rootContainer: HostElement, isHydrate?: boolean): any { if (!isMounted) { // 1.创建vnode const vnode = createVNode( rootComponent as ConcreteComponent, // App组件 rootProps ) vnode.appContext = context // 保存context在根节点上 // HMR root reload if (__DEV__) { // ... } if (isHydrate && hydrate) { hydrate(vnode as VNode<Node, Element>, rootContainer as any) } else { render(vnode, rootContainer) // 2.进入render,函数来源于runtime-core } isMounted = true // 修改状态 app._container = rootContainer // for devtools and telemetry ;(rootContainer as any).__vue_app__ = app if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { devtoolsInitApp(app, version) } return vnode.component!.proxy // 返回vnode.component的代理 } else if (__DEV__) { // ... } },
runtime-core中mount方法主要做了两件事:创建vnode和调用render进入渲染。这里我们先简略的介绍一下这两个函数的作用。
在创建vnode时调用了createVNode(),并将根组件作为参数传入。
在得到vnode之后又调用了render()开始进行渲染。
最后mount函数的返回值为vnode.component的代理。
import { isArray, isString, ShapeFlags } from '@vue/shared'; export const Text = Symbol('Text'); export function isVNode(value) { return !!(value && value.__v_isVnode); } /** * 创建虚拟节点 * @param type 虚拟节点类型 * @param props 属性 * @param children 子节点 */ export function createVNode(type, props, children = null) { let shapeFlag = isString(type) ? ShapeFlags.ELEMENT : 0; // 虚拟dom,可以跨平台,性能好 const vnode = { __v_isVnode: true, // 是否是虚拟节点 shapeFlag, // 类型标识 type, // 节点类型 props, // 属性 children, // 子节点 key: props?.key, // key /** * 对应的真实节点,后续diff算法比对两个vnode时会替换新的属性值,并更新el */ el: null, }; if (children) { let type = 0; if (isArray(children)) { type = ShapeFlags.ARRAY_CHILDREN; } else { children = String(children); type = ShapeFlags.TEXT_CHILDREN; } // 通过位运算将当前vnode类型及子节点类型存储起来 vnode.shapeFlag |= type; } return vnode; }
虚拟节点不用考虑平台兼容,并且可以将虚拟节点利用js存储并进行比对后再渲染真实dom,不用频繁操作dom元素,性能更好
// packages/runtime-core/src/vnode.ts export const createVNode = (__DEV__ ? createVNodeWithArgsTransform : _createVNode) as typeof _createVNode // 实际调用 function _createVNode( type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, props: (Data & VNodeProps) | null = null, children: unknown = null, patchFlag: number = 0, // patch flag默认为0 dynamicProps: string[] | null = null, isBlockNode = false ): VNode { // ... // ... // class & style normalization // 处理props,标准化calss和style if (props) { // for reactive or proxy objects, we need to clone it to enable mutation. if (isProxy(props) || InternalObjectKey in props) { props = extend({}, props) } let { class: klass, style } = props if (klass && !isString(klass)) { props.class = normalizeClass(klass) // 标准化class } 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) // 标准化style } } // 定义shapeFlag // 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 // 根组件shapeFlag : isFunction(type) ? ShapeFlags.FUNCTIONAL_COMPONENT : 0 // ... // 创建vnode对象 const vnode: VNode = { __v_isVNode: true, [ReactiveFlags.SKIP]: true, type, props, key: props && normalizeKey(props), ref: props && normalizeRef(props), scopeId: currentScopeId, children: null, 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 } // ... normalizeChildren(vnode, children) // 标准化子节点 // ... return vnode // 返回创建完的vnode }
可以看到createVNode主要做了四件事:
shapeFlag的定义如下:
// packages/runtime-core/src/renderer.ts const patch: PatchFn = ( n1, // 旧 n2, // 新 container, // 容器 anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false ) => { // 如果两者类型不同,则直接卸载n1 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 // 根据不同的节点类型来进行不同的process方法 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: // 片段(dom数组) processFragment( // ... ) break default: if (shapeFlag & ShapeFlags.ELEMENT) { // 原生节点(div) processElement( // ... ) } else if (shapeFlag & ShapeFlags.COMPONENT) { // 组件,根组件即通过processComponent处理 processComponent( // ... ) } else if (shapeFlag & ShapeFlags.TELEPORT) { // 传送组件 ;(type as typeof TeleportImpl).process( // ... ) } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { // 挂起组件-异步渲染 ;(type as typeof SuspenseImpl).process( // ... ) } else if (__DEV__) { warn('Invalid VNode type:', type, `(${typeof type})`) } } // 处理节点之后处理ref if (ref != null && parentComponent) { setRef(ref, n1 && n1.ref, parentSuspense, n2) } }
我们可以看到在创建vnode时定义的shapeFlag,在这里发挥了作用。根组件经过逻辑流程之后也进入了processComponent之中。
Vue3源码学习之路-实现runtime-core
【Vue3源码Runtime-core篇】 第二章初始化Component
代码先锋网
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。