preface

It’s been a while since the official release of VUE3.0, and as a technician, it’s important to keep your technology up to date. This article takes a look at how 3.0 compares to 2.x.

@TOC

First, establish the project

Vue3.0 has two ways to build scaffolding Vite

npm init vite-app hello-vue3 # OR yarn create vite-app hello-vue3
Copy the code

Scaffolding vue – cli

npm install -g @vue/cli # OR yarn global add @vue/cli
vue create hello-vue3
# select vue 3 preset
Copy the code

Set up the scaffolding using yarn create viet-app hello-vue3. After installing the dependencies using the yarn command, type yarn dev to start the project.

The project is shown in the figure below

Incompatible changes

1. V-model new grammar sugar

In 2.x, using v-Models on components is equivalent to binding value Prop and input events

<ChildComponent v-model="pageTitle"/ > <! --><ChildComponent :value="pageTitle" @input="pageTitle = $event" />
Copy the code

In 3.x, v-Models on custom components are equivalent to passing modelValue prop and receiving update:modelValue events thrown

<ChildComponent v-model="pageTitle"/ > <! --><ChildComponent
  :modelValue="pageTitle"
  @update:modelValue="pageTitle = $event"
/>
Copy the code

Allows us to use multiple V-Models on custom components

<ChildComponent v-model:title="pageTitle" v-model:content="pageContent"/ > <! --><ChildComponent
  :title="pageTitle"
  @update:title="pageTitle = $event"
  :content="pageContent"
  @update:content="pageContent = $event"
/>
Copy the code

2. A new global API: createApp

Vue 2.x has a number of global apis and configurations; for example, to create global components, you can use an API like Vue.com Ponent

Vue.component('button-counter', {
  data: () = > ({
    count: 0
  }),
  template: '<button @click="count++">Clicked {{ count }} times.</button>'
})
Copy the code

Global directives use vue. directive declarations

Vue.directive('focus', {
  inserted: el= > el.focus()
})
Copy the code

But global configuration can easily accidentally contaminate other test cases, requiring some side effects of its own

In Vue 3 we introduced createApp, which returns an application instance

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App).mount('#app')
Copy the code

Here is a table of the current global apis and their corresponding instance apis:

2. X global API 3. X Instance API (app)
Vue.config app.config
Vue.config.ignoredElements app.config.isCustomElement
Vue.component app.component
Vue.directive app.directive
Vue.mixin app.mixin
Vue.use app.use

3. Global API Treeshaking

In Vue 3, the global and internal apis were refactored and tree-shaking support was taken into account. As a result, the global API can now only be accessed as named exports of ES module builds. Unused code does not end up in the final package. This optimizes project volume. Of course the usage needs to change accordingly:

import { nextTick } from 'vue'

nextTick(() = > {
  // Something DOM related
})
Copy the code

NextTick /this.$nextTick will cause undefined is not a function error

4. Other changes

VUE3.0 also has some other changes, for example

  • The Render function argument H is now imported globally instead of being passed as an argument automatically
  • V-if and V-for priorities used on the same element have changed v-if always takes effect before V-for
  • If an element defines both V-bind =”object” and an identical separate property, merge the last defined property
    <! -- template --><div id="red" v-bind="{ id: 'blue' }"></div><! -- result --><div id="blue"></div><! -- template --><div v-bind="{ id: 'blue' }" id="red"></div><! -- result --><div id="red"></div>
    Copy the code
  • Filters have been removed and are no longer supported
  • $on, $OFF, and $once instance methods have been removed
  • The use of numbers (i.e. keyCodes) as v-on modifiers is no longer supported, and config.keycodes are no longer supported
  • .

More changes can be found in the VUe3 documentation: the VUe3 documentation

New features Composition API

  • Prior to Vue3, writing a component was writing an “object that contains Options that describe the component,” a form called the Options API
  • The Options API classifies components according to methods, computed, data, and props. When components are small, this kind of classification is obvious. However, in a large component, a component has multiple function points. When using the Options API, each function point has its own Options. If you need to modify a function point, you need to constantly switch up and down in a single file and find the corresponding function point for optimization.

As shown in the figure below, this page uses the paging function of elementUI. It is necessary to write the corresponding page-turning logic in data and methods, separated by components and Created. To optimize the paging logic, it is necessary to switch and search up and down.


The Composition API is an enhancement of the API. It is not the paradigm for vue.js 3.0 component development. If your components are simple enough, you can still use the Options API.

Look at the Composition API with the following code

<template>
  <button @click="increment">
    Count is: {{ state.count }}, double is: {{ state.double }}
  </button>
</template>
<script>
import { reactive, computed } from 'vue'
export default {
  setup() {
    const state = reactive({
      count: 0.double: computed(() = > state.count * 2)})function increment() {
      state.count++
    }
    return {
      state,
      increment
    }
  }
}
</script>
Copy the code

In this code, you can see that compared to vue.js 2.x, there is a setup startup function, and options such as props, data, and computed are not defined in the component.

In the Setup function, a reactive object state is created through the Reactive API. The state object has two attributes, count and double, where count corresponds to the value of a numeric attribute. Double, on the other hand, creates a value for a computed property using computed API. In addition, increment method is defined. Finally, expose the state object and increment method externally, and use the exposed contents in template.

4. How to implement Composition API

Here is the link to the setupComponent method in VUE3 source code

The main logic of the setup launcher is during the rendering of vNode

  const mountComponent: MountComponentFn = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) = > {
    // Create a component instance
    const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))

  // Set the component instance
  setupComponent(instance)
  // Set up and run the render function
  setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )
  }
Copy the code

In the process of creating a component instance, we will focus on the implementation of the createComponentInstance method

export function createComponentInstance(
  vnode: VNode,
  parent: ComponentInternalInstance | null,
  suspense: SuspenseBoundary | null
) {
  const type = vnode.type as ConcreteComponent
  // Inherits the appContext from the parent component instance, or directly from the root vNode if it is the root component.
  const appContext =
    (parent ? parent.appContext : vnode.appContext) || emptyAppContext

  const instance: ComponentInternalInstance = {
	// Unique id of the component
    uid: uid++,
    / / component vnode
    vnode,
    // Parent component instance
    parent,
    // App context
    appContext,
    // VNode node type
    type: vnode.type,
    // Root component instance
    root: null.// New component vNode
    next: null.// Child node vnode
    subTree: null.// Update function with side effects
    update: null.// Render function
    render: null.// Render context proxy
    proxy: null.// Render context proxy with with block
    withProxy: null.// Responsive related objects
    effects: null.Dependency injection
    provides: parent ? parent.provides : Object.create(appContext.provides),
    // Render proxy's property access cache
    accessCache: null.// Render cache
    renderCache: [].// Render context
    ctx: EMPTY_OBJ,
    / / data data
    data: EMPTY_OBJ,
    / / props data
    props: EMPTY_OBJ,
    // Common attributes
    attrs: EMPTY_OBJ,
    // Slot related
    slots: EMPTY_OBJ,
    // A component or DOM ref reference
    refs: EMPTY_OBJ,
    // The reactive result returned by the setup function
    setupState: EMPTY_OBJ,
    // Setup function context data
    setupContext: null.// The registered component
    components: Object.create(appContext.components),
    // Register the directive
    directives: Object.create(appContext.directives),
    / / suspense
    suspense,
    // Suspense for asynchronous dependencies
    asyncDep: null.Suspense // Whether asynchronous dependencies are all handled
    asyncResolved: false.// Whether to mount
    isMounted: false.// Whether to uninstall
    isUnmounted: false.// Whether to activate
    isDeactivated: false.// Life cycle, before create
    bc: null.// Lifecycle, created
    c: null.// Lifecycle, before mount
    bm: null.// Mounted
    m: null.// Life cycle, before update
    bu: null.// Lifecycle, updated
    u: null.// Life cycle, unmounted
    um: null.// Lifecycle, before unmount
    bum: null.// Lifecycle deactivated
    da: null.// Lifecycle activated
    a: null.// Animate animate
    rtg: null.// Lifecycle render tracked
    rtc: null.// Lifecycle error captured
    ec: null.// Issue event methods
    emit: null  
   }
   // Initialize the render context
  instance.ctx = { _: instance }
  // Initialize the root component pointer
  instance.root = parent ? parent.root : instance
  // Initializes the dispatch event method
  instance.emit = emit.bind(null, instance)
  return instance
}
Copy the code

Process for setting up a component instance

export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  const { props, children, shapeFlag } = instance.vnode
  // Check if it is a stateful component
  const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT
  // Initialize props
  initProps(instance, props, isStateful, isSSR)
  // Initialize the slot
  initSlots(instance, children)
  // Set stateful component instances
  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined
  isInSSRComponentSetup = false
  return setupResult
}
Copy the code

Next, the Setup function judges the processing and completes the component instance setup

function setupStatefulComponent(instance: ComponentInternalInstance, isSSR: boolean) {
  const Component = instance.type as ComponentOptions

  // 0. Create the attribute access cache for the rendering proxy
  instance.accessCache = Object.create(null)
  // 1. Create the render context proxy
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
  // 2. Decide to handle the setup function
  const { setup } = Component
  if (setup) {
  	If the setup function takes parameters, a setupContext is created
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null)

    currentInstance = instance
    pauseTracking()
    // Execute the setup function to get the result
    const setupResult = callWithErrorHandling(
      setup,
      instance
    )
	// Process the setup execution result
    if (isPromise(setupResult)) {
      instance.asyncDep = setupResult
    } else {
      handleSetupResult(instance, setupResult, isSSR)
    }
  } else {
    finishComponentSetup(instance, isSSR)
  }
}
Copy the code

Next we need to know to create PublicInstanceProxyHandlers rendering context agent functions, we access the instance. The CTX rendering in the context of a property, We enter the get function, and we enter the set function when we modify properties in the instance. CTX rendering context.

export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
  get({ _: instance }: ComponentRenderContext, key: string) {
    const {
      ctx,
      setupState,
      data,
      props,
      accessCache,
      type,
      appContext
    } = instance

    let normalizedProps
    if (key[0]! = ='$') {
      // setupState / data / props / ctx
      // The render agent's properties are accessed in the cache
      constn = accessCache! [key]if(n ! = =undefined) {
      	// If there is content in the cache, the data is fetched from the cache
        switch (n) {
          case AccessTypes.SETUP:
            return setupState[key]
          case AccessTypes.DATA:
            return data[key]
          case AccessTypes.CONTEXT:
            return ctx[key]
          case AccessTypes.PROPS:
            returnprops! [key] } }else if(setupState ! == EMPTY_OBJ && hasOwn(setupState, key)) {// Fetch data from setupStateaccessCache! [key] = AccessTypes.SETUPreturn setupState[key]
      } else if(data ! == EMPTY_OBJ && hasOwn(data, key)) {// Fetch data from dataaccessCache! [key] = AccessTypes.DATAreturn data[key]
      } else if (
        (normalizedProps = instance.propsOptions[0]) &&
        hasOwn(normalizedProps, key)
      ) {
      	// Get data from propsaccessCache! [key] = AccessTypes.PROPSreturnprops! [key] }else if(ctx ! == EMPTY_OBJ && hasOwn(ctx, key)) {// Fetch data from CTXaccessCache! [key] = AccessTypes.CONTEXTreturn ctx[key]
      } else if(! __FEATURE_OPTIONS_API__ || ! isInBeforeCreate) { accessCache! [key] = AccessTypes.OTHER } }const publicGetter = publicPropertiesMap[key]
    let cssModule, globalProperties
    // Public $XXX attribute or method
    if (publicGetter) {
      return publicGetter(instance)
    } else if (
      // The CSS module is injected when compiled by vue-loader
      (cssModule = type.__cssModules) &&
      (cssModule = cssModule[key])
    ) {
      return cssModule
    } else if(ctx ! == EMPTY_OBJ && hasOwn(ctx, key)) {// User-defined attributes, also starting with '$'accessCache! [key] = AccessTypes.CONTEXTreturn ctx[key]
    } else if (
      // Globally defined attributes
      ((globalProperties = appContext.config.globalProperties),
      hasOwn(globalProperties, key))
    ) {
      return globalProperties[key]
    } else if( __DEV__ && currentRenderingInstance && (! isString(key) ||// #1091 avoid internal isRef/isVNode checks on component instance leading
        // to infinite warning loop
        key.indexOf('__v')! = =0)) {if( data ! == EMPTY_OBJ && (key[0= = ='$' || key[0= = ='_') &&
        hasOwn(data, key)
      ) {
      	// If data defined in data starts with $, a warning is reported because $is a reserved character and does not act as a proxy
        warn(
          `Property The ${JSON.stringify(
            key
          )} must be accessed via $data because it starts with a reserved ` +
            `character ("$" or "_") and is not proxied on the render context.`)}else {
      	// Warning if a variable used in a template is not defined
        warn(
          `Property The ${JSON.stringify(key)} was accessed during render ` +
            `but is not defined on instance.`
        )
      }
    }
  },

  set(
    { _: instance }: ComponentRenderContext,
    key: string,
    value: any
  ): boolean {
    const { data, setupState, ctx } = instance
    if(setupState ! == EMPTY_OBJ && hasOwn(setupState, key)) {// Assign setupState
      setupState[key] = value
    } else if(data ! == EMPTY_OBJ && hasOwn(data, key)) {// Assign a value to data
      data[key] = value
    } else if (key in instance.props) {
      // Can't assign props directly
      return false
    }
    if (key[0= = ='$' && key.slice(1) in instance) {
      // Cannot assign a value to a reserved attribute starting with $inside Vue
      return false
    } else {
      // User-defined data assignment
      ctx[key] = value
    }
    return true}}Copy the code

SetupState, data, and props.

Then go back to setupStatefulComponent and determine the number of arguments to the setup function. If greater than one, createSetupContext using createSetupContext:

  function createSetupContext(
    instance: ComponentInternalInstance
    ) :SetupContext {
      const expose: SetupContext['expose'] = exposed= > {
        instance.exposed = proxyRefs(exposed)
      }
      return {
        / / property
        attrs: instance.attrs,
        / / slots
        slots: instance.slots,
        // Send events
        emit: instance.emit,
        // 
        expose
      }
    }
Copy the code

Execute the setup function to get the result

function callWithErrorHandling(
  fn: Function,
  instance: ComponentInternalInstance | null, type: ErrorTypes, args? : unknown[]) {
  let res
  try {
    // Execute setup with parameters passed inres = args ? fn(... args) : fn() }catch (err) {
    handleError(err, instance, type)
  }
  return res
}
Copy the code

Executing handleSetupResult handles the results of the setup function execution

export function handleSetupResult(instance: ComponentInternalInstance, setupResult: unknown) {
  if (isFunction(setupResult)) {
    instance.render = setupResult as InternalRenderFunction
  } else if (isObject(setupResult)) {
    instance.setupState = proxyRefs(setupResult)
  }
  finishComponentSetup(instance)
}
Copy the code

Next is the finishComponentSetup function, mainly to do the standard template or render function and compatible Options API

function finishComponentSetup(instance: ComponentInternalInstance, isSSR: boolean) {
  const Component = instance.type as ComponentOptions

  // Normalize the template or render function
  if(! instance.render) {if(compile && Component.template && ! Component.render) {// Compile at runtime
      Component.render = compile(Component.template, {
        isCustomElement: instance.appContext.config.isCustomElement,
        delimiters: Component.delimiters
      })
    }
    // The component object's render function is assigned to instance
    instance.render = (Component.render || NOOP) as InternalRenderFunction

    if (instance.render._rc) {
      // For run-time compiled rendering functions that use the with block, use a proxy for the new rendering context
      instance.withProxy = new Proxy(
        instance.ctx,
        RuntimeCompiledPublicInstanceProxyHandlers
      )
    }
  }

  // Compatible with vue.js 2.x Options API
  if (__FEATURE_OPTIONS_API__) {
    currentInstance = instance
    pauseTracking()
    applyOptions(instance, Component)
    resetTracking()
    currentInstance = null}}Copy the code

Source code to achieve Composition API link diagram

Through the analysis of VUE3 source code we understand the component initialization process, create component instance, set up component instance. By going further, we also introduced the proxy process for rendering context. You learned when setup initiates function execution in the Composition API, and how to make a connection between the results returned by Setup and template rendering.

conclusion

This paper started from the construction of VUE3 project, listed the incompatible changes of VUE3 and VUe2. X, demonstrated the Composition API of VUE3, and some advantages compared with Options API. Finally, we implemented the Composition API. Through the source code to give you analysis of the implementation principle. I hope you can have a preliminary understanding of VUE3 and gain something from this article.