Vue-lit Uses only 70 lines of code to implement the Vue Composition API to the template engine for developing Web Components based on lit-HTML + @vue/reactivity.
An overview of the
<my-component></my-component>
<script type="module">
import {
defineComponent,
reactive,
html,
onMounted,
onUpdated,
onUnmounted
} from 'https://unpkg.com/@vue/lit'
defineComponent('my-component'.() = > {
const state = reactive({
text: 'hello'.show: true
})
const toggle = () = >{ state.show = ! state.show }const onInput = e= > {
state.text = e.target.value
}
return () = > html`
<button @click=${toggle}>toggle child</button>
<p>
${state.text} <input value=${state.text} @input=${onInput}>
</p>
${state.show ? html`<my-child msg=${state.text}></my-child>` : ` `}
`
})
defineComponent('my-child'['msg'].(props) = > {
const state = reactive({ count: 0 })
const increase = () = > {
state.count++
}
onMounted(() = > {
console.log('child mounted')
})
onUpdated(() = > {
console.log('child updated')
})
onUnmounted(() = > {
console.log('child unmounted')})return () = > html`
<p>${props.msg}</p>
<p>${state.count}</p>
<button @click=${increase}>increase</button>
`
})
</script>
Copy the code
My-component and my-Child components are defined above, with my-Child as the default child of my-Component.
import {
defineComponent,
reactive,
html,
onMounted,
onUpdated,
onUnmounted
} from 'https://unpkg.com/@vue/lit'
Copy the code
DefineComponent defines a Custom Element. The first parameter is a customElement component name. The component name must follow the customElements.
Reactive is a reactive API provided by @vue/reactivity that creates a reactive object that is automatically dependent collected when called in a rendering function, which can be captured when a Mutable value is changed, and automatically triggers rerendering of the corresponding component.
HTML is a Template function provided by lit-HTML, which can describe templates using Template strings native syntax. It is a lightweight Template engine.
OnMounted, onUpdated, and onUnmounted are lifecycle functions created based on Web Component lifecycle to listen for component creation, update, and destruction.
Now look at defineComponent:
defineComponent('my-component'.() = > {
const state = reactive({
text: 'hello'.show: true
})
const toggle = () = >{ state.show = ! state.show }const onInput = e= > {
state.text = e.target.value
}
return () = > html`
<button @click=${toggle}>toggle child</button>
<p>
${state.text} <input value=${state.text} @input=${onInput}>
</p>
${state.show ? html`<my-child msg=${state.text}></my-child>` : ` `}
`
})
Copy the code
With the ability of the template engine lit-html to pass variables and functions in the template at the same time, and with the @vue/reactivity ability to generate new templates as variables change, updating the component DOM.
Intensive reading
As you can see from reading the source code, VUE-Lit is a clever fusion of three technical solutions in the following ways:
- Create reactive variables using @vue/reactivity.
- Create HTML instances using these reactive variables using the template engine lit-HTML.
- Components are created with isolation capability by rendering HTML instances generated by the Web Component template engine.
The reactive capability and template capability are provided by @vue/reactivity and lit-html respectively. We only need to look for the remaining two functions in the source code: how to trigger template refresh after modifying values, and how to construct lifecycle functions.
First look at how to trigger a template refresh after a value change. I have extracted the code related to re-rendering below:
import {
effect
} from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'
customElements.define(
name,
class extends HTMLElement {
constructor() {
super(a)const template = factory.call(this, props)
const root = this.attachShadow({ mode: 'closed' })
effect(() = > {
render(template(), root)
})
}
}
)
Copy the code
You can clearly see that customElements. Define creates a native Web Component and uses its API to create a closed node during initialization. This node is closed for external API calls. Create a Web Component that is immune to external interference.
Then the HTML function is called within the effect callback, that is, the template function returned in the use document. Since the variables used in the template function are defined by Reactive, effect can accurately capture their changes and re-call the effect callback after their changes. Realize the function of “value change after rendering”.
Then look at how the life cycle is implemented, because the life cycle runs through the whole implementation process, so it must be combined with the full source code, the following post the full core code, the above part can be ignored to see, just look at the implementation of the life cycle:
let currentInstance
export function defineComponent(name, propDefs, factory) {
if (typeof propDefs === 'function') {
factory = propDefs
propDefs = []
}
customElements.define(
name,
class extends HTMLElement {
constructor() {
super(a)const props = (this._props = shallowReactive({}))
currentInstance = this
const template = factory.call(this, props)
currentInstance = null
this._bm && this._bm.forEach((cb) = > cb())
const root = this.attachShadow({ mode: 'closed' })
let isMounted = false
effect(() = > {
if (isMounted) {
this._bu && this._bu.forEach((cb) = > cb())
}
render(template(), root)
if (isMounted) {
this._u && this._u.forEach((cb) = > cb())
} else {
isMounted = true}})}connectedCallback() {
this._m && this._m.forEach((cb) = > cb())
}
disconnectedCallback() {
this._um && this._um.forEach((cb) = > cb())
}
attributeChangedCallback(name, oldValue, newValue) {
this._props[name] = newValue
}
}
)
}
function createLifecycleMethod(name) {
return (cb) = > {
if(currentInstance) { ; (currentInstance[name] || (currentInstance[name] = [])).push(cb) } } }export const onBeforeMount = createLifecycleMethod('_bm')
export const onMounted = createLifecycleMethod('_m')
export const onBeforeUpdate = createLifecycleMethod('_bu')
export const onUpdated = createLifecycleMethod('_u')
export const onUnmounted = createLifecycleMethod('_um')
Copy the code
ForEach ((cb) => cb())) is a loop because, for example, onMount(() => cb()) can be registered multiple times, so each lifecycle can register multiple callback functions. So the traversal executes them in turn.
Lifecycle functions also have the advantage of not grouping component instances, so there must be a currentInstance to indicate which component instance the current callback is registered on, and this registration is synchronized during the execution of the defineComponent callback factory. Hence the following code:
currentInstance = this
const template = factory.call(this, props)
currentInstance = null
Copy the code
In this way, we always point to the currently executing component instance, and all lifecycle functions are executed during this process, so when the lifecycle callback function is called, the currentInstance variable must point to the currently executing component instance.
Next, for convenience, the createLifecycleMethod function is encapsulated and some arrays of _BM and _bu are mounted on the component instance, such as _BM for beforeMount and _bu for beforeUpdate.
The next step is to call the corresponding function at the corresponding location:
First execute _BM-onbeforemount before attachShadow is executed, as this process is really the last step in preparing the component for mounting.
We then call both life cycles in Effect, since effect is executed on every render, so we specifically store isMounted to initialize the render:
effect(() = > {
if (isMounted) {
this._bu && this._bu.forEach((cb) = > cb())
}
render(template(), root)
if (isMounted) {
this._u && this._u.forEach((cb) = > cb())
} else {
isMounted = true}})Copy the code
This is easy to see, only after the initial render, starting from the second render, call _bu-onbeforeUpdate before render (this function comes from the lit-HTML template engine), _U-onupdated is called after the render function is executed.
Since render(template(), root) will mount the HTML element returned by template() directly to the root node according to lit-HTML syntax, Root is the shadow DOM node generated by the Web Component attachShadow, so rendering is complete when this is done, so onBeforeUpdate and onUpdated.
The last few lifecycle functions are implemented using the Native Web Component API:
connectedCallback() {
this._m && this._m.forEach((cb) = > cb())
}
disconnectedCallback() {
this._um && this._um.forEach((cb) = > cb())
}
Copy the code
Mount and unmount are implemented respectively. This illustrates the clarity of the browser API layering, which only provides create and destroy callbacks, while the update mechanism is entirely implemented by the business code, regardless of the @vue/reactivity effect or addEventListener. So if you do a complete framework on top of this, you need to implement the onUpdate lifecycle yourself.
Finally, the attributeChangedCallback lifecycle is used to listen for changes to the HTML attribute of the custom component, and then map it directly to changes to this._props[name].
attributeChangedCallback(name, oldValue, newValue) {
this._props[name] = newValue
}
Copy the code
Take a look at the following code snippet to see why:
const props = (this._props = shallowReactive({}))
const template = factory.call(this, props)
effect(() = > {
render(template(), root)
})
Copy the code
At initialization, _props is created as a response variable, so as long as it is used as an argument to the lit-HTML template expression (corresponding to factory.call(this, props), DefineComponent (‘my-child’, [‘ MSG ‘], (props) => {.. So any change to this parameter triggers re-rendering of the child components because the props are already being Reactive.
conclusion
Vue-lit implementation is very clever, learn its source code can understand several concepts at the same time:
- Reative.
- The web component.
- The string of the template.
- Simplified implementation of template engine.
- Life cycle.
And how to string them together to create an elegant rendering engine in 70 lines of code.
Finally, a Web Component created in this pattern introduces a runtime lib that is only 6KB after gzip, but provides the responsive development experience of a modern framework. If you think the runtime size is negligible, This is an ideal lib for creating maintainable Web Components.
The discussion address is: close reading vue-lit source · Issue #396 · dT-fe /weekly
If you’d like to participate in the discussion, pleaseClick here to, with a new theme every week, released on weekends or Mondays. Front end Intensive Reading – Helps you filter the right content.
Copyright Notice: Freely reproduced – Non-commercial – Non-derivative – Remain signed (Creative Commons 3.0 License)