大家好 我是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
则是在自定义组件渲染完成后触发。