本地调试源码
拷贝一份源码
安装依赖yarn install
执行yarn dev -s生成dist文件夹,里面有打包后的文件和sourceMap文件。
写一个.html文件,引用生成的打包文件,/packages/vue/dist/vue.global.js
此时就可以开始debug了目录结构 此为package下面的目录结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 . ├── compiler-core ├── compiler-dom ├── compiler-sfc ├── compiler-ssr ├── global .d.ts ├── reactivity ├── runtime-core ├── runtime-dom ├── runtime-test ├── server-renderer ├── shared ├── size-check ├── template-explorer └── vue
先从入口函数来分析,从组件到DOM的过程
入口 1.创建App对象 demo.html
1 2 3 4 5 6 7 8 9 const { ref } = Vue const vm = Vue.createApp({ setup ( ) { const count = ref(1 ); return { count } } }).mount('#app' );
当我们调用Vue.createApp时候,会执行
1 2 3 4 5 6 7 8 9 10 export const createApp = ((...args ) => { const app = ensureRenderer().createApp(...args) const { mount } = app app.mount = (containerOrSelector: Element | ShadowRoot | string): any => { ... } return app })
ensureRenderer() 来延时创建渲染器,好处是当用户只依赖响应式包的时候,就不会创建渲染器,因此可以通过 tree-shaking 的方式移除核心渲染逻辑相关的代码。渲染器是为跨平台渲染做准备。
1 2 3 4 5 6 7 8 9 let renderer: Renderer<Element> | HydrationRendererfunction ensureRenderer ( ) { return renderer || (renderer = createRenderer<Node, Element>(rendererOptions)) }
ensureRenderer()会调用createRenderer()会返回baseCreateRenderer(),这个函数有2000多行,也是Vue渲染的主要逻辑,重点关注最终返回的createApp()
1 2 3 4 5 6 7 8 9 function baseCreateRenderer (options: RendererOptions, createHydrationFns?: typeof createHydrationFunctions ) { ...... return { render, hydrate, createApp: createAppAPI(render, hydrate) } }
createAppAPI是我们在调用Vue.createApp()真正被调用到的方法,上面这几个方法大致调用如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const app = Vue.createApp(App)export const createApp = ((...args ) => { const app = ensureRenderer().createApp(...args) }function ensureRenderer ( ) { return renderer || (renderer = createRenderer<Node, Element>(rendererOptions)) }export function createRenderer (options: RendererOptions<HostNode, HostElement> ) { return baseCreateRenderer(options) }function baseCreateRenderer ( ) { ...... return { render, hydrate, createApp: createAppAPI(render, hydrate) } }
里面的第一个rootComponent参数就是传入的配置对象。 当我们在调用Vue.create(App),会把App组件对象传入createApp中的rootComponent对象。createApp 内部就创建了一个 app 对象,它会提供 mount 方法,这个方法是用来挂载组件的。 这里通过函数柯里化返回createApp,避免大量的if else,也为了可以跨平台和参数复用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 export function createAppAPI <HostElement >(render: RootRenderFunction, hydrate?: RootHydrateFunction ) { return function createApp (rootComponent, rootProps = null ) { if (rootProps != null && !isObject(rootProps)) { __DEV__ && warn(`root props passed to app.mount() must be an object.` ) rootProps = null } let isMounted = false const context = createAppContext() const installedPlugins = new Set () const app: App = context.app = { _uid: uid++, _component: rootComponent as ConcreteComponent, _props: rootProps, _container: null , _context: context, version, get config () { return context.config }, set config (v ) { if (__DEV__) { warn( `app.config cannot be replaced. Modify individual options instead.` ) } }, use ( ) {...}, mixin ( ) {...}, component ( ) {...}, directive ( ) {...}, mount (rootContainer ) { const vnode = createVNode(rootComponent, rootProps) render(vnode, rootContainer) app._container = rootContainer return vnode.component.proxy } unmount ( ) {...}, } return app } }
createAppContext创建AppContext默认的上下文对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 ) } }
创建好的app对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { component: component(name, component), config: (), directive: directive(name, directive), mixin:mixin(mixin), mount: mount (rootContainer, isHydrate, isSVG ) {}, provide:provide(key, value), unmount: unmount(), use: use(plugin, ...options), version: "3.1.0-beta.3" , _component: {data : (){}, mounted : (){}, setup : (){}}, _container: null , _context: {app : {…}, config : {…}, mixins : Array (), components : {}, directives : {}}, _props: null , _uid: 0 , get config: ƒ config(), set config: ƒ config(v) }
最终入口函数返回app就是上面的app对象
1 2 3 4 5 6 7 8 9 10 export const createApp = ((...args ) => { const app = ensureRenderer().createApp(...args) const { mount } = app app.mount = (containerOrSelector: Element | ShadowRoot | string): any => { ... } return app })
2.mount重写和挂载 可以看到,createApp入口函数中的app对象中的mount方法被重写。 返回的app.mount被重写,在执行Vue.create().mount('#app')会执行这里的挂载函数,通过normalizeContainer获取到挂载的dom节点,如果没有render,template字符串等,就获取容器的innerHTML作为模板挂载到component.template,这点有点像Vue2.x,最后调用mount做真正的挂载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 const { mount } = app app.mount = (containerOrSelector: Element | ShadowRoot | string): any => { const container = normalizeContainer(containerOrSelector) if (!container) return const component = app._component if (!isFunction(component) && !component.render && !component.template) { component.template = container.innerHTML 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 } } } } container.innerHTML = '' const proxy = mount(container, false , container instanceof SVGElement) if (container instanceof Element) { container.removeAttribute('v-cloak' ) container.setAttribute('data-v-app' , '' ) } return proxy }return app
可以看到,这里最终返回了app对象,也就是我们在应用里面最终返回的app
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const App = { setup ( ) { const a = Vue.ref(1 ) return { a } }, }const app = Vue.createApp(App) app.mount('#app' )
为什么要重写mount方法? 因为Vue不仅仅是支持web端。而是支持跨平台,app内部的mount方法是一个标准的跨平台流程。
创建vnode和渲染vnode 在Vue2.x中,没有render函数情况下,当获取完dom中的template,就会开始将template编译为vnode,Vue3也类似
上面创建好app对象,获取到容器的innetHTML作为template,调用了app.mount方法,看一下mount函数的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 export function createAppAPI <HostElement >(render: RootRenderFunction ) { return function createApp (rootComponent, rootProps = null ) { _uid: uid++, _component: rootComponent as ConcreteComponent, _props: rootProps, _container: null , _context: context, version, get config () { return context.config }, set config (v ) { if (__DEV__) { warn( `app.config cannot be replaced. Modify individual options instead.` ) } }, use ( ) {...}, mixin ( ) {...}, component ( ) {...}, directive ( ) {...}, mount( rootContainer: HostElement, isHydrate?: boolean, isSVG?: boolean ): any { if (!isMounted) { const vnode = createVNode( rootComponent as ConcreteComponent, rootProps ) vnode.appContext = context isMounted = true app._container = rootContainer return vnode.component!.proxy } } } }
可以看到,核心就是将当前的rootComponent,也就是当前的App 组件对象通过createVNode转换为vnode,template是之前调用被重写的mount()方法而来的。基于App根选项创建根vnode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 if (!isMounted) { const vnode = createVNode( rootComponent as ConcreteComponent, rootProps ) }
createVNode 其最终调用的是_createVNode方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 export const createVNode = (__DEV__? createVNodeWithArgsTransform: _createVNode) as typeof _createVNodefunction _createVNode ( type, NULL_DYNAMIC_COMPONENT,props = null ,children = null , patchFlag = 0 , dynamicProps = null , isBlockNode = false ) { ... }
其中有几个关键的步骤 第一个是格式化组件props中的class 和 style
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function _createVNode ( ) { if (props) { if (isProxy(props) || InternalObjectKey in props) { props = extend({}, props) } let { class : klass, style } = props if (klass && !isString(klass)) { props.class = normalizeClass(klass) } if (isObject(style)) { if (isProxy(style) && !isArray(style)) { style = extend({}, style) } props.style = normalizeStyle(style) } } }
第二个是对当前的type对象做判断,创建vnode会用到,用来标志当前的vnode是哪个类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function _createVNode ( ) { if (props) { }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 }
这里需要额外注意shapeFlags,是通过或位于运算实现的,通过或位于运算判断当前符合类型的shape,权限判断也可以这样去做。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 export const enum ShapeFlags { ELEMENT = 1 , FUNCTIONAL_COMPONENT = 1 << 1 , STATEFUL_COMPONENT = 1 << 2 , TEXT_CHILDREN = 1 << 3 , ARRAY_CHILDREN = 1 << 4 , SLOTS_CHILDREN = 1 << 5 , TELEPORT = 1 << 6 , SUSPENSE = 1 << 7 , COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8 , COMPONENT_KEPT_ALIVE = 1 << 9 , COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT }
如果判断一个节点是不是符合上面的类型,只需要 将 ·未知节点·&上面其中一个节点 , 结果大于0就满足。按钮级别权限可以用这个方法
后面很多地方都会有这个判断,比如
1 2 3 if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { ... }
这就判断当前的shapeFlag是不是属于/包含ShapeFlags.STATEFUL_COMPONENT
第三个是创建vnode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 function _createVNode ( ) { if (props) { } const shapeFlag = ... const vnode: VNode = { __v_isVNode: true , __v_skip: true , type, props, key: props && normalizeKey(props), ref: props && normalizeRef(props), scopeId: currentScopeId, slotScopeIds: null , 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
1 2 3 4 5 6 7 8 9 normalizeChildren(vnode, children)if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { const { content, fallback } = normalizeSuspenseChildren(vnode) vnode.ssContent = content vnode.ssFallback = fallback } return vnode
最后生成的vnode大致如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 const vnode: VNode = { __v_isVNode: true , __v_skip: true , type:{ data: data() mounted: mounted() setup: setup() template: "\n <p>\n </p><div>\n 454654\n </div>\n <p></p>\n <p></p>\n " }, props, key: props && normalizeKey(props), ref: props && normalizeRef(props), scopeId: currentScopeId, slotScopeIds: null , 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:4 , patchFlag, dynamicProps, dynamicChildren: null , appContext: { app: null as any, config: { isNativeTag: NO, performance: false , globalProperties: {}, optionMergeStrategies: {}, errorHandler: undefined , warnHandler: undefined , compilerOptions: {} }, mixins: [], components: {}, directives: {}, provides: Object .create(null ) } }
render 渲染VNode 再回到上面的mount方法,在调用mount时候,返回了创建好vnode,之后还将app对象缓存在了vnode.appContext中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function createAppAPI <HostElement >( ) { return function createApp (rootComponent, rootProps = null ) { mount( rootContainer: HostElement, isHydrate?: boolean, isSVG?: boolean){ const vnode = createVNode( rootComponent as ConcreteComponent, rootProps ) vnode.appContext = context render(vnode, rootContainer, isSVG) isMounted = true app._container = rootContainer } } }
创建好之后,准备开始执行关键的render函数。在Vue2源码中,我们知道,是通过_render()调用createElement创建的Vnode,通过_update()将dom patch到页面中,来看下Vue3中的实现
1 2 3 4 5 6 7 render(vnode, rootContainer, isSVG)
这里的render方法就是那个开头见到的2000多行的函数中,也是createApp调用的地方baseCreateRenderer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 function baseCreateRenderer ( options: RendererOptions, createHydrationFns?: typeof createHydrationFunctions ): any { ...省略2000 行 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) } flushPostFlushCbs() container._vnode = vnode } return { render, hydrate, createApp: createAppAPI(render, hydrate) } }
首先判断vnode不存在,执行销毁,这部分有些像Vue2的patch函数,也是首先判断是不是销毁,然后执行patch,看一下patch函数的实现,patch函数的主要是用来挂载和更新dom的。在当前函数482行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const patch: PatchFn = ( n1, n2, container, anchor = null , parentComponent = null , parentSuspense = null , isSVG = false , slotScopeIds = null , optimized = false ) => { ... }
在这个里面,再出现了patchFlag概念,第一次是在创建vnode时候
patchFlag patchFlag是用来做什么的?简单来讲,就是通过对vnode进行标记,采用不同的patch方法,在Vue3里面,diff有两种模式
优化模式-不同的vnode不同的patch方法(optimized mode)
普通模式-对vnode进行全量diff,比如render函数,renderSlot(),手动克隆vnode等
ShapeFlag是具有形状的元素,作用是帮助 Rutime 时的 render 的处理。patchFlag是标记vnode应该怎样进行diff
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 export const enum PatchFlags { TEXT = 1 , CLASS = 1 << 1 , STYLE = 1 << 2 , PROPS = 1 << 3 , FULL_PROPS = 1 << 4 , HYDRATE_EVENTS = 1 << 5 , STABLE_FRAGMENT = 1 << 6 , KEYED_FRAGMENT = 1 << 7 , UNKEYED_FRAGMENT = 1 << 8 , NEED_PATCH = 1 << 9 , DYNAMIC_SLOTS = 1 << 10 , DEV_ROOT_FRAGMENT = 1 << 11 , HOISTED = -1 , BAIL = -2 }
理解完patchFlag,继续往下看patch过程。
patch patch函数的主要是用来挂载和更新dom的。 首先判断了是否存在同时存在新旧节点。如果都存在并且不是一个sameVnode,则销毁旧的节点,一个元素由UL变成了div,那么就会直接删除原来的节点,直接去做挂载。之后,会根据传入的type判断当前流程patch参数有很多,其中的意思是:n1是旧的vnode,n1旧节点不存在表示挂载,初次渲染为空n2是新的vnode,后续会根据这个vnode执行不同的处理逻辑container是DOM容器,vnode生成dom之后的挂载节点optimized表示是否采用优化模式patchFlag的概念,用来标记一个节点应该如何进行diff更新策略,没有patchFlag,执行全量diff
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 const patch = ( n1, n2, container, anchor = null , parentComponent = null , parentSuspense = null , isSVG = false , optimized = false ) => { 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, shapeFlag } = n2 switch (type) { case Text: break case Comment: break case Static: break case Fragment: break default : if (shapeFlag & 1 ) { processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) } else if (shapeFlag & 6 ) { processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) } else if (shapeFlag & 64 ) { } else if (shapeFlag & 128 ) { } } }
这里主要关注对普通元素的处理processElement()和组件的处理processComponent()。
组件patch 刚开始我们的App是一个组件,组件vnode会通过processComponent方法来处理,经过转换再次执行会变成普通dom, 一般情况下,第一次渲染的就是一个组件对象,然后执行里面的组件挂载流程。
函数逻辑为如果n1为空,那么意味着没有旧的vnode,直接挂载,否则执行更新逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const processComponent = ( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => { if (n1 == null ) { mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) } else { updateComponent(n1, n2, parentComponent, optimized) } }