VUE the LIT- based the LIT-HTML + @ VUE / Reactivity only 70 lines of code template engine implements give Vue the API Composition , to develop web component.
Overview
<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>
The components my-component
and my-child
are defined above, with my-child
as the default child element of my-component
.
import {
defineComponent,
reactive,
html,
onMounted,
onUpdated,
onUnmounted
} from 'https://unpkg.com/@vue/lit'
defineComponent
defines custom element. The first parameter is the name of the custom element component, which must follow the native API customElements.define specification for component names. The component name must contain a dash.
reactive
belongs to the responsive API provided by @vue/reactivity , which can create a responsive object, which will automatically collect dependencies when called in the rendering function, so that when the value is modified in the Mutable method, it can be captured and automatically trigger the corresponding component Re-render.
html
is a template function provided by lit-html , through which the template can be described with the native syntax of Template strings , which is a lightweight template engine.
onMounted
, onUpdated
, onUnmounted
are lifecycle functions created based on web component lifecycle , which can monitor the timing of component creation, update and destruction.
Next, look at the content of 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>` : ``}
`
})
With the ability of the template engine lit-html , variables and functions can be passed in the template at the same time, and then with the help of the @vue/reactivity ability, a new template can be generated when the variable changes, and the component DOM can be updated.
intensive reading
Reading the source code, you can find that vue-lit cleverly integrates three technical solutions, and they cooperate in the following ways:
- Use @vue/reactivity to create reactive variables.
- Use the template engine lit-html to create HTML instances that use these reactive variables.
- Use the web component render the HTML instance generated by the template engine, so that the created component has the ability to isolate.
Among them, the responsive capability and the template capability are provided by the two packages @vue/reactivity , lit-html , and we only need to find the remaining two functions from the source code: how to trigger the template refresh after modifying the value , and how the lifecycle functions are constructed.
First, let's see how to trigger a template refresh after the value is modified. Below I have extracted the code related to re-rendering:
import {
effect
} from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'
customElements.define(
name,
class extends HTMLElement {
constructor() {
super()
const template = factory.call(this, props)
const root = this.attachShadow({ mode: 'closed' })
effect(() => {
render(template(), root)
})
}
}
)
It can be clearly seen that first, customElements.define
creates a native web component, and uses its API to create a closed
node during initialization. This node is closed to external API calls, that is, a web component that is not subject to external interference is created.
Then call the html
function in the effect
callback function, that is, use the template function returned in the document. Since the variables used in this template function are defined by reactive
, effect
can accurately capture its changes and call effect
again after the changes. The callback function implements the function of "re-rendering after value changes".
Then look at how the life cycle is implemented. Since the life cycle runs through the entire implementation process, it must be viewed in combination with the full source code. The full core code is posted below. The parts introduced above can be ignored and only 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()
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')
The life cycle implementation is in the form of this._bm && this._bm.forEach((cb) => cb())
. The reason for the cycle is that, for example, onMount(() => cb())
can be registered multiple times, so each life cycle may register multiple callback functions, so the traversal will execute them in sequence.
Another feature of the life cycle function is that it is not divided into component instances, so there must be a currentInstance
mark which component instance the current callback function is registered in, and the synchronization process of this registration is during the execution of the defineComponent
callback function factory
. There will be the following code:
currentInstance = this
const template = factory.call(this, props)
currentInstance = null
In this way, we always point currentInstance
to the currently executing component instance, and all life cycle functions are executed during this process. Therefore, when the life cycle callback function is currentInstance
must point to the current component instance .
Next, for convenience, the createLifecycleMethod
function is encapsulated, and some arrays such as _bm
and _bu
are mounted on the component instance. For example, _bm
means beforeMount
, and _bu
means beforeUpdate
.
The next step is to call the corresponding function in the corresponding position:
First execute _bm
- onBeforeMount
before attachShadow
executes, as this process is indeed the last step in preparing the component mount.
Then two life cycles are called in effect
, because effect
will be executed every time it is rendered, so it is also specially stored whether the isMounted
mark is initialized rendering:
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
}
})
This is easy to understand, only after the initial rendering, from the second rendering, call _u
- onUpdated
before executing render
(this function comes from the lit-html
rendering template engine), and call _bu
- onBeforeUpdate
after executing the render
function.
Since render(template(), root)
according to lit-html
syntax directly to template()
returned HTML elements mounted to root
node, and root
is this the Component Web attachShadow
generated shadow dom node, thus rendering is performed after the end of this sentence is complete, so onBeforeUpdate
and onUpdated
One after the other.
The last few lifecycle functions are implemented using the web component native API:
connectedCallback() {
this._m && this._m.forEach((cb) => cb())
}
disconnectedCallback() {
this._um && this._um.forEach((cb) => cb())
}
Implement mount
and unmount
respectively. This also shows the clarity of the browser API layering. It only provides callbacks for creation and destruction, and the update mechanism is completely implemented by business code, whether it is 06209b7afc818e or addEventListener
of @vue/reactivity , or effect
. Don't care, so if you do a complete framework on top of this, you need to implement onUpdate
life cycle yourself.
In the end, it also uses the attributeChangedCallback
life cycle to monitor the change of the html attribute of the custom component, and then directly maps it to the change to this._props[name]
. Why is this?
attributeChangedCallback(name, oldValue, newValue) {
this._props[name] = newValue
}
See the code snippet below to see why:
const props = (this._props = shallowReactive({}))
const template = factory.call(this, props)
effect(() => {
render(template(), root)
})
As early as initialization, _props
is created as a responsive variable, so as long as it is used as a parameter of the lit-html template expression (corresponding to factory.call(this, props)
, and factory
is the third parameter of defineComponent('my-child', ['msg'], (props) => { ..
), so, As long as this parameter changes, it will trigger the re-rendering of the child component, because this props
has been processed by Reactive.
Summarize
The implementation of vue-lit is very clever. Learning his source code can understand several concepts at the same time:
- reative。
- web component。
- string template。
- A stripped-down implementation of the template engine.
- life cycle.
And how to string them together to implement an elegant rendering engine in 70 lines of code.
Finally, the runtime lib introduced by web components created with this mode is only 6kb after gzip, but it can enjoy the responsive development experience of modern frameworks. If you think this runtime size is negligible, then this is a very ideal. Create libs that maintain web components.
The discussion address is: Intensive Reading "vue-lit Source Code" Issue #396 dt-fe/weekly
If you want to join the discussion, please click here , there are new topics every week, published on weekends or Mondays. Front-end intensive reading - help you filter reliable content.
Pay attention to front-end intensive reading WeChat public number
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
Copyright notice: Free to reprint - non-commercial - non-derivative - keep attribution ( Creative Commons 3.0 License )
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。