Intensive reading of Vue lit source code

vue-lit be based on lit-html + @vue/reactivity The template engine is implemented in only 70 lines of code Vue Composition API , used to develop web component s.

summary

<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 above defines my component and my child components, and takes 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 component name of custom element, which must follow the native API customElements.define For the specification of the component name, the component name must contain the middle dash.

reactive belongs to @vue/reactivity The responsive API can create a responsive object. It will automatically rely on collection when invoking the rendering function, so that the value can be captured in Mutable mode and automatically trigger the renderings of the corresponding components.

html is lit-html Provided template function, through which you can use Template strings Native syntax description template is a lightweight template engine.

onMounted, onUpdated and onUnmounted are based on web component lifecycle The created life cycle function can monitor the time of component creation, update and destruction.

Next, let's look at the contents 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 help of template engine lit-html Can transfer variables and functions in the template at the same time @vue/reactivity Ability to generate a new template and update the component dom when the variable changes.

intensive reading

Reading the source code, we can find that Vue lit skillfully integrates three technical solutions, and their cooperation methods are as follows:

  1. use @vue/reactivity Create a responsive variable.
  2. Engine utilization template lit-html Create an HTML instance that uses these responsive variables.
  3. utilize web component Render the HTML instance generated by the template engine, so that the created components can be isolated.

The response capability and template capability are @vue/reactivity,lit-html Provided by these two packages, we only need to find the remaining two functions from the source code: how to trigger template refresh after modifying the value, and how to construct the life cycle function.

First, let's look at how to trigger template refresh after value modification. I've picked out 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()
      const template = factory.call(this, props)
      const root = this.attachShadow({ mode: 'closed' })
      effect(() => {
        render(template(), root)
      })
    }
  }
)

You can clearly see that first, customelements Define creates a native web component and uses its API to create a closed node during initialization. The node closes the external API call, that is, it creates a web component that will not be disturbed by the outside.

Then, the html function is called in the effect callback function, that is, the template function returned in the use document. Since the variables used in this template function are defined reactive ly, the effect can accurately capture its changes and call the effect callback function again after its changes, realizing the function of "re rendering after value changes".

Then look at how the life cycle is implemented. Since the life cycle runs through the whole implementation process, it must be combined with the full amount of source code. The full amount of core code is posted below. The parts described above can be ignored and only 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')

Life cycle implementation is like this_ bm && this._ bm. Foreach ((CB) = > CB ()) is a loop because, for example, onmount (() = > CB ()) can be registered multiple times, so multiple callback functions may be registered in each life cycle, so traversal will execute them in turn.

The life cycle function has another feature, that is, it is not divided into component instances. Therefore, there must be a currentInstance to mark the component instance in which the current callback function is registered, and the synchronization process of this registration is during the execution of the defineComponent callback function factory. Therefore, the following code is available:

currentInstance = this
const template = factory.call(this, props)
currentInstance = null

In this way, we always point the currentInstance to the currently executing component instance, and all lifecycle functions are executed in this process. Therefore, when calling the lifecycle callback function, the currentInstance variable must point to the current component instance.

Next, for convenience, the createLifecycleMethod function is encapsulated, and some shapes such as_ bm,_ An array of bu, such as_ BM means beforeMount_ bu means beforeUpdate.

Next, call the corresponding function at the corresponding position:

First, execute before attachShadow is executed_ bm - onBeforeMount, because this process is really the last step in preparing for component mount.

Then the two lifecycle is invoked in effect, because effect is executed at every rendering, so it specifically stores 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 initialization is rendered, from the second rendering, call render before executing the function (from the lit-html rendering template engine). bu - onBeforeUpdate, calling render after performing the function of the u - onUpdated.

Because render(template(), root) will directly mount the HTML element returned by template() to the root node according to the syntax of lit HTML, and root is the shadow dom node generated by the web component attachShadow, the rendering is completed after the execution of this sentence, so onBeforeUpdate and onUpdated are one before and one after.

The last several life cycle 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())
}

mount and unmount are implemented respectively. This also shows the clarity of browser API layering, which only provides callbacks for creation and destruction, while the update mechanism is completely implemented by business code, whether it is @vue/reactivity Neither the effect nor the addEventListener care, so if you build a complete framework on this, you need to implement the onUpdate life cycle according to your own requirements.

Finally, we also use the attributeChangedCallback life cycle to listen for changes in the html attribute of the custom component, and then map it directly to this_ The change of props [name], why?

attributeChangedCallback(name, oldValue, newValue) {
  this._props[name] = newValue
}

Look at the following code snippet to see why:

const props = (this._props = shallowReactive({}))
const template = factory.call(this, props)
effect(() => {
  render(template(), root)
})

As early as initialization, the_ props is created as a responsive variable so that it can be used as lit-html The parameters of the template expression (corresponding to the section of factory.call(this, props), and factory is the third parameter of definecomponent ('My child ', ['msg'], (props) = > {.. in this way, as long as this parameter changes, it will trigger the re rendering of the sub component, because this props has been processed reactively.

summary

vue-lit The implementation is very ingenious. You can learn several concepts at the same time by learning his source code:

  • reative.
  • web component.
  • string template.
  • Simplified implementation of template engine.
  • Life cycle.

And how to string them together and use 70 lines of code to realize an elegant rendering engine.

Finally, the runtime lib introduced by the web component created in this mode is only 6kb after gzip, but it can enjoy the responsive development experience of modern framework. If you think the runtime size can be ignored, it is a very ideal lib for creating maintainable web components.

The discussion address is: Intensive reading of Vue lit source code · Issue #396 · DT Fe / weekly

If you want to participate in the discussion, please click here , there are new themes every week, released on weekends or Mondays. Front end intensive reading - help you filter reliable content.

Focus on front-end intensive reading WeChat official account

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

Copyright notice: Free Reprint - non commercial - non derivative - keep signature( Creative sharing 3.0 License)

Keywords: Javascript Front-end

Added by ams53 on Mon, 14 Feb 2022 03:22:30 +0200