深入浅出 setState 原理篇

前言

想起自己(2021年) 8 月份面试时,被面试官们问了好几个 setState 的问题,现在想想,虽然回答上问题,但是了解的不深刻。我知道 setState 被设计成“异步”是为了性能,但是涉及到源码解读我就歇菜了;我知道如何让它同步,但是遇到真实的代码情况时,却不知道如何下手。说到底,当时是准备了面经把这些概念记下来,而没有真正理解它

在认识 setState 前,我们问几个常见问题

  • setState 是同步还是异步?
  • 如果是异步,怎么让它同步?
  • 为什么要这样设计?

基本概念和使用

React 的理念之一是 UI=f(data),修改 data 即驱动 UI 变化,那么怎么修改呢?React 提供了一个 API ——setState(类组件的修改方法)

官网介绍

setState() 将对组件 state 的更新排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。这是用于更新用户界面以响应事件处理器和处理服务器数据的主要方式

为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。React 并不会保证 state 的变更会立即生效

setState() 并不总是立即更新组件。它会批量推迟更新。这使得在调用 setState() 后立即读取 this.state 成为了隐患。为了消除隐患,请使用 componentDidUpdate 或者 setState 的回调函数(setState(updater, callback)),这两种方式都可以保证在应用更新后触发

除非 shouldComponentUpdate() 返回 false,否则 setState() 将始终执行重新渲染操作。如果可变对象被使用,且无法在 shouldComponentUpdate() 中实现条件渲染,那么仅在新旧状态不一致调用 setState()可以避免不必要的重新渲染

使用方法

setState(updater, [callback]);

参数一为带有形式参数的 updater 函数:

(state, props) => stateChange;

// 例如
// this.setState((state, props) => {
//   return {counter: state.counter + props.step};
// });

setState 的第一个参数除了接受函数外,还可以接受对象类型:

setState(stateChange[, callback])

// 例如:this.setState({count: 2})

setState 的第二个参数为可选的回调函数,它将在 setState 完成合并重新渲染组件后执行。通常,我们建议使用 componentDidUpdate 来代替此方法

setState(stateChange[, callback])

// 例如: this.setState({count: 2}, () => {console.log(this.state.count)})

与 setState 回调相比,使用 componentDidUpdate 有什么优势?

stackoverflow 有人问过,也有人回答过:

  • 一致的逻辑

  • 批量更新

  • 什么时候 setState 会比较好?

    • 当外部代码需要等待状态更新时,如 Promise

setState 的特性——批处理

如果在同一周期内对多个 setState 进行处理,例如,在同一周期内多次设置商品数据,相当于:

this.setState({count: state.count + 1});
this.setState({count: state.count + 1});
this.setState({count: state.count + 1});
// ===
Object.assign(
  count,
  {quantity: state.quantity + 1},
  {quantity: state.quantity + 1},
  ...
)

后调的 setState 将覆盖同一周期内先调用 setState 的值

  • setState(stateChange[, callback])
  • setState((state, props) => stateChange[, callback])

setState 必引发更新过程,但不一定会引发 render 被执行,因为 shouldCompomentUpdate 可以返回 false

批处理引发的问题

问题1:连续使用 setState,为什么不能实时改变

state.count = 0;
this.setState({ count: state.count + 1 });
this.setState({ count: state.count + 1 });
this.setState({ count: state.count + 1 });
// state.count === 1,不是 3

因为 this.setState 词 API 为会进行批处理,后调的 setState 会覆盖统一周期内先调用的 setState 的值,如下图所示:

state.count = 0;
this.setState({ count: state.count + 2 });
this.setState({ count: state.count + 3 });
this.setState({ count: state.count + 4 });
// state.count === 4

问题2:为什么要 setState,而不是直接 this.state.xx = oo?

因为 setState 做的事情不仅仅只是修改了 this.state 的值,另外最重要的是它会触发 React 的更新机制,会进行diff,然后将 patch 部分更新到真实 dom 里

如果你直接 this.state.xx = oo 的话,state 的值确实会改,但是它不会驱动 React 重渲染。setState 能帮助我们更新视图,引发 shouldComponentUpdate、render 等一系列函数的调用。至于批处理,React 会将 setState 的效果放入队列中,在事件结束之后产生一次重新渲染,为的就是把 Virtual DOM 和 DOM 树操作降到最小,用于提高性能

当调用 setState 后,React 的 生命周期函数 会依次顺序执行

  • static getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • getSnapshotBeforeUpdate
  • componentDidUpdate

问题3:那为什么会出现异步的情况呢?(为什么这么设计?)

因为性能优化。假如每次 setState 都要更新数据,更新过程就要走五个生命周期,走完一轮生命周期再拿 render 函数的结果去做 diff 对比和更新真实 DOM,会很耗时间。所以将每次调用都放一起做一次性处理,能降低对 DOM 的操作,提高应用性能

问题4:那如何在表现出异步的函数里可以准确拿到更新后的 state 呢?

通过第二个参数 setState(partialState, callback) 中的 callback 拿到更新后的结果

onHandleClick() {
    this.setState(
        {
            count: this.state.count + 1,
        },
        () => {
            console.log("点击之后的回调", this.state.count); // 最新值
        }
    );
}

或者可以直接给 state 传递函数来表现出同步的情况

this.setState((state) => {
  console.log("函数模式", state.count);
  return { count: state.count + 1 };
});

执行原理

首先先了解三种渲染模式

  • legacy 模式:ReactDOM.render(<App />, rootNode) 。这是当前 React app 使用的方式。当前没有计划删除本模式,但是这个模式可能不支持新功能
  • blocking 模式:ReactDOM.createBlockingRoot(rootNode).render(<App />) 。目前正在实验中,作为迁移到 concurrent 模式的第一个步骤
  • concurrent 模式 :ReactDOM.createRoot(rootNode).render(<App />)。目前再实验中,未来稳定之后,打算作为 React 的模式开发模式。这个模式开启了所有的新功能
    • 拥有不同的优先级,更新的过程可以被打断

在 legacy 模式下,在 React 的 setState 函数实现中,会根据一个变量 isBatchingUpdates 判断是直接更新 this.state 还是放到队列中回头再说,而 isBatchingUpdates 默认是 false,也就表示 setState 会同步更新 this.state,但是,有一个函数 batchedUpdates,这个函数会把 isBatchingUpdates 修改为 true,而当 React 在调用事件处理函数之前就会调用这个 batchedUpdates,造成的后果,就是由 React 控制的事件处理过程 setState 不会同步更新 this.state

像 addEventListener 绑定的原生事件、setTimeout/setInterval 会走同步,除此之外,也就是 React 控制的事件处理 setState 会异步

而 concurrent 模式都是异步,这也是未来 React 18 的默认模式

总结

首先,我们总结下关键知识点

  • setState 不会立即改变 React 组件中 state 的值
  • setState 通过引发一次组件的更新过程来引发重新绘制
  • 多次 setState 函数调用产生的效果会合并(批处理)

其次,回答一下文章开头的问题(第二第三问题在文中已经回答)

setState 是同步还是异步?

  • 代码同步,渲染看模式
    • legacy 模式,非原生事件、setTimeout/setInterval 的情况下为异步;addEventListener 绑定原生事件、setTimeout/setInterval 时会同步
    • concurrent 模式:异步

image-20220221160905787

参考资料

芝麻开门,显示全文!

非计算机专业的人如何转行程序员看后观感

最近在 bilibili 上看到宿琛的《非计算机专业的人如何转行程序员》的分享,有兴趣的可以看一下啊。他分享了自己的经历,很厉害,大学就知道自己不喜欢什么想要什么,想要什么后就去努力去读研究生,读完了之后发现编程能力比较弱,就去线上看公开课,因为英文可以,看英文视频能学进去,这样的经历不是一般人能复制的,他推荐了公开课资源平台 coursera 课程edx 课程,以及一些列觉得不错的课程,我

在评论区里又看到 CS自学指南

image-20220217151547811

天呐,要成为一个计算机高手,要学习这么多

学习有必要学这么多吗?即使你都学会了,

你说要取法乎上,但是这个上是要有英语基础,你没有良好的英语怎么听得懂,即使你磕磕绊绊听完了,你能理解多少,

成为工程师是把自己

我不反对学习,而且我也一直有在学习,

我认为的学习,先从功能点入手,慕课的实战课蛮好的,学到即赚到

芝麻开门,显示全文!

碎皮扯淡:好奇是第一原动力

我的一切知识源于好奇。

  • 好奇《挪威的森林》到底有多色情,先去看电影,再去看小说,发现一点都不色情,纯爱情文学小说
  • 好奇《金瓶梅》到底有多色情,先去看了杨思敏版的,再去看小说(新加坡南洋出版社出版无删减版)。电影是刺激,但小说中有关色情的部分少的可怜,还各种隐晦,远没有现代影视直接
  • 这些好奇带来的知识引发了我的思考。一切知识都需要自我感受。别人说电影好看,也许她是个电影小白,没看过几部电影,所以逢看称好。人们说《金瓶梅》淫秽,搁现在,这裸露程度比美剧还少了几分,怎么能说淫秽呢

在大学时期的我来说:好奇是驱动人进步的钥匙,色情是第一原动力

现阶段,虽然对色情已经没有太多研究,对理财感兴趣了,只要有资本,就可以通过资本获利,而且对于接下来十年中的我来言,钱无疑是最重要的努力动力之一

关于性方面的老师可推荐几位供君学习:

性文学老师:王小波、李银河、亨利·米勒、兰陵笑笑生、李渔、冯唐、爱伦坡

性电影老师:伊娃·格林电影、多看美女的电影、多看资本主义国家(美日韩)的电影

芝麻开门,显示全文!

笔记:npm换源更新

看到一篇文章,说是 npm.taobao.orgregistry.npm.taobao.org 域名将于 2022 年 05 月 31 日起停止服务。有必要更新一下 npm 的源,顺便复习一下 npm 的一些列命令

查看 npm 配置

npm config list // 查看基本配置
npm config list -l // 查看所有配置

image-20220211095840101

没找到,拿查看所有配置看看,一查发现 metrics-registry = "https://registry.npmjs.org/",那就说明源还是 npmjs 的源,看来公司电脑的 npm 用的还是 npm 官方的源,而全局还下载了 cnpm,因为是 npm 包,所以看版本,远程拉去最新的即可,先看看版本

cnpm -v

image-20220211101038446

查看 cnpm 的官网:中国 NPM 镜像 ,文中有这么一句:

当前 registry.npmmirror.com 是从 r.cnpmjs.org 进行全量同步的.

发现也没什么关系,所以我应该不会被影响

如何更新安装包

  1. 手动更新
    • 修改 package.json 中依赖包版本,执行 npm install --force
  2. 使用 yarn 代替 npm
    • yarn upgrade
  3. 使用第三方插件
    • npm install npm-check-updates -g
    • ncu // 查看可更新包
    • ncu -u // 更新 package.json
    • npm install // 升级最新版本

发现第三种最有用

参考资料

芝麻开门,显示全文!

面试题:渲染十万条数据解决方案

笔者在之前的面试中遇到过“一次性给你 10000 条数据,怎么让它不卡之类的问题“,当初准备不充分,不知道怎么回答这类问题,说的方案过于简单,还扯到防抖节流之类的性能优化点上,这篇文章原本2022年1月29日计划写,后因春节过年耽误,直至近日动笔

查阅资料后发现有三种解决方案:

  • 虚拟列表(也叫按需渲染或可视区域渲染)
  • 延迟渲染(即懒渲染)
  • 时间分片

虚拟列表是最主流的解决方案,不渲染所有的数据,只渲染可视区域中的数据。当用户滑(滚)动时,通过监听 scroll 来判断是上滑还是下拉,从而更新数据。同理 IntersectionObserver 和 getBoundingClientRect 都能实现

延迟渲染,也叫懒加载。顾名思义,最开始不渲染所有数据,只渲染可视区域中的数据(同虚拟列表一致)。当滚动到页面底部时,添加数据(concat),视图渲染新增DOM

时间分片主要是分批渲染DOM,使用 requestAnimationFrame 来让动画更加流畅

先说最主流的方案

虚拟列表

什么是虚拟列表

虚拟列表是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,是对长列表渲染的优化手段

说的明白一点,就是展示可视区域中的内容,当你向上向下滚动时,通过 DOM API 替换可视区域中的数据,做到动态加载十万条数据

两种解决思路

关于无限滚动,早期通过监听 scroll 事件,这是最常见的解决方案。可去 图片懒加载 中查看,简单来说,就是通过子项的 offsetTop(偏移高度)与 innerHeight(视窗高度)+ scrollTop(滚动高度)做对比来实现,当偏移高度 < 视窗高度+滚动高度时,说明已经滚到下方,就可展示图片

图片懒加载 中我们也提及 IntersectionObserver(交叉观察者)API,以此来解决 scroll 所不具备的效果,即 IntersectionObserver API 是异步的,不随目标元素的滚动同步触发,性能消耗小。当然还可以通过 getBoundingClientRect 来实现,getBoundingClientRect 方法返回元素的大小机器相对于视窗的位置

PS:所以目前来说有三种方法,在文末 demo 中会附上单独使用这三种解决方案的代码

在这里,因本人实力有限,未破解 getBoundingClientRect 向上滑动时的页面抖动问题,只有 scroll 和 IntersectionObserver 两种解决方案(getBoundingClientRect 方法也放在代码中,但向上滑动会抖动)

scroll 解决方案

先说 scroll 解决方案,简单来说,就是对其传来的数据进行分割展示,用到 slice 方法,它会返回一个新的数组

我们假设单个列表高度为 30px,一页展示的列表数量为 const count = Math.ceil(列表高度 / 30),展示的数据就是 visibleData = data.slice(start, start + count)(start 一开始为0)

当滚动时,动态修改 start 和 visibleData

import React, { useEffect, useState, useRef } from "react";

const VirtualList = (props) => {
  const { data } = props;

  const [start, setStart] = useState(0);
  const [visibleCount, setVisibleCount] = useState(null);
  const [visibleData, setVisibleData] = useState([]);
  const virtualRef = useRef(null);
  const virtualContentRef = useRef(null);

  useEffect(() => {
    const count = Math.ceil(virtualRef.current.clientHeight / 30);
    setVisibleCount(count);
    setVisibleData(data.slice(start, start + count));
  }, []);

  const onHandleScroll = () => {
    const scrollTop = virtualRef.current.scrollTop;
    const fixedScrollTop = scrollTop - (scrollTop % 30);
    virtualContentRef.current.style.webkitTransform = `translate3d(0, ${fixedScrollTop}px, 0)`;
    setStart(Math.floor(scrollTop / 30));
    setVisibleData(data.slice(start, start + visibleCount));
  };

  return (
    <div className="virtual-list" ref={virtualRef} onScroll={onHandleScroll}>
      <div
        className="virtual-list-phantom"
        style={{ height: data.length * 30 + "px" }}
      ></div>
      <div className="virtual-list-content" ref={virtualContentRef}>
        {visibleData.map((item) => (
          <div className="virtual-list-item" key={item.key}>
            {item.key}
            {item.value}
          </div>
        ))}
      </div>
    </div>
  );
};

export default VirtualList;

注:virtual-list-phantom 会让滚动条看起来很高,个人认为有无都不影响观感

(有virtual-list-phantom)效果如下:

虚拟列表scroll有virtual-list-phantom

(无virtual-list-phantom)效果如下:

虚拟列表scroll无virtual-list-phantom

这种方法的精髓在于设置开始渲染的点和展示的数据,当他滚动时动态修改,但是因为 scroll 会频繁触发,当渲染的数据变多后会有性能问题

IntersectionObserver 解决方案

通过 IntersectionObserver 的特性,当目标对象中的 entry.isIntersecting 为 true 或者 intersectionRatio > 0 (元素与祖先元素交叉、可见)时,说明本来不可见的元素浮现在视图中,表示它向上或向下滑动,我们动态设置视图中的顶部和底部 id 即可对其判断。当下滑时 entry.traget.id === 'bottom',我们修改 start 和 end;同理,当上滑时entry.traget.id === 'top 时,我们也一样修改 start 和 end

附上部分代码:

...
const [start, setStart] = useState(0);
const [end, setEnd] = useState(THRESHOLD);
const [observer, setObserver] = useState(null);
const bottomElement = useRef();
const topElement = useRef();
...
const Observer = new IntersectionObserver(callback, options);
const callback = (entries, observer) => {
    entries.forEach(entry => {
        const dataLength = data.length;
        if (entry.isIntersecting && entry.target.id === "bottom") {
            const maxStartIndex = dataLength - 1 - THRESHOLD;
            const maxEndIndex = dataLength - 1;
            const newStart = end - 5 <= maxStartIndex ? end - 5 : maxStartIndex;
            const newEnd = end + 10 <= maxEndIndex ? end + 10 : maxEndIndex;
            setStart(newStart);
            setEnd(newEnd);
        }
        if (entry.isIntersecting && entry.target.id === "top") {
            const newEnd =
                  end === THRESHOLD
            ? THRESHOLD
            : end - 10 > THRESHOLD
            ? end - 10
            : THRESHOLD;
            const newStart = start === 0 ? 0 : start - 10 > 0 ? start - 10 : 0;
            setStart(newStart);
            setEnd(newEnd);
        }
    });
};
const updatedList = data.slice(start, end);
return (
    <div style={{ position: "relative", textAlign: "center" }}>
      {updatedList.map((item, index) => {
        const top = height * (index + start) + "px";
        const refVal = getReference(index, index === lastIndex);
        const id = index === 0 ? "top" : index === lastIndex ? "bottom" : "";
        return (
          <div
            className="io-virtual-list-item"
            key={item.key}
            style={{ top }}
            ref={refVal}
            id={id}
          >
            {item.key}
          </div>
        );
      })}
    </div>
);
...

效果如下:

IntersectionObserver效果

推荐用这种方法,IntersectionObserver 是异步 API,性能消耗小,缺点是有些落后浏览器不支持,如公司需要兼容这类用户,需引入 polyfill

懒加载

不多介绍,一句话解释:最开始不渲染所有数据,只展示视图上可见的数据,当滚动到页面底部时,加载更多数据

实现原理:通过监听父级元素的 scroll 事件,当然也可以通过 IntersectionObserver 或 getBoundingClientRect 等 API 实现

但 scroll 事件会频繁触发,所以需要手写节流;滚动元素内有大量 DOM ,容易造成卡顿,建议使用 IntersectionObserver

因为之前在讲 图片懒加载 时说过思路,这里就不贴,文末会附上demo

时间分片

参考 如何高性能的渲染十万条数据(时间分片) 所举例子,对于大量数据渲染时,JS 运算并不是性能的瓶颈,性能的瓶颈主要在于渲染阶段。也就是说 JS 执行是很快的,页面卡顿是因为同时渲染大量 DOM 所引起的,可采用分批渲染的方式来解决

let ul = document.getElementById("container");
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
// 总页数
let page = total / once;
// 每条记录的索引
let index = 0;
// 循环加载数据
function loop(curTotal, curIndex) {
  if (curTotal <= 0) {
    return false;
  }
  // 每页多少条
  let pageCount = Math.min(curTotal, once);
  setTimeout(() => {
    for (let i = 0; i < pageCount; i++) {
      let li = document.createElement("li");
      li.innerText = curIndex + i + " : " + ~~(Math.random() * total);
      ul.appendChild(li);
    }
    loop(curTotal - pageCount, curIndex + pageCount);
  }, 0);
}
loop(total, index);

我的理解是,通过递归来渲染DOM,刚开始可以是20个,20个渲染完后再渲染剩下的,循环如此,将其全部渲染完。又因为浏览器的渲染机制是“宏任务—微任务—GUI渲染—宏任务…”。遂第一个 loop 执行后,先等页面渲染完,再执行下一轮的 setTimeout(宏任务)

使用 setTimeout 来做分片会有问题,就是当我们快递下拉时,会出现闪屏或白屏现象?

这是因为人眼识别帧数为24帧。当帧数为24帧时,连续的画面会形成动画,老一辈的动画片,例如《大闹天空》《哪吒闹海》之类都是一秒里有24个画面(24帧),平滑动画的最佳循环间隔就是 1000 / 24,约等于 41.67ms

而电脑显示器的刷新频率为 60 帧,大概相当于每秒重绘 60 次。同理,如果想骗过人眼,平滑动画的最佳循环时间就是 1000 / 60,约等于 16.7ms

而 setTimeout 的执行时间并不是确定的,虽然我们写了 setTimeout(() => {}, 0) ,但这是不准确的,按照 H5 标准规定 setTimeout 的第二个参数不能小于 4ms,不足会自动增加

所以当第一个宏任务完成,第一个微任务完成,第一次渲染页面后,4 毫秒后再执行第二个宏任务,这样就导致了实际执行时间慢了 4 毫秒,当一个周期(宏任务+微任务+GUI渲染+4ms)的总和时间大于16.7ms,就会出现掉帧现象,这也是为什么 React 要使用 Fiber 架构的原因

加上各类电子设备的刷新频率不同,也会导致一个周期的总时间大于16.7ms

requestAnimationFrame 正是解决这一问题的关键API,它告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。

因为他是浏览器所提供的原生 API,所以各类电子设备都能使用,根据不同的刷新频率,给于不同的动画执行时间,就不会引起丢帧现象

...
function loop(curTotal, curIndex) {
    if (curTotal <= 0) {
        return false;
    }
    // 每页多少条
    let pageCount = Math.min(curTotal, once);
    window.requestAnimationFrame(function () {
        for (let i = 0; i < pageCount; i++) {
            let li = document.createElement('li');
            li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
            ul.appendChild(li)
        }
        loop(curTotal - pageCount, curIndex + pageCount)
    })
}
loop(total, index);

总结

渲染十万条数据有三种解决方案,为虚拟列表、懒加载、时间分片。最优选是虚拟列表,DOM 树上只挂载有限的DOM;懒加载和时间分片的缺点在于插入大量的DOM,占内存运行时会造成卡顿

无论是虚拟列表还是懒加载,传统的做法是 scroll + 节流,这种做法的优势是老 API,兼容性刚刚的,缺点是,滑多了还是会引起性能问题,当然 IntersectionObserver 也是一样的,无非是换了个 API 做“元素是否出现在视图”判断,最好的方案是用 IntersectionObserver(交叉观察器),异步加载、性能消耗小

附上线上demo示例:

参考资料

芝麻开门,显示全文!

面试题:promise的链式怎么实现的

写文章为了让自己好记住知识点,也为了后续当作笔记来看待

前言

之前有过一次面试,面试官问 promise 相关的知识点,然后我回答了,他追问说 promise 的链式怎么实现?我当时没反应过来。面试官很有耐心,说 jquery 也是有链式的,你看过 jquery 的源码吗?它的链式怎么写的?我还是不知道,后来没通过面试,这个知识点常被我回忆,现在正好有时间,来写一写

正文

答案是:返回this

先来一个例子:

var person = {
  name: "johan",
  age: 28,
  sayName: function () {
    console.log("my name is" + this.name);
  },
  sayAge: function () {
    console.log("my age is " + this.age);
  },
};

命名一个对象,它有两个方法,sayName 和 sayAge ,如果我想这样表示 person.sayName().sayAge() 呢?怎么做,在方法 sayName 和 sayAge 中返回 this,即

var person = {
  name: "johan",
  age: 28,
  sayName: function () {
    console.log("my name is" + this.name);
    return this;
  },
  sayAge: function () {
    console.log("my age is " + this.age);
    return this;
  },
};

这就是表示,调用方法 sayName 、sayAge 后,返回调用者,即例子 person.sayName() ,person 调用 sayName,调用完后返回值还是 person。所以它可以继续链式调用 sayAge,因为它表示的还是 person

Promise 中的链式

Promise 本身没有链式,但是 Promise 的实例对象中的 then 有链式

function MyPromise(executor) {

}

MyPromise.prototype.then = function (onFulfilled, onRejected) {
    return new Promise((resolve, reject) => {
        ...
    })
}

当你使用 Promise 时,一般是这样使用:

let promise = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000);
});
promise.then(() => {
  console.log("1s后显示");
});

如果加上链式

promise
  .then(() => {
    console.log("1s后显示,第一个");
  })
  .then(() => {
    console.log("1s后显示,第二个");
  });

所以很明显,每调用一次 then,就是返回一个实例对象(return new Promise

Jquery 中的链式

源码太多内容,就拿 core.js 中的代码为例子

jQuery.fn = jQuery.prototype = {
  // The current version of jQuery being used
  jquery: version,

  constructor: jQuery,

  // The default length of a jQuery object is 0
  length: 0,

  toArray: function () {
    return slice.call(this);
  },

  // Get the Nth element in the matched element set OR
  // Get the whole matched element set as a clean array
  get: function (num) {
    // Return all the elements in a clean array
    if (num == null) {
      return slice.call(this);
    }

    // Return just the one element from the set
    return num < 0 ? this[num + this.length] : this[num];
  },

  // Take an array of elements and push it onto the stack
  // (returning the new matched element set)
  pushStack: function (elems) {
    // Build a new jQuery matched element set
    var ret = jQuery.merge(this.constructor(), elems);

    // Add the old object onto the stack (as a reference)
    ret.prevObject = this;

    // Return the newly-formed element set
    return ret;
  },

  // Execute a callback for every element in the matched set.
  each: function (callback) {
    return jQuery.each(this, callback);
  },

  map: function (callback) {
    return this.pushStack(
      jQuery.map(this, function (elem, i) {
        return callback.call(elem, i, elem);
      })
    );
  },

  slice: function () {
    return this.pushStack(slice.apply(this, arguments));
  },

  first: function () {
    return this.eq(0);
  },

  last: function () {
    return this.eq(-1);
  },

  even: function () {
    return this.pushStack(
      jQuery.grep(this, function (_elem, i) {
        return (i + 1) % 2;
      })
    );
  },

  odd: function () {
    return this.pushStack(
      jQuery.grep(this, function (_elem, i) {
        return i % 2;
      })
    );
  },

  eq: function (i) {
    var len = this.length,
      j = +i + (i < 0 ? len : 0);
    return this.pushStack(j >= 0 && j < len ? [this[j]] : []);
  },

  end: function () {
    return this.prevObject || this.constructor();
  },
};

我们不用看全部,但看其中的方法,是不是和最开始的例子很像——return this

所以链式不可怕,可怕的是,动都不动就自我劝退

芝麻开门,显示全文!

UU笔记

这位博主叫 UU,以前微博名叫:拉瓦铀,后来因为微博误封,他索性换了个号,现在的号叫 笛卡吾 ,他有个自己的网站叫锡安,我在春节放假期间,付费读了他的文章,因为他写的东西太长,有些内容又晦涩,我只记录下一些个人觉得有用的话

  • 地理决定生物,生物影响历史,历史衍生文化,文化催生科学,科学发现物理,物理解释地理
  • 绝大多数人根本想不明白,不理财 == 被剥削
  • 虚拟币不仅是黄金还同时是古董
  • 做人不要自我设限
  • 在知识的海洋里,不要做一个农民,要做一个游牧者
  • 人都有惰性,都喜欢能「一招鲜,吃遍天」,都盼着一劳永逸地在社会里找到一个舒服的位置躺着生活,但这注定不可能实现
  • 稳定,就是中国政治的金线
  • 不要让生活品质下降,否则人的心态会受到很大的影响
  • 很多没有历史常识的人,羡慕民国,甚至羡慕魏晋,就是这个原因——下面的人不算人,看不见;上面的人很稳定,所以有足够的钱和闲花样作,又是大师,又是七贤
  • 300 多万同志在扶贫一线,1800 名英雄牺牲。6 亿中国人,平均月收入不到 1000 元。时刻提醒自己这两组数据,就不会脱离群众,就不会站在人民的对立面
  • 人一定要趁年轻多运动,做一些负重,不要只顾着心肺。人老了,肌肉会持续萎缩,力量越来越小,再想增肌基本上不可能了,就是维持着。肌肉量越小,越容易疲惫,精神越差,越不可能增肌,恶性循环。很多老人,一早就开始慢性疼痛,等死,就是因为掉进了这个循环里。有什么办法吗?其实没有……医学再发达,只能有病治病,没病止痛,不可能凭空生肌。到了那个时候,就晚了。这是真正意义上的「等你老了就知道了」。因为雌性的雄激素水平远低于雄性,而肌肉量主要与雄激素有关,所以,年轻女性更要注意提前装备一些骨骼肌,等老了维持体能。女性的激素水平波动大,如果涉及生育哺乳的话,波动就更大,更年期又是一道坎。千万不要中年发福,变成梨形,不然过了 40 再想瘦下来,难上加难。而且人越老,嘴越馋,意志力下降的同时,生活兴趣收缩。会活得越来越像动物,这是大规律,无法逆转。假如只剩下吃喝了,那养老生活就跟堆肥是一样的。趁年轻,多运动。懂的人懂。

芝麻开门,显示全文!

圣杯布局和双飞翼布局

前言

虽然这类面试题已经很久没看到了,但作为2022年春节假期的第一天,轻松为主,拿来试试刀

作用

首先要解释一下:无论是圣杯布局还是双飞翼布局,都是为了实现一个效果:左右固定宽度,中间部分自适应。两者的区别在于,圣杯布局给中间的 div 设置 padding-left 和 padding-right;而双飞翼布局则在中间的 div 内部创建子 div 放置内容,并在该 div 里用 margin-left 和 margin-right 留出左右宽度

圣杯布局

HTML 结构如此:

<body>
  <header>组成头部</header>
  <section class="container">
    <div class="middle">中间部分自适应</div>
    <div class="left">左边栏固定宽度</div>
    <div class="right">右边栏不顾宽度</div>
  </section>
  <footer>组成尾部</footer>
</body>

CSS 样式如此:

body {
  min-width: 700px;
}

header,
footer {
  background: grey;
  border: 1px solid #333;
  text-align: center;
}

.left,
.middle,
.right {
  position: relative;
  float: left;
  min-height: 130px;
}

.container {
  padding: 0 220px 0 200px;
  overflow: hidden;
}

.middle {
  width: 100%;
  background: red;
}

.left {
  margin-left: -100%;
  left: -200px;
  width: 200px;
  background: green;
}

.right {
  margin-left: -220px;
  right: -220px;
  width: 220px;
  background: blue;
}

footer {
  clear: both;
}

效果如此:

圣杯布局

代码说明:

  1. 首先在容器 container 中给予 padding:0 220px 0 200px ,这一步是为了给两边固定宽预留位置
  2. 中间(middle)元素设置 width: 100%,它自然就自适应了
  3. 设置左边(left)元素 position: relative, left: -200px。这样做是让它做到左边的固定位置,做到此时,效果如下
    • 圣杯布局
    • 接着用margin-left: -100%,让其回到与中间部分一样高的位置,这就是圣杯布局的关键
    • margin-left:-100% 表示向左移动整个屏幕的距离
    • 因为三个元素都加了浮动,所以配合 margin-left: -100% 左边容器就能与中间部分同高
  4. 同理,设置右边(right)元素position: relative, right:-220px, margin-left: -220px
    • 注意,这里的 margin-left 是 220px。为什么这个是220px,和它自身宽度一致
    • 注意 ,自身 margin-left 为负时,就往左移动,等它等于自身高度时就“升格”到上一层,当它等于 -100% 时,这个 100% 表示的是中间部分的宽度,所以就固定在左边了

双飞翼布局

有人说“双飞翼布局是玉伯大大提出来的,始于淘宝UED”,其效果和圣杯布局一样,只是它把三栏布局比作一只鸟,中间内容部分分为三部分,左翅膀、中间、右翅膀。其技术关键在于它还有一层 div

HTML 结构如此:

<body>
  <header>组成头部</header>
  <section class="container">
    <div class="main">
      <div class="middle">中间部分自适</div>
      <div class="left">左边栏固定宽度</div>
      <div class="right">右边栏固定宽度</div>
    </div>
  </section>
  <footer>组成尾部</footer>
</body>

CSS 结构如此:

body {
  min-width: 700px;
}

header,
footer {
  background: grey;
  border: 1px solid #333;
  text-align: center;
}

.left,
.right,
.main {
  float: left;
  min-height: 130px;
}

.main {
  width: 100%;
  background: red;
}

.main-inner {
  margin: 0 220px 0 200px;
  min-height: 130px;
}

.left {
  margin-left: -100%;
  width: 200px;
  background: green;
}

.right {
  margin-left: -220px;
  width: 220px;
  background: blue;
}

footer {
  clear: both;
}

效果如圣杯布局一致,不重复展示,它代码的关键在于先构建中间部分展示出,再通过 margin-left 完成浮动流

思考:为什么会考三栏布局?

以前的布局难点就是三栏布局,而且三栏布局还能引出 BFC,BFC 的作用之一就是自适应布局。而现在 flex: 1就能解决自适应布局的问题,所以这类考题已经不多见了

总结

以前一直担心考这类破问题,因为完全没准备过。除了一次考左边固定宽,右边自适应外,就没考过 CSS 布局方面的问题,大概是因为已经过时了

三栏布局两种解决方法

  • 圣杯布局
    • 浮动 + margin-left + 自身相对定位
  • 双飞翼布局
    • 浮动 + margin-left + 中间部分再包裹一层

相同点:浮动 、margin-left

margin-left: -100% :左边上升

margin-left: -220px:右边上升

线上demo:

参考资料

芝麻开门,显示全文!

面试题:箭头函数和普通函数的区别

一个东西你知道知道的多,就能写出的多

去年面试的时候,五位面试官有三位问到了这个问题,可见这是一个面试常题,我都忘记自己是怎么回答的,要我现在说:箭头函数没有 this 绑定,它的 this 指向父作用域

果然,记忆记不牢是有原因的,因为没有写文章,没有理解真正理解它

真正的答案是什么?

阮一峰版

  • 箭头函数没有自己的 this 对象,函数体内的 this 是定义时所在的对象而不是使用时所在的对象
  • 不可以当作构造函数,也就是说,不可以对箭头函数使用 new 命令,否则会抛出一个错误
  • 不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替
  • 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数
  • 返回对象时必须在对象外面加上括号

尼古拉斯版

  • 没有 this、super、arguments 和 new.target 绑定。this、super、arguments 和 new.target 的值由最近的不包含箭头函数的作用域决定
  • 不能被 new 调用,箭头函数内部没有 [[Construct]] 方法,因此不能当作构造函数使用,使用 new 调用箭头函数会抛出错误
  • 没有 prototype,既然你不能使用 new 调用箭头函数,那么 prototype 就没有存在的理由。箭头函数没有 prototype 属性
  • 不能更改 this, this 的值在函数内部不能被修改。在函数的整个生命周期内 this 的值是永恒不变的
  • 没有 arguments 对象,既然箭头函数没有 arguments 绑定,你必须依赖于命名或者剩余参数来访问该函数的参数
  • 不允许重复的命名参数

尼古拉斯是写《深入理解 ES6》的作者,阮一峰就不解释了

结合起来,就是说箭头函数和普通函数的区别在于:

  • 它不能被当作构造函数,因为它不能被new,不能被 new 的原因在于箭头函数内部没有 [[Construct]] 方法。又因为它不能被 new,所以也就没有 prototype
  • 它没有自己的 this,它的 this 由定义时所在的对象决定而不是使用时所在的对象
  • 它也没有 arguments 对象
  • 不可以使用 yield 命令,不能用作生成器函数

我们依次说说这四点

new 从何来

先复习一下 new 调用构造函数会执行什么

  1. 在内存中创建一个新对象
  2. 这个新对象内部的 [[prototype]] 特性被赋值为构造函数的 prototype 属性
  3. 构造函数内部的 this 被赋值为这个新对象(this指向新对象)
  4. 执行构造函数内部的代码(给新对象添加属性)
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象

我们可以手写一个 new

function new2(Constructor, ...args) {
  var obj = Object.create(null);
  obj.__proto__ = Constructor.prototype;
  var result = Constructor.apply(obj, ...args);
  return typeof result === "object" ? result : obj;
}

复习完 new,回过头看为什么不能调用 new

JavaScript 函数内部有两个内部方法:[[Call]] 和 [[Construct]]

  • 直接调用时执行[[Call]] 方法,直接执行函数体
  • new 调用时执行 [[Construct]] 方法,创建实例对象

箭头函数设计之初是为了设计一种更简短的函数,没有 [[Construct]] 方法。具体99.9%的人都不知道的箭头函数不能当做构造函数的秘密 摘出了很多英文材料佐证这个事实

我们可以这样说,因为它没有[[Construct]] 内部方法,所以它不能被 new。而因为它不能被 new,所以它也没有 prototype

prototype 的理解可以看这篇: 原型

this 谁人调用你

JavaScript 中的 this 是词法作用域,与你在哪里定义无关,而与你在哪里调用有关,所以会有各种 this “妖”的问题,改变 this 有 4 种方法

  • 作为对象方法调用
  • 作为函数调用
  • 作为构造函数调用
  • 使用 apply 或 call 调用

但是箭头函数没有自己的 this 对象,内部的 this 就是定义时上层作用域中的this。也就是说,箭头函数内部的 this 指向是固定的

arguments 老一辈的类数组

arguments 是一个对应于传递给函数的参数的类数组对象。arguments 对象标识所有(非箭头)函数可用的局部变量,可以说只要是(非箭头)函数就自带 arguments,它表示所有传递给函数的参数

什么是类数组对象

所谓类数组对象,就是指可以通过索引属性访问元素并且拥有 length 属性的对象

var arrLike = {
	0: 'name',
	1: 'age',
	length: 2
}

箭头函数没有

yield 是什么

说 yield 之前,先了解下生成器

生成器是 ES6 新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力。

生成器的形式是一个函数,函数名称前面加一个星号(*)表示它是一个生成器。只要是可以定义(非箭头)函数的地方,就可以定义生成器

// 生成器函数声明
function* generatorFn() {}

// 生成器函数表达式
let generatorFn = function* () {};

// 作为对象字面量方法的生成器函数
let foo = {
  *generatorFn() {},
};

// 作为类实例方法的生成器函数
class Foo {
  *generatorFn() {}
}

// 作为类静态方法的生成器函数
class Bar {
  static *generatorFn() {}
}

标识生成器函数的星号不受两侧空格的影响

而 yield 关键字是可以让生成器停止和开始执行,也是生成器最有用的地方。生成器函数在遇到 yield 关键字之前会正常执行。遇到这个关键字后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用 next() 方法来恢复执行

// umi 项目中请求接口时的例子
*fetchData({ payload }, { call, put }) {
    const resData = yield call(fetchApi, payload);
    if (resData.code === 'OK') {
        yield put({
            type: 'save',
            payload: {
                data: resData,
            },
        });
    } else {
        Toast.show(resData.resultMsg);
    }
},

因为箭头函数不能用来定义生成器函数才不能使用 yield 关键字

模拟面试

面试官:对 ES6 了解吗

面试者:嗯呢,项目中一直有用

面试官:你说说你平时都用哪些 ES6 的新特性

面试者:例如箭头函数、let、const、模板字符串、扩展运算符、Promise…

面试官:嗯嗯,箭头函数和普通函数有什么区别

面试者:箭头函数不能被 new、没有 arguments、它的 this 在那里定义相关、它不能用 yield 命令,返回对象时必须在对象外面加上括号

面试官:箭头函数为什么不能被 new

面试者:因为箭头函数没有 [[Construct]] 方法,在 new 时,JavaScript 内部会调用 [[Construct]] 方法,因为箭头函数没有,所以 new 时会报错。当然,因为不能被 new ,所以箭头函数也没有 prototype

面试官:你刚刚说到没有 arguments,简单介绍下它

面试者:它是所有参数的合集,每个(非箭头)函数自带 arguments,其结构是类数组对象

面试官:什么是类数组对象

面试者:可以通过索引访问元素且拥有 length 属性的对象…

面试官:我问问其他的

参考资料

芝麻开门,显示全文!

图片懒加载的三种解决方法

写作提高思考

前言

我想写一个系列,关于图片懒加载、React 渲染十万条数据、无限下拉方案文章,因为其三者有共性,都有使用了 与 DOM 相关的 offsetTop、innerHeight、getBoundingClientRect、IntersectionObserver 等,这些知识点如果单独放在一篇文章中,其价值点就是1,如果相互连接,价值点就是3。用好梅特卡夫定律,能让自己的效率提升不少

正文

学习前端,三板斧,HTML、CSS、JavaScript。JavaScript 基础由 DOM、BOM、ECMAScript 组成,其中 ECMAScript 为规范语言,现在说的 ES6(ES2016~ES2022) 指的就是它,隔段时间就会发布,目前一年发布一次,以年份来说,现在是 ECMAScript 2022。BOM是什么,BOM是浏览器对象模型(Browser Object Model)。它有六大对象

  • document: DOM(对 BOM 包含了DOM,但是 DOM 重要,其地位和 BOM 一样)
  • event:事件对象
  • history:浏览器的历史记录
  • location:窗口的 url 地址栏信息
  • screen:显示设备的信息
  • navigator:浏览器的配置信息

DOM我们也很了解,文本对象模型,指操作HTML(超级文本标识语言)的API。DOM 会将文档解析为一个由节点和对象(包含属性和方法的对象)组件的结构集合

以前开发页面时,我们在 script 标签中,先获取节点(DOM Api),再操作 DOM ,所以以前是 《JavaScript 面向对象编程》,《JavaScript dom编程艺术》,但操作 DOM 的 API 太长,不易书写,JQuery 集大成,简化Api,统一了操作写法。如果展开,会有很多可以延伸,而我铺垫了这么多,就是想引出 document

这次我们要讲的 offset、scroll、client 就是出自“ document 家”。先配两张图来看看这三个到底是什么

document

document2

client

client 指元素本身的可视内容。不包括 overflow 被折叠部分,不包括滚动条、border,包括 padding

有四属性:

  • clientHeight:对象可见的高度
  • clientWidth:对象可见宽度
  • clientTop:元素距离顶部的厚度,一般为0,因为滚动条不会出现在顶部
  • clientLeft:元素距离左侧的厚度,一般为0,因为滚动条不会出现在左侧

offset

offset 指偏移。包括这个元素在文档中占用的所有显示宽度,包括滚动条、padding、border,不包括 overflow 隐藏的部分

有五属性:

offsetHeight:该对象自身的绝对高度

  • offsetHeight: = border-width * 2 + padding-top + height + padding-bottom

offsetWidth:该对象自身的绝对宽度

  • offsetWidth = border-width * 2 + padding-left + width + padding-right

offsetParent:返回一个对象的引用,字面意思,相对父元素的偏移

  • 如果当前元素的父元素没有 CSS 定位(position为absolute/relative),offsetParent 为 body
  • 如果当前元素的父元素有 CSS 定位(position为absolute/relative),offsetParent 取父级中最近的元素

offsetTop:相对版面或 offsetParent 属性指定父坐标的顶部距离

  • offsetTop = offsetParent 的 padding-top + 中间元素的 offsetHeight + 当前元素的 margin-top

offsetLeft:相对版面或 offsetParent 属性指定父坐标的左部距离

  • offsetLeft = offsetParent 的 padding-left + 中间元素的 offsetWidth + 当前元素的 margin-left

Scroll

Scroll 指滚动。包括这个元素没有显示出来的实际宽度,包括 padding,不包括滚动条、border

scrollHeight:获取对象的滚动高度,对象的实际高度

scrollWidth:获取对象的滚动宽度

scrollTop:当前元素与窗口最顶端的距离

scrollLeft:当前元素与窗口最左端的距离

其他

innerHeight 和 clientHeight 有什么区别

准确来说,clientHeight 是针对 body,innerHeight 是 window 的

document.body.clientHeight:网页可见区域高

window.innerHeight:可视窗口高度,不包括浏览器顶部工具栏

监听图片高度实现懒加载

通过图片的 offsetTop(偏移高度)和 window 的 innerHeight、scrollTop 判断图片是否位于可视区域

即很多图片,先显示视窗中的图片,没看见的先不展示,加快页面加载速度。当你向下滚,当后续图片的 offsetTop(偏移高度) 小于 innerHeight(视窗高度) + scrollTop(滚动高度) 时,意味着此图片已经出现在视窗中,将真正图片替换loading

关键代码在于

function lazyload() {
  let seeHeight = window.innerHeight;
  let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
  for (let i = n; i < img.length; i++) {
    if (img[i].offsetTop < seeHeight + scrollTop) {
      // 对比图片的偏移高度和屏幕高度+滚动高度
      if (img[i].getAttribute("src") === "loading.gif") {
        img[i].src = img[i].getAttribute("data-src");
      }
      n = i + 1;
    }
  }
}

效果如下:

监听图片高度实现懒加载

Element.getBoundingClientRect

Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置

getBoundingClientRect 返回值是一个 DOMRect 对象,这个对象是由该元素的 getClientRects() 方法返回的一组矩形的集合,即与该元素相关的 CSS 边框集合

语法

domRect = element.getBoundingClientRect();

返回坐标、宽高、在视口中的位置

  • x
  • y
  • width
  • height
  • top
  • right
  • bottom
  • left

rect

如果是标准盒子模型,元素的尺寸等于 width/height + padding + border-width 的总和。如果 box-sizing:border-box ,元素的尺寸等于 width/height

我们用这个 API 来获取每张图片的 top 值,如果 top 值小于可视区的高度就视为已经进入可视区,直接加载图片即可

function lazyload() {
  let seeHeight = document.documentElement.clientHeight;
  for (let i = n; i < img.length; i++) {
    if (img[i].getBoundingClientRect().top < seeHeight) {
      if (img[i].getAttribute("src") === "loading.gif") {
        img[i].src = img[i].getAttribute("data-src");
      }
      n = i + 1;
    }
  }
}

效果如下:

getBoundingClientRect实现懒加载

通过 IntersectionObserver 实现懒加载

IntersectionObserver 接口(从属于 Intersection Observer API)提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)

IntersectionObserver 可以不用监听 scroll 事件,做到元素一可见便调用回调,在回调里面我们来判断元素是否可见

if (IntersectionObserver) {
  let lazyImageObserver = new IntersectionObserver((entries, observer) => {
    entries.forEach((entry, index) => {
      let lazyImage = entry.target;
      // 如果元素可见
      if (entry.intersectionRatio > 0) {
        if (lazyImage.getAttribute("src") === "loading.gif") {
          lazyImage.src = lazyImage.getAttribute("data-src");
        }
        lazyImageObserver.unobserve(lazyImage);
      }
    });
  });
  for (let i = 0; i < img.length; i++) {
    lazyImageObserver.observe(img[i]);
  }
}

上述代码表示,遍历所有的图片,对其进行观察原生是否可见,如果元素可见,就把真正图片替换loading

IntersectionObserver 可以自动“观察”元素是否可见,其本质是目标元素与视窗产生一个交叉去,所以这个 API 叫做“交叉观察器”

使用方式

let io = new IntersectionObserver(callback, option);

上面代码中, IntersectionObserver 是浏览器原生提供的构造函数,接受两个参数:callback 是可见性变化时的回调函数,option 是配置对象(该参数可选)

构造函数的返回值是一个观察器实例。实例的 observe 方法可以指定观察哪个 DOM 节点

// 开始观察
io.observe(document.getElementById("example"));

// 停止观察
io.unobserve(element);

// 关闭观察器
io.disconnect();

callback 参数

目标元素的可见性变化时,就会调用观察器的回调函数 callback

callback 一般会触发两次。一次时目标元素刚刚进入视窗(开始可见),另一次时完全离开视窗(开始不可见)

let io = new IntersectionObserver((entries) => {
  console.log(entries);
});

上面代码中,回调函数采用的是箭头函数的写法。callback 函数的参数(entries)是一个数组,每个成员都是一个 IntersectionObserverEntry 对象。举例来说,如果同时有两个被观察的对象的可见性发生变化,entries数组就会有两个成员。

IntersectionObserverEntry 对象

IntersectionObserverEntry 对象提供目标元素的信息,一共有六个属性

  • time: 可见性发生变化的时间,单位毫秒
  • target:被观察的目标,是个 DOM 节点对象
  • rootBounds:根元素的矩形区域的信息,getBoundingClientRect() 方法的返回值,如果没有根元素(即直接相对于视窗滚动),则返回null
  • boundingClientRect:目标元素的矩形区域的信息
  • intersectionRect:目标元素与视窗(或根元素)的交叉区域的信息
  • intersectionRatio:目标元素的可见比例,即 intersectionRect 占 boundingClientRect 的比例,完全可见时为1,完全不可见时小于等于 0

如图所示:

IntersectionObserverEntry参数

兼容性如何

caniuse 兼容性报告目前支持率是 93.67%,但是iOS的支持度要在 iOS12.2 以上,如果是iPhoneX(2018.11)之后的手机都是支持的,如果是之前的,升级系统才支持,考虑到一些人是不会升级,所以这个兼容性还不支持大众化的场景,但它的能力和性能都非常的好

image-20220125104012150

总结

面试的时候被问到懒加载,我那个时候没做过相关的准备,我说不知道,途虎的面试官会引导,其实引导才能测试出一个人真正的水平,但是那个时候我竟然连 scroll 都想不起来。现在回想起来,实在是准备的方向搞错了。

说到图片懒加载,有两种方法:

  • 监听图片高度
    • 技术要点:监听scroll,滚动的时候遍历所有的图片,如果图片的偏移高度小于屏幕高度+滑动高度,说明已经出现在视窗,就替换图片
    • 优点:兼容性好
    • 缺点:单纯使用 scroll 滑动来监听高度,会引发性能问题,所以要搭配节流
  • Element.getBoundingClientRect
    • 技术要点:与监听图片无太大区别,无非视把图片的偏移高度改成 getBoundingClientRect().top,对比每张图片的自身高度是否出现在视窗(视口)中,有就替换图片
    • 优点:兼容性好,代码相对监听图片高度少了一些
    • 缺点:也是使用 scroll 滑动来监听,会引发性能问题
  • 使用 IntersectionObserver Api
    • 技术要点:通过 IntersectionObserver Api 来实现,图片元素一可见就调用回调,在回调中判断元素是否可见
    • 优点:写起来方便,性能好
    • 缺点:兼容性适配iOS12.2以上,安卓5以上

附上线上 demo:

参考资料

芝麻开门,显示全文!