林间有风

郭朝夕的日记小站

  • 首页
  • 关于
  • 标签
  • 归档

原始值的响应式方案

发表于 2024-03-01

在本篇文章中,将讨论在Vue中原始值的响应式方案。原始值指的是Boolean、Number、String、BigInt、Symbol、undefined和null等类型的值。在JavaScript中,原始值是按值传递的,而非按引用传递。这意味着如果一个函数接受原始值作为参数,那么形参与实参之间没有引用关系,它们两个是完全独立的值,对形参的修改不会影响实参。另外,JavaScript中的Proxy无法提供对原始值的代理,因此想要将原始值变为响应式数据,就必须对其做一层包裹,也就是我们接下来要介绍的ref。

引入 ref 的概念

由于Proxy的代理目标必须是非原始值,所以我们没有任何手段拦截原始值的操作。例如:

1
2
3
let str = 'hello'

str = 'wolrd'

对于这个问题,我们唯一能够想到的办法就是使用一个非原始值去“包裹”原始值。例如使用一个对象去包裹原始值:

1
2
3
4
5
6
7
const wrapper = {
value: 'hello',
}
const name = reactive(wrapper)
name.value // hello
// 修改值可以触发响应
name.value = 'world'

但这样做会导致两个问题:

  • 用户为了创建一个响应式的原始值,不得不顺带创建一个包裹对象;
  • 包裹对象由用户来定义,意味着不规范。

为了解决上面这两个问题,我们可以封装一个函数,将包裹对象的创建工作封装到这个函数中:

1
2
3
4
5
6
function ref(val) {
const wrapper = {
value: val,
}
return reactive(wrapper)
}

如上代码所示,我们把创建包裹对象的工作封装到ref函数内部,然后使用reactive函数将包裹对象变成响应式数据并返回,这样就解决了上面这两个问题。运行如下测试代码:

1
2
3
4
5
6
7
8
9
// 创建原始值的响应式数据
const refVal = ref(1)

effect(() => {
// 在副作用函数中通过value属性读取原始值
console.log(refVal.value)
})
// 修改值能够触发副作用函数的重新执行
refVal.value = 2

上面这段代码看似没什么问题了,其实并不是这样的。我们接下来要面临的第一个问题就是如何区分 refVal 到底是原始值的包裹对象,还是一个非原始值的响应式数据呢?

1
2
const refVal1 = ref(1)
const refVal2 = reactive({ value: 1 })

这段代码中的refVal1和refVal2在实现上并没有什么区别。但是我们有必要区分一个数据到底是不是 ref,因为涉及到了后面的自动脱 ref 能力。
想要区分一个数据是不是ref很简单。怎么做呢?如下面代码所示:

1
2
3
4
5
6
7
8
9
10
function ref(val) {
const wrapper = {
value: val,
}
// 使用Object.defineProperty()在wrapper对象上定义一个不可枚举的属性__v_isRef,并且值为true
Object.defineProperty(wrapper, '__v_isRef', {
value: true,
})
return reactive(wrapper)
}

我们使用Object.defineProperty为包裹对象wrapper定义了一个不可枚举且不可写的属性__v_isRef,它的值为true。代表这个对象是一个ref,而非一个普通对象。这样我们就可以通过检查__v_isRef属性来判断一个数据是不是ref了。

阅读全文 »

leetcode

发表于 2024-02-28

两数之和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const twoSum = (nums, target) => {
// 声明一个map 用来存储
// 它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
// Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。
// 如果你需要“键值对”的数据结构,Map 比 Object 更合适。
// map.size | map.set() map.get() map.has() map.delete() map.clear()
// map.keys() | map.entries() | map.values() | map.forEach()
let map = new Map()
for (let i = 0; i<nums.length;i++) {
// 计算target 和数组中每一个元素的差值
const differenceValue = target - nums[i]
// 如果map中已经有这个差值了,我们直接拿出来并且找到它的索引,和当前数值的索引,返回出去
if (map.has(differenceValue)) {
return [map.get(differenceValue), i]
}
// 把数组每一项存进map中去 以值,索引 2, 1 这样的形式存进去
map.set(nums[i], i)
}
return []
}

回文数

1
2
3
4
5
6
7
8
9
const isPalindrome = function(x) {
const arr = String(x).split("")
for (let i = 0; i<arr.length/2;i++) {
if (arr[i]!== arr[.length - (i + 1)]) {
return false
}
}
return true
}

罗马数字转整数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const romanToInt = function(s) {
const romanMap = {
'I': 1,
'V': 5,
'X': 10,
'L': 50,
'C': 100,
'D': 500,
'M': 1000,
'IV': 4,
'IX': 9,
'XL': 40,
'XC': 90,
'CD': 400,
'CM': 900
}
let result = 0
const romanSplit = s.match(/(CM)|(CD)|(XC)|(XL)|(IX)|(IV)|(\W)/g)
for (const v of romanSplit) {
result+=romanMap[v]
}
return result
}

最长公共前缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const longestCommonPrefix = function(strs) {
if (strs.length === 0 || strs === null) {
return ''
}
let prefix = strs[0]
for (let i = 0; i<prefix.length; i++) {
let c = prefix.charAt(i)
for (let j = 0; j < strs.length; j++) {
if (i === prefix.length || strs[j].charAt(i) !== c) {
return prefix.substring(0, i)
}
}
}
return prefix
}

删除有序数组中的重复项

1
2
3
4
5
6
7
8
9
10
11
12
13
const removeDuplicates = function(nums) {
if(nums.length === 0) {
return 0
}
let slow = 0
for (let fast = 0; fast < nums.length; fast++) {
if (nums[fast] !== nums[slow]) {
slow++
nums[slow] = nums[fast]
}
}
return slow + 1
}

移除元素

1
2
3
4
5
6
7
8
9
10
const removeElement = function(nums,val) {
let i = 0
for (let j = 0; j < nums.length; j++) {
if (nums[j] !== val) {
nums[i] = nums[j]
i++
}
}
return i
}

响应系统的作用与实现

发表于 2024-02-19

响应式数据与副作用函数

副作用函数

副作用函数指的是会产生副作用的函数,如下面代码所示:

1
2
3
function effect() {
document.body.innerText = 'hello vue3'
}

当effect函数执行的时候,它会设置 body 的文本内容,但是除了effect函数之外的任何函数都可以读取或者设置 body 的文本内容。也就是说,effect函数的执行会直接或者间接影响其他函数的执行,这个时候我们就说effect函数产生了副作用。

副作用很容易产生,例如一个函数修改了全局变量。这其实也是一个副作用,如下面代码所示:

1
2
3
4
5
let val = 1

function effect() {
val = 2
}

响应式数据
理解了什么是副作用函数,再来看看什么是响应式数据。 假设在一个副作用函数中读取了某个对象的属性:

1
2
3
4
const obj = { text: 'hello guozhaoxi' }
function effect() {
document.body.innerText = obj.text
}

副作用函数effect会设置body元素的innerText属性,它的值为obj.text,当obj.text发生变化的时候,我们希望副作用函数effect会重新执行:

1
obj.text = 'hello world' // 修改obj.text的值,同时希望effect函数能够重新执行

上面这句代码修改了字段 obj.text 的值,我们希望当值发生变化以后,副作用函数自动重新执行,如果能实现这个目标,那么对象obj就是响应式数据。

响应式数据的基本实现

阅读全文 »

Message组件

发表于 2024-02-03

Message 组件常用于主动操作后的反馈提示。下面是效果图
Message组件效果动图

看完了效果图以后,我们再来看看看如何在页面中使用我们的Message组件。

1
2
3
4
5
6
7
8
9
10
<template>
<TmButton @click="open">打开Message组件</TmButton>
</template>
<script lang="ts" setup>
import TmButton from '@/components/Button/Button.vue'
import { CreateMessage } from '@/components/Message/method.ts'
const open = () => {
CreateMessage({ message: 'hello world', duration: 0, showClose: true })
}
</script>

需求分析

Message 组件属性和方法
看完了上面的效果图以及调用Message组件的方式以后,我们来分析一下它可能具备哪些属性和方法。我写了以下几个属性、方法:

  • message 提示信息
  • duration 延时关闭提示框的时间,如果为 0 则需要手动关闭
  • showClose 是否显示关闭按钮
  • transitionName 过渡动画的名称
  • id 每一个 Message 组件实例的id
  • zIndex 每一个 Message 组件实例的zIndex
  • onDestory 每一个 Message 组件实例的销毁函数 从body中移除自己的 DOM
  • type 类型 不同的类型有不同的css风格

如何挂载和销毁
组件写好了,我们如何挂载到页面的<body></body>的内容中去,同时还要思考在用户点击了关闭按钮或者自动关闭以后如何从<body></body>中移除自身。在 vue 中提供了createApp和render两个函数,createApp有些重量了,因为它返回的是一个应用;而我们这里只是需要将我们写好的组件挂载到页面中去。所以选择使用render这个函数。 销毁自身的话可以调用组件内部的方法,实际上就是将控制组件显示、隐藏的变量visible设置为 false。然后再调用render将其相关的dom从
body中去除。

如何获取组件实例信息
通过效果图我们可以看出,Message组件之间是有一定距离的,这样显示的时候不会重叠在一起;那么我们就要思考如何在创建Message组件的时候让它移动一定的偏移量。那么我们就要拿到Message组件的实例信息。所以我们给每一个Message组件赋一个唯一的id,通过id找到对应的组件实例。在编写组件的时候,通过defineExpose向外导出需要用到的属性。比如组件底部的偏移量bottomOffset和控制组件显示、隐藏的visible。

如何添加动画效果
在Vue中,添加动画使用<Transition></Transition>标签。监听@afterLeave和@enter两个事件。在动画结束以后注销实例,在动画进入以后更新一下组件的高度,从而实现对应的css样式,避免Message组件之间重叠。

代码实现

阅读全文 »

阅读《Vue.js设计与实现》总结

发表于 2024-02-02

第一章:权衡的艺术

在这一章中,开头的一句话,描述了框架设计的精髓,这句话也是尤雨溪在多个开发者大会中经常提到的,那就是:框架的设计,本身就是一种权衡的艺术。

在本章中,我们首先讨论了命令式和声明式这两种范式的差异,其中命令式更加关注过程,而声明式更加关注结果。命令式理论上可以做到极致优化,但是用户要承受巨大的心智负担;而声明式能够有效减轻用户的心智负担,但是性能上有一定的牺牲,框架设计者要想办法尽量使性能损耗最小化。

接着讨论了虚拟DOM的性能,并给出了一个公式:声明式的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗。虚拟DOM的意义就在于使找出差异的性能消耗最小化。对比虚拟DOM、原生JavaScript、innerHTML三者操作页面的性能。发现与页面大小、变更部分的大小都有关系,除此之外,还与是创建页面还是更新页面也有关系。最后发现虚拟DOM是个不错的选择。

最后,介绍了运行时、编译时的相关知识,了解纯运行时、纯编译时以及两者都支持的框架各有什么特点,并总结出Vue.js3是一个编译时+运行时的框架,它在保持灵活性的基础上,还能通过编译手段分析用户提供的内容,从而进一步提升性能。

在这一章中,书中分别从三个方面来去分析了所谓权衡的艺术,到底是什么意思。

命令式和声明式

所谓命令式指的就是:关注过程的范式。而声明式指的就是:关注结果的范式。

张三的妈妈,让张三去买酱油。
那么对于张三而言,他就需要:拿钱、出门、下楼、进超市、拿酱油、付钱、回家。
而对于张三的妈妈来说,她完全不需要关心张三做了什么,只需要对张三说一声就可以了。

那么在这个例子中,张三就是一个典型的命令式,他需要完成整件事情的所有过程。

而张三的妈妈,就是典型的声明式,她不关心过程只关心结果。

那么这里大家来想一下,vue 是声明式的?还是命令式的?

对于 vue 而言,它的内部实现一定是 命令式 的,而我们在使用 vue 的时候,则是通过 声明式 来使用的。

也就是说: vue 封装了命令式的过程,对外暴露出了声明式的结果

性能与可维护性的权衡

阅读全文 »

Tooltip组件

发表于 2024-01-30

需求分析

Tooltip组件要实现的需求就是当我们点击或者鼠标移入的时候显示对应的文字提示。同时文字的内容、位置以及显示、隐藏的延迟时间还有是否使用过渡效果等。这里要注意的是我们使用popperjs来实现文字位置的展示。它的原理也很简单:通过计算和定位,将弹出元素放在与目标元素相关的位置上。另外popperjs还会监听窗口的变化,以便在窗口大小或滚动位置发生变化时,重新计算和定位弹出元素。那么我们先看一下Tooltip组件有哪些配置项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// types.ts
import { Placement, Options } from '@popperjs/core'
export interface TooltipProps {
content?: string
trigger?: 'hover' | 'click'
placement?: Placement
popperOptions?: Partial<Options>
transition?: string
openDelay?: number
closeDelay?: number
manual?: boolean
}

export interface TooltipEmits {
(e: 'visible-change', value: boolean): void
(e: 'click-outside', value: boolean): void
}

export interface TooltipInstance {
show: () => void
hide: () => void
}

TooltipProps定义了我们这个Tooltip组件可以接受哪些属性; TooltipEmits是我们定义的Tooltip组件的事件类型接口,有两个事件,一个是点击了元素以外区域,另一个是当显示、隐藏发生变化的时候;TooltipInstance是我们向外暴露一个实例,它的类型接口有两个方法分别是show和hide用来控制显示和隐藏。

阅读全文 »

组件测试

发表于 2024-01-25

需求分析

我们在写完组件以后,要对组件进行测试,前端测试框架较为出名的有mocha、jest和今天我们要用到的vitest。以Button组件为例来看看怎么使用vitest编写测试用例。

安装

1
2
3
npm instlal vitest -D

npm install @vue/test-utils -D

编写测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { describe, test, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from './Button.vue'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import IconVue from '../Icon/Icon.vue'

describe('Button.vue', () => {
test('basic button', () => {
const wrapper = mount(Button, {
props: {
type: 'primary'
},
slots: {
default: 'button'
}
})
console.log(wrapper.html())
expect(wrapper.classes()).toContain('tm-button--primary')
})
})

遇到的问题

当我们使用vitest测试上面这个用例的时候会遇到一个报错 document is not defined。这是因为在非浏览器环境下是没有document这个对象的,解决这个问题的一种常见方法就是使用jsdom库来模拟浏览器环境,为mount方法提供一个虚拟的document对象。那么我们来安装一下这个库,并且对vitest进行一些简单的配置。

1
2
3
4
5
6
7
npm install jsdom -D

npm install unplugin-vue-macros -D

npm install @vitejs/plugin-vue-jsx -D

npm install @vitejs/plugin-vue -D
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// <reference types="vitest" />

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import VueMacros from 'unplugin-vue-macros'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [
VueMacros.vite({
plugins: {
vue: vue(),
vueJsx: vueJsx(),
},
})
],
test: {
globals: true,
environment: 'jsdom'
}
})

Icon组件

发表于 2024-01-24

需求分析

Icon组件的核心其实就是一个i标签,引入了一些字体图标,再添加一些样式,当然也可以自定义css样式。按照惯例,我们先来写一下Icon组件所需要的属性以及他们的类型有哪些。除此之外呢,我们还要找一些合适的字体图标,那么@fortawesome/fontawesome-svg-core、@fortawesome/free-solid-svg-icons和@fortawesome/vue-fontawesome就很适合我们这个Icon组件。

代码实现

先来看看Icon组件可以接受哪些属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import type { IconDefinition } from '@fortawesome/fontawesome-svg-core'
export interface IconProps {
border?: boolean
fixedWidth?: boolean
flip?: 'horizontal' | 'vertical' | 'both'
icon: object | Array<string> | string | IconDefinition
mask?: object | Array<string> | string
listItem?: boolean
pull?: 'right' | 'left'
pulse?: boolean
rotation?: 90 | 180 | 270 | '90' | '180' | '270'
swapOpacity?: boolean
size?: '2xs' | 'xs' | 'sm' | 'lg' | 'xl' | '2xl' | '1x' | '2x' | '3x' | '4x' | '5x' | '6x' | '7x' | '8x' | '9x' | '10x'
spin?: boolean
transform?: object | string
symbol?: boolean | string
title?: string
inverse?: boolean
bounce?: boolean
shake?: boolean
beat?: boolean
fade?: boolean
beatFade?: boolean
spinPulse?: boolean
spinReverse?: boolean
type?: 'primary'| 'success'| 'warning'| 'danger'| 'info'
color?: string
}

组件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<i class="tm-icon" :class="{[`tm-icon--${type}`]: type}" :style="customStyles" v-bind="$attrs">
<font-awesome-icon v-bind="filteredProps"></font-awesome-icon>
</i>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { omit } from 'lodash-es'
import type { IconProps } from './types'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
defineOptions({
name: 'TmIcon',
inheritAttrs: false
})
const props = defineProps<IconProps>()
const filteredProps = computed(() => omit(props, ['type', 'color']))
const customStyles = computed(() => {
return props.color ? { color: props.color } : {}
})
</script>

Icon组件的代码结构也很简单,它的核心就是一个<i>标签,添加了一些属性和自定义的样式。

引入字体图标库

1
2
3
4
import { library } from '@fortawesome/fontawesome-svg-core'
import { fas } from '@fortawesome/free-solid-svg-icons'

library.add(fas)

在其他组件中使用Icon组件

在Button组件中使用

1
2
3
<!-- Button.vue -->
<Icon v-if="loading" :icon="spinner" spin></Icon>
<Icon v-if="icon" :icon="icon"></Icon>
1
2
3
4
5
6
7
8
<!-- 在App.vue中引入Button组件 -->
<script lang="ts" setup>
import TmButton from '@/components/Button/Button.vue'
</script>

<template>
<TmButton loading size="large"></TmButton>
</template>

Collapse组件

发表于 2024-01-22

需求分析

折叠面板组件,一般来说由两部分组成,它们分别是Collapse和CollapseItem。现在我们来想一想,它们可能会有哪些属性和方法。

Collapse

属性:

  • model-value / v-model:当前活动面板,在手风琴模式下是 string 类型,在其他模式下是 array,默认值是一个空数组。
  • accordion:是否手风琴模式,是一个 boolean 类型。

事件:

  • change:切换当前活动面板,参数在手风琴模式下是 string,在其他模式下是 array
  • update:modelValue:实现 v-model 要用到

插槽:

  • default:默认插槽,自定义默认内容

CollapseItem

属性:

  • name:唯一标识符,类型是 string 或者 number
  • title:面板的标题,类型是 string
  • disabled:是否禁用,类型是 boolean
    事件:
    无

插槽:

  • title:具名插槽,CollapseItem 的标题
  • default: 默认插槽,CollapseItem 的内容

代码实现

阅读全文 »

Button组件

发表于 2024-01-20

需求分析

Button组件大部分关注样式,没有交互。我们可以分析得到Button组件具体的属性列表:

  • type:不同的样式(primary、danger、warning、info、success)
  • size:不同的尺寸(large、small、default)
  • plain:是否为朴素按钮(样式的不同展现模式)
  • round:是否开启圆角
  • circle:是否圆形按钮
  • disabled:是否禁用
  • loading:是否为加载中状态
  • icon:图标组件

Button组件的本质

Button组件的本质其实就是class类名的组合

1
<button class="tm-button tm-button--primary tm-button--large is-plain is-disabled">按钮</button>

初始化项目

分析完了需求以后,我们就开始着手写代码,先使用脚手架工具初始化一个vite + vue3 + typescript + eslint的项目。

1
2
3
4
5
6
7
npm create vue@latest
#or
pnpm create vue@latest
#or
yarn create vue@latest
#or
bun create vue@latest

确定项目文件结构

  • components
    • Button
      • Button.vue 组件
      • style.css 样式
      • types.ts 一些辅助的typescript类型
      • Button.test.ts 测试文件
阅读全文 »
上一页1…345…7下一页

62 日志
19 标签
© 2025 郭朝夕
冀ICP备18008237号