前言
注:本文只适合未先子后父顺序加载组件的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接受值,变化字体的背景颜色。
//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);
//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);
//使用 <jcode-card title="卡片标题" content="卡片内容卡片内容卡片内容卡片内容卡片内容" background="skyblue"> </jcode-card>
可以看到 this.shadowRoot.querySelector('.card jcode-card').setAttribute
这个方法报错,这也就是上面提到的子组件还没mounted
完成,从而也就无法设置attribute
.
要解决这一问题,主要的是能够拿到子组件加载完成的回调或者事件就行。此时我们就会用到connectedCallback
这个生命周期函数,它定义了自定义element
被添加到页面的时候触发。当组件被添加到页面就可以表明它的dom元素
已经存在于页面中,slot
可能除外,此时我们触发一个mounted
自定义事件,父组件监听它从就得到了子组件是否被加载。这也就可以写剩下的逻辑了。
jcode-card组件监听事件
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组件触发事件
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
则是在自定义组件渲染完成后触发。