Web Component如何手动处理父子组件的生命周期

大家好 我是Jcode

前言

注:本文只适合未先子后父顺序加载组件的js文件,父组件先于子组件加载的情况

再聊原生组件生命周期之前,我们先看看vue的生命周期

顺序 生命周期钩子 所在组件
1 setup() 父组件
2 setup() 子组件
3 beforeMount 子组件
4 mounted 子组件
5 beforeMount 父组件
6 mounted 父组件

很明显的看到都是遵从先从父组件执行、子组件挂载、父组件挂载这样的。所以我们在组件上面进行传参不会有子组件还没mounted完成,就开始接受父组件传递的props这种情况。

巧了的是,web Component就是这样!子组件还没mounted完成,如果此时对它进行获取dom进行设置attribute,就会发生子组件的dom获取不到。

造成这个的原因是因为html自上而下解析,解析过程中遇到自定义的组件会当成HTMLUnknownElement来进行处理,保持到执行customElements.define方法之前,也就是说只有在执行了这个方法后,才会被当成一个htmlElement元素来进行渲染

解释一下为啥需要对dom设置attribute呢?,为什么不直接通过html模板来传值,因为模板上的值无法发生响应变化,只会在第一次加载的时候进行赋值,而设置attribute则可以被attributeChangedCallback监听到,从而响应相应的逻辑。

例子

先看这个例子,下面写了jcode-card和jcode-text-overflow两个自定义组件,jcode-text-overflow组件从jcode-card接受值,变化字体的背景颜色。

js 复制代码
        //text-overflow
        class TextOverflow extends HTMLElement {
            constructor() {
                super();
                const shadow = this.attachShadow({ mode: 'open' });
                shadow.innerHTML = `
                <span class="overflow">
                    <slot></slot>
                </span>
                `;
            }
            static get observedAttributes() {
                return ['background'];
            }
            attributeChangedCallback(name, oldValue, newValue) {
                this.shadowRoot.querySelector('.overflow').style.backgroundColor = newValue;
            }
        }
        
        customElements.define('jcode-text-overflow', TextOverflow);
js 复制代码
//card
        class Card extends HTMLElement {
            constructor() { 
                super();
                const shadow = this.attachShadow({ mode: 'open' });

                const title = this.getAttribute('title') ?? '';

                shadow.innerHTML = `
                    <div class="card">
                        <div class="card-title">
                                ${title}
                        </div>
                        <jcode-text-overflow style="width: 120px;"><jcode-text-overflow/>           
                    </div>
                    `;
            }
            static get observedAttributes() {
                return ['background'];
            }
            attributeChangedCallback(name, oldValue, newValue) {
                console.log(name, oldValue, newValue);
                if(name === 'background'){
                    // Cannot read properties of null (reading 'setAttribute')
                    this.shadowRoot.querySelector('.card jcode-card').setAttribute('background', newValue);
                }
            }
        }
    
            customElements.define('jcode-card', Card);
js 复制代码
//使用
<jcode-card title="卡片标题" content="卡片内容卡片内容卡片内容卡片内容卡片内容" background="skyblue">
 </jcode-card>

可以看到 this.shadowRoot.querySelector('.card jcode-card').setAttribute这个方法报错,这也就是上面提到的子组件还没mounted完成,从而也就无法设置attribute.

要解决这一问题,主要的是能够拿到子组件加载完成的回调或者事件就行。此时我们就会用到connectedCallback这个生命周期函数,它定义了自定义element被添加到页面的时候触发。当组件被添加到页面就可以表明它的dom元素已经存在于页面中,slot可能除外,此时我们触发一个mounted自定义事件,父组件监听它从就得到了子组件是否被加载。这也就可以写剩下的逻辑了。

jcode-card组件监听事件

js 复制代码
class Card extends HTMLElement {
            constructor() { 
                super();
                const shadow = this.attachShadow({ mode: 'open' });

                const title = this.getAttribute('title') ?? '';

                shadow.innerHTML = `
                    <style>
                        .card {
                        border: 1px solid #ddd;
                        border-radius: 8px;
                        padding: 16px;
                        box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
                        background: white;
                        width: 300px;
                  
                        }
                        .card-title {
                        font-weight: bold;
                        margin-bottom: 12px;
                        font-size: 16px;
                        }
                    </style>
                    <div class="card">
                        <div class="card-title">
                                ${title}
                        </div>
                        <jcode-text-overflow style="width: 120px;">
                            card的内容                            
                         <jcode-text-overflow/>           
                    </div>
                    `;
            }
            static get observedAttributes() {
                return ['background'];
            }
            // 写一个promise 
            getMounted(){
                if(this.mounted){
                    return Promise.resolve();
                }
                return new Promise((resolve, reject) => {
                    this.addEventListener('text-overflow-mounted', () => {
                        this.mounted = true;
                        resolve();
                    });
                });
            }
            
            attributeChangedCallback(name, oldValue, newValue) {
                if(name === 'background'){
                //等待mounted完成 再进行设置属性
                    this.getMounted().then(() => {
                        this.shadowRoot.querySelector('.card jcode-text-overflow').setAttribute('background', newValue);
                    });
                }
            }
        }

jcode-text-overflow组件触发事件

js 复制代码
  class TextOverflow extends HTMLElement {
            constructor() {
                super();
                const shadow = this.attachShadow({ mode: 'open' });

                shadow.innerHTML = `
                <style>
                    .overflow {
                    white-space: nowrap;
                    overflow: hidden;
                    text-overflow: ellipsis;
                    display: block;
                    }
                </style>
                <span class="overflow">
                    <slot></slot>
                </span>
                `;
            }
            static get observedAttributes() {
                return ['background'];
            }
            attributeChangedCallback(name, oldValue, newValue) {
                console.log(name, oldValue, newValue);
                this.shadowRoot.querySelector('.overflow').style.backgroundColor = newValue;
            }
            connectedCallback() {
                // 触发元素添加到dom的事件
                //{bubbles: true,composed: true} bubbles允许冒泡,composed允许穿透shadown
                this.dispatchEvent(new CustomEvent('text-overflow-mounted', {bubbles: true,composed: true}));
            }
        }

最后我们回顾一下:

其实也就是在子组件connectedCallback中触发事件父组件监听到返回Promise状态,拿到状态则可以异步处理每一次的setAttribute

如果遇到了slot插槽也可以这样处理,监听插槽的slotchange,判断slot.assignedElements()是否有dom节点,实现插槽的生命周期监控。

但是仍然需要注意组件的slot是组件触发了connectedCallback后才会去渲染插槽内容,如果slot传入的是自定义组件,那么自定义组件的生命周期与slot相互独立,在被插入到slot的过程中也会正常触发组件本身的生命周期。而slot的slotchange则是在自定义组件渲染完成后触发。

相关推荐
天天扭码5 分钟前
总所周知,JavaScript中有很多函数定义方式,如何“因地制宜”?(ˉ﹃ˉ)
前端·javascript·面试
一个专注写代码的程序媛10 分钟前
为什么vue的key值,不用index?
前端·javascript·vue.js
장숙혜21 分钟前
ElementUi的Dropdown下拉菜单的详细介绍及使用
前端·javascript·vue.js
火柴盒zhang24 分钟前
websheet之 编辑器
开发语言·前端·javascript·编辑器·spreadsheet·websheet
某公司摸鱼前端27 分钟前
uniapp 仿企微左边公司切换页
前端·uni-app·企业微信
WKK_30 分钟前
uniapp自定义封装tabbar
前端·javascript·小程序·uni-app
莫问alicia30 分钟前
react 常用钩子 hooks 总结
前端·javascript·react.js
Mintopia39 分钟前
图形学中的数学基础与 JavaScript 实践
前端·javascript·计算机图形学
Mintopia1 小时前
Three.js 制作飘摇的草:从基础到进阶的全流程教学
前端·javascript·three.js
BillKu1 小时前
Vue3父子组件数据双向同步实现方法
前端·javascript·vue.js