Release notes
Composition-api v1.0.0-RC.6: Composition-API v1.0.0-RC.6
- What does Vue do when it installs composition-API?
- Vue executes each component’s
setup
What 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:
- If null, return directly
- Phi is a function of phi
render
Methods to deal with - 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:
- If the attribute value is a function, the function is already called
this
That’s the current instance - If the property value is a non-object, non-function value, it is automatically passed
ref
packaging - Passes if the property value is a normal object and has child property values
reactive
After the array, this ordinary object is also converted to passreactive
Packaging 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:
- By checking Vue’s
__composition_api_installed__
Property to determine whether the installation is repeated - Check whether the Vue version is 2.x
- Add using merge policy
setup
api - Tag installation
- Use global blending to pair
setup
Initialize
When you execute setup, you do the following:
- Check whether the current component is in use
render
Method, if any, to mark the current instance before it so thatrender
The method can be passed internallygetCurrentInstance
Method to access the current instance. - Check that the current component has
setup
API, no directly return, otherwise in the initializationdata
So let’s initialize itsetup
- The initialization
setup
The thing to do is to constructsetup
Accept two parameters: props and CTX - Then perform
setup
According 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.