Vue3源码学习1-初始化流程

108 阅读2分钟

源码调试

  1. git clone github.com/vuejs/core.…
  2. npm install pnpm -g
  3. pnpm install
  4. pnpm dev
  5. 可在packages/vue目录下,创建文件调试 vue/core 使用pnpm包管理工具(pnpm安装速度更快,依赖安全性更强) 示例:
    <div id="app">{{title}}</div>
    <script src="../../dist/vue.global.js"></script>
    <script> 
      const { createApp } = Vue
      createApp({
        setup() {
          return {
            title: 'hello vue3'
          }
        }
        // data() {
        //   return {
        //     title: 'hello vue3'
        //   }
        // }
      }).mount('#app')

初始化过程

createApp

// packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)
   //xxx中间部分暂时省略
  return app
}) as CreateAppFunction<Element>

获取renderer

function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
  const {
    insert: hostInsert,
    remove: hostRemove,
    patchProp: hostPatchProp,
    createElement: hostCreateElement,
    createText: hostCreateText,
    createComment: hostCreateComment,
    setText: hostSetText,
    setElementText: hostSetElementText,
    parentNode: hostParentNode,
    nextSibling: hostNextSibling,
    setScopeId: hostSetScopeId = NOOP,
    insertStaticContent: hostInsertStaticContent
  } = options // dom操作等工具类
 
  const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
  ) => { 
    // 这里vnode.children如果是string类型,则直接插入节点;如果vnode.children为数组,则遍历、递归patch
    if (n1 === n2) {
      return
    }

    // n1存在,且新旧节点的key或者type不同,卸载旧节点
    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
    // 根据vnode类型
    switch (type) {
      case Text:
        // 初始化过程直接将n2插入dom,更新则缓存旧节点,更新节点的nodeValue即可
        processText(n1, n2, container, anchor)
        break
      case Fragment:
        break
      default:
        if(shapeFlag & ShapeFlags.COMPONENT) { 
          // 初始化过程中, mountComponent
          // 创建组件实例()
          setupComponent()
          // 依赖收集
          setupRenderEffect()
          // 组件真正挂载是在依赖收集时立即执行过程中
        }
        break
    } 
  }
  // 大多数情况下的组件都是有状态的组件,即setupStatefulComponent
  function setupStatefulComponent(instance: ComponentInternalInstance,
  isSSR: boolean) {
      const Component = instance.type 
      if(Component.setup) {
          // 根据setup函数 创建setup上下文(attrs、props、emit、slots、expose)
          const setupContext = (instance.setupContext =
          setup.length > 1 ? createSetupContext(instance) : null)
      }else {
          // 创建组件的render函数
          // 兼容vue2.x写法,data/registerLifeCycleHooks/methods...
      }
  }

  
  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)
    }
    flushPreFlushCbs()
    flushPostFlushCbs()
    container._vnode = vnode
  }
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

createAppAPI

export function createAppAPI<HostElement>(
  render: RootRenderFunction<HostElement>,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
      const context = createAppContext() // 获取基础的上下文对象
      const app: App = (context.app = {
           _uid: uid++,
          _component: rootComponent as ConcreteComponent,
          _props: rootProps,
          _container: null,
          _context: context,
          _instance: null,
          // app的属性及方法:use/mixin/directive/provide/xxx
          // 重点:mount方法,调用mount之后生成vnode传给render以渲染到真实dom
          mount(
            rootContainer: HostElement,
            isHydrate?: boolean,
            isSVG?: boolean
          ): any {
            if (!isMounted) {
              // 获取vnode: {shapeFlag, type, children, ctx,...}
              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
              // 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)
              }
              isMounted = true
              app._container = rootContainer
              return getExposeProxy(vnode.component!) || vnode.component!.proxy
            } else if (__DEV__) {}
          },
      })
  }
}

总结

graph TD
获取渲染器 --> createApp --> mount --> render --> patch --> mountComponent --> setupStatefulComponent & setupRenderEffect

手写简易初始化过程

    <div id="app"></div>
    <script>
      const createAppAPI = render => {
        return rootComponent => {
          const app = {
            mount: container => {
              const vnode = {
                tag: rootComponent
              }
              render(vnode, container)
            }
          }
          return app
        }
      }

      const createRenderer = options => {
        const patch = (v1, v2, container) => {
          const rootComponent = v2.tag
          const ctx = rootComponent.data()
          const vnode = rootComponent.render.call(ctx)
          const child = options.createElement(vnode.tag)
          const parent = options.querySelector(container)
          if (typeof vnode.children === 'string') {
            child.textContent = vnode.children
            options.insert(parent, child)
          } else {
            // process other types
          }
        }
        const render = (vnode, container) => {
          patch(container._vnode || null, vnode, container)
          container._vnode = vnode
        }
        return {
          render,
          createApp: createAppAPI(render)
        }
      }

      const renderer = createRenderer({
        createElement: tag => {
          return document.createElement(tag)
        },
        querySelector: sel => {
          return document.querySelector(sel)
        },
        insert: (parent, child, anchor = null) => {
          parent.insertBefore(child, anchor)
        }
      })
      const createApp = rootComponent => {
        return renderer.createApp(rootComponent)
      }
      createApp({
        data() {
          return {
            name: 'test'
          }
        },
        render() {
          return {
            tag: 'h2',
            children: this.name
          }
        }
      }).mount('#app')
    </script>