vue-lit基于lit-html+@vue/reactivity实现模板引擎VueCompositionAPI,仅70行代码开发web组件。概要上面定义了my-component和my-child组件,并使用my-child作为my-component的默认子元素import{defineComponent,reactive,html,onMounted,onUpdated,onUnmounted}from'https://unpkg.com/@vue/lit'defineComponent定义自定义元素,第一个参数是自定义元素组件的名称,必须遵循原生APIcustomElements.define对组件名称的规范,组件名称必须包含破折号。reactive属于@vue/reactivity提供的响应式API,可以创建一个响应式对象,在渲染函数中调用时会自动收集依赖,这样在Mutable模式下修改值时,可以捕获并自动触发相应组件的重新渲染。html是lit-html提供的一个模板函数,通过它可以用Template字符串的原生语法来描述模板,是一个轻量级的模板引擎。onMounted、onUpdated、onUnmounted是基于web组件生命周期创建的生命周期函数,可以监控组件何时创建、更新、销毁。接下来看defineComponent的内容:defineComponent('my-component',()=>{conststate=reactive({text:'hello',show:true})consttoggle=()=>{state.show=!state.show}constonInput=e=>{state.text=e.target.value}return()=>html`
${state.text}
${state.show?html`
my-child>`:``}`})借助模板引擎lit-html的能力,可以在模板中同时传递变量和函数,然后利用@vue/reactivity能力生成新的template当变量改变时,更新组件dom。精读源码可以看出,vue-lit巧妙的融合了三种技术方案。他们合作的方式是:使用@vue/reactivity创建响应式变量。使用模板引擎lit-html创建使用这些反应变量的HTML实例。使用Web组件渲染模板引擎生成的HTML实例,这种方式创建的组件具有隔离的能力。其中,响应式能力和模板能力分别由@vue/reactivity和lit-html这两个包提供。我们只需要从源码中找到剩下的两个函数:修改值后如何触发模板刷新,以及生命周期函数的构造。我们先看看修改值后如何触发模板刷新。下面我提取了重新渲染的相关代码:constructor(){super()consttemplate=factory.call(this,props)constroot=this.attachShadow({mode:'closed'})effect(()=>{render(template(),root)})}})可以清楚的看到,首先customElements.define创建了一个nativewebcomponent,在初始化的时候使用它的API创建了一个封闭节点,对外部API调用是关闭的,也就是创建了一个webcomponent。然后调用effect回调函数中的html函数,即use文档中返回的模板函数。由于这个模板函数中使用的变量都是reactive定义的,所以effect可以准确捕捉到它的变化,并在变化后再次调用effect回调函数,实现了“值变化后重新渲染”的功能。然后看看生命周期是如何实现的。由于生命周期贯穿于整个实现过程,因此必须结合完整的源码来看。完整的核心代码贴在下面。上面介绍的部分可以忽略,只介绍生命周期的实现:(name,classextendsHTMLElement{constructor(){super()constprops=(this._props=shallowReactive({}))currentInstance=thisconsttemplate=factory.call(this,props)currentInstance=nullthis._bm&&this._bm.forEach((cb)=>cb())constroot=this.attachShadow({mode:'closed'})letisMounted=falseeffect(()=>{if(isMounted){this._bu&&this._bu.forEach((cb)=>cb())}render(template(),root)if(isMounted){this._u&&this._u.forEach((cb)=>cb())}else{isMounted=真}})}connectedCallback(){this._m&&this._m.forEach((cb)=>cb())}disconnectedCallback(){this._um&&this._um.forEach((cb)=>cb())}attributeChangedCallback(name,oldValue,newValue){this._props[name]=newValue}})}functioncreateLifecycleMethod(name){return(cb)=>{if(currentInstance){;(currentInstance[name]||(currentInstance[名称]=[])).push(cb)}}}exportconstonBeforeMount=createLifecycleMethod('_bm')exportconstonMounted=createLifecycleMethod('_m')exportconstonBeforeUpdate=createLifecycleMethod('_bu')exportconstonUpdated=createLifecycleMethod('_u')exportconstonUnmounted=createLifecycleMethod('_um')生命周期的实现是这样的._bm&&this._bm.forEach((cb)=>cb()),原因是一个循环,因为对于exampleonMount(()=>cb())可以注册多次,所以每个生命周期可能会注册多个回调函数,所以遍历会依次执行生命周期函数还有一个特点,就是不划分组件实例,所以必须有一个currentInstance来标记当前回调函数注册在哪个组件实例中,而这个注册的同步过程是在执行的过程中defineComponent的回调函数工厂,所以它会有如下代码:currentInstance=thisconsttemplate=factory.call(this,props)currentInstance=null这样,我们会一直将currentInstance指向当前正在执行的组件实例,所有生命周期函数是在这个过程中执行的,所以调用生命周期回调函数时,currentInstance变量必须指向当前组件实例。接下来为了方便,封装了createLifecycleMethod函数,在组件实例上挂载了_bm、_bu等一些数组,比如_bm表示beforeMount,_bu表示beforeUpdate。接下来就是在相应的位置调用相应的函数:在执行attachShadow之前先执行_bm-onBeforeMount,因为这个过程确实是准备组件挂载的最后一步。然后在effect中调用了两个生命周期,因为每次渲染都会执行effect,所以特地存储了isMounted标志是否是初始渲染: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}})这个很好理解,只有在第一次渲染之后,从第二次渲染开始,在执行render之前调用_bu-onBeforeUpdate(这个函数来自lit-html渲染模板引擎),在执行之后_u-onUpdated在渲染函数之后调用。因为render(template(),root)会按照lit-html的语法,直接将template()返回的HTML元素挂载到root节点,而root就是这个web组件attachShadow生成的shadowdom节点,所以这句话executes结束后,渲染完成,所以onBeforeUpdate和onUpdated是串联的。最后几个生命周期函数都是使用web组件的原生API实现的:._um.forEach((cb)=>cb())}分别实现挂载和卸载。这也说明了浏览器API分层的清晰,只提供了创建和销毁的回调,而更新机制完全由业务代码实现,不管是@vue/reactivity还是addEventListener的作用,所以如果你让在此之上的完整框架,需要自己实现onUpdate生命周期。最后用attributeChangedCallback生命周期监听自定义组件html属性的变化,然后直接映射到this._props[name]的变化,为什么呢?attributeChangedCallback(name,oldValue,newValue){this._props[name]=newValue}查看以下代码片段以了解原因:constprops=(this._props=shallowReactive({}))consttemplate=factory.call(this,props)effect(()=>{render(template(),root)})早在初始化时,_props就被创建为响应式变量,所以只要作为lit-html模板表达式的参数(对应factory.call(this,props),而factory是defineComponent('my-child',['msg'],(props)=>{..)的第三个参数,这样只要这个参数变化会触发子组件的重新渲染,因为这个props经过了Reactive的处理,总结一下,vue-lit的实现非常巧妙,学习他的源码可以同时理解几个概念:reactive。webcomponent.stringtemplate.templateengine精简实现.生命周期.以及如何串起来,用70行代码实现一个优雅的渲染引擎。最后,这种模式创建的web组件引入的runtimelib在gzip之后只有6kb,但是你可以享受到现代框架的响应式开发体验,如果你认为runtime大小可以忽略不计,那么这是一个理想的创建lib一个可维护的网络组件。讨论地址为:精读《vue-lit 源码》·Issue#396·dt-fe/weekly想参与讨论的请点这里,每周都有新话题,周末或周一发布。前端精读——帮你过滤靠谱的内容。关注前端精读微信公众号版权声明:免费转载-非商业-非衍生保留属性(CreativeCommons3.0License)