vue3源码解析
依赖安装
yarn --ignore-scripts
准备调试
- 添加 --sourcemap
"dev": "node scripts/dev.js --sourcemap", - 执行 yarn dev
入口文件
-
从执行命令npm run dev开始
// "dev": "node scripts/dev.js --sourcemap" // 找到 scripts/dev.js 发现 TARGET 默认为 vue -
从rollup打包配置文件rollup.config.js中入手
// 打包的入口文件 // 我们这次看的是浏览器的版本,所以是src/index.ts // runtime webpack运行时的版本 const entryFile = /runtime$/.test(format) ? `src/runtime.ts` : `src/index.ts` // packages/ const packagesDir = path.resolve(__dirname, 'packages') // 默认的packages/vue const packageDir = path.resolve(packagesDir, process.env.TARGET) const name = path.basename(packageDir) const resolve = p => path.resolve(packageDir, p) // 入口文件配置 // input: resolve(entryFile), // 由此找到入口文件是 packages/vue/src/index.ts -
src/index.ts文件最后导出 runtime-dom 中的所有方法其中包含有 createApp
export * from '@vue/runtime-dom'
createApp方法
// runtime-dom.ts
export const createApp = ((...args) => {
// 首先获取一个渲染器
// 实际上createApp方法是由渲染器提供的
const app = ensureRenderer().createApp(...args)
// ...此处省略部分代码
return app
}
-
此时createApp中调用了ensureRenderer 方法
import { createRenderer, } from '@vue/runtime-core' function ensureRenderer() { // 渲染器 // 单例模式 // 存在就返回,不存在就创建 return renderer || (renderer = createRenderer<Node, Element>(rendererOptions)) } -
ensureRenderer 方法中又调用了createRenderer
-
打开@vue/runtime-core/index.ts
-
createRenderer来自./renderer.ts
// @vue/runtime-core/index.ts export { createRenderer } from './renderer'// ./renderer.ts export function createRenderer< HostNode = RendererNode, HostElement = RendererElement >(options: RendererOptions<HostNode, HostElement>) { return baseCreateRenderer<HostNode, HostElement>(options) } /* export function createRenderer(options) { return baseCreateRenderer(options) } */ -
大块头来了,baseCreateRenderer函数比较大将近2000行的代码量(渲染器的工厂函数)
-
看到后折磨了我好久,最后放弃,决定从最底部的return返回值入手
-
所以其他代码先略过
function baseCreateRenderer( options: RendererOptions, createHydrationFns?: typeof createHydrationFunctions ): any { // .....这里省略2000行代码 // 此处返回的对像,就是渲染器 // 有三个方法 return { // 渲染方法, 把虚拟dom转换为真实dom追加到容器container中 render(vnode, container) render, // 注水,服务器渲染用到 hydrate, // 创建应用程序实例的方法 createApp: createAppAPI(render, hydrate) } } -
接着发现 createApp 是createAppAPI方法返回的
-
以下这些方法都反回了app实例,所以可以链式调用
-
对比Vue2以下方法由静态方法转为了实例方法
// 为何要调整为实例方法? // 1. 避免实例之间的污染 // 2. 语义上更好理解 // 3. 摇树优化 tree-shake // 摇树优化: // 如 app.use({install(){}})初始化了一个插件 // 但是在代码中,实际没有使用到这个插件 // 所代码在打包时,这个插件是不会被打包进去-
好开心终于看到好多老朋友
// import { createAppAPI, CreateAppFunction } from './apiCreateApp' // packages\runtime-core\src\apiCreateApp.ts export function createAppAPI<HostElement>( render: RootRenderFunction, hydrate?: RootHydrateFunction ): CreateAppFunction<HostElement> { // 外面返回的方法 return function createApp(rootComponent, rootProps = null) { if (rootProps != null && !isObject(rootProps)) { rootProps = null } const context = createAppContext() const installedPlugins = new Set() let isMounted = false // 应用程序实例 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) {/*...*/}, // 插件使用方法初始化 // 传进去的第一个参数是app实例 // vue2中传入的第一个参数为Vue本身(构造函数) use(plugin: Plugin, ...options: any[]) { if (plugin && isFunction(plugin.install)) { installedPlugins.add(plugin) plugin.install(app, ...options) } else if (isFunction(plugin)) { installedPlugins.add(plugin) plugin(app, ...options) } return app }, // 混入方法初始化 mixin(mixin: ComponentOptions) { if (__FEATURE_OPTIONS_API__) { if (!context.mixins.includes(mixin)) { context.mixins.push(mixin) // global mixin with props/emits de-optimizes props/emits // normalization caching. if (mixin.props || mixin.emits) { context.deopt = true } } } return app }, // 初始化组件方法 component(name: string, component?: Component): any { if (!component) { return context.components[name] } context.components[name] = component return app }, // 初始化指令方法 directive(name: string, directive?: Directive) { if (!directive) { return context.directives[name] as any } context.directives[name] = directive return app }, // 挂载初始化走这里 mount( rootContainer: HostElement, isHydrate?: boolean, isSVG?: boolean ): any { if (!isMounted) { // 初始化的虚拟dom树 const vnode = createVNode( 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 if (isHydrate && hydrate) { hydrate(vnode as VNode<Node, Element>, rootContainer as any) } else { // 不是服务端渲染,客户端渲染默认走这里 render(vnode, rootContainer, isSVG) } isMounted = true app._container = rootContainer // for devtools and telemetry ;(rootContainer as any).__vue_app__ = app return vnode.component!.proxy } }, // 卸载初始化 unmount() { if (isMounted) { render(null, app._container) delete app._container.__vue_app__ } }, // 依赖注入 多层次嵌套通信 provide(key, value) { // TypeScript doesn't allow symbols as index type // https://github.com/Microsoft/TypeScript/issues/24587 context.provides[key as string] = value return app } }) return app } }
-
-
-
承接上面,执行挂载操作mount
-
mount内部最终执行了render函数
// mount内部实现 // 不是服务端渲染,客户端渲染默认走这里 render(vnode, rootContainer, isSVG) -
render函数中执行了patch
// 初始化走这里,这里就类似vue2 // 参数1存在则走更新 // 参数1一不存在则走挂载流程 const render: RootRenderFunction = (vnode, container, isSVG) => { // ... 省略部分代码 patch(container._vnode || null, vnode, container, null, null, null, isSVG) // ... 省略部分代码 } // pacth内部区分了 文本 注释 静态节点(不会变的节点) // Fragment 抽象的虚拟节点(父容器),解决vue2单根节点问题,Vue3可以多根节点 // element元素节点 组件 等 case Text: processText(n1, n2, container, anchor) case Comment: processCommentNode(n1, n2, container, anchor) // ... 省略部分代码 default: // ... 省略部分代码 else if (shapeFlag & ShapeFlags.COMPONENT) { // 初始化走这里 // 因为初始化执行mount时, 做了以下处理 /* 把createApp({})中的传参当成了一个vnode组件处理 类型为一个对象 const vnode = createVNode( rootComponent as ConcreteComponent, rootProps ) // 不是服务端渲染,客户端渲染默认走这里 render(vnode, rootContainer, isSVG) */ processComponent( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } // ... 省略部分代码 -
接着初始化线路 processComponent
- 由于n1(旧的vnode)不存在 所以执行了 mountComponent 方法
- 在vue2中mountComponent声明了updateComponent函数new 了一个渲染Watcher干了组件粒度的依赖收集这件事
- 以及Vnode的patch操作 vm._update(vm._render(), hydrating) ,生成真实节点最终执行根节点的替换操作,nodeOps.insertBefore到页面上
- 以及callHook(vm, 'beforeMount ')与 callHook(vm, 'mounted') 一前一后两个钩子的触发
- 那么显然Vue3中的套路也包含了这些,主要干的事情把vnode转成真实dom,以及更新,setupComponent 组件的安装
const processComponent = ( n1: VNode | null, // 旧vnode n2: VNode, // 新vnode container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, slotScopeIds: string[] | null, optimized: boolean ) => { n2.slotScopeIds = slotScopeIds // 初始化时为null 因为 container 为#app 调用mount时传入的id // 所以没有旧的vnode if (n1 == null) { // 是否为keep-alive缓存组件 // 初始化时n2为createApp的options转换的vnode 非 缓存组件 if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { ;(parentComponent!.ctx as KeepAliveContext).activate( n2, container, anchor, isSVG, optimized ) } else { // 所以初始化走这,即初始的挂载就在这里处理了 mountComponent( n2, // 新vnode container, // #app anchor, parentComponent, parentSuspense, isSVG, optimized ) } } else { // 再次验证有旧vnode走更新,没有走挂载 mountComponent updateComponent(n1, n2, optimized) } } -
接着上面的初始化 mountComponent
- 创建组件实例
- 增加渲染函数副作用
const mountComponent: MountComponentFn = ( initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) => { // 1. 创建组件实例 const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance( initialVNode, parentComponent, parentSuspense )) // inject renderer internals for keepAlive if (isKeepAlive(initialVNode)) { ;(instance.ctx as KeepAliveContext).renderer = internals } // setupComponent 组件的安装: 类似vue2的new Vue()时vue2构造器中执行的 this._init(options) 做初始化 // 回想vue2中new Vue时做了哪些操作呢 // 1. 做了用户配置选项和系统配置选项的合并 // 2. 实例相关的属性进了初始化 如: $parent $root $children $refs // 3. 监听自己的自定义事件 // 4. 解析自己的插槽 // 5. 同时会把自己内部的一些数据进行响应式的处理 如: props(属性) methosds(方法) data computed watch // 这里其实做的也是这些操作 setupComponent(instance) // 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 } // 增加渲染函数副作用 // 里面执行的是渲染函数,然后让当前的渲染函数重新获得虚拟dom // 当前组件重新更新,然后重新patch setupRenderEffect( instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized ) }-
承接上面的流程 setupComponent 组件的安装
export function setupComponent( instance: ComponentInternalInstance, isSSR = false ) { isInSSRComponentSetup = isSSR const { props, children } = instance.vnode const isStateful = isStatefulComponent(instance) // 处理props initProps(instance, props, isStateful, isSSR) // 处理插槽 initSlots(instance, children) const setupResult = isStateful // 数据响应式处理 ? setupStatefulComponent(instance, isSSR) : undefined isInSSRComponentSetup = false return setupResult }-
setupStatefulComponent 数据响应式处理
function setupStatefulComponent( instance: ComponentInternalInstance, isSSR: boolean ) { // 由于初始化时,传进来的是一个配置obj转换的vnode, // 所以这里type 就是根组件配置对象 // createApp({ // data() { // return { aa: 'haha' } // }, // setup选项和data(){return {}}可同时存在,但setup优先级更高 // setup() { // const aa = ref('haha') // return { aa } // } // }) const Component = instance.type as ComponentOptions // 0. create render proxy property access cache instance.accessCache = Object.create(null) // 给根组件配置对象 做了代理操作 即渲染函数的上下文,数据响应式代理 // 渲染函数中将来访问的响应式数据都在proxy这里拿 // 通过 const instance = getCurrentInstance(); 可以在 setup(){}中获取 // const { ctx, proxy } = instance // PublicInstanceProxyHandlers中会先从setup中去查找属性,如果没有才会去data中查找 instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers) // call setup() // 处理setup选项 // setup选项和data(){return {}}可同时存在,但setup优先级更高 // PublicInstanceProxyHandlers中会先从setup中去查找属性,如果没有才会去data中查找 const { setup } = Component if (setup) { const setupContext = (instance.setupContext = setup.length > 1 ? createSetupContext(instance) : null) currentInstance = instance pauseTracking() const setupResult = callWithErrorHandling( setup, instance, ErrorCodes.SETUP_FUNCTION, [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext] ) resetTracking() currentInstance = null if (isPromise(setupResult)) { if (isSSR) { // return the promise so server-renderer can wait on it return setupResult .then((resolvedResult: unknown) => { handleSetupResult(instance, resolvedResult, isSSR) }) .catch(e => { handleError(e, instance, ErrorCodes.SETUP_FUNCTION) }) } else if (__FEATURE_SUSPENSE__) { // async setup returned Promise. // bail here and wait for re-entry. instance.asyncDep = setupResult } } else { // handleSetupResult 方法内部最终也调用了 // finishComponentSetup(instance, isSSR) handleSetupResult(instance, setupResult, isSSR) } } else { // createApp配置项中没有setup 则走这里 finishComponentSetup(instance, isSSR) } } -
接着 看 finishComponentSetup函数
-
内部又调用了 applyOptions(instance, Component),兼容vue2.0处理
const { // composition mixins, extends: extendsOptions, // state data: dataOptions, computed: computedOptions, methods, watch: watchOptions, provide: provideOptions, inject: injectOptions, // assets components, directives, // lifecycle beforeMount, mounted, beforeUpdate, updated, activated, deactivated, beforeDestroy, beforeUnmount, destroyed, unmounted, render, renderTracked, renderTriggered, errorCaptured, // public API expose } = options
-
-
-
组件安装完了后 走 setupRenderEffect
// 函数内部访问到的数据有变化,则函数就会重新执行 // 和vue2比这里没有了watcher instance.update = effect(function componentEffect() { // 先获取当前根组件的vnode // 其实就是执行了render函数得到了虚拟dom const subTree = (instance.subTree = renderComponentRoot(instance)) // 初始化patch // 由于初始化时,是一个 Fragment // 走了patch后内部会分别对和类型节点做处理,并递归查找 patch( null, subTree, // 初始化时是一个 Fragment container, anchor, instance, parentSuspense, isSVG ) }) // patch -> processFragment const processFragment = ( n1: VNode | null, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, slotScopeIds: string[] | null, optimized: boolean ) => { // 初始化时n1旧vnode传了一个null // n2新vnode传了subTree过来 if (n1 == null) { hostInsert(fragmentStartAnchor, container, anchor) hostInsert(fragmentEndAnchor, container, anchor) // 挂载孩子节点 mountChildren( n2.children as VNodeArrayChildren, container, fragmentEndAnchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } } // mountChildren 挂载孩子节点 const mountChildren: MountChildrenFn = ( children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, slotScopeIds, start = 0 ) => { // 遍历递归子节点分别做patch处理 // 至此初始化已走完 // patch中会根据节类型做相应处理 // 如文本 标签 注释等等 for (let i = start; i < children.length; i++) { const child = (children[i] = optimized ? cloneIfMounted(children[i] as VNode) : normalizeVNode(children[i])) patch( null, child, container, anchor, parentComponent, parentSuspense, isSVG, optimized, slotScopeIds ) } }
注册全局方法
-
Vue2中可用原型,但是Vue3中不能使用了
-
provide / inject 使用依赖注入(官方推荐)
-
// provide 和 inject 启用依赖注入。只有在使用当前活动实例的 setup() 期间才能调用这两者。 import { ref, provide } from 'vue' setup() { let title = ref('这个要传的值') // provide的第一个为名称,第二个值为所需要传的参数 provide('title', title); let setTitle = () => { // 点击后都会有响应式哦! title.value = '点击后,title会变成这个'; } return { title, setTitle } } // 子组件 import { inject } from 'vue' setup() { // inject的参数为provide过来的名称 let title = inject('title'); }
-
-
app.config.globalProperties 需在app挂载前使用
const app = createApp(App) app.config.globalProperties.http = () => {} app.mount('#app') import { ref, computed, watch, getCurrentInstance, onMounted } from "vue"; export default { components: { TestComp }, setup( ) { // 获取上下文实例,ctx相当于vue2的this // 但是ctx不是响应式的 // proxy 是响应式的 const { ctx, proxy } = getCurrentInstance(); onMounted(() => { // 这样在本地环境可以, 但是线上环境会报错 console.log(ctx, "ctx") ctx.http() // 下面这种才能正常使用 const instance = getCurrentInstance() instance.appContext.config.globalProperties.http() }); }, };
-