Modal 实现
然后又无意间刷到“Portal
”,才知道Modal
的实现还有如此妙的方式,顺而想着干脆把UI
组件库的实现原理看完。
1. Modal
弹窗的基本原理
我给弹窗类的定义是脱离固定的层级关系,不再受制于层叠上下文的组件。
常见的Modal
模态框、Dialog
对话框、Notification
通知框等都是最最常用的交互方式。
在我们页面有时需要一些特定的弹窗时,通过改UI
组件过于麻烦。
这时切图仔级别的会想:简单啊,创建一个<div/>
给绝对定位不就得了。
倘若只是当前路由页用,也还凑合。可一旦涉及到了组件复用以及抽象为声明式,就会有很大的隐患:
- 若无封装,组件代码需要处处粘贴。
- 即使封装了,都是在每个路由页下创建
<div/>
,易造成样式污染。 - 类购物车的弹窗,又该如何处理数据及渲染?
- 再进一步想,万一组件库会作为绩效考核,拿到每个环境都长得不一样,咋整?
1.1 Jquery
时代的弹窗实现
初初入行时,去各种资源站,找Jquery
的UI
组件,想必三四年经验的前端们都曾乐此不疲。
这个时代(也就三四年前)的弹窗,因为没有React
/Vue
根节点的概念,普遍都是:
- 直接操作真实 dom,使用熟知的dom 操作方法将指令所在的元素 append 到另外一个 dom 节点上去。 如:
document.body.appendChild
。 - 再通过
overflow: hidden
或display:none
(或调整z-index
)来隐藏。
这种操作真实dom
的代价,在大型项目中不停触发重绘/回流,是很糟糕的,且内部数据/样式不易更改。像以下这种情况就容易出现:
- 原本图片固定在区域内。
- 小弹窗展示后,溢出了。
随着
React / Vue
先进库的发展,也陆续有了多种方案选择。。。
1.2 React / Vue
早期实现。
其实React / Vue
早期的实现和Jquery
时代的并无二异:依赖于父节点数据,在当前组件内挂载弹窗。
Vue
的情况稍好,有自定义指令这条路走。
以下引自:《Vue中的Portal技术》
以vue-dom-portal
为例,代码非常简单无非就是将当前的 dom
移动到指定地方:
function (node = document.body) {
if (node === true) return document.body;
return node instanceof window.Node ? node : document.querySelector(node);
}
const homes = new Map();
const directive = {
inserted(el, { value }, vnode) {
const { parentNode } = el;
const home = document.createComment("");
let hasMovedOut = false;
if (value !== false) {
parentNode.replaceChild(home, el); // moving out, el is no longer in the document
getTarget(value).appendChild(el); // moving into new place
hasMovedOut = true;
}
if (!homes.has(el)) homes.set(el, { parentNode, home, hasMovedOut }); // remember where home is or should be
},
componentUpdated(el, { value }) {
// 对比子组件更新
const { parentNode, home, hasMovedOut } = homes.get(el); // recall where home is
if (!hasMovedOut && value) {
parentNode.replaceChild(home, el);
getTarget(value).appendChild(el);
homes.set(el, Object.assign({}, homes.get(el), { hasMovedOut: true }));
} else if (hasMovedOut && value === false) {
parentNode.replaceChild(el, home);
homes.set(el, Object.assign({}, homes.get(el), { hasMovedOut: false }));
} else if (value) {
getTarget(value).appendChild(el);
}
},
unbind(el, binding) {
homes.delete(el);
}
};
function plugin(Vue, { name = "dom-portal" } = {}) {
Vue.directive(name, directive);
}
plugin.version = "0.1.6";
export default plugin;
if (typeof window !== "undefined" && window.Vue) {
window.Vue.use(plugin);
}
可以看到在 inserted
的时候就拿到实例的 el(真实 dom),然后进行替换操作,在 componentUpdated
的时候再次根据指令的值去操作 dom。
为了能够在不同声明周期函数中使用缓存的一些数据,这里在 inserted
的时候就把当前节点的父节点和替换成的 dom
节点(一个注释节点),以及节点是否移出去的状态都记录在外部的一个 map
中,这样可以在其他的声明周期函数中使用,可以避免重复计算。
但是React / Vue