After reading the previous article, I believe that we have been able to have a more in-depth grasp of Vue3’s responsive principle. But it’s not enough to just grasp the responsive principle, and I think Vue3 has three pillars.

  • One is the responsive system that we talked about;
  • Second, it is written by ustemplate,jsxCode conversion toVirtual NodeThis process is made up ofcompiler-dom,compiler-coreTo provide thecompilerFunction, whose return value is arenderDelta function, I’ll call that in a few more articlesrenderFunction forCompile the render;
  • Third, we willVirtual NodeintoReal NodetherenderDelta function, I’ll call that in a few more articlesrenderFunction forRendering render;

Note:

  1. Because the source code analyzed in this article isruntime-dom,runtime-coreIn this article unless otherwise specifiedrenderFunction refers toApply colours to a drawingrenderFunction.
  2. The virtual Node mentioned above is often referred to as the virtual DOM, which has the same meaning as a real Node and a real DOM.

In fact, the above description of the three pillars has already highly summarized the core functions of the Vue3 framework. This article will start with a brief example of how to implement createApp. In the course of this analysis, we will discuss the code collaboration between the Run-time dom and run-time core, and the implementation logic of createApp. The implementation logic covers the call to the render function. The implementation details of the Render function will be analyzed step by step in subsequent articles.

Example – Initialize a Vue3 application

In real development we usually initialize a Vue application with the following:

// Snippet 1
import { createApp } from 'vue'
// import the root component App from a single-file component.
import App from './App.vue'
const vueApp = createApp(App)
vueApp.mount('#app')
Copy the code

It’s a few lines of code, but it’s actually a lot of work, because the first thing you need to do is convert the contents of app. vue into a virtual Node. After compiling, the parameter App passed to the function createApp in snippet 1 is a component object. VueApp is an object that has a method called mount. This function converts the component object App into a virtual Node, which in turn converts the virtual Node into a real Node and mounts it under the DOM element that # App points to. As for the compilation process, it will be explained in detail in future articles analyzing Compiler-DOM and Compiler-core, which will not be mentioned in this article.

Write code that does not compile the transformation

To understand the normal operation of the program, it is necessary to use a virtual Node, we transformed the program into the following form:

<! -- Code snippet 2-->
<html>
    <body>
        <div id="app1"></div>
    </body>
    <script src="./runtime-dom.global.js"></script>
    <script>
       const { createApp, h } = VueRuntimeDOM
       const RootComponent = {
           render(){
               return h('div'.'Yang Yitao likes to study source code')
           }
       }
       createApp(RootComponent).mount("#app1")
    </script>
</html>
Copy the code

The most obvious change is that we are defining component objects directly, instead of compiling the contents of the app. vue file into component objects, we are also compiling a render function in the component object by hand, and we do not need to compile the render function from the template. Note that there are two compilation processes involved, one is to convert the.vue file into a component object, and the other is to convert the template involved in the component object into the render function, both of which will be covered in more detail in future articles.

In fact, the compile render function of the RootComponent object in snippet 2 executes at some point, as explained in this article when we examine the internal implementation of createApp.

Compile the render function

But we know that an important feature of Vue3 is the freedom to control which data is responsive, and this is due to our setup method. We further convert snippet 2 to the following form:

<! -- Code snippet 3-->
<html>
    <body>
        <div id="app" haha="5"></div>
    </body>
    <script src="./runtime-dom.global.js"></script>
    <script>
       const { createApp, h, reactive } = VueRuntimeDOM
       const RootComponent = {
           setup(props, context){
               let relativeData = reactive({
                   name:'Yang Yitao'.age: 60
               })
               let agePlus = () = >{
                   relativeData.age++
               }
               return {relativeData, agePlus}
           },
           render(proxy){
               return h('div', {
                   onClick: proxy.agePlus,
                   innerHTML:`${proxy.relativeData.name}already${proxy.relativeData.age}Aged, click here to continue to increase the age '
               } )
           }
       }
       createApp(RootComponent).mount("#app")
    </script>
</html>
Copy the code

As you can see from snippet 3, the return value of the setup method can be obtained by compiling the Render function with the prxoy argument. You might think it’s a little redundant, and it is. Because the compile render function here is itself a product of Vue2. In Vue3 we can write this directly, and the code changes as follows:

<! -- Code snippet 4-->
<html>
    <body>
        <div id="app" haha="5"></div>
    </body>
    <script src="./runtime-dom.global.js"></script>
    <script>
       const { createApp, h, reactive } = VueRuntimeDOM
       const RootComponent = {
           setup(props, context){
               let relativeData = reactive({
                   name:'Yang Yitao'.age: 60
               })
               let agePlus = () = >{
                   relativeData.age++
               }
               return () = >h('div', {
                   onClick: agePlus,
                   innerHTML:`${relativeData.name}already${relativeData.age}Aged, click here to continue to increase the age '
               } )
           }
       }
       createApp(RootComponent).mount("#app")
    </script>
</html>
Copy the code

In real development, setup typically returns either an object or a function that returns JSX, where the JSX code is converted at compile time into something like snippet 4, in this case in a TSX file format. If you return an object, you usually write template code in a.vue file. You can do either, but remember that Template has compile-time static analysis to improve performance, whereas JSX is more flexible.

summary

Above we briefly looked at some simple forms of component encoding in Vue3 and understood how the component object passed to the function createApp plays a fundamental role in the real world. Let’s move on to the implementation of createApp. When analyzing createApp, I will sometimes revisit some of the effects mentioned above to make it easier to understand Vue3 by comparing them to the source code.

CreateApp code implementation

The outer wrapper of createApp

CreateApp (core/ Packages/Runtime-dom); createApp (core/packages/ Runtime-dom); createApp (createApp);

// Snippet 5
// Omit some code here...
import { nodeOps } from './nodeOps'
import { patchProp } from './patchProp'
// Omit some code here...
const rendererOptions = extend({ patchProp }, nodeOps)
// Omit some code here...
function ensureRenderer() {
  return (
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}

// Omit some code here...
export const createApp = ((. args) = > {
  constapp = ensureRenderer().createApp(... args)// Omit some code here...
  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string) :any= > {
     // Omit some code here...
    const proxy = mount(container, false, container instanceof SVGElement)
    // Omit some code here...
    return proxy
  }

  return app
}) as CreateAppFunction<Element>
// Omit some code here...
Copy the code

After making a series of cuts to the code, we found three key points:

  1. The realVueThe application object is the executionensureRenderer().createApp(... args)Created, andensureRendererThe function is called internallycreateRendererFunction. thiscreateRendererFunction inruntime-core;
  2. Calling a functioncreateRenderFunction is passed in as an argumentrendererOptionsThese parameters are operationsDOMNodes andDOMSpecific methods of node attributes.
  3. To create theVueThe application objectappAfter, rewrite itmountMethod, rewrittenmountInside the method, some browser-specific operations are done, such as clearingDOMNode. It then calls the pre-overridemountMethod to mount.

In summary, what the Run-time DOM really provides is the ability to manipulate the browser platform DOM nodes. RendererOptions is a method for manipulating specific DOM nodes, and rendererOptions is a method for exposing run-time core. Because the method of manipulating the real browser DOM is passed in as an argument, this can be the specific method of manipulating nodes on other platforms as well. In other words, run-time Core only knows that certain nodes need to be added, modified, or removed, but it doesn’t matter whether those nodes are browser DOM nodes or nodes from other platforms. Run-time Core calls whatever parameters are passed in. In fact, we can use this kind of hierarchical coding idea for reference in actual coding.

createRenderer

Core /packages/ Run-time core/ SRC /render. Ts createRenderer

// Snippet 6
export function createRenderer<
  HostNode = RendererNode.HostElement = RendererElement> (options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}
Copy the code

Let’s move on to the function baseCreateRenderer, which has over 2000 lines of code that I’ve streamlined considerably:

// Snippet 7
function baseCreateRenderer(options: RendererOptions, createHydrationFns? :typeof createHydrationFunctions
) :any {
  // Omit about 2000 lines of code here...
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}
Copy the code

Snippet 7 omits most of the code, and I leave only the return value. In fact, the baseCreateRenderer function is the core of the runtime-Core, because all the logic for converting a virtual Node to a real Node is included in this function, including the oft-mentioned diff algorithm. BaseCreateRenderer createApp createAppAPI(render, hydrate) CreateAppAPI (Render, hydrate) actually returns a function. Const app = ensureRenderer().createApp(); const app = ensureRenderer().createApp(… CreateApp args).

createAppAPI

We go to createAppAPI in core/ Packages/Runtime-core/SRC/apicReateApp. ts:

// Snippet 8
export function createAppAPI<HostElement> (render: RootRenderFunction, hydrate? : RootHydrateFunction) :CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    // Omit some code here...
    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null._context: context,
      _instance: null,

      version,

      get config() {
        return context.config
      },

      set config(v) {
        // Omit some code here...
      },

      use(plugin: Plugin, ... options:any[]) {
        // Omit some code here...
      },

      mixin(mixin: ComponentOptions) {
        // Omit some code here...
      },

      component(name: string, component? : Component):any {
        // Omit some code here...
      },

      directive(name: string, directive? : Directive) {
        // Omit some code here...}, mount( rootContainer: HostElement, isHydrate? :boolean, isSVG? :boolean) :any {
        // Omit some code here...
      },

      unmount() {
        // Omit some code here...
      },

      provide(key, value) {
        // Omit some code here...}})// Omit some code here...
    return app
  }
}
Copy the code

As you can see from code snippet 8, createAppAPI returns a function createApp, and the return value of this function is an object app. App is actually the Vue application that we created. The app has many properties and methods that represent the information and capabilities of the Vue application object.

The mount method

As shown in code snippet 1, the first operation after creating a Vue application is to call the mount method to mount it. We can ignore the rest of the content for the moment and focus on the implementation of the mount method of the app:

// Snippet 9mount( rootContainer: HostElement, isHydrate? :boolean, isSVG? :boolean) :any {
    if(! isMounted) {const vnode = createVNode(
        rootComponent as ConcreteComponent,
        rootProps
      )
      vnode.appContext = context
      // Omit some code here...
      if (isHydrate && hydrate) {
        hydrate(vnode as VNode<Node, Element>, rootContainer as any)}else {
        render(vnode, rootContainer, isSVG)
      }
      isMounted = trueapp._container = rootContainer ; (rootContaineras any).__vue_app__ = app
      // Omit some code here...
      returngetExposeProxy(vnode.component!) || vnode.component! .proxy }// Omit some code here...
}
Copy the code

Snippet 9 omits a lot of development-phase code, which can be summarized as follows:

  1. Root the component objectrootComponent(Code snippet4The value passed in toRootComponent) intoVirtual Node;
  2. callrenderThe function takes thisVirtual NodeConverted intoReal NodeAnd mountrootContainerOn the element pointing to. That hererenderWhere does the function come from? From snippets8It’s not hard to see that it’s passed in as an argument, so where does that argument come from, so let’s go back to the code snippet7Find exactly the functionbaseCreateRendererInternally declaredrenderFunction.
  3. callgetExposeProxyThe function takes a proxy object and returns it.

How to convert a component object into a virtual Node and how to implement the render function are not discussed in this article, as they are both large and new topics that need to be covered in a new article. Let’s take a look at the getExposeProxy function here, because it’s related to the reactive systems we talked about earlier, and it should be easier to understand now that you’ve learned a lot about reactive systems.

getExposeProxy

// Snippet 10
export function getExposeProxy(instance: ComponentInternalInstance) {
  if (instance.exposed) {
    return (
      instance.exposeProxy ||
      (instance.exposeProxy = new Proxy(proxyRefs(markRaw(instance.exposed)), {
        get(target, key: string) {
          if (key in target) {
            return target[key]
          } else if (key in publicPropertiesMap) {
            return publicPropertiesMap[key](instance)
          }
        }
      }))
    )
  }
}
Copy the code

The heart of code snippet 10 lies in this newly created Proxy instance. The object initialized by this Proxy is the execution result of proxyRefs(markRaw(instance.exposed)). Regardless of the exact meaning of instance.exposed, it can be understood from the logic of the program that if you get data through instance.exposeProxy, you can only get properties that instance.exposed or publicPropertiesMap have. Otherwise, undefined is returned. As for the reason why markRaw is called first and proxyRefs is called, proxyRefs makes a conditional judgment internally. If the object passed in is responsive itself, it will be returned directly, so it needs to process the non-responsive object first. ProxyRefs are used to access the original value of a reactive object without writing.value, which was analyzed in the previous article.

Special use of ref

So what exactly is instance.exposed? Let’s first look at a practical application where ref gets the content of a child component:

<script> import Child from './ child.vue 'export default {components: { Child }, mounted() { // this.$refs.child will hold an instance of <Child /> } } </script> <template> <Child ref="child" /> </template>Copy the code
// Snippet 2, filename: child.vue
export default {
  expose: ['publicData'.'publicMethod'].data() {
    return {
      publicData: 'foo'.privateData: 'bar'}},methods: {
    publicMethod() {
      / *... * /
    },
    privateMethod() {
      / *... * /}}}Copy the code

This particular use of ref can be explained in more detail in the official documentation, but it’s important to note that if a child component sets a value for the Expose property, the parent component only gets the value of those properties that expose declares. This is why there is such a proxy object in snippet 10, and in turn we have seen the implementation of a mechanism to protect the content of the child component from arbitrary access by the parent component.

conclusion

This article first to throw a specific case, since createApp speak again, following the function call stack, mentioned the compilation render, rendering render two functions, analyzes the createRenderer, createAppAPI, mount, getExposeProxy etc. Function. Here you can understand the basic process of creating a Vue application. This article has laid a foundation for the analysis of the specific implementation of render function, about the specific implementation of render function I will be introduced in the next article, please friends look forward to.

Write in the last

After reading the article, you can do the following things to support you:

  • ifLike, look, forwardCan let the article help more people in need;
  • If you are the author of wechat public account, you can find me to openWhite list.reprintedMy original articles;

Finally, please pay attention to my wechat public account: Yang Yitao, you can get my latest news.