Release notes

Composition-api v1.0.0-RC.6: Composition-API v1.0.0-RC.6

  1. What does Vue do when it installs composition-API?
  2. Vue executes each component’ssetupWhat does method do?

All right, without further ado, let’s get right to it.

First, the installation process

1. Check whether the installation is complete

// src/install.ts

if (isVueRegistered(Vue)) {
  if (__DEV__) {
    warn(
      '[vue-composition-api] already installed. Vue.use(VueCompositionAPI) should be called only once.')}return
}
Copy the code

First check whether there is duplication of installation, if so send a warning in the development environment, mainly call isvueregigistered method to detect, its definition is as follows:

// src/runtimeContext.ts

const PluginInstalledFlag = '__composition_api_installed__'

export function isVueRegistered(Vue: VueConstructor) {
  return hasOwn(Vue, PluginInstalledFlag)
}
Copy the code

Check whether the composition_API_installed__ attribute of Vue is installed.

That will obviously be set later when you actually install the composition-API.

2. Check the Vue version

if (__DEV__) {
  if (Vue.version) {
    if (Vue.version[0]! = ='2' || Vue.version[1]! = ='. ') {
      warn(
        `[vue-composition-api] only works with Vue 2, v${Vue.version} found.`)}}else {
    warn('[vue-composition-api] no Vue version found')}}Copy the code

Then determine the Vue version in the development environment, which must be 2.x to use composition-API.

3. Add the Setup option API

Vue.config.optionMergeStrategies.setup = function(
  parent: Function,
  child: Function
) {
  return function mergedSetupFn(props: any, context: any) {
    return mergeData(
      typeof parent === 'function' ? parent(props, context) || {} : undefined.typeof child === 'function' ? child(props, context) || {} : undefined)}}Copy the code

The SETUP API is then added through Vue’s custom option merge policy.

Ps: Does anyone not know that we can customize Vue options? Try using this API to implement an asyncComputed and multiWatch for fun!

4. Set the installed mark

// src/runtimeContext.ts

const PluginInstalledFlag = '__composition_api_installed__'

export function setVueConstructor(Vue: VueConstructor) {
  // @ts-ignore
  if(__DEV__ && vueConstructor && Vue.__proto__ ! == vueConstructor.__proto__) { warn('[vue-composition-api] another instance of Vue installed')
  }
  vueConstructor = Vue
  Object.defineProperty(Vue, PluginInstalledFlag, {
    configurable: true.writable: true.value: true})}Copy the code

As mentioned above, this is where you set a flag indicating that it has been installed.

5. Set global blending

Vue.mixin({
  beforeCreate: functionApiInit
  // ... other
})
Copy the code

Then add a global mixin and perform the functionApiInit method on each component’s beforeCreate life cycle.

That’s all you need to do to install the composition-API, which we’ll cover in more detail in the next section.

2. Execute setup

We know that the composition-API basically adds a setup option and a set of hooks, and that steup is not a simple call, it requires a few things like passing in two parameters: How props, CTX came from, and how the setup return value can be used in template, etc.

Compsition-api will execute the functionApiInit method on each component’s beforeCreate:

Vue.mixin({
  beforeCreate: functionApiInit
  // ... other
})
Copy the code

Here’s what this method basically does.

1. Check whether render is present

The first step is to check if the Render method is defined and, if it is, modify its interior.

const vm = this
const $options = vm.$options
const { setup, render } = $options

if (render) {
  // keep currentInstance accessible for createElement
  $options.render = function(. args: any) :any {
    return activateCurrentInstance(vm, () = > render.apply(this, args))
  }
}
Copy the code

ActivateCurrentInstance is used to set the current instance, so we can access the current instance with getCurrentInstance in Render.

Ps: It’s worth noting that even though we wrote template, at this stage it’s already been converted to the render function.

2. Check whether setup exists

If setup is not defined, the component is not using composition-API, so skip the component directly:

if(! setup) {return
}
if (typeofsetup ! = ='function') {
  if (__DEV__) {
    warn(
      'The "setup" option should be a function that returns a object in component definitions.',
      vm
    )
  }
  return
}
Copy the code

3. Initialize setup in the data method

If setup exists, the component’s data method is modified to initialize it before the actual data method is initialized:

const { data } = $options
// wrapper the data option, so we can invoke setup before data get resolved
$options.data = function wrappedData() {
  initSetup(vm, vm.$props)
  return typeof data === 'function'
    ? (data as (this: ComponentInstance, x: ComponentInstance) => object).call(
        vm,
        vm
      )
    : data || {}
}
Copy the code

Remember when Vue initialized data? The answer is between beforeCreate and Created, so setup is the same.

4. Initialize setup

There’s a lot more going on inside the initSetup method. Here’s a look at the whole thing, and we’ll break it down a bit later:

function initSetup(vm: ComponentInstance, props: Record<any, any> = {}) {
  const setup = vm.$options.setup!
  const ctx = createSetupContext(vm)
  // fake reactive for `toRefs(props)`
  def(props, '__ob__', createObserver())
  // resolve scopedSlots and slots to functions
  // @ts-expect-error
  resolveScopedSlots(vm, ctx.slots)
  let binding: ReturnType<SetupFunction<Data, Data>> | undefined | null
  activateCurrentInstance(vm, () = > {
    // make props to be fake reactive, this is for `toRefs(props)`
    binding = setup(props, ctx)
  })
  if(! binding)return
  if (isFunction(binding)) {
    // keep typescript happy with the binding type.
    const bindingFunc = binding
    // keep currentInstance accessible for createElement
    vm.$options.render = () = > {
      // @ts-expect-error
      resolveScopedSlots(vm, ctx.slots)
      return activateCurrentInstance(vm, () = > bindingFunc())
    }
    return
  } else if (isPlainObject(binding)) {
    if (isReactive(binding)) {
      binding = toRefs(binding) as Data
    }
    vmStateManager.set(vm, 'rawBindings', binding)
    const bindingObj = binding
    Object.keys(bindingObj).forEach((name) = > {
      let bindingValue: any = bindingObj[name]
      if(! isRef(bindingValue)) {if(! isReactive(bindingValue)) {if (isFunction(bindingValue)) {
            bindingValue = bindingValue.bind(vm)
          } else if(! isObject(bindingValue)) { bindingValue = ref(bindingValue) }else if (hasReactiveArrayChild(bindingValue)) {
            // creates a custom reactive properties without make the object explicitly reactive
            // NOTE we should try to avoid this, better implementation needed
            customReactive(bindingValue)
          }
        } else if (isArray(bindingValue)) {
          bindingValue = ref(bindingValue)
        }
      }
      asVmProperty(vm, name, bindingValue)
    })
    return
  }
  if (__DEV__) {
    assert(
      false.`"setup" must return a "Object" or a "Function", got "The ${Object.prototype.toString
        .call(binding)
        .slice(8, -1)}"`)}}Copy the code

4.1. Initialize context

This CTX is the second argument accepted in setup. How is the contents of this object generated?

const ctx = createSetupContext(vm)
Copy the code

Here’s what createSetupContext does, first defining all the keys in the CTX object:

const ctx = { slots: {}}as SetupContext

const propsPlain = [
  'root'.'parent'.'refs'.'listeners'.'isServer'.'ssrContext',]const propsReactiveProxy = ['attrs']
const methodReturnVoid = ['emit']
Copy the code

The next step is to use object.defineProperty as a proxy for these attributes, which are of course read-only:

propsPlain.forEach((key) = > {
  let srcKey = ` $${key}`
  proxy(ctx, key, {
    get: () = > vm[srcKey],
    set() {
      warn(`Cannot assign to '${key}' because it is a read-only property`, vm)
    }
  })
})
Copy the code

The other two propsReactiveProxy and methodReturnVoid are similar, so I’ll skip them here.

4.2. Responsive props

Then pass the props object as an Observer:

def(props, '__ob__', createObserver())

// src/reactivity/reactive.ts
export function createObserver() {
  return observe < any > {}.__ob__
}
Copy the code

Vue.Observer creates an __ob__ attribute for the Vue.Observer instance. This article will not repeat.

Then add an __ob_ attribute to props that points to the __ob__ we got earlier.

4.3. Analytical slots

Then we delegate the current instance of slots to ctx.slots, which is just an empty object:

resolveScopedSlots(vm, ctx.slots)
Copy the code

Here is an implementation of resolveScopedSlots:

export function resolveScopedSlots(
  vm: ComponentInstance,
  slotsProxy: { [x: string]: Function }
) :void {
  const parentVNode = (vm.$options as any)._parentVnode
  if(! parentVNode)return

  const prevSlots = vmStateManager.get(vm, 'slots') | | []const curSlots = resolveSlots(parentVNode.data.scopedSlots, vm.$slots)
  // remove staled slots
  for (let index = 0; index < prevSlots.length; index++) {
    const key = prevSlots[index]
    if(! curSlots[key]) {delete slotsProxy[key]
    }
  }

  // proxy fresh slots
  const slotNames = Object.keys(curSlots)
  for (let index = 0; index < slotNames.length; index++) {
    const key = slotNames[index]
    if(! slotsProxy[key]) { slotsProxy[key] = createSlotProxy(vm, key) } } vmStateManager.set(vm,'slots', slotNames)
}
Copy the code

In simple terms, we delegate the parent component’s slots array (which is actually used) to ctx.slots, and ctX. slots updates accordingly when the slots array changes.

4.4. The setup

Finally, the most important moment is setup:

activateCurrentInstance(vm, () = > {
  // make props to be fake reactive, this is for `toRefs(props)`
  binding = setup(props, ctx)
})
Copy the code

ActivateCurrentInstance is a method that allows component setup to access the current instance via getCurrentInstance. For those of you who have used composition-API, this method is very convenient. Have you ever had a getCurrentInstance return null? If you want to know why, check out this article: Why getCurrentInstance() Returns NULL from Composition API source Code.

Then pass in the props and CTX obtained earlier, and assign the return value to binding.

4.6. Process the setup return value

A return value needs to be typed before processing it. There are three conditional branches:

  1. If null, return directly
  2. Phi is a function of phirenderMethods to deal with
  3. Is a normal object that does a series of transformations

If the return value is a function, treat it as the Render method. Of course, we need to re-call resolveScopedSlots to check the slots update and call activateCurrentInstance:

if (isFunction(binding)) {
  // keep typescript happy with the binding type.
  const bindingFunc = binding
  // keep currentInstance accessible for createElement
  vm.$options.render = () = > {
    // @ts-expect-error
    resolveScopedSlots(vm, ctx.slots)
    return activateCurrentInstance(vm, () = > bindingFunc())
  }
  return
}
Copy the code

Ps: You can also return JSX directly in setup, because Babel will turn it into a function.

But usually we return an object in setup, and we can use these values directly in template, so let’s see if the return value is an object:

else if (isPlainObject(binding)) {
  if (isReactive(binding)) {
    binding = toRefs(binding) as Data
  }

  vmStateManager.set(vm, 'rawBindings', binding)
  const bindingObj = binding

  Object.keys(bindingObj).forEach((name) = > {
    let bindingValue: any = bindingObj[name]

    if(! isRef(bindingValue)) {if(! isReactive(bindingValue)) {if (isFunction(bindingValue)) {
          bindingValue = bindingValue.bind(vm)
        } else if(! isObject(bindingValue)) { bindingValue = ref(bindingValue) }else if (hasReactiveArrayChild(bindingValue)) {
          // creates a custom reactive properties without make the object explicitly reactive
          // NOTE we should try to avoid this, better implementation needed
          customReactive(bindingValue)
        }
      } else if (isArray(bindingValue)) {
        bindingValue = ref(bindingValue)
      }
    }
    asVmProperty(vm, name, bindingValue)
  })

  return
}
Copy the code

First, if the returned object is reactive, toRefs is called to make its child properties ref-wrapped, and vmStatemanager.set is called to store those properties for use elsewhere.

We then iterate over the object and, after a series of type judgments and processing, set its child properties to variables of the current instance so that we can access them either in templte or through this.xxx.

A simple summary of the type handling here is:

  1. If the attribute value is a function, the function is already calledthisThat’s the current instance
  2. If the property value is a non-object, non-function value, it is automatically passedrefpackaging
  3. Passes if the property value is a normal object and has child property valuesreactiveAfter the array, this ordinary object is also converted to passreactivePackaging works, so we want to avoid the following situations in development:
setup() {
  return {
   	obj: {
      arr: reactive([1.2.3.4])}}}Copy the code

Finally, determining that the returned value is not an object in a development environment throws an error. This completes the execution of the setup function.

conclusion

That’s it for installing and executing the commination-API, so let’s briefly summarize. When you install the commination-API, you do the following:

  1. By checking Vue’s__composition_api_installed__Property to determine whether the installation is repeated
  2. Check whether the Vue version is 2.x
  3. Add using merge policysetup api
  4. Tag installation
  5. Use global blending to pairsetupInitialize

When you execute setup, you do the following:

  1. Check whether the current component is in userenderMethod, if any, to mark the current instance before it so thatrenderThe method can be passed internallygetCurrentInstanceMethod to access the current instance.
  2. Check that the current component hassetupAPI, no directly return, otherwise in the initializationdataSo let’s initialize itsetup
  3. The initializationsetupThe thing to do is to constructsetupAccept two parameters: props and CTX
  4. Then performsetupAccording to its return value type

Of course, the real power of Compsition-API lies in the hooks, and next time I’ll talk about how a series of hooks are implemented in composition-API that will help us write elegant, reusable code using those hooks methods.

That’s all for this article. Thank you for reading.