普通视图

发现新文章,点击刷新页面。
今天 — 2025年1月18日首页

JavaScript 数组方法大盘点:从新手到大佬,快速掌握所有必备技能!🎉

2025年1月17日 21:37

前言

大家好!今天我们将继续深入探索 JavaScript 数组的奥秘!你可能以为 pushpop 就是数组操作的全部,但其实数组的世界远不止如此。除了这些基础方法,还有许多其他方法能够让你的数组操作如鱼得水,大幅提高开发效率。今天的目标是帮助你从新手晋升为 JavaScript 数组高手,不仅掌握常用方法,还会揭秘一些鲜为人知的“宝藏”方法。让我们继续这段魔法般的数组之旅吧!🚀


相关方法

1. push()pop()

  • push():将一个或多个元素添加到数组末尾,并返回数组的新长度。
  • pop():从数组末尾删除一个元素,返回被删除的元素。
const fruits = ['apple', 'banana'];
fruits.push('orange'); // 添加橙子
console.log(fruits); // ['apple', 'banana', 'orange']

const lastFruit = fruits.pop(); // 删除最后一个元素
console.log(lastFruit); // 'orange'
console.log(fruits); // ['apple', 'banana']

2. shift()unshift()

  • shift():删除数组中的第一个元素,返回被删除的元素。
  • unshift():将一个或多个元素添加到数组的开头,返回数组的新长度。
const numbers = [1, 2, 3, 4];
numbers.unshift(0); // 在数组开头添加 0
console.log(numbers); // [0, 1, 2, 3, 4]

const firstNumber = numbers.shift(); // 删除第一个元素
console.log(firstNumber); // 0
console.log(numbers); // [1, 2, 3, 4]

3. map()

  • map():创建一个新数组,数组中的每个元素是原数组元素调用指定函数处理后的结果。
let numbers = [1, 2, 3, 4];
let squared = numbers.map((num) => num ** 2); // 数组中每个数字平方
console.log(squared); // [1, 4, 9, 16]

4. filter()

  • filter():创建一个新数组,包含所有通过指定条件函数筛选出的元素,原数组不受影响。
let ages = [12, 18, 25, 30, 15];
let adults = ages.filter((age) => age >= 18); // 筛选出年龄大于等于 18 的人
console.log(adults); // [18, 25, 30]

5. reduce()

  • reduce():对数组中的每个元素执行指定的累加操作,最终返回单一结果(如求和、拼接等)。
let numbers = [1, 2, 3, 4];
let sum = numbers.reduce((acc, num) => acc + num, 0); // 数组求和
console.log(sum); // 10

6. forEach()

  • forEach():对数组的每个元素执行指定的回调函数,不返回结果,常用于遍历。
let colors = ['red', 'green', 'blue'];
colors.forEach(color => console.log(color));  // 打印每个颜色
// 输出:
// red
// green
// blue

7. find()findIndex()

  • find():返回第一个满足条件的元素,如果没有找到,则返回 undefined
  • findIndex():返回第一个满足条件的元素的索引,如果没有找到,则返回 -1。
let users = [{ name: 'Tom', age: 20 }, { name: 'Jerry', age: 25 }];
let user = users.find(user => user.name === 'Tom');  // 查找 Tom
console.log(user);  // { name: 'Tom', age: 20 }

let index = users.findIndex(user => user.name === 'Jerry');  // 查找 Jerry 的索引
console.log(index);  // 1

8. sort()

  • sort():对数组元素进行排序,默认按字符串字典序排列。如果要按数字大小排序,需要提供比较函数。
let nums = [4, 2, 8, 5];
nums.sort((a, b) => a - b);  // 数字升序排序
console.log(nums);  // [2, 4, 5, 8]

9. slice()splice()

  • slice():返回数组的一个新数组,包含指定起始和结束位置之间的元素,不会改变原数组。
  • splice():可以删除或插入数组中的元素,直接改变原数组。
let animals = ['dog', 'cat', 'rabbit', 'bird'];
let selectedAnimals = animals.slice(1, 3);  // 获取从索引 1 到 3 的元素
console.log(selectedAnimals);  // ['cat', 'rabbit']
console.log(animals);  // ['dog', 'cat', 'rabbit', 'bird']

animals.splice(2, 1, 'fish');  // 删除第 2 个元素,插入 'fish'
console.log(animals);  // ['dog', 'cat', 'fish', 'bird']

10. concat()

  • concat():合并两个或多个数组,返回一个新数组。
let array1 = [1, 2];
let array2 = [3, 4];
let combined = array1.concat(array2);  // 合并数组
console.log(combined);  // [1, 2, 3, 4]

11. join()

  • join():将数组中的所有元素连接成一个字符串,元素间可以指定分隔符。
let fruits = ['apple', 'banana', 'cherry'];
let fruitString = fruits.join(' & ');  // 用 '&' 连接数组元素
console.log(fruitString);  // 'apple & banana & cherry'

12. some()every()

  • some():只要有至少一个元素符合条件,返回 true,否则返回 false
  • every():只有所有元素都符合条件,返回 true,否则返回 false
let numbers = [10, 20, 30, 40];
let hasLargeNumber = numbers.some(num => num > 25);  // 判断是否有大于 25 的元素
console.log(hasLargeNumber);  // true

let allLargeNumbers = numbers.every(num => num > 5);  // 判断所有数字是否都大于 5
console.log(allLargeNumbers);  // true

13. from()

  • from():将类数组对象或可迭代对象转换为数组。
let str = 'hello';
let arr = Array.from(str);  // 将字符串转换为数组
console.log(arr);  // ['h', 'e', 'l', 'l', 'o']

14. fill()

  • fill():用指定的值填充数组的指定位置,填充的元素会改变原数组。
let numbers = [1, 2, 3, 4];
numbers.fill(0, 2, 4);  // 从索引 2 到 4 填充为 0
console.log(numbers);  // [1, 2, 0, 0]

15. includes()

  • includes():判断数组中是否包含某个特定的元素,返回布尔值。
let fruits = ['apple', 'banana', 'cherry'];
console.log(fruits.includes('banana'));  // true
console.log(fruits.includes('orange'));  // false

16. reverse()

  • reverse():将数组的元素反转,直接修改原数组。
let numbers = [1, 2, 3, 4];
numbers.reverse();  // 反转数组
console.log(numbers);  // [4, 3, 2, 1]

17. indexOf()lastIndexOf()

  • indexOf() :返回数组中首次出现指定元素的索引,若没有找到,返回 -1
  • lastIndexOf() :返回数组中最后一次出现指定元素的索引,若没有找到,返回 -1
let fruits = ['apple', 'banana', 'cherry', 'banana'];
console.log(fruits.indexOf('banana'));  // 1(返回第一个 banana 的索引)
console.log(fruits.lastIndexOf('banana'));  // 3(返回最后一个 banana 的索引)
console.log(fruits.indexOf('grape'));  // -1(未找到 grape)

18. Array.isArray()

  • Array.isArray():判断传入的值是否是一个数组,返回 truefalse
console.log(Array.isArray([1, 2, 3]));  // true
console.log(Array.isArray('hello'));  // false
console.log(Array.isArray({ name: 'Tom' }));  // false

总结

这些 JavaScript 数组方法就像是数组的超级英雄,掌握它们,你就能让数组操作事半功倍!从基础的增删查改到高级的 mapreduce,以及一些不常用但极具威力的方法如 fromfillincludes,都能帮助你在开发中大显身手。希望今天的分享能够让你在

数组的世界里游刃有余!如果你有其他的数组操作技巧,欢迎留言讨论!🔧

昨天 — 2025年1月17日首页

图片预览的两种方式

2025年1月17日 21:38

图片预览的方式

1.base64

<input type="file" id="fileInput">  
<img id="previewImage" src="" alt="Preview Image">  
<script>  
const fileInput = document.getElementById('fileInput');  
const previewImage = document.getElementById('previewImage');  
fileInput.addEventListener('change', function () {  
const file = fileInput.files[0];  
const reader = new FileReader();  
reader.onload = function (e) {  
const base64String = e.target.result;  
previewImage.src = base64String;  
console.log('图片读取的Base64的值为--->', base64String);  
};  
reader.readAsDataURL(file);  
});  
</script>  
  

2.bloburl

  
<input type="file" id="fileInput">  
<img id="previewImage" src="" alt="Preview Image">  
<script>  
const fileInput = document.getElementById('fileInput');  
const previewImage = document.getElementById('previewImage');  
fileInput.addEventListener('change', function () {  
const file = fileInput.files[0];  
let tempUrl = window.URL.createObjectURL(file)  
console.log('blob--->', tempUrl); // blob:http://127.0.0.1:5500/84d2e951-33dc-4fea-840a-f3d8f3396766  
previewImage.src = tempUrl;  
});  
</script>  
```tu

为什么组件再次渲染了?

作者 snow分享
2025年1月17日 10:12

Props 改变导致重新渲染

我来先看一个简单的例子,当我点击这个按钮时,count 会加 1。

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);

  return <div>
    <p>Count: {count}</p>
    <button onClick={() => setCount(count + 1)}>Increment</button>
  </div>
}

需要注意的是,这里讨论的 Props 改变并非指 initialCount 或其他 Props 的值发生变化,而是指 Props 对象本身被重新创建了。

让我们深入理解一下:当 React 渲染这个组件时,本质上是在调用 Counter({initialCount: 0})。每次调用时,都会创建一个全新的对象作为 Props 传入,即使对象的内容完全相同。

function App() {
  return <>
    <Counter initialCount={0} />
    {/* 其他组件 */}
  </>
}

当我们的父组件重新渲染时,Props 对象本身被重新创建,所以 Counter 和其他组件都会重新渲染。

这显然不是我们希望看到的,那么我们如何优化这个组件呢?

使用 React.memo

const Counter = React.memo(({ initialCount }: { initialCount: number }): JSX.Element => {
  const [count, setCount] = useState(initialCount)

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
})

它会对 Props 的值进行一个浅比较,如果值没有发生变化,则不会重新渲染。

提升到组件外部、useMemo、useCallback

然而需要注意的是,即使使用了 memo,我们也要谨慎处理 Props 的值。 由于 memo 只进行浅比较,如果我们在传递 Props 时直接使用对象或内联函数,组件仍然会重新渲染

function App() {
  return <>
    // 都使用 memo 包裹
    <SlowComponent data={{foo:"bar"}} />
    <SlowComponentTwo data={["foo","bar"]} />
    <OtherSlowComponent setCount={() => setCount(count + 1)} />
    {/* 其他组件 */}
  </>
}

以上可能是我们在日常开发中经常会写的代码,这些代码会造成不必要的重新渲染,那么我们如何优化这些代码呢?

对于静态数据,我们可以将其提升到组件外部。而对于依赖 state、props 或需要在 useEffect 中使用的数据, 我们可以通过 useMemo 或 useCallback 这些 Hook 来进行性能优化。这样可以避免在每次渲染时重新创建数据或函数。

const data = ["foo","bar"]

function App() {
  const setCount = useCallback(() => {
    setCount(count + 1)
  }, [count])

 const memoizedData = useMemo(() => data, [count])

  return <>
    <SlowComponent data={data} />
    <SlowComponentTwo data={memoizedData} />
    <OtherSlowComponent setCount={setCount} />
    {/* 其他组件 */}
  </>
}

显然,这种写法与我们编写代码的初衷相去甚远。作为开发者,我们希望能够简单直接地传递函数或对象,而不是因此导致组件频繁地重新渲染。 虽然我们可以通过手动优化或使用 lint 规则来规避这些问题,但这并不是一个理想的解决方案。这种复杂的优化过程,反而增加了开发的心智负担。

React Compiler

React Compiler 为这些问题提供了一个优雅的解决方案。尽管目前仍处于实验阶段,但是我觉得它是一个非常值得期待的解决方案。

只需启用 React Compiler,我们就可以摆脱 memo、useMemo、useCallback 等优化方案的困扰。它支持 React 17/18/19 版本,可以在项目或指定目录级别启用。

这种优化方式如此简单直接,可能这就是它讨论热度很低的原因。未来关于组件优化的面试题可能会变得很简单 - 启用 React Compiler 就够了。

Ryan Carniato 在直播中有一个关于 React Compiler 的性能测试

react-compiler-performance.png

用原生 js 做为基准值,通过加权几何平均值可以看出,React 相关实现相比原生都有一定的性能损耗

  • 经过手动优化后 react-hooks: 慢 1.51 倍
  • 经过 react compiler 优化后的 react-hooks: 慢 1.57 倍
  • 没有经过优化的 react-hooks: 慢 1.85 倍

从这次 benchmark 可以看出,React Compiler 并不一定可以让 react 更快,但是他大概率让你的 react 代码更快

State 改变导致重新渲染

还是第一个例子,当 count 改变时,组件会重新渲染,这当然是没问题

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);

  return <div>
    <p>Count: {count}</p>
    <button onClick={() => setCount(count + 1)}>Increment</button>
  </div>
}

然而,随着代码层级的增加和多个自定义 hooks 的引入,state 的管理会变得愈发复杂。 这种复杂性往往会导致组件树中出现不必要的多层级渲染,影响应用性能。为了有效应对这个问题,我们可以采取以下策略:

  • 降低组件复杂度,将 state 下移到真正需要它的子组件
  • 将不依赖 state 的 JSX 上移,这样这部分在 state 改变时不会重新渲染
  • 如果数据不需要响应式更新,考虑使用 useRef 替代 useState

Context 变化导致重新渲染

prop drilling

有了以上这些优化,我们还需要解决最后一个问题,那就是 prop drilling。那么什么是 prop drilling 呢?

prop-drilling.png

当多个组件需要共享同一个 state 数据时,传统的做法是将这个 state 提升到它们最近的共同父组件中, 然后通过 props 层层传递给需要使用该数据的子组件。这种将数据在组件树中自上而下逐级传递的模式被称为 prop drilling。 然而,这种模式的一个明显缺陷是:即使某些中间组件完全不需要这个 state,它们也会因为 props 的变化而被迫重新渲染。

通过创建 Context,我们可以实现数据的跨层级传递,使数据能够直接到达需要它的组件,而无需通过中间组件的 props 层层传递,简化了数据流, 避免不必要的中间组件渲染,

Context 的问题

Context 也有一些问题,当我们的应用变得复杂的时候。

context-problem.png

当我们的 Context 值被多个组件共享时,一旦 Context 的值发生变化,即使只有 A 组件的 UI 需要更新, 所有订阅了该 Context 的组件及其子组件都会被触发重新渲染。为此,使用 context 时,也有一些建议

  1. 将 Context Provider 放在组件树顶层

    建议将 Provider 放置在根组件或 App 组件中,这样可以确保 Provider 只在应用初始挂载时渲染一次, 避免频繁的重新渲染和不必要的性能开销

  2. 合理拆分 Context

    避免创建一个包含所有状态的大型 Context 例如,不要将用户信息、主题设置等所有状态都放在同一个 Context 中 应该根据功能将其拆分为独立的 Context(如 ThemeContext、UserContext) 这样可以实现更细粒度的更新,提高渲染性能

  3. 谨慎在自定义 Hook 中使用嵌套过深的 state (context,useState)

    随着应用复杂度增加,过多的嵌套过深的 state 会导致:

    • 代码可维护性降低
    • 调试难度增加
    • 渲染逻辑难以追踪

    建议保持 hooks 结构扁平化,适度使用组合 这样不仅提高了代码可读性,也便于性能优化和问题排查

总结

Props 改变导致的重新渲染

  • 使用 React.memo 包裹组件
  • 将静态数据提升到组件外部
  • 使用 useMemo 和 useCallback 缓存数据和函数
  • 使用 React Compiler(实验阶段)自动优化

State 改变导致的重新渲染

  • 将 state 下移到真正需要的子组件
  • 将不依赖 state 的 JSX 上移
  • 考虑使用 useRef 替代不需要响应式更新的数据

Context 改变导致的重新渲染

  • 将 Context Provider 放在组件树顶层
  • 根据功能合理拆分 Context
  • 谨慎在自定义 hooks 中使用嵌套过深的 state (context,useState)

以上文字参考
the big problem with React useContext
how does React re-render?

为了解决内存泄露,我把 vue 源码改了

2025年1月16日 22:26

前言

彦祖们,好久不见,最近一直忙于排查单位业务的终端内存泄露问题,已经吃了不下 10 个 bug

但是排查内存泄露在前端领域属于比较冷门的领域了

这篇文章笔者将带你一步步分享业务实践中遇到的内存泄露问题以及如何修复的经历

本文涉及技术栈

  • vue2

场景复现

如果之前有看过我文章的彦祖们,应该都清楚

笔者所在的单位有一个终端叫做工控机(类似于医院挂号的终端),没错!所有的 bug 都源自于它😠

因为内存只有 1G 所以一旦发生内存泄露就比较可怕

不过没有这个机器 好像也不会创作这篇文章😺

复现 demo

彦归正传,demo 其实非常简单,只需要一个最简单的 vue2 demo 就可以了

  • App.vue
<template>
  <div id="app">
    <button @click="render = true">render</button>
    <button @click="render = false">destroy</button>
    <Test v-if="render"/>
  </div>
</template>
<script>
import Test from './test.vue'
export default {
  name: 'App',
  components: {
    Test
  },
  data () {
    return {
      render: false
    }
  }
}
</script>
<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}
</style>

  • test.vue
<template>
  <div class="test">
    <div>{{ total }}</div>
    <div
      v-for="(item,index) in 1000"
      :key="`${item}-${index}`"
      class="item"
    >
      {{ item }}ipc-prod2.8
    </div>
  </div>
</template>

<script>
export default {
  name: 'Test',
  data () {
    return {
      total: 1000
    }
  },
  mounted () {
    this.timer = setTimeout(() => {
      this.total = 10000
    }, 500)
  },
  beforeDestroy () {
    clearTimeout(this.timer)
  }
}
</script>

复现流程

以下流程建议彦祖们在 chrome 无痕模式下执行

  1. 我们点击 render 按钮渲染 test 组件,此时我们发现 dom 节点的个数来到了 2045

image.png

考虑到有彦祖可能之前没接触过这块面板,下图展示了如何打开此面板

image.png

  1. 500ms 后(定时器执行完成后,如果没复现可以把 500ms 调整为 1000ms, 1500ms),我们点击 destroy 按钮
  2. 我们点击面板这里的强制回收按钮(发现节点并没有回收,已发生内存泄露)

image.png

如果你的浏览器是最新的 chrome,还能够点击这里的 已分离的元素(detached dom),再点击录制

image.png

我们会发现此时整个 test 节点已被分离

image.png

问题分析

那么问题到底出在哪里呢?

vue 常见泄露场景

笔者搜遍了全网,网上所说的不外乎以下几种场景

1.未清除的定时器

2.未及时解绑的全局事件

3.未及时清除的 dom 引用

4.未及时清除的 全局变量

好像第一种和笔者的场景还比较类似,但是仔细看看代码好像也加了

beforeDestroy () {
  clearTimeout(this.timer)
}

这段代码啊,就算不加,timer 执行完后,事件循环也会把它回收掉吧

同事提供灵感

就这样笔者这段代码来回测试了半天也没发现猫腻所在

这时候同事提供了一个想法说"total 更新的时候是不是可以提供一个 key"

改了代码后就变成了这样了

  • test.vue
<template>
  <div class="test">
    <div :key="renderKey">{{ total }}</div>
    <div
      v-for="(item,index) in 1000"
      :key="`${item}-${index}`"
      class="item"
    >
      {{ item }}ipc-prod2.8
    </div>
  </div>
</template>

<script>
export default {
  name: 'Test',
  data () {
    return {
      renderKey: 0,
      total: 1000
    }
  },
  mounted () {
    this.timer = setTimeout(() => {
      this.total = 10000
      this.renderKey = Date.now()
    }, 500)
  },
  beforeDestroy () {
    clearTimeout(this.timer)
  }
}
</script>

神奇的事情就这样发生了,笔者还是按以上流程测试了一遍,直接看结果吧

image.png

我们看到这个 DOM 节点曲线,在 destroy 的时候能够正常回收了

问题复盘

最简单的 demo 问题算是解决了

但是应用到实际项目中还是有点困难

难道我们要把每个更新的节点都手动加一个 key 吗?

其实仔细想想,有点 vue 基础的彦祖应该了解这个 key 是做什么的?

不就是为了强制更新组件吗?

等等,强制更新组件?更新组件不就是 updated 吗?

updated 涉及的不就是八股文中我们老生常谈的 patch 函数吗?(看来八股文也能真有用的时候😺)

那么再深入一下, patch 函数内部不就是 patchVnode 其核心不就是 diff 算法吗?

首对首比较,首对尾比较,尾对首比较,尾对尾比较 这段八股文要是个 vuer 应该都不陌生吧?😺

动手解决

其实有了问题思路和想法

那么接下来我们就深入看看 vue 源码内部涉及的 updated 函数到底在哪里吧?

探索 vue 源码

我们找到 node_modules/vue/vue.runtime.esm.js

image.png

我们看到了 _update 函数真面目,其中有个 __patch__ 函数,我们再重点查看一下

image.png

image.png

createPatchFunction 最后 return 了这个函数

image.png

我们最终来看这个 updateChildren 函数

image.png

其中多次出现了上文中所提到的八股文,每个都用 sameVnode进行了对比

  • function sameVnode
function sameVnode (a, b) {
    return (a.key === b.key &&
        a.asyncFactory === b.asyncFactory &&
        ((a.tag === b.tag &&
            a.isComment === b.isComment &&
            isDef(a.data) === isDef(b.data) &&
            sameInputType(a, b)) ||
            (isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error))));
}

果然这里我们看到了上文中 key 的作用

key 不一样就会认作不同的 vnode

那么就会强制更新节点

对应方案

既然找到了问题的根本

在判定条件中我们是不是直接加个 || a.text !== b.text

强制对比下文本节点不就可以了吗?

修改 sameVnode

看下我们修改后的 sameVnode

function sameVnode (a, b) {
    if(a.text !== b.text) return false // 文本不相同 直接 return
    return (a.key === b.key &&
        a.asyncFactory === b.asyncFactory &&
        ((a.tag === b.tag &&
            a.isComment === b.isComment &&
            isDef(a.data) === isDef(b.data) &&
            sameInputType(a, b)) ||
            (isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error))));
}

方案效果

让我们用同样的代码来测试下

image.png

测试了几次发现非常的顺利,至此我们本地的修改算是完成了

如何上线?

以上的方案都是基于本地开发的,那么我们如何把代码应用到线上呢?

其他开发者下载的 vue 包依旧是 老的 sameVnode

不慌,接着看

patch-package

对比了好几种方式,最终我们选择了这个神器

其实使用也非常简单

1.npm i patch-package

2.修改 node_modules/vue 源码

3.在根目录执行 npx patch-package vue(此时如果报错,请匹配对应 node 版本的包)

我们会发现新增了一个这样的文件

image.png

4.我们需要在package.json scripts 新增以下代码

  • package.json
"scripts": {
    +"postinstall":"patch-package"
}

至此上线后,其他开发者执行 npm i 后便能使变动的补丁生效了

优化点

其实我们的改造还有一定的进步空间,比如说在指定节点上新增一个 attribute

在函数内部判断这个 attributereturn false

这样就不用强制更新每个节点了

当然方式很多种,文章的意义在于解决问题的手段和耐心

写在最后

最后再次感谢同事 juejin.cn/user/313102… 的提供的灵感和协助

感谢彦祖们的阅读

个人能力有限

如有不对,欢迎指正🌟 如有帮助,建议小心心大拇指三连🌟

2025真实面试题

作者 风无雨
2025年1月16日 18:46

1.9下午三点

1.为什么离职
2.js数据类型
3.如何判断数据类型
4.js == 和 === 区别
== 为什么不判断类型
5.js数组操作方法有哪些,那些会改变原数组
6.es6新增特性
7.let,const,var区别
8.箭头函数和普通函数区别
9.promise是什么?promise的静态方法有什么区别
10.vue2和vue3区别
11.vue生命周期
12.vue组件通信
13.vue路由跳转方式,vue路由模式
14.如何实现一个自定义指令
15.如何实现响应式布局,rem是什么的尺寸
16.实现一个左侧固定宽度,右侧宽度自适应布局
17.组件库实现考虑那些东西

1.16上午10点

image.png 1.vite打包配置
2.事件循环
3.跨域,什么算跨域
4.call apply,bind
5.数组常用api
6.vue2vue3区别
7.vue2vue3响应式原理区别
8.v-model
9.ts中any,never,unknow区别
10.es6新特性

1.16下午三点

1.vue2,vue3区别
2.teleport使用(项目中)
3.token无感刷新,静默登录(项目中)
4.前端存储方案,区别,存在cookie里,关闭浏览器会消失吗
5.路由守卫权限验证
6.按钮权限控制如何实现,v-if,v-show区别
7.组件通信方式
8.pinia用来做什么
9.react中怎么更新组件的状态
10.websocket 项目中使用场景,websocket有哪些事件
11.行内元素有哪些
12.行内元素设置宽高会生效吗,如果让他生效怎么做
13.插槽的作用
14.按钮如何实现防抖
15.那些比较有挑战性的工作

Vue3: 二次封装组件的原则与方法

作者 鲤鱼池
2025年1月16日 18:19

原则

  • 保持单一职责
    原则:每个组件应专注于完成一种特定功能,不要添加与组件主要功能无关的逻辑。
    举例:封装一个按钮组件时,应该专注于样式和事件绑定,而不是处理复杂的表单逻辑。
  • 尽量减少重复代码
    原则:通过封装复用逻辑,减少多处使用同样代码的情况。
    举例:可以对常用的输入框组件进行封装,将其通用功能提取为单一组件。
  • 提供清晰的接口
    原则:定义合理的 props 和 events,避免过多的参数,保持易用性和清晰度。
    举例:组件需要的所有配置项应通过 props 提供,事件通过 $emit 通知父组件。
  • 支持扩展性
    原则:为封装组件提供插槽、动态样式或回调接口,让开发者可以扩展功能。
    举例:提供 slot 和 props,使组件可以适应不同场景。
  • 遵循团队规范
    原则:封装组件时应符合团队的命名、代码风格和功能设计规范。
    举例:Vue 组件的 props 使用驼峰命名,事件使用 update:modelValue 风格

方法

  • 继承属性:使用 v-bind="$attrs" 来接收和传递父组件传递的属性。常用于向原组件传递未声明的属性。

  • 继承事件:使用 emits 配置声明事件,或者通过 $emit 进行事件的触发和传播。

  • 继承方法:使用 ref 来暴露内部方法给父组件,通过 defineExpose 明确暴露。

  • 继承插槽:使用 slots 对象来接收和转发插槽内容。

  • vue2与vue3的区别:在 vue3 中,取消了listeners这个组件实例的属性,将其事件的监听都整合到了attrs上,因此直接通过v-bind=$attrs属性就可以进行props属性和event事件的透传

  • 在 vue2 中,需要用到 $slots(插槽) 和 $scopedSlots(作用域插槽)

  • 在 vue3 中,取消了作用域插槽 $scopedSlots,将所有插槽都统一在 $slots 当中

v-model

大家应该都知道v-model只是一个语法糖,实际就是给组件定义了modelValue属性和监听update:modelValue事件,所以我们以前要实现数据双向绑定需要给子组件定义一个modelValue属性,并且在子组件内要更新modelValue值时需要emit出去一个update:modelValue事件,将新的值作为第二个字段传出去。

原因是因为从vue2开始就已经是单向数据流,在子组件中是不能直接修改props中的值。而是应该由子组件中抛出一个事件,由父组件去监听这个事件,然后去修改父组件中传递给props的变量。如果这里我们给input输入框直接加一个v-model="props.modelValue",那么其实是在子组件内直接修改props中的modelValue

如果父组件和子组件中都使用了 v-model,并且绑定的是同一个变量,这个时候就会出问题了,因为子组件直接更改了父组件的数据,违背了单向数据流,这样会导致如果出现数据问题不好调试,无法定位出现问题的根源。

第一种方法:将 v-model 拆开,通过 emit 让父组件去修改数据

第二种方法:使用计算属性的 get set 方法

但是如果子组件中有多个表单项,不管是上面哪种方法,都要写很多重复的代码

<!-- 父组件 -->
<template>
  <my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
  text: '',
  password: '',
  name: ''
})
</script>

<!-- 子组件 -->
<template>
  <el-input v-model="name"></el-input>
  <el-input v-model="text"></el-input>
  <el-input v-model="password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
  modelValue: {
    type: Object,
    default: () => {},
  }
});

const emit = defineEmits(['update:modelValue']);

const name = computed(() => {
  get() {
    return props.modelValue.name
  },
  set(val) {
    emit('update:modelValue', {
      ...props.modelValue,
      name: val
    })
  }
})

const text = computed(() => {
  get() {
    return props.modelValue.text
  },
  set(val) {
    emit('update:modelValue', {
      ...props.modelValue,
      text: val
    })
  }
})

const password = computed(() => {
  get() {
    return props.modelValue.password
  },
  set(val) {
    emit('update:modelValue', {
      ...props.modelValue,
      password: val
    })
  }
})
</script>

上面使用计算属性监听单个属性,所以需要每个属性都写一遍,我们可以考虑在计算属性中监听整个对象:

<!-- 父组件 -->
<template>
  <my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
  text: '',
  password: '',
  name: ''
})
</script>

<!-- 子组件 -->
<template>
  <el-input v-model="modelList.name"></el-input>
  <el-input v-model="modelList.text"></el-input>
  <el-input v-model="modelList.password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
  modelValue: {
    type: Object,
    default: () => {},
  }
});

const emit = defineEmits(['update:modelValue']);

const modelList = computed(() => {
  get() {
    return props.modelValue
  },
  set(val) {
    emit('update:modelValue', val)
  }
})
</script>

读取属性的时候能正常调用 get,但是设置属性的时候却无法触发 set,原因是 modelList.value = xxx,才会触发 set,而 modelList.value.name = xxx,无法触发。这个时候,Proxy 代理对象可以完美的解决这个问题:

<!-- 父组件 -->
<template>
  <my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
  text: '',
  password: '',
  name: ''
})
</script>

<!-- 子组件 -->
<template>
  <el-input v-model="modelList.name"></el-input>
  <el-input v-model="modelList.text"></el-input>
  <el-input v-model="modelList.password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
  modelValue: {
    type: Object,
    default: () => {},
  }
});

const emit = defineEmits(['update:modelValue']);

const modelList = computed(() => {
  get() {
    return new Proxy(props.modelValue, {
      get(target, key) {
        return Reflect.get(target, key)
      },
      set(target, key, value) {
        emit('update:modelValue',{
          ...target,
          [key]: value
        })
        return true
      }
    })
  },
  set(val) {
    emit('update:modelValue', val)
  }
})
</script>

我们还可以考虑把这段代码进行封装,可以在多处引入进行使用: useVModel.ts,其实 vueuse 里面有提供了这么一个方法,基本的逻辑是一样的。

useVModel

<!-- 父组件 -->
<template>
  <my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
  text: '',
  password: '',
  name: ''
})
</script>

<!-- 子组件 -->
<template>
  <el-input v-model="modelList.name"></el-input>
  <el-input v-model="modelList.text"></el-input>
  <el-input v-model="modelList.password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useVModel } from './useVModel.ts'

const props = defineProps({
  modelValue: {
    type: Object,
    default: () => {},
  }
});

const emit = defineEmits(['update:modelValue']);

const modelList = useVModel(props, 'modelValue', emit)
</script>

使用defineModel实现数据双向绑定

defineModel是一个宏,所以不需要从vue中import导入,直接使用就可以了。这个宏可以用来声明一个双向绑定 prop,通过父组件的 v-model 来使用

<template>
  <CommonInput v-model="inputValue" />
</template>

<script setup lang="ts">
import { ref } from "vue";

const inputValue = ref();
</script>
<template>
  <input v-model="model" />
</template>

<script setup lang="ts">
const model = defineModel();
model.value = "xxx";
</script>

在上面的例子中我们直接将defineModel的返回值使用v-model绑定到input输入框上面,无需定义 modelValue 属性和监听 update:modelValue 事件,代码更加简洁。defineModel的返回值是一个ref,我们可以在子组件中修改model变量的值,并且父组件中的inputValue变量的值也会同步更新,这样就可以实现双向绑定。

原理:defineModel其实就是在子组件内定义了一个叫model的ref变量和modelValue的props,并且watch了props中的modelValue。当props中的modelValue的值改变后会同步更新model变量的值。并且当在子组件内改变model变量的值后会抛出update:modelValue事件,父组件收到这个事件后就会更新父组件中对应的变量值。

例子

<template>
  <my-input v-model="inputValue" ref="myInputRef" :name="'lyw'" :year="18" @input="onInput">
    <!-- 动态插槽定义 -->
    <template #prefix>
      <span>Prefix Content</span>
    </template>
    <template #suffix="{ year }">
      <span>END: {{ year }}</span>
    </template>
    <template #default="{ name }">
      <p>Default Content: {{ name }}</p>
    </template>
  </my-input>
  <div>当前输入的值是:{{ inputValue }}</div>
</template>

<script lang="ts" setup>
import MyInput from '@/components/MyInput.vue'
import { onMounted } from 'vue'
import { ref } from 'vue'

const inputValue = ref('')
const myInputRef = ref()
function onInput(value: string) {
  console.log('Input value:', value)
}
onMounted(() => {
  // 光标聚焦
  // 避免ref 链式调用,比如 this.$refs.tableRef.$refs.table.clearSort()
  // 前提:子组件把方法暴露
  myInputRef.value.focus()
})
</script>
<template>
  <!-- 简化:直接使用 computed 绑定到 v-bind -->
  <el-input v-model="localValue" v-bind="$attrs" ref="inputRef">
    <!-- 动态插槽 -->
    <template v-for="(_slot, slotName) in $slots" #[slotName]="slotProps">
      <slot :name="slotName" v-bind="slotProps"></slot>
    </template>
  </el-input>
</template>

<script lang="ts" setup>
import { onMounted } from 'vue'
import { computed, ref } from 'vue'

// 定义 props 和 emits
const props = defineProps({
  modelValue: {
    type: String,
    default: ''
  }
})
const emit = defineEmits(['update:modelValue'])

// 使用 computed 来同步 props 和 emits
const localValue = computed({
  get: () => props.modelValue,
  set: (value) => emit('update:modelValue', value)
})
const inputRef = ref('')
let exposeObj: Record<string, any> = {}
const getMethod = () => {
  const entries = Object.entries(inputRef.value)
  for (const [method, fn] of entries) {
    exposeObj[method] = fn
  }
}
onMounted(getMethod)
defineExpose(exposeObj)
</script>
❌
❌