Setting up the Debugging Environment

To clarify Vue3 initialization, you are advised to clone Vue3 locally.

git clone https://github.com/vuejs/vue-next.git

Copy the code

Install dependencies

npm install

Copy the code

Modify package.json, add dev to –sourcemap for debugging, and run NPM run dev

// package.json
// ...
"scripts": {
  "dev""node scripts/dev.js --sourcemap".// ...
}
// ...

Copy the code

Add index.html to the Packages /vue directory as follows

<! -- index.html -->
<div id="app">
  {{ count }}
</div>
<script src="./dist/vue.global.js"></script>
<script>
  Vue.createApp({
    setup() {
      const count = Vue.ref(0);
      return { count };
    }
  }).mount('#app');
</script>

Copy the code

Open index.html in your browser, and if the program works, you can start debugging the next step.

debug

If you get lost in the process below, it is recommended to look at the summary at the end and then come back to this paragraph.

debugging

Create a breakpoint at createApp, then refresh the page to enter the breakpoint and debug.

You can debug by yourself. Here I will describe the initialization process.

createApp

The createApp instance of Packages/Runtime-dom/SRC /index.ts is displayed.

// packages/runtime-dom/src/index.ts
export const createApp = ((. args) = > {
  constapp = ensureRenderer().createApp(... args)// ...
  return app
}) as CreateAppFunction<Element>

Copy the code

The implementation of ensureRenderer can then be continued.

// packages/runtime-dom/src/index.ts
function ensureRenderer({
  return (
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}

Copy the code

The renderer here is a singleton, created initially with a call to createRenderer, and then further.

If you go to Packages/Runtime-core/SRC /renderer.ts, you can see that createRenderer will call baseCreateRenderer.

// packages/runtime-core/src/renderer.ts
export function createRenderer<
  HostNode = RendererNode.HostElement = RendererElement> (options: RendererOptions<HostNode, HostElement>{
  return baseCreateRenderer<HostNode, HostElement>(options)
}

Copy the code

The implementation of baseCreateRenderer is 2000 lines long, and we only need to focus on a few key points.

The return value is one of ensureRenderer()

// packages/runtime-core/src/renderer.ts
function baseCreateRenderer(options: RendererOptions, createHydrationFns? :typeof createHydrationFunctions
) :any {
    // Omit 2000 lines here
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

Copy the code

EnsureRenderer () is then returned to the start position and createApp, which is the same one returned in the previous step, is followed by implementation of the ensureRenderer() idea, and see what has been done.

// packages/runtime-dom/src/index.ts
export const createApp = ((. args) = > {
  constapp = ensureRenderer().createApp(... args)// ...
  return app
}) as CreateAppFunction<Element>

Copy the code

CreateApp returned by ensureRenderer is implemented with createAppAPI. Let’s see how createAppAPI is implemented.

// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement> (render: RootRenderFunction, hydrate? : RootHydrateFunction) :CreateAppFunction<HostElement{
  return function createApp(rootComponent, rootProps = null{
    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _containernull._context: context,
      _instancenull,

      version,

      get config() {},

      set config(v) {},

      use(plugin: Plugin, ... options:any[]) {},

      mixin(mixin: ComponentOptions) {},

      component(name: string, component? : Component):any {},

      directive(name: string, directive? : Directive){}, mount( rootContainer: HostElement, isHydrate? :boolean, isSVG? :boolean) :any {},

      unmount() {},

      provide(key, value){}})return app
  }
}

Copy the code

You can see that createAppAPI is going to return a createApp function, so we’re going to call createApp, and when createApp is done it’s going to return the app instance. App instances also have use, mixin, Component, directive, and other methods that add extensions to the global app.

In this example, the first parameter passed in is the rootComponent from the previous step, which is the rootComponent.

// index.html
const app = Vue.createApp({})
 .use(xxx)
 .component(xxx)
 .mount(xxx)

Copy the code

mount

CreateApp After creating the app instance, mount is also called to render to the page. Let’s see what Mount does next. It’s still inside createAppAPI

// packages/runtime-core/src/apiCreateApp.tsmount( rootContainer: HostElement, isHydrate? :boolean, isSVG? :boolean) :any {
  if(! isMounted) {const vnode = createVNode(
      rootComponent as ConcreteComponent,
      rootProps
    )
    // ...
    if (isHydrate && hydrate) {
      hydrate(vnode as VNode<Node, Element>, rootContainer as any)}else {
      render(vnode, rootContainer, isSVG)
    }
    isMounted = true
    app._container = rootContainer
    // ...
    returnvnode.component! .proxy }else if (__DEV__) {
    // The development environment warning reminds you that app cannot be remounted}}Copy the code

The render(vnode, rootContainer, isSVG) line is executed, and the renderer passed in when createAppAPI is called.

Back in baseCreateRenderer, you can see the renderer passed in when createAppAPI is called on return.

// packages/runtime-core/src/renderer.ts
function baseCreateRenderer(options: RendererOptions, createHydrationFns? :typeof createHydrationFunctions
) :any {
    // Omit 2000 lines here
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

Copy the code

The renderer is defined in the base reaterenderer

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
}

Copy the code

In the index.html example, the vNode is created by the rootComponent when render is first executed, which is the object passed in when createApp is created.

Container is the app container, that is, the div whose ID is app. Container. _vnode is undefined.

So it will eventually enter patch.

Patch logic is also located in baseCreateRenderer. The code is too long, so here’s the idea. In patch, the system determines the VNode type or shapeFlag and performs corresponding operations.

For the first patch, vNode is a component and will enter the judgment of ShapeFlags.COMPONENT and execute processComponent to process the component.

It then triggers the mountComponent to mount the component, which triggers the setupComponent(instance) to initialize the components props, slots, setup, and so on. And compile template to render.

// packages/runtime-core/src/renderer.ts
const mountComponent: MountComponentFn = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) = > {
  // 2.x compat may pre-creaate the component instance before actually
  // mounting
  const compatMountInstance =
        __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
  const instance: ComponentInternalInstance =
        compatMountInstance ||
        (initialVNode.component = createComponentInstance(
          initialVNode,
          parentComponent,
          parentSuspense
        ))
  // ...
  // resolve props and slots for setup context
  if(! (__COMPAT__ && compatMountInstance)) {// ...
    setupComponent(instance)
    // ...
  }
  // ...
  setupRenderEffect(
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  )
  // ...
}

Copy the code

This is followed by setupRenderEffect, which encapsulates the rendering function as a side effect that is automatically reexecuted when the dependent reactive data changes.

Instance. isMounted = undefined, instance.isMounted = undefined, instance.isMounted = undefined Const subTree = (instance.subTree = renderComponentRoot(instance)); Instance. isMounted = true.

Instance.ismounted (true) {componentUpdateFn (componentUpdateFn);}

As to why dependencies change ComponentUpdate N is re-executed, we’ll leave that to the next article, so keep an eye on me.

conclusion

  1. In the index. The HTML callscreateAppWill pass by firstensureRendererbaseCreateRendererGenerate the following objects
// baseCreateRenderer returns a value
return {
  render,
  hydrate,
  createApp: createAppAPI(render, hydrate)
}

Copy the code
  1. Continue calling createApp returned by baseCreateRenderer, where createApp actually calls the function returned by createAppAPI.

    CreateAppAPI The app instance is returned.

  2. After creating the app, mount it with a call to mount it inside createAppAPI.

    The mount execution calls the render function, which is passed in at baseCreateRenderer.

  3. Render starts the patch for rendering, and the recursive rendering child nodes will be carried out inside the patch.

This is how Vue3 createApp and mount work. Why is one need to pass ensureRenderer and baseCreateRenderer in the first step?

BaseCreateRenderer is primarily platform-independent logic processing stored in Run-time Core.

When dom needs to be operated during patch, the external method will be called for operation, which makes it more convenient to achieve cross-end.

“EnsureRenderer” is one that is stored in the Runtime-DOM and provides a series of dom operable functions to the baseCreateRenderer.

If one wants to define a custom renderer, one need only implement ensureRenderer. Instead of having to fork a copy like Vue2, Vue3 has a much wider range of applications.


Well, that’s all for this article. If there is any mistake, I hope you can point it out in the comment section, thanks!

The next article will analyze Vue3’s responsivity principle. If you are interested, don’t forget to follow me. Let’s learn and make progress together.