人们用大量的时间去优化网页滚动的流畅度,包括尽量缩短 JS 执行时间,发明 Passive event listener 等。 相同的页面效果,如果不使用 JS 就能实现,那一定是最流畅的。例如下面的优酷世界杯的页面,如果使用 JS 实现,可以监听滚动事件,在滚动事件的回调函数中,计算每个比赛的 element 的位置,判断是否在视口中,并控制相应的明星 element 出现或隐藏。
而如果有一个 CSS 伪类,当元素在视口的时候,就会匹配 CSS 规则,那么就可以使用它代替 JS 方法,很完美地实现这个效果,因此创造了一个 CSS 伪类 :in-viewport。
.popup {
width: 100%;
transform: translate(0,15vw);
transition:transform 200ms;
}
.container:in-viewport .popup {
transform: translate(0,0);
}
其实 :in-viewport 这个概念以前就已经有讨论过,不过是带参数的,而且最终也没有浏览器实现,讨论可见:
https://lists.w3.org/Archives/Public/www-style/2016Jun/0015.html
https://lists.w3.org/Archives/Public/www-style/2016Nov/0116.html 基于 Chromium,我们提出一种具体实现:
A、参照 hover,增加一个 CSS 伪类 :in-viewport。
主要就是增加一个枚举: kPseudoInViewPort,并将字符串「in-viewport 」与新增的枚举做对应。
B、增加对 :in-viewport 的匹配
在计算元素的 CSS 样式的时候,元素会一一匹配 CSS 规则是否匹配,所以增加对 :in-viewport 的匹配判断:
SelectorChecker::CheckPseudoClass(…){
…
case CSSSelector::kPseudoInViewPort:
if (mode_ == kResolvingStyle) {
element.SetObserveVisibility(true);
if (context.in_rightmost_compound) {
element_style_->SetAffectedByInViewPort();
} else {
element_style_->SetUnique();
element.SetChildrenOrSiblingsAffectedByInViewPort();
}
}
probe::forcePseudoState(&element, CSSSelector::kPseudoInViewPort,
&force_pseudo_state);
if (force_pseudo_state)
return true;
return element.IsInViewPort();
…
}
上面的代码中主要做了两件事:
1、 element.SetObserveVisibility:给元素添加一个 Observer,观察元素是否在视口中。
2、 return element.IsInViewPort():返回元素是否在视口中,这是由上述 Observer 设置的。
C、观察元素是否在视口中
这步也即 SetObserveVisibility 的实现:
void Element::SetObserveVisibility(bool observe){
if (observe == !!visibility_observer_)
return;
if(observe){
visibility_observer_ = new ElementVisibilityObserver(
this, WTF::Bind(&ContainerNode::SetInView,
WrapWeakPersistent(this)));
visibility_observer_->Start(1.0);
}
else{
visibility_observer_->Stop();
visibility_observer_.Clear();
}
}
ElementVisibilityObserver 是 Chromium 中用于方便观察元素是否可见的类,通过构造函数中注册的回调函数,回调元素是否可见。
在这里,注册的回调函数是 ContainerNode::SetInView :
void ContainerNode::SetInView(bool in_view) {
if (in_view == IsInViewPort())
return;
Node::SetInView(in_view);
const ComputedStyle* style = GetComputedStyle();
if (!style || style->AffectedByInViewPort()) {
StyleChangeType change_type = kLocalStyleChange;
if (style && style->HasPseudoStyle(kPseudoIdFirstLetter))
change_type = kSubtreeStyleChange;
SetNeedsStyleRecalc(
change_type,
StyleChangeReasonForTracing::CreateWithExtraData(
StyleChangeReason::kPseudoClass, StyleChangeExtraData::g_in_viewport));
}
if (IsElementNode() && ToElement(this)->ChildrenOrSiblingsAffectedByInViewPort())
ToElement(this)->PseudoStateChanged(CSSSelector::kPseudoInViewPort);
}
这段代码中,也是主要做了两件事:
1、Node::SetInView:给元素设置一个标志,element.IsInViewPort() 根据该标识返回元素是否在视口。
2、SetNeedsStyleRecalc:将元素设置为需要重新计算样式,使得 CSS 样式得以发生改变。
小结
:in-viewport 的实现不算复杂,从上面的例子又可以看到,:in-viewport 确实能比较优雅地实现视口相关的一些页面效果,所以还是期待 CSS 标准中可以增加伪类 :in-viewport 。
原文链接
https://zhuanlan.zhihu.com/p/42290141