妙妙屋方法实践

428 阅读5分钟

ResizeObserver

介绍

ResizeObserver 是一个 Web API,用于监听元素的大小变化。它可以观察一个或多个元素的尺寸改变,并在尺寸变化时触发回调函数。使用 ResizeObserver,您可以监测元素的宽度、高度或者它们的任意变化,而不需要轮询或使用其他复杂的解决方案。

案例

比如在开发h5的时候需要对屏幕是横屏还是竖屏进行监听

const target = document.querySelector('body');
const observer = new ResizeObserver(entries => {
  for (let entry of entries) {
    const { width, height } = entry.contentRect;

    if (width < height) {
      // 屏幕从横屏变为竖屏
      // 执行逻辑处理
    } else {
      // 屏幕从竖屏变为横屏
      // 执行逻辑处理
    }
  }
});
// 启动观察
observer.observe(target);
// 停止观察 
observer.unobserve(target);

getBoundingClientRect

介绍

getBoundingClientRect 是一个 DOM API,用于获取元素的大小和位置信息。它返回一个 DOMRect 对象,包含了元素的位置、宽度、高度和其他相关属性。

示例

比如图片懒加载的原理

 window.addEventListener('scroll', () => {
      const lazyImages = document.querySelectorAll('.lazy-image');

      for (let i = 0; i < lazyImages.length; i++) {
        const image = lazyImages[i];

        if (isInViewport(image)) {
          image.src = image.dataset.src;
          image.classList.remove('lazy-image');
        }
      }
    });

    function isInViewport(element) {
      const rect = element.getBoundingClientRect();

      return (
        rect.top < (window.innerHeight || documentElement.clientHeight)
      );
    }

IntersectionObserver

介绍

IntersectionObserver是一个JavaScript API,用于异步检测目标元素与其祖先或视窗之间的交叉状态。它提供了一种有效的方法来监听元素是否进入或离开视窗,或者与其祖先元素相交的程度。这对于实现懒加载、无限滚动、元素可见性等功能非常有用。

示例

实现一个简单的传送门的效果,类似于vue3的Teleport,在我们看b站视频的时候,下拉看评论的时候,会看到右下角会出现一个小的弹窗同步我们看视频的效果。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Video Portal</title>
  <style>
    #video {
      width: 250px;
      height: auto;
    }

    #portal {
      display: none;
      position: fixed;
      bottom: 20px;
      right: 20px;
      width: 250px;
      height: auto;
      background-color: rgba(0, 0, 0, 0.7);
      color: #fff;
      padding: 20px;
      text-align: center;
      cursor: pointer;
    }
  </style>
</head>

<body>
  <h1>Video Portal</h1>

  <div id="v-box">
    <video id="video" src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm" controls></video>
  </div>
  <div style="height: 1000px;width: 100%;"></div>
  <div id="portal"></div>

  <script>
    // 创建 IntersectionObserver 实例
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          hidePortal();
        } else {
          showPortal();
        }
      });
    });

    // 获取视频元素和传送门元素
    const video = document.getElementById('video');
    const portal = document.getElementById('portal');
    const vBox = document.getElementById('v-box');

    document.addEventListener('scroll', function () {
      // 观察视频元素
      observer.observe(vBox);
    })
    // 停止观察
    // observer.unobserve(vBox)

    // 显示传送门
    function showPortal() {
      portal.appendChild(video)
      portal.style.display = 'block'
    }

    // 隐藏传送门
    function hidePortal() {
      vBox.appendChild(video)
      portal.style.display = 'none'
    }

  </script>
</body>

</html>

requestAnimationFrame

介绍

requestAnimationFrame是一个用于优化动画和渲染性能的JavaScript方法。它是浏览器提供的一个API,用于在下一次浏览器重绘之前调用指定的回调函数。

使用requestAnimationFrame来执行动画和其他需要频繁更新的操作比使用传统的setTimeoutsetInterval方法更有效,原因如下:

  1. requestAnimationFrame会在浏览器准备好进行重绘时触发回调函数,通常是每秒60次,以匹配大多数显示器的刷新频率。这样可以避免在不必要的时候进行重绘,提高性能和电池寿命。
  2. 当页面处于非激活状态时,requestAnimationFrame会暂停,避免不必要的计算和功耗。

示例

实现一个可以暂停和继续加载的进度条动画,通过切换浏览器的窗口可以发现进度条会暂停,切换回去之后又重新加载,非常的智能。

<!DOCTYPE html>
<html>
<head>
  <title>Loading Progress Bar Example</title>
  <style>
    .progress-bar-container {
      width: 100%;
      height: 20px;
      background-color: #ddd;
    }

    .progress-bar {
      height: 100%;
      background-color: #007bff;
      width: 0;
      transition: width 0.5s;
    }

    #start-button {
      margin-top: 10px;
    }
  </style>
</head>
<body>
  <div class="progress-bar-container">
    <div class="progress-bar"></div>
  </div>
  当前进度:<span id="number"></span>
  <button id="start-button">开始加载</button>
  <button id="stop-button">暂停</button>

  <script>
    const startButton = document.getElementById('start-button');
    const progressBar = document.querySelector('.progress-bar');
    const progressNum = document.getElementById('number');
    const stopButton = document.getElementById('stop-button');
    let tag = true
    let width = 0;

    startButton.addEventListener('click', () => {
      function animate() {
        if ( tag && width < 100) {
          width++;
          progressBar.style.width = width + '%';
          progressNum.innerHTML = width + '%';

          requestAnimationFrame(animate);
        }
      }

      animate();
    });

    stopButton.addEventListener('click', () => {
      if(tag){
        tag = false
        stopButton.innerHTML = '暂停'
      } else {
        tag = true
        stopButton.innerHTML = '继续';
        startButton.click();
      }
    })

  </script>
</body>
</html>

Reflect & Object

1. 方法的区别:

  • Object 对象中提供了一些方法,例如 Object.definePropertyObject.getOwnPropertyDescriptorObject.setPrototypeOf 等。这些方法用于操作对象的属性和原型链。
  • Reflect 对象提供了与 Object 对象中相同的方法,例如 Reflect.definePropertyReflect.getOwnPropertyDescriptorReflect.setPrototypeOf 等。与 Object 对象不同的是,Reflect 的方法具有一致的函数签名。这使得在使用 Reflect 方法时更加统一和方便,便于函数式编程。

2. 性能的区别:

  • 从性能的角度来看,Reflect 的方法通常比 Object 的相应方法更快。这是因为 Reflect 的方法不会进行类型转换,而且它们是针对内部方法的直接映射,因此可以更高效地执行操作。
  • 另外,使用 Reflect 方法还可以避免因为对象原型链上的方法重写而导致的性能问题。因为 Reflect 方法总是会调用目标对象的原型链上的对应方法,而不会像 Object 方法一样受到重写方法的影响。

引入 Reflect 的原因:

  • Reflect 对象的引入是为了提供一种更统一、更直观的方式来操作对象。它使得对象操作的语义更加一致,避免了 Object 对象中的一些不一致性,例如 Object.defineProperty 的返回值和异常处理等。
  • 另外,Reflect 方法的引入也是为了实现元编程(metaprogramming),即在运行时操作和修改代码结构的能力。通过使用 Reflect,可以更容易地实现对象的动态代理和元编程的功能。

总结起来,Reflect 提供了一组更统一、更直观的方法来操作对象,并且在性能上通常比 Object 对象中的相应方法更好。它的设计目的是为了提供一种一致的、更强大的对象操作能力,并支持元编程的需求。

对比两者用法

属性获取

   
   const obj = { name: 'Alice' };
   const value = Object.getOwnPropertyDescriptor(obj, 'name').value;
   console.log(value); // 输出:Alice
   
   const obj = { name: 'Alice' };
   const value = Reflect.get(obj, 'name');
   console.log(value); // 输出:Alice


属性设置

   const obj = { name: 'Alice' };
   Object.defineProperty(obj, 'name', { value: 'Bob' });
   console.log(obj.name); // 输出:Bob

   const obj = { name: 'Alice' };
   Reflect.set(obj, 'name', 'Bob');
   console.log(obj.name); // 输出:Bob

函数调用

   function greet(name) {
     console.log(`Hello, ${name}!`);
   }
   Reflect.apply(greet, null, ['Alice']); // 输出:Hello, Alice!
   
   greet('Jack')

创建对象实例

class Person {
  constructor(name) {
    this.name = name;
  }
}

const obj = new Person('Alice');
console.log(obj instanceof Person); // 输出:true

   class Person {
     constructor(name) {
       this.name = name;
     }
   }
   const obj = Reflect.construct(Person, ['Alice']);
   console.log(obj instanceof Person); // 输出:true

检查对象是否存在

   const obj = { name: 'Alice' };
   const hasProperty = obj.hasOwnProperty('name');
   console.log(hasProperty); // 输出:true

   const obj = { name: 'Alice' };
   const hasProperty = Reflect.has(obj, 'name');
   console.log(hasProperty); // 输出:true

删除属性

   const obj = { name: 'Alice' };
   delete obj.name;
   console.log(obj.name); // 输出:undefined

   const obj = { name: 'Alice' };
   const bool = Reflect.deleteProperty(obj, 'name');
   console.log(obj.name, bool); // 输出:undefined true

defineProperty & Proxy

Object.defineProperty 和 Proxy 是 JavaScript 中用于劫持对象的两种不同机制。它们的使用方式和性能特点有一些区别。

Object.defineProperty 是一个对象方法,用于在一个对象上定义一个新的属性或修改现有属性的特性。它允许你精确地控制属性的各个方面,如可写性、可枚举性、可配置性等。

  • @descriptor
  • configurable 为 true 时,属性才能重新被定义,默认为 false。
  • enumerable 为 true 时,该属性才能够出现在对象的枚举属性中,此时才能通过for in遍历属性。默认为 false。
  • writable 为 true 时,value属性值才能被修改。默认为 false,此时value属性值为只读状态。
  • value 该属性对应的初始值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
  • get 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行。默认为 undefined。
  • set 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。默认为 undefined。
const obj = {};
Object.defineProperty(obj, 'name', {
  get() {
    console.log('Getting property: name');
    return this._name;
  },
  set(value) {
    console.log('Setting property: name =', value);
    this._name = value;
  }
});

console.log(obj.name); // 输出:Getting property: name,然后输出:undefined

obj.name = 'Alice'; // 输出:Setting property: name = Alice

console.log(obj.name); // 输出:Getting property: name,然后输出:Alice


Proxy 是 ES6 引入的一个特性,它允许你创建一个代理对象,可以拦截、修改和扩展目标对象的行为。通过使用 Proxy,你可以劫持对象的各种操作,如属性读取、属性设置、函数调用等。无论是只能删改查,都可以使用这个方法来对对象进行劫持然后做相应的处理

const obj = {};

const proxy = new Proxy(obj, {
  get(target, property) {
    console.log(`Getting property: ${property}`);
    return target[property];
  },
  set(target, property, value) {
    console.log(`Setting property: ${property} = ${value}`);
    target[property] = value;
  }
});

proxy.name = 'Alice'; // 输出:Setting property: name = Alice
proxy.age = 18 // Setting property: add = add
delete proxy.age // true
console.log(proxy.name); // 输出:Getting property: name,然后输出:Alice

##示例 在html中用Object.defineProperty实现一个简单的响应式劫持

miniVue简单实现

总结

上面这些方法我只是浅尝辄止的说明了下,但是无论是日常开发还是性能优化都十分有用,不仅仅是了解,我们应该更多的实践让我们的开发方式更加灵活,做到有的放矢,优雅的开发起来。