Preact isomophic style 实现 CSS 同构

logo.svg

实现SSR不仅需要输出HTML字符串,还要包括关键渲染路径的CSS插入到HTML中,如果样式需要浏览器加载完 CSS 后才会加上,这个样式添加的过程就会造成页面的闪动,SSR也就没有意义了。

在服务端我们不能使用style-loader,因为它只能在浏览器环境将CSS插入到页面中,我们采用isomophic style load在服务器端输出 html 字符串的同时,也将页面用到的样式抠出来,然后插入到 html 字符串当中,将结果一同传送到客户端。

下面是webpack中用到了loader部分。

    var insertCss = require(${stringifyRequest(this, `!${insertCss}`)});
    var content = typeof css === 'string' ? [[module.id, css, '']] : css;
    
    exports = module.exports = css.locals || {};
    exports._insertCss = function() { return insertCss(css) };
    exports._toString = css.toString;
    exports._module_id = id

我们实现同构 css 函数 insertCss,这里只展示服务端部分,函数接收style参数,这是css-loader生成的style对象,包括

  const [module, css, media, sourceMap] = style[0]

  // Generate Id based on length of css , because css module give different id between browser and node
  const dev = process.env.NODE_ENV === 'development'
  const id = dev ? module : style.locals._module_id

  // Server Side
  if (typeof window === 'undefined' || typeof document === 'undefined') {
    return {id, css}
  }

最后我们在服务端实现一个insertCss函数收集用到的style,然后使用<StyleContext.Provider value={insertCss}>将插入该函数注入。

const isomorphicStyle = new Map();
const insertCss = (styles) => styles.forEach(style => {
    const {id,css} = style._insertCss();
    isomorphicStyle.set(id,css)
});

我们使用 withStyles 方法装饰组件,使用useContext拿到服务端中的insertCss函数,然后调用该函数将页面导入的style 对象传进去,该函数返回移除样式的方法removeCss ,我们在componentWillUnmount 是移除CSS。

constructor(props) {
      super(props)
      const insertCss = useContext(StyleContext)
      this.removeCss = insertCss(styles)
    }

    componentWillUnmount() {
      if (this.removeCss) setTimeout(this.removeCss, 0)
    }

项目地址

https://github.com/zhangchen915/preact-isomorphic-style-loader
安装 npm i preact-isomorphic-style-loader

iOS Safari 中 Fix 定位元素在滚动时闪烁的解决

如果你不得不在滚动时固定元素,则可能是iOS Safari(和其他移动设备)存在问题。元素通常会闪烁,然后消失,直到滚动完全停止为止。

只需通过添加transform: translate3d(0,0,0);来强制开启GPU加速。

如果要在固定元素中设置元素的样式,则需要将transform3d hack应用于嵌套元素,以使其不会闪烁/消失。

.Element-header {
  transform: translate3d(0,0,0);
}

.Element-header--fixed {
  top: 0;
  position: fixed;
}

原文地址: https://muffinman.io/ios-safari-scroll-position-fixed/

子元素 diff 算法的优化

5a58d2b2bbd4b-min.jpg

React 的 Diff

React先遍历新数组来找到插入和需要移动的节点,为了避免不必要的移动,react使用了一个哨兵值lastIndex,记录访问过的源数组下标最大值,如果当前元素的下标小于lastIndex,表示原相对位置无法满足需要了需要移动,否则更新lastIndex,这样来最大限度地利用原相对位置。
最后遍历一遍旧数组找到需要删除的元素。

但这种方式并不是总能得到最优结果。

例如:'1234' ➔ '4123'
React 并没有将4移到最前面,而是将234都移动一次。因为对新数组的遍历是从左向右的依次进行,所以这种位置相对关系总是局部的。

Diff 的改进

缩小问题规模

首先考虑一种情况,我们只对字符串进行了一次插入或删除操作,其实是没有必要进行diff,我们通过匹配两个字符串的通用前缀/后缀,可以直接找出差异,即便字符串进行了多次插入删除,查找公共前后缀也能在一定程度上减小diff的计算量,因为使用二分搜索的时间复杂度是O(nlogn)。

例如下面的两个字符串的公共前缀为abc,后缀为g,可以直接去掉。

A: -> [a b c d e f g] <-
B: -> [a b c h f e g] <-

寻找最小的移动次数

diff 算法首先需要一个旧数组的索引{1:0,2:1,3:2,4:3},通过该索引计算出新数组的对应位置。然后计算数组索引的最长上升子序列,通常使用动态规划实现,具体不做展开。

A: [a b c d]
B: [d a b c]
P: [3 0 1 2]
LIS: [0 1 2]

然后我们自右向左遍历新数组和 LIS 序列,查看元素的位置是否能与 LIS 序列的任何一个值匹配,若匹配则位置无需移动,然后取前一个LIS值继续,否则将其移动到当前遍历新数组元素的前面。
如上面的例子中,中有遍历到第一位时才不匹配,所以将B[1]=d移动到A[1]=a的前面即可。

为什么LIS能够做到这一点,因为它在整体上保留了数组升序排列的相对位置。

参考文章:
https://www.zhihu.com/question/65824137
https://github.com/NervJS/nerv/issues/3

限制 requestAnimation 渲染帧数

当机器性能不足以维持高帧数渲染时,一个较低的稳定的帧数相比不断在60帧上下波动能带来更好的体验。所以在一些动画交互较多的场景,有必要限制渲染帧数。

class AnimationFrame {
  constructor(fps = 60, animate) {
    this.requestID = 0;
    this.fps = fps;
    this.animate = animate;
  }

  start() {
    let then = performance.now();
    const interval = 1000 / this.fps;
    const tolerance = 0.1;

    const animateLoop = (now) => {
      this.requestID = requestAnimationFrame(animateLoop);
      const delta = now - then;

      if (delta >= interval - tolerance) {
        then = now - (delta % interval);
        this.animate(delta);
      }
    };
    this.requestID = requestAnimationFrame(animateLoop);
  }

  stop() {
    cancelAnimationFrame(this.requestID);
  }
}

requestAnimationFrame永远以60FPS运行,也就是每16.6ms执行一次,以我们要限制在10FPS(100ms)为例。递归7次时116.2ms>100ms,我们再执行下一次渲染。为了避免误差导致整体帧数低于设置,这里不能直接使用then = now,而是 then = now - (delta % interval)

See the Pen Limiting FPS (framerate) with Request Animation Frame by Rishabh (@rishabhp) on CodePen.

参考文章:
Controlling the Frame Rate with requestAnimationFrame
limitLoop.js

深入了解包含then属性的对象的promise解析

我们将包含then属性的对象称为thenable对象。

TC39规范

  • If Type(resolution) is not Object, then
    Return FulfillPromise(promise, resolution).
  • Let then be Get(resolution, "then").
  • If then is an abrupt completion, then
    Return RejectPromise(promise, then.[[Value]]).
  • Let thenAction be then.[[Value]].
  • If IsCallable(thenAction) is false, then
    Return FulfillPromise(promise, resolution).
  • Perform EnqueueJob("PromiseJobs", PromiseResolveThenableJob, « promise, resolution, thenAction »).

让我们一步一步解释:

resolve 一个非对象元素

Promise.resolve('Hello').then(
  value => console.log(`Resolution with: ${value}`)
);

// log: Resolution with: Hello

resolves 一个包含 abruptCompletion 的 then 对象

abruptCompletion 表示任何非正常的完成值。

const value = {};
Object.defineProperty(
  value,
  'then',
  { get() { throw new Error('no then!'); } }
);

Promise.resolve(value).catch(
  e => console.log(`Error: ${e}`)
);

// log: Error: no then!

resolves 包含 then 且其属性不为函数的对象

Promise.resolve(
  { then: 42 }
).then(
  value => console.log(`Resolution with: ${JSON.stringify(value)}`)
);

// log: Resolution with: {"then":42}

resolves 包含 then 且其属性为函数的对象

这部分是本文探讨的重点,也是递归promise链的基础。
当一个promise使用包含then方法的对象解析时,解析过程将then使用通常的promise参数调用resolve 和 reject。如果resolve没有被调用,promise将无法完成。

Promise.resolve(
  { then: (...args) => console.log(args) }
).then(value => console.log(`Resolution with: ${value}`));

// log: [fn, fn]
//        |   \--- reject
//     resolve

// !!! No log of a resolution value
Promise.resolve(
  { 
    then: (resolve) => { 
      console.log('Hello from then');
      resolve(42);
    }
  }
).then(value => console.log(`Resolution with: ${value}`));

// log: Hello from then
// log: Resolution with: 42

动态导入

当您使用动态import函数加载JavaScript模块时,import遵循相同的过程,因为它返回一个promise。导入模块的结构值将是一个包含所有导出值和方法的对象。

// file.mjs
export function then (resolve) {
  resolve('Not what you expect!');
}

export function getValue () {
  return 42;
}

// index.mjs
import('./file.mjs').then(
  resolvedModule => console.log(resolvedModule)
);

// log: Not what you expect

内存泄露的风险

参考co库的这个 issue https://github.com/tj/co/issues/180

原文地址:https://www.stefanjudis.com/today-i-learned/promise-resolution-with-objects-including-a-then-property/