1. Rendering components
From the user's perspective, a stateful component is actually an option object.
const Componetn = { name: "Button", data() { return { val: 1 } } }
For the renderer, a stateful component is actually a special vnode.
const vnode = { type: Component, props: { val: 1 }, }
Generally speaking, the return value of a component rendering function must be the virtual DOM of its component itself.
const Component = { name: "Button", render() { return { type: 'button', children: 'Button' } } }
In this way, in the renderer, you can call the component's render method to render the component.
function mountComponent(vnode, container, anchor) { const componentOptions = ; const { render } = componentOptions; const subTree = render(); patch(null, subTree, container, anchor); }
2. Component status and self-update
In the component, we agree that the component uses the data function to define the state of the component itself. At the same time, in the rendering function, we can call this to access the state in the data.
const Component = { name: "Button", data() { return { val: 1 } } render() { return { type: 'button', children: `${}` } } } function mountComponent(vnode, container, anchor) { const componentOptions = ; const { render, data } = componentOptions; const state = reactive(data); // Encapsulate data into responsive objects effect(() => { const subTree = (state,state); // Specify data itself as this during the render function call process patch(null, subTree, container, anchor); }); }
However, while responsive data is modified, the corresponding components will also be re-rendered. When the component state is modified multiple times, the components will be rendered several times in succession, which is obviously very expensive. Therefore, we need to implement a task buffer queue so that component rendering will only run after the last modification operation.
const queue = new Set(); let isFlushing = false; const p = (); function queueJob(job) { (job); if(!isFlushing) { isFlushing = true; (() => { try { (job=>job()); } finally { isFlushing = false; = 0; } }) } } function mountComponent(vnode, container, anchor) { const componentOptions = ; const { render, data } = componentOptions; const state = reactive(data); // Encapsulate data into responsive objects effect(() => { const subTree = (state,state); // Specify data itself as this during the render function call process patch(null, subTree, container, anchor); }, { scheduler: queueJob }); }
3. Component instances and life cycles
A component instance is actually a state collection, which maintains all state information during component operation.
function mountComponent(vnode, container, anchor) { const componentOptions = ; const { render, data } = componentOptions; const state = reactive(data); // Encapsulate data into responsive objects const instance = { state, isMounted: false, // Whether the component is mounted subTree: null // Component instance } = instance; effect(() => { const subTree = (state,state); // Specify data itself as this during the render function call process if(!) { patch(null, subTree, container, anchor); = true; } else{ ptach(, subTree, container, anchor); } = subTree; // Update component instance }, { scheduler: queueJob }); }
Because the isMounted state can distinguish component mounting and update, we can easily insert life cycle hooks in this process.
function mountComponent(vnode, container, anchor) { const componentOptions = ; const { render, data, beforeCreate, created, beforeMount, mounted, beforeUpdate, updated } = componentOptions; beforeCreate && beforeCreate(); // Call the beforeCreate hook before state creation const state = reactive(data); // Encapsulate data into responsive objects const instance = { state, isMounted: false, // Whether the component is mounted subTree: null // Component instance } = instance; created && (state); // After the status creation is completed, call the created hook effect(() => { const subTree = (state,state); // Specify data itself as this during the render function call process if(!) { beforeMount && (state); // Call the beforeMount hook before mounting to the real DOM patch(null, subTree, container, anchor); = true; mounted && (state); // After mounting to the real DOM, call the mounted hook } else{ beforeUpdate && (state); // Before the component update status is mounted to the real DOM, call the beforeUpdate hook ptach(, subTree, container, anchor); updated && (state); // After the component update status is mounted to the real DOM, call the updated hook } = subTree; // Update component instance }, { scheduler: queueJob }); }
Passive updates to component status
Usually, we specify the props received by the component. Therefore, there will be two parts of the definition of props for a component: props passed to the component and props defined by the component.
const Component = { name: "Button", props: { name: String } } function mountComponent(vnode, container, anchor) { const componentOptions = ; const { render, data, props: propsOptions, beforeCreate, created, beforeMount, mounted, beforeUpdate, updated } = componentOptions; beforeCreate && beforeCreate(); // Call the beforeCreate hook before state creation const state = reactive(data); // Encapsulate data into responsive objects // Call the resolveProps function to parse the final props data and attrs data const [props, attrs] = resolveProps(propsOptions, ); const instance = { state, // Package the parsed props data into shallowReactive and define it on the component instance props: shallowReactive(props), isMounted: false, // Whether the component is mounted subTree: null // Component instance } = instance; // ... } function resolveProps(options, propsData) { const props = {}; //Storing props attributes defined in the component const attrs = {}; // Store props attributes that are not defined in the component for(const key in propsData ) { if(key in options) { props[key] = propsData[key]; } else { attrs[key] = propsData[key]; } } return [props, attrs]; }
We call child component updates caused by parent component self-update as passive updates of child components. When a child component is updated passively, what we need to do is:
- Detect whether the subcomponent really needs to be updated, because the props of the subcomponent may be unchanged;
- If update is required, update the props, slots, etc. of the child components.
function patchComponet(n1, n2, container) { const instance = ( = ); const { props } = instance; if(hasPropsChanged(, )) { // Check whether props need to be updated const [nextProps] = resolveProps(, ); for(const k in nextProps) { // Update props props[k] = nextProps[k]; } for(const k in props) { // Delete props that do not if(!(k in nextProps)) delete props[k]; } } } function hasPropsChanged( prevProps, nextProps) { const nextKeys = (nextProps); if( !== (preProps).length) { // If the number of new and old props is not equal, it means that the new and old props have changed. return true; } for(let i = 0; i < ; i++) { // If the properties of the new and old props are not equal, it means that the new and old props have changed. const key = nextKeys[i]; if(nextProps[key] !== prevProps[key]) return true; } return false; }
Since both props data and the data of the component itself need to be exposed to the rendering function and enable the rendering function to access them through this, we need to encapsulate a rendering context object.
function mountComponent(vnode, container, anchor) { // ... const instance = { state, // Package the parsed props data into shallowReactive and define it on the component instance props: shallowReactive(props), isMounted: false, // Whether the component is mounted subTree: null // Component instance } = instance; const renderContext = next Proxy(instance, { get(t, k, r) { const {state, props} = t; if(state && k in state) { return state[k]; } else if (k in props) [ return props[k]; ] else { ("Attribute does not exist"); } }, set(t, k, v, r) { const { state, props } = t; if(state && k in state) { state[k] = v; } else if(k in props) { props[k] = v; } else { ("Attribute does not exist"); } } }); // When calling a lifecycle function, you must bind the rendering context object. created && (renderContext); // ... }
Function and implementation
The new component options added by Vue3 when setting function are different from other component options in Vue2. The setup function is mainly used to cooperate with the combination API to provide users with a place for creating combination logic, creating responsive data, creating general functions, registering life cycle hooks, etc. During the entire life cycle of a component, the setup function will only be executed once when it is mounted, and its return value may be in two cases:
- Returns a function that serves as the render function of the component
- Returns an object whose data is exposed to the template
In addition, the setup function receives two parameters. The first parameter is the props data object, and the other is the setupContext, which is some important data related to the component interface.
cosnt { slots, emit, attrs, expose } = setupContext; /** slots: The slot received by the component emit: A function to emit custom events attrs: No attributes declared in component's props expose: A function used to explicitly expose component data */
Let’s implement the setup component options below.
function mountComponent(vnode, container, anchor) { const componentOptions = ; const { render, data, setup, /* ... */ } = componentOptions; beforeCreate && beforeCreate(); // Call the beforeCreate hook before state creation const state = reactive(data); // Encapsulate data into responsive objects const [props, attrs] = resolveProps(propsOptions, ); const instance = { state, props: shallowReactive(props), isMounted: false, // Whether the component is mounted subTree: null // Component instance } const setupContext = { attrs }; const setupResult = setup(shallowReadOnly(), setupContext); let setupState = null; if(typeof setResult === 'function') { if(render) ('setup function returns the render function, the render option will be ignored'); render = setupResult; } else { setupState = setupResult; } = instance; const renderContext = next Proxy(instance, { get(t, k, r) { const {state, props} = t; if(state && k in state) { return setupState[k]; // Add support for setupState } else if (k in props) [ return props[k]; ] else { ("Attribute does not exist"); } }, set(t, k, v, r) { const { state, props } = t; if(state && k in state) { setupState[k] = v; // Add support for setupState } else if(k in props) { props[k] = v; } else { ("Attribute does not exist"); } } }); // When calling a lifecycle function, you must bind the rendering context object. created && (renderContext); }
6. Implementation of component events and emit
In the component, we can use the emit function to emit custom events.
function mountComponent(vnode, container, anchor) { const componentOptions = ; const { render, data, setup, /* ... */ } = componentOptions; beforeCreate && beforeCreate(); // Call the beforeCreate hook before state creation const state = reactive(data); // Encapsulate data into responsive objects const [props, attrs] = resolveProps(propsOptions, ); const instance = { state, props: shallowReactive(props), isMounted: false, // Whether the component is mounted subTree: null // Component instance } function emit(event, ...payload) { const eventName = `on${event[0].toUpperCase() + (1)}`; const handler = [eventName]; if(handler) { handler(...payload); } else { ('The event does not exist'); } } const setupContext = { attrs, emit }; // ... }
Since properties not declared in component props will not be added to props, all events will not be added to props. For this, we need to do some special processing on the resolveProps function.
function resolveProps(options, propsData) { const props = {}; //Storing props attributes defined in the component const attrs = {}; // Store props attributes that are not defined in the component for(const key in propsData ) { if(key in options || ('on')) { props[key] = propsData[key]; } else { attrs[key] = propsData[key]; } } return [props, attrs]; }
7. Working principle and implementation of slots
As the name suggests, a slot means that the component will reserve a slot, and the contents in this slot need to be inserted by the user.
<templete> <header><slot name="header"></slot></header> <div> <slot name="body"></slot> </div> <footer><slot name="footer"></slot></footer> </templete>
When using it in the parent component, you can use the slot as follows:
<templete> <Component> <templete #header> <h1> title </h1> </templete> <templete #body> <section>content</section> </templete> <tempelte #footer> <p> footnote </p> </tempelte> </Component> </templete>
The above parent component will be compiled into the following functions:
function render() { retuen { type: Component, children: { header() { return { type: 'h1', children: 'title' } }, body() { return { type: 'section', children: 'content' } }, footer() { return { type: 'p', children: 'footnote' } } } } }
The Component component will be compiled as:
function render() { return [ { type: 'header', children: [this.$()] }, { type: 'bdoy', children: [this.$()] }, { type: 'footer', children: [this.$()] } ] }
In the mountComponent function, we just need to directly take the children object of vnode. Of course, we also need to do some special treatments for slots.
function mountComponent(vnode, container, anchor) { // ... const slots = || {}; const instance = { state, props: shallowReactive(props), isMounted: false, // Whether the component is mounted subTree: null, // Component instance slots } const setupContext = { attrs, emit, slots }; const renderContext = next Proxy(instance, { get(t, k, r) { const {state, props} = t; if(k === '$slots') { // Some special treatments for slots return slots; } // ... }, set(t, k, v, r) { // ... } }); // ... }
8. Registration Lifecycle
In setup, there are some combination APIs used to register lifecycle function hooks. For the acquisition of the lifecycle function, we can define a currentInstance variable to store the instance currently being initialized.
let currentInstance = null; function setCurrentInstance(instance) { currentInstance = instance; }
Then we add a mounted array to the component instance to store the mounted hook function of the current component.
function mountComponent(vnode, container, anchor) { // ... const slots = || {}; const instance = { state, props: shallowReactive(props), isMounted: false, // Whether the component is mounted subTree: null, // Component instance slots, mounteds } const setupContext = { attrs, emit, slots }; // Set the current instance before setup execution setCurrentInstance(instance); const setupResult = setup(shallowReadonly(),setupContext); //Reset after execution setCurrentInstance(null); // ... }
Then there is the implementation and execution timing of onMounted itself.
function onMounted(fn) { if(currentInstance) { (fn); } else { ("OnMounted hook can only be executed in the setup function"); } } function mountComponent(vnode, container, anchor) { // ... effect(() => { const subTree = (state,state); // Specify data itself as this during the render function call process if(!) { beforeMount && (state); // Call the beforeMount hook before mounting to the real DOM patch(null, subTree, container, anchor); = true; && ( hook => { (renderContext); }) // After mounting to the real DOM, call the mounted hook } else{ // ... } = subTree; // Update component instance }, { scheduler: queueJob }); }
This is the end of this article about the detailed analysis of the implementation principles of Vue components. For more related Vue components, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!