阅读视图
初探 Vue 3响应式源码(五):Ref
为什么需要ref
我们前两章讲讲解了reactive源码解析和effect源码解析,并且知道了它们是如何实现响应式的,还没看过的小伙伴可以先阅读一下。
我们回顾一下,reactive
函数可以创建通过Proxy
实现的响应式对象,响应式对象需要在effect
中使用才能收集到依赖,在更改响应式对象时,代理会通过trigger
通知所有依赖的effect
对象,并执行effect
的监听方法。
正因为reactive
创建的响应式对象是通过Proxy
来实现的,所以传入数据不能为基础类型,比如number
、string
、boolean
。
什么是ref
ref
对象是对reactive
不支持的数据的一个补充,让如基础数据响应式进行支持,以及更方便的对象替换操作推出的。下面我们先了解一下ref
的特性。
-
使用
ref
或shallowRef
函数创建ref
对象,ref
通过value
属性进行访问和修改传入参数。 -
与
reactive
不同,ref
的参数没有任何限制。 -
使用
reactive
可接受的对象为ref
参数对象时,isReactive(ref.value)
为true
。 -
ref
在effect
监听函数中使用可响应式 -
ref
在effect
中只有value
属性是可响应式的 -
customRef
可以创建自定义getter
、setter
的ref
,创建时需要提供一个创建get, set
工厂方法,工厂方法会传入收集方法和触发方法,由用户主动触发。如let value = 1 const custom = customRef((track, trigger) => ({ get() { track() return value }, set(newValue: number) { value = newValue trigger() } }))
-
使用
toRef
可以通过proxy
的某个属性生成为可以有默认值的ref
对象 -
使用
toRefs
可以通过proxy
的数据结构以及所有属性,生成与proxy
数据结构一致的,所有属性值为ref
对象的对象
综合上面的特性和之前讲解effect
的实现原理,能猜得到ref
对象会对value
属性的修改和获取时进行拦截,在value
被get
的时候收集依赖,在set
的时候获取依赖关联的effect
再触发依赖函数。ref
对属性修改和获取时不能通过proxy
来实现,ref
支持基础类型而proxy
不支持。收集依赖时不能使用effect
文件中的targetMap
关联effect
,targetMap
是WeakMap
类型,WeakMap
类型仅支持对象作为key
,不支持基础类型。
ref和shallowRef的具体实现
接下来我们看看ref
和shallowRef
的具体实现:
// 是否是ref根据属性的__v_isRef决定
export function isRef(r: any): r is Ref {
return Boolean(r && r.__v_isRef === true)
}
export function ref(value?: unknown) {
return createRef(value, false)
}
export function shallowRef(value?: unknown) {
return createRef(value, true)
}
// 创建ref对象,传入raw和是否是shallow
function createRef(rawValue: unknown, shallow: boolean) {
// 如果之前时ref则直接返回
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
与reactive
一样ref
创建的响应式对象也分为是否是shallow
,ref
对象支持对value
深度响应式,也就是说ref.value.a.b
中的修改都能被拦截,shallowRef
对象只支持对value
值的响应式。
ref
和shallowRef
函数都使用createRef
来创建ref
对象,只是参数的区别。创建的ref
对象会附加__v_isRef
属性来标识是否是ref
对象。在创建ref
对象之前会检查入参是否是ref
如果是就直接返回入参参数。
我们看到ref
函数创建的真实对象是RefImpl
,采用了class
写法,将raw
和shallow
作为构造函数,下面我们看看这个class
的实现:
// Ref对象类
class RefImpl<T> {
// 存放 reactive(raw) 后的proxy
private _value: T
// 存放 raw
private _rawValue: T
// 建立与effect的关系
public dep?: Dep = undefined
// 是否ref的标识
public readonly __v_isRef = true
// 构造,传入raw 和 shallow
constructor(value: T, public readonly _shallow: boolean) {
// 存储 raw
this._rawValue = _shallow ? value : toRaw(value)
// 如果是不是shallow则 存储 reactive proxy 否则存储传入参数
this._value = _shallow ? value : toReactive(value)
}
// getter value拦截器
get value() {
// track Ref 收集依赖
trackRefValue(this)
return this._value
}
// setter value拦截器
set value(newVal) {
// 如果是需要深度响应的则获取 入参的raw
newVal = this._shallow ? newVal : toRaw(newVal)
// 查看要设置值是否与当前值是否修改
if (hasChanged(newVal, this._rawValue)) {
// 存储新的 raw
this._rawValue = newVal
// 更新value 如果是深入创建的还需要转化为reactive代理
this._value = this._shallow ? newVal : toReactive(newVal)
// 触发value,更新关联的effect
triggerRefValue(this, newVal)
}
}
}
如果不是shallow
传入的value
会通过toReactive
转化为reactive
,然后存在ref._value
中。在get
的时候直接返回这个reactive
,这就是使用reactive
可接受的对象为ref
参数对象时,isReactive(ref.value)
为true
的原因,也是为什么能深度响应的原因。
ref
还会存储入参和set
的原始值,如果不是shallow
则通过toRaw
获取,存储在_rawValue
属性中,存储这个值是为了能正确的判断值是否被修改。所以下方这种情况是不会调用triggerRefValue
的,因为原始值是一样的。
const target = { name: 'bill' }
const reTarget = reactive(target)
const targetRef = ref(reTarget)
targetRef.value = target
ref
对象还有个非常重要的属性dep
,reactive
对象是通过targetMap
与Dep
关联的。reactive
收集时通过track
函数获取dep
,然后通过dep
对象调用trackEffects
函数来将effect
与Dep
关联。
reactive
触发时通过trigger
函数整理相关联的多个dep
最终合并成一个dep
,然后通过dep
调用triggerEffects
获取关联的effect
收集函数并触发。
dep
中的具体细节管理是通过trackEffects
函数和effect
对象管理的,将dep
与effect
是由trackEffects
函数处理的, 触发是由triggerEffects
函数执行的。
也就是说基于现有effect
的基础上,创建响应式对象只需要收集时获取dep
并调用trackEffects(dep)
, 触发时获取收集时的dep
并调用triggerEffects(dep)
。 dep
属性就是ref
能成为响应式对象的根本原因。
接下来我们看看ref
是如何实现trackEffects(dep)
和triggerEffects(dep)
的。ref
在get value
时会调用trackRefValue
,在set value
时,如果value
值发生了更改则调用triggerRefValue
。可以猜到这两个方法就是实现响应式的关键,接下来我们看看他们的具体实现
// 收集 ref 依赖 调用trackEffects(dep)
export function trackRefValue(ref: RefBase<any>) {
// 如果当前开启了跟踪
if (isTracking()) {
// 获取raw ref数据
ref = toRaw(ref)
// 如果当前ref还未初始化dep则创建
if (!ref.dep) {
ref.dep = createDep()
}
// 如果是开发环境,则传入track细节,
if (__DEV__) {
trackEffects(ref.dep, {
target: ref,
type: TrackOpTypes.GET,
key: 'value'
})
} else {
trackEffects(ref.dep)
}
}
}
// 触发 ref 调用trackEffects(dep)
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
// 获取raw ref数据
ref = toRaw(ref)
// 如果当前ref 有关联的dep
if (ref.dep) {
// 如果当前是开发环境则发送具体触发细节
if (__DEV__) {
triggerEffects(ref.dep, {
target: ref,
// SET引起的变化
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal
})
} else {
triggerEffects(ref.dep)
}
}
}
trackRefValue
会在适当的时候初始化dep
并调用trackEffects
,triggerRefValue
会获取ref
的dep
并调用triggerEffects
,就是我们上面说的内容。
大家注意到传入的ref
会调用toRaw
方法来重新赋值,这个方法是获取reactive
的原始数据的。因为用户可能使用reactive(ref(raw))
来获取数据,如果直接使用可能会收集到dep
属性的依赖。另外大家思考一下下面这段代码的effect
监听函数会触发几次?
const countRef = ref(0)
const reCount = reactive(countRef)
effect(() => {
console.log(reCount.value)
})
reCount.value = 3
答案是四次,第一次是首次收集依赖,reactive
会收到value
的获取,存储value
属性的dep
附加到targetMap
。然后调用ref.value
,ref
在获取value
时会调用trackRefValue
,创建dep
附加到自身属性上。注意ref.value
返回this._value
,这时候reactive
收到_value
属性的获取,存储_value
属性的dep
,附加到targetMap
中。所以创建了三个dep
。当发生更改新值存储到ref._value
中,而对于reactive
来说value
和_value
是完全没关联的所以会触发两次,而ref
自身会触发一次没所以一共是四次。
customRef
接下来我们看看自定义ref
方法customRef
是如何实现的:
// 自定义ref对象类
class CustomRefImpl<T> {
// 依赖dep 存储effets
public dep?: Dep = undefined
// 缓存getter setter
private readonly _get: ReturnType<CustomRefFactory<T>>['get']
private readonly _set: ReturnType<CustomRefFactory<T>>['set']
// mark ref
public readonly __v_isRef = true
// 传入ref工厂函数
constructor(factory: CustomRefFactory<T>) {
// 构建getter setter,传入track trigger函数
const { get, set } = factory(
() => trackRefValue(this),
() => triggerRefValue(this)
)
this._get = get
this._set = set
}
get value() {
return this._get()
}
set value(newVal) {
this._set(newVal)
}
}
// 创建自定义ref
export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
return new CustomRefImpl(factory) as any
}
customRef
会创建CustomRefImpl
的一个实例并返回,CustomRefImpl
的实现和Ref
差不多,使用trackRefValue
和triggerRefValue
将dep
与effect
关联实现响应式。不过CustomRefImpl
会在工厂函数中传入trackRefValue
和triggerRefValue
,将收集依赖和触发执行权交给用户。让用户在适当的时候调用。在使用value
时候调用生产的get
方法,在设置value
是调用生产的set
方法。一般是在get
的时候调用收集函数,set
的时候触发函数。
toRefs和ObjectRefImpl
我们在使用reactive
时通过缓存属性值很可能会失去响应式特性。因为属性值可能是reactive
不支持深入响应的值,这时候缓存属性值,或者是通过ES6
解构出来的值是不具备响应特性的。比如在下面这两种使用方式:
const reuser = reactive({ name: 'bill', sex: '男' })
const { name } = reuser
const sex = reuser.sex
这样的话就得一直使用reuser.name
的方式来进行访问,vue
有两个api
能很好的解决这个问题,就是toRef
和toRefs
。
toRef
通过reactive
代理和代理的某个属性生成为ref
并且可以携带默认值。而toRefs
根据reactive
代理生成所有属性值为ref
的对象。生成的ref
的value
是代理属性值的映射,两端更改都会实时同步,我们看看是如何使用的:
const reuser = reactive({ name: 'bill', sex: '男' })
const name = toRef(reuser, 'name', '未命名')
const { sex } = toRefs(reuser)
console.log(reuser.name, reuser.sex) //bill 男
console.log(name.value, sex.value) //bill 男
name.value = 'lzb'
sex.value = '女'
console.log(reuser.name, reuser.sex) //lzb 女
console.log(name.value, sex.value) //lzb 女
delete reuser.name
console.log(reuser.name, reuser.sex) //undefined 女
console.log(name.value, sex.value) //未命名 女
接下来我们看看toRef
和toRefs
的具体实现:
// 将proxy对象和目标的属性 转化为ref 并拥有默认值
class ObjectRefImpl<T extends object, K extends keyof T> {
public readonly __v_isRef = true
constructor(
// 记录代理
private readonly _object: T,
// 要辅助的key
private readonly _key: K,
// 默认值
private readonly _defaultValue?: T[K]
) {}
// 获取value
get value() {
const val = this._object[this._key]
return val === undefined ? (this._defaultValue as T[K]) : val
}
set value(newVal) {
this._object[this._key] = newVal
}
}
// 将proxy对象和目标的属性 转化为ref 并拥有默认值
export function toRef<T extends object, K extends keyof T>(
object: T,
key: K,
defaultValue?: T[K]
): ToRef<T[K]> {
const val = object[key]
return isRef(val)
? val
: (new ObjectRefImpl(object, key, defaultValue) as any)
}
// 将proxy对象所有属性转化为ref值
export function toRefs<T extends object>(object: T): ToRefs<T> {
// 只有内部代理才能toRefs
if (__DEV__ && !isProxy(object)) {
console.warn(`toRefs() expects a reactive object but received a plain one.`)
}
// 分别对所有属性toRef
const ret: any = isArray(object) ? new Array(object.length) : {}
for (const key in object) {
ret[key] = toRef(object, key)
}
return ret
}
toRef
会先查看proxy[key]
是否是ref
如果是的话直接返回,如果不是则创建ObjectRefImpl
并且将参数传入,ObjectRefImpl
会标识当前对象是ref
类型 (通过__v_isRef
属性) ,并且缓存proxy
、key
和默认值。get value
时直接通过proxy[key]
来获取并返回,如果回去的值是undefined
则使用默认值。set value
时则通过proxy[key] = newVal
来设置。
toRefs
则是将每个属性都调用一次没有默认值的toRef
,并且返回与proxy
一致的数据结构。
为什么这里的ObjectRefImpl
类不需要dep
属性和收集依赖和触发更改呢?这是因为_object
属性本身是proxy
类型,当我们在使用proxy[key]
就实现了收集依赖,在proxy[key] = newVal
是就触发了更改。
其他辅助方法
ref
文件中还声明了其他辅助方法,比如triggerRef
手动触发ref
的更改使关联的effect
重新执行收集函数;unref
获取ref
的原始值。这两个方法比较简单直接看源码即可,这里就不再讲解了。
// 手动触发 ref
export function triggerRef(ref: Ref) {
// 开发环境用当前值做最新值变化
triggerRefValue(ref, __DEV__ ? ref.value : void 0)
}
// 解构ref,直接返回value
export function unref<T>(ref: T | Ref<T>): T {
return isRef(ref) ? (ref.value as any) : ref
}
还有一个辅助方法proxyRefs
,这个方法将一个对象直属属性内的所有ref
属性值解构访问 (不需要通过value
下标访问) 。什么是直属属性就是第一层属性,比如下方的代码:
const name = ref('bill')
const unUser = proxyRefs({
name: name,
adderss: {
city: ref('珠海')
}
})
console.log(unUser.name) // bill
console.log(unUser.address.city) // Ref
unUser.name = 'lzb'
console.log(unUser.name) // lzb
proxyRefs
只对第一层属性的ref
解构。我们看看它的源码:
// 浅解构ref处理器
const shallowUnwrapHandlers: ProxyHandler<any> = {
// getter将unref方便访问
get: (target, key, receiver) => {
return unref(Reflect.get(target, key, receiver))
},
// setter先查看是否是ref,如果是则更新value
set: (target, key, value, receiver) => {
const oldValue = target[key]
// 如果新数据不是ref但旧数据是则更新value
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
} else {
return Reflect.set(target, key, value, receiver)
}
}
}
// 创建代理ref 解构方便使用
export function proxyRefs<T extends object>(
objectWithRefs: T
): ShallowUnwrapRef<T> {
// 如果是reactive对象则无需解构
return isReactive(objectWithRefs)
? objectWithRefs
: new Proxy(objectWithRefs, shallowUnwrapHandlers)
}
当proxyRefs
入参是reactive
对象时则直接返回,reactive
对象本身会对ref
解构,而且是深度的,这里就不需要处理。有个特殊情况shallowReactive
对象不会对ref
解构,但是也直接返回了,也就是说这个方法对shallowReactive
对象时无效的!
如果proxyRefs
入参不是reactive
对象,则创建代理,get
拦截器通过unref
来获取值返回,set
拦截器通过判断当前要更新的是否是ref
如果是则更新value
。
到这里我们ref
的所有内容就已经讲完了,接下来日常小结。
小结
ref
对象自身附加了dep
,在收集依赖时通过trackEffects
函数,触发时通过triggerEffects
函数ref
能够创建深度响应式是依赖了reactive
Proxy
代理对象可以通过toRef
和toRefs
辅助方法保持对单个属性的引用,赋值修改会映射到Proxy
customRef
函数可以创建自由度极高的响应式对象
如何使用javascript模拟鼠标滚动事件?我在自动化测试中的实践与坑
最近在做一些自动化测试的时候,遇到了一个需求:需要模拟用户在浏览网页时的滚动操作。我的目标是通过 JavaScript 模拟鼠标滚动事件,从而触发页面上的懒加载或滚动监听器。我发现页面的内容区域是通过一个有固定高度的容器滚动的,
window.scrollTop
完全没有任何反应。我才意识到,不能总是监听 window 的滚动,很多情况下我们监听的是某个具体的元素。又引发了一些新的问题,能不能触发事件,但是不滚动元素?如何判断是否已经滚动到了页面的最底部?如何判断一个元素是否可以滚动呢?
场景一:直接调用滚动API,实现页面或元素的实际滚动
在我进行自动化测试或实现某些交互效果时,最常用、最直接的做法就是调用浏览器提供的滚动API,比如 scrollBy
、scrollTo
或 scrollIntoView
。这些 API 无需模拟用户行为,直接改变页面或元素的滚动位置,具备良好的兼容性和控制力。
下面是我经常用的一个简单例子,用于让页面平滑滚动:
const deltaY = 100;
window.scrollBy({
top: deltaY,
left: 0,
behavior: 'smooth' // 可选参数,实现平滑滚动
});
这个方法的原理很简单:直接改变页面或元素的滚动位置,适合控制页面滚动条、实现锚点跳转、滚动动画等需求。也可以替换掉 window
,针对具体的 DOM 元素进行滚动:
const container = document.getElementById('scrollableContainer');
container.scrollTop += 100;
但是,会不会触发 scroll 事件?:
这点经常被误解。其实只要滚动位置发生了变化(即 scrollTop 或 scrollLeft 改变),scroll
事件就会触发,无论是用户滚动还是代码控制。
📚 官方说明:
- MDN - Element.scrollTo():“This method triggers the scroll event if the element’s scroll position changes.”
- MDN - scroll event:“The scroll event is fired when the document view or an element has been scrolled.”
但是有时候会在调用滚动 API 后未看到事件触发,我给大家一些思考的方向:
-
scroll
事件绑定的元素跟实际触发滚动的元素不是同一个元素 - 元素本身没有实际发生滚动;
- 滚动事件被节流(debounce/throttle)过滤了;
- 懒加载逻辑使用的是
IntersectionObserver
而不是scroll
。
这就引出了我思考的问题:如果我只是想触发滚动监听器,而不是实际滚动页面,应该怎么做?
场景二:模拟滚动事件,触发监听器但不移动页面
后来我发现,在某些测试场景下,我们只需要“让代码以为用户滚动了”,并不要求实际滚动页面。比如测试埋点逻辑是否触发、验证动画联动是否响应,或者检查某些组件在监听滚动时的表现。这种情况下,用事件模拟是更优雅的选择。
我们可以通过构造 WheelEvent
的方式,来模拟鼠标滚动操作,并显式触发:
function simulateMouseScroll(element, deltaY) {
// 创建一个模拟的WheelEvent
var event = new WheelEvent('wheel', {
deltaY: deltaY, // 正数表示向下滚动,负数表示向上滚动
bubbles: true, // 事件是否应该冒泡
cancelable: true, // 事件是否可以取消
});
// 触发这个事件
element.dispatchEvent(event);
}
// 使用示例
var myElement = document.getElementById('yourElementId');
simulateMouseScroll(myElement, 100); // 模拟向下滚动100个
注意:
模拟 WheelEvent
只会触发 wheel
事件监听器,不会自动引起元素滚动,也不会触发 scroll
事件,除非你在 wheel
的处理器中显式调用了 scrollTo
/ scrollBy
等方法。
📚 官方说明:
MDN - WheelEvent :“Dispatching a
WheelEvent
manually does not automatically scroll the page unless your code explicitly does so.”
在现代浏览器中,为了防止脚本模拟用户行为带来安全问题,WheelEvent
可能被限制,无法完全模拟真实的滚动行为。因为这些事件通常由用户交互触发,且浏览器的安全策略可能会阻止非用户触发的这类事件。
请注意,由于安全策略和浏览器差异,这种方法可能不会在所有场景下都能按预期工作,尤其是当涉及到跨域iframe或受到严格安全策略限制的环境。在实际应用中,应当首先考虑使用元素的滚动属性和方法来直接改变滚动位置。
两种滚动模拟方式,选哪种?
- 想让页面真的滚动,就用原生滚动 API(推荐)。
-
只想触发
wheel
相关逻辑(如交互响应、监听测试),可以模拟事件。 - 注意:懒加载通常依赖的是
IntersectionObserver
,而不是scroll
事件。
滚动相关的常见判断逻辑:我怎么判断滚动到底了?
另一个让我经常用到的逻辑是:页面是否滚动到底了?
最常见的场景是懒加载、无限滚动、滚动触底提示等。我的实现是比较常规的三段式判断:当前滚动距离 + 可视区域高度 与页面总高度的对比。
function isBottomReached() {
// 获取窗口的滚动位置
var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
// 获取整个文档的高度(可视区域加上滚动出去的部分)
var docHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
// 获取可视窗口的高度
var winHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
// 判断是否滚动到底部
return scrollTop + winHeight >= docHeight;
}
配合滚动事件监听器使用,就能实现动态判断了:
window.addEventListener("scroll", function() {
if (isBottomReached()) {
console.log("滚动到了页面最底部!");
// 可以在这里触发懒加载、打点等逻辑
}
});
怎么判断某个元素是否支持滚动?
还有一个需求也很常见——我需要判断一个元素是否具有滚动能力。这个判断在做组件封装时尤其有用,比如判断内容区域是否超出容器、是否该展示滚动条或加载更多。
以下是我自己封装的判断函数:
function canElementScroll(selector) {
// 根据传入的selector(类名或ID)查找DOM元素
const element = document.querySelector(selector);
// 检查是否找到了元素
if (!element) {
console.error(`无法找到匹配的元素:${selector}`);
return false;
}
// 检查元素是否可以在水平方向上滚动
const canScrollHorizontally = element.scrollWidth > element.clientWidth;
// 检查元素是否可以在垂直方向上滚动
const canScrollVertically = element.scrollHeight > element.clientHeight;
// 如果元素可以在任一方向上滚动,则返回true
return canScrollHorizontally || canScrollVertically;
}
// 使用示例
// 通过类名判断
const canScrollByClass = canElementScroll('.scrollable-class');
if (canScrollByClass) {
console.log('通过类名找到的元素支持滚动');
} else {
console.log('通过类名找到的元素不支持滚动');
}
// 通过ID判断
const canScrollById = canElementScroll('#scrollable-id');
if (canScrollById) {
console.log('通过ID找到的元素支持滚动');
} else {
console.log('通过ID找到的元素不支持滚动');
}
你可以传入 .class
或 #id
来判断任意元素的滚动能力。
一分钟解决一道高频面试算法题——盛水最多的容器(双指针最优解)
一分钟解决一道高频面试算法题——移动零(双指针)
前置知识(可跳过)
1.splice()
方法
splice()
方法用于修改数组的内容,可以删除、替换或添加元素。它会直接修改原始数组。
语法:
array.splice(start, deleteCount, item1, item2, ...)
参数:
-
start
: 必需。指定修改开始的位置(索引)。- 如果
start
超出数组的长度,则从数组末尾开始添加元素。 - 如果
start
是负数,则从数组末尾开始计算,例如-1
表示数组的最后一个元素。
- 如果
-
deleteCount
: 可选。指定要删除的元素数量。- 如果
deleteCount
为 0,则不删除任何元素。 - 如果
deleteCount
大于start
之后的元素总数,则从start
开始删除所有元素。 - 如果省略
deleteCount
,则从start
开始删除所有元素。
- 如果
-
item1, item2, ...
: 可选。要添加到数组中的元素。从start
位置开始插入。
返回值:
splice()
方法返回一个包含被删除元素的数组。如果没有删除任何元素,则返回一个空数组。
2.push()
方法
push()
方法用于将一个或多个元素添加到数组的末尾,并返回修改后的数组的新长度。
语法:
array.push(item1, item2, ...);
参数:
-
item1, item2, ...
: 要添加到数组末尾的元素。
返回值:
push()
方法返回修改后的数组的新长度。
二、题目描述——移动零
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:
输入: nums = [0]
输出: [0]
提示:
1 <= nums.length <= 104
-231 <= nums[i] <= 231 - 1
进阶: 你能尽量减少完成的操作次数吗?
三、题解
本人错误解法
var moveZeroes = function(nums) {
let count = 0;
for(let i=0;i<nums.length;i++){
if(nums[i]===0){
nums.splice(i,1);
count++;
}
}
for(let i=0; i<count;i++){
nums.push(0)
}
return nums;
};
为何错误
1.splice
导致索引错乱: 这是最主要的问题。 当你在循环中使用 splice(i, 1)
删除一个元素后,数组的长度会减小,并且从 i
之后的所有元素的索引都会向前移动一位。 这会导致以下问题:
- **跳过元素:** 如果 `nums[i]` 是 0, 你删除了它。然后,原来在 `nums[i+1]` 的元素会移动到 `nums[i]`。 但是,循环会直接进入 `i+1` 的下一轮迭代。 这样就跳过了原来 `nums[i+1]` (现在在 `nums[i]` 的位置) 的元素,导致可能没有正确处理它。
- **越界访问:** 极端情况下,当数组末尾有连续的 0 时, 可能会导致数组越界访问。
2.未完全满足原地操作的要求: 虽然结果是在原数组上修改的,但在循环中使用 splice
操作涉及到数组的重构,严格意义上来说,不算是最高效的原地操作 (虽然题目没有明确禁止使用)。
正确题解一
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
var moveZeroes = function(nums) {
let insertPos = 0; //指向下一个要插入非零元素的位置
// 第一次遍历:将所有非零元素移动到数组的前面
for (let i = 0; i < nums.length; i++) {
if (nums[i] !== 0) {
nums[insertPos] = nums[i];
insertPos++;
}
}
// 第二次遍历:将数组剩余的位置填充为 0
while (insertPos < nums.length) {
nums[insertPos] = 0;
insertPos++;
}
};
-
思路:
- 把所有非零的元素挪到数组的前面,保持它们的相对顺序。
- 剩下的位置都用零填充。
-
具体步骤:
1.第一次遍历(收集非零元素) :
-
insertPos
相当于一个指针,它始终指向下一个应该放置非零元素的位置。 - 当
nums[i]
不是 0 时,把它放到nums[insertPos]
的位置上。 然后,insertPos
向后移动一位,准备存放下一个非零元素。 - 循环结束后,
insertPos
前面的所有元素都变成了非零元素,并且保持了它们在原数组中的相对顺序。 关键是,insertPos
也记录了非零元素有多少个。
2.第二次遍历(填充零) :
- 从
insertPos
开始到数组的末尾,把所有元素都设置为 0。 因为第一次遍历后,insertPos
指向的位置以及后面的所有位置,原来都是零或者被非零元素覆盖掉的位置,现在应该填充为零.
-
正确题解二(最优题解)
function moveZeroes(nums) {
if (!nums || nums.length === 0) {
return;
}
let left = 0;
for (let right = 0; right < nums.length; right++) {
if (nums[right] !== 0) {
if (left !== right) { // 避免不必要的交换
nums[left] = nums[right];
nums[right] = 0; // 直接将 right 位置设为 0
}
left++;
}
}
}
-
核心思路:
- 使用两个指针
left
和right
,left
指向下一个非零元素应该放置的位置,right
用于遍历数组。 -
right
指针遍历数组遇到非零元素时,将其移动到left
指针指向的位置。 - 移动后,
right
原来的位置设置为0。 这就保证了所有非零元素都在数组的前面,而所有零都在数组的后面。 -
关键优化: 避免不必要的交换。 只有当
left
和right
指针不相等时,才进行交换操作。 这可以减少当非零元素已经在正确位置上时的操作。
- 使用两个指针
-
具体步骤分解:
-
初始化:
-
left = 0
:left
指针初始化为 0,表示数组的起始位置,也就是下一个非零元素应该放的位置。
-
-
遍历数组:
-
right
指针从数组的起始位置开始,向右移动,遍历整个数组。 -
如果
nums[right] !== 0
(遇到非零元素) :-
检查是否需要交换:
if (left !== right)
。 如果left
和right
指向不同的位置,说明right
指针当前指向的非零元素需要移动到left
指针指向的位置。 -
交换元素:
nums[left] = nums[right]; nums[right] = 0;
将right
指针指向的非零元素移动到left
指针指向的位置,并将right
指针指向的位置设置为 0。 这一步是关键,它保证了零被移动到数组的末尾。 -
left++
:left
指针向右移动一位,指向下一个非零元素应该放置的位置。
-
检查是否需要交换:
-
-
遍历完成: 当
right
指针遍历完整个数组后,所有非零元素都被移动到了数组的前面,而所有零都被移动到了数组的末尾。
-
-
为什么这个解法更优?
- 一次遍历: 只需要一次遍历即可完成操作,提高了效率。
-
减少交换次数: 通过
if (left !== right)
的判断,避免了不必要的交换操作,进一步提高了效率。 例如,如果数组前几个元素都是非零元素,left
和right
会同步前进,此时不需要进行任何交换。 - 原地修改: 直接在原数组上操作,空间复杂度为 O(1)。
-
对比前一个解法
- 前一个解法使用了两次遍历,第一次遍历找到非零元素并移动,第二次遍历填充零。
- 这个解法只使用一次遍历,通过一次遍历完成元素的移动和零的填充。
实例与展示
四、结语
再见
webpack 检出图 第 四 节 ChunkGraph.js
🔧 模块资源类型相关(SourceTypes)
-
setChunkModuleSourceTypes(chunk, module, sourceTypes)
为某个 chunk 中的某个模块设置sourceTypes
,并更新缓存。 -
getChunkModuleSourceTypes(chunk, module)
获取某个 chunk 中某个模块的sourceTypes
,优先从缓存中取,默认使用模块自身的值。 -
getModuleSourceTypes(module)
获取某个模块在其所有 chunk 中合并后的sourceTypes
,用于跨 chunk 判断。 -
_getOverwrittenModuleSourceTypes(module)
内部方法:合并所有包含该模块的 chunk 中的sourceTypes
,如果类型不同就合并成一个新集合。
📦 Chunk 模块集合操作
-
getOrderedChunkModulesIterable(chunk, comparator)
获取某个 chunk 的模块集合(可迭代),并按给定排序函数排序。 -
getOrderedChunkModulesIterableBySourceType(chunk, sourceType, comparator)
获取指定 chunk 中指定sourceType
类型的模块集合(可迭代、已排序)。 -
getChunkModules(chunk)
获取 chunk 中所有模块的数组(无序、缓存版本,不可修改)。 -
getOrderedChunkModules(chunk, comparator)
获取 chunk 中所有模块的数组(有序、缓存版本,不可修改)。
🔗 Chunk 模块 ID/Hash 映射
-
getChunkModuleIdMap(chunk, filterFn, includeAllChunks = false)
遍历 chunk 的所有异步子 chunk(或所有引用 chunk),获取符合条件的模块 ID 映射表。
格式:{ chunkId: [moduleId, ...] }
-
getChunkModuleRenderedHashMap(chunk, filterFn, hashLength = 0, includeAllChunks = false)
获取 chunk 的模块 ID 到模块渲染 hash 的映射表,可截断 hash 长度。
格式:{ chunkId: { moduleId: hash } }
/**
* 设置指定 chunk 中指定模块的 sourceTypes(资源类型集合)
* @param {Chunk} chunk chunk 实例
* @param {Module} module 模块实例
* @param {Set<string>} sourceTypes 模块的资源类型集合
*/
setChunkModuleSourceTypes(chunk, module, sourceTypes) {
// 获取与 chunk 相关联的内部结构
const cgc = this._getChunkGraphChunk(chunk);
// 如果还未初始化 sourceTypesByModule,就创建一个 WeakMap
if (cgc.sourceTypesByModule === undefined) {
cgc.sourceTypesByModule = new WeakMap();
}
// 设置模块对应的资源类型集合
cgc.sourceTypesByModule.set(module, sourceTypes);
// 通过 modulesBySourceType 工具函数重新计算反向映射缓存
cgc._modulesBySourceType = modulesBySourceType(cgc.sourceTypesByModule);
}
/**
* 获取某个 chunk 中某个模块的 sourceTypes
* @param {Chunk} chunk chunk 实例
* @param {Module} module 模块实例
* @returns {SourceTypes} 模块对应的资源类型集合
*/
getChunkModuleSourceTypes(chunk, module) {
const cgc = this._getChunkGraphChunk(chunk);
// 如果没有缓存映射,则返回模块默认的资源类型
if (cgc.sourceTypesByModule === undefined) {
return module.getSourceTypes();
}
// 否则优先使用缓存的 sourceTypes,如果不存在则 fallback
return cgc.sourceTypesByModule.get(module) || module.getSourceTypes();
}
/**
* 获取某个模块所有 chunk 中合并后的 sourceTypes
* @param {Module} module 模块实例
* @returns {SourceTypes} 合并后的资源类型集合
*/
getModuleSourceTypes(module) {
// 优先返回合并覆盖值,否则返回模块默认 sourceTypes
return (
this._getOverwrittenModuleSourceTypes(module) || module.getSourceTypes()
);
}
/**
* 遍历所有包含该模块的 chunk,合并其 sourceTypes
* @param {Module} module 模块实例
* @returns {Set<string> | undefined} 合并后的资源类型集合
*/
_getOverwrittenModuleSourceTypes(module) {
let newSet = false; // 是否需要创建新的 Set 以合并 sourceTypes
let sourceTypes; // 合并后的结果
// 遍历包含该模块的所有 chunk
for (const chunk of this.getModuleChunksIterable(module)) {
const cgc = this._getChunkGraphChunk(chunk);
// 如果没有定义 sourceTypesByModule,则说明未设置,直接返回
if (cgc.sourceTypesByModule === undefined) return;
const st = cgc.sourceTypesByModule.get(module); // 获取该 chunk 上该模块的 sourceTypes
if (st === undefined) return;
if (!sourceTypes) {
// 第一次赋值,直接使用引用
sourceTypes = st;
continue;
} else if (!newSet) {
// 如果之前未创建新 Set,检查是否需要扩展集合
for (const type of st) {
if (!sourceTypes.has(type)) {
// 一旦发现有新增的类型,就新建 Set 并标记
newSet = true;
sourceTypes = new Set(sourceTypes);
sourceTypes.add(type);
}
}
} else {
// 如果已经是新 Set,则直接添加所有类型
for (const type of st) sourceTypes.add(type);
}
}
return sourceTypes; // 返回合并后的资源类型
}
/**
* 获取某个 chunk 中模块列表的可迭代对象,并根据传入的 comparator 排序
* @param {Chunk} chunk chunk 实例
* @param {function(Module, Module): -1|0|1} comparator 排序函数
* @returns {Iterable<Module>} 排序后的模块集合
*/
getOrderedChunkModulesIterable(chunk, comparator) {
const cgc = this._getChunkGraphChunk(chunk);
// 对模块集合进行排序
cgc.modules.sortWith(comparator);
// 返回排序后的模块列表
return cgc.modules;
}
/**
* 获取指定 sourceType 的模块列表,并排序
* @param {Chunk} chunk chunk 实例
* @param {string} sourceType 要筛选的资源类型
* @param {function(Module, Module): -1|0|1} comparator 排序函数
* @returns {Iterable<Module> | undefined} 对应资源类型的排序模块列表
*/
getOrderedChunkModulesIterableBySourceType(chunk, sourceType, comparator) {
const cgc = this._getChunkGraphChunk(chunk);
// 通过缓存中的 sourceType → modules 获取指定类型模块集合
const modulesWithSourceType = cgc.modules
.getFromUnorderedCache(cgc._modulesBySourceType)
.get(sourceType);
if (modulesWithSourceType === undefined) return;
// 对该模块集合排序
modulesWithSourceType.sortWith(comparator);
return modulesWithSourceType;
}
/**
* 获取某个 chunk 中所有模块(缓存版本,不排序,不能修改)
* @param {Chunk} chunk chunk 实例
* @returns {Module[]} 模块列表数组
*/
getChunkModules(chunk) {
const cgc = this._getChunkGraphChunk(chunk);
// 获取缓存数组(无序)
return cgc.modules.getFromUnorderedCache(getArray);
}
/**
* 获取某个 chunk 中排序后的模块列表(缓存版本,不可修改)
* @param {Chunk} chunk chunk 实例
* @param {function(Module, Module): -1|0|1} comparator 排序函数
* @returns {Module[]} 排序后的模块数组
*/
getOrderedChunkModules(chunk, comparator) {
const cgc = this._getChunkGraphChunk(chunk);
const arrayFunction = createOrderedArrayFunction(comparator);
// 获取排序后的模块数组(缓存中)
return cgc.modules.getFromUnorderedCache(arrayFunction);
}
/**
* 生成 chunk → 模块 ID 数组 的映射表
* @param {Chunk} chunk 源 chunk
* @param {ModuleFilterPredicate} filterFn 模块过滤函数
* @param {boolean} includeAllChunks 是否包含所有引用的 chunk(true)或仅 async chunks(false)
* @returns {Record<string|number, (string|number)[]>} 映射表
*/
getChunkModuleIdMap(chunk, filterFn, includeAllChunks = false) {
const chunkModuleIdMap = Object.create(null);
// 遍历所有异步 chunk 或所有引用的 chunk
for (const asyncChunk of includeAllChunks
? chunk.getAllReferencedChunks()
: chunk.getAllAsyncChunks()) {
let array;
// 遍历该 chunk 中的模块(按 ID 排序)
for (const module of this.getOrderedChunkModulesIterable(
asyncChunk,
compareModulesById(this)
)) {
// 模块满足过滤条件
if (filterFn(module)) {
if (array === undefined) {
array = [];
// 初始化该 chunk 对应的模块 ID 列表
chunkModuleIdMap[asyncChunk.id] = array;
}
// 获取模块 ID 并加入数组
const moduleId = this.getModuleId(module);
array.push(moduleId);
}
}
}
return chunkModuleIdMap;
}
/**
* 获取 chunk → moduleId → 渲染 hash 的映射表
* @param {Chunk} chunk 源 chunk
* @param {ModuleFilterPredicate} filterFn 模块过滤函数
* @param {number} hashLength hash 截断长度(0 表示不截断)
* @param {boolean} includeAllChunks 是否包含所有引用的 chunk
* @returns {Record<string|number, Record<string|number, string>>} 最终映射表
*/
getChunkModuleRenderedHashMap(
chunk,
filterFn,
hashLength = 0,
includeAllChunks = false
) {
const chunkModuleHashMap = Object.create(null);
for (const asyncChunk of includeAllChunks
? chunk.getAllReferencedChunks()
: chunk.getAllAsyncChunks()) {
let idToHashMap;
for (const module of this.getOrderedChunkModulesIterable(
asyncChunk,
compareModulesById(this)
)) {
if (filterFn(module)) {
if (idToHashMap === undefined) {
idToHashMap = Object.create(null);
chunkModuleHashMap[asyncChunk.id] = idToHashMap;
}
const moduleId = this.getModuleId(module);
const hash = this.getRenderedModuleHash(module, asyncChunk.runtime);
// 可能对 hash 进行截断(用于缩短输出)
idToHashMap[moduleId] = hashLength
? hash.slice(0, hashLength)
: hash;
}
}
}
return chunkModuleHashMap;
}
Lodash源码阅读-baseSetToString
Lodash 源码阅读-baseSetToString
概述
baseSetToString
是一个内部工具函数,用于设置函数的 toString
方法。它通过 defineProperty
为函数添加一个自定义的 toString
方法,如果 defineProperty
不可用,则返回原函数。这个函数在 Lodash 中主要用于为内部函数提供更好的调试信息。
前置学习
依赖函数
- defineProperty:定义对象属性的方法
- identity:返回第一个参数的函数
- constant:返回固定值的函数
技术知识
- 函数 toString:函数的字符串表示
- 属性描述符:对象属性的配置选项
- 函数元数据:函数的附加信息
源码实现
var baseSetToString = !defineProperty
? identity
: function (func, string) {
return defineProperty(func, "toString", {
configurable: true,
enumerable: false,
value: constant(string),
writable: true,
});
};
实现思路
baseSetToString
函数的主要目的是为函数设置自定义的 toString
方法。它通过以下步骤实现:
- 检查
defineProperty
是否可用 - 如果不可用,返回原函数
- 如果可用,使用
defineProperty
为函数添加toString
方法 - 使用
constant
函数确保toString
始终返回相同的字符串
这种实现方式确保了函数在调试时能够提供有用的信息,同时保持了向后兼容性。
源码解析
函数签名:
function baseSetToString(func, string)
-
func
: 要设置toString
方法的函数 -
string
:toString
方法要返回的字符串
实现细节:
var baseSetToString = !defineProperty
? identity
: function (func, string) {
return defineProperty(func, "toString", {
configurable: true,
enumerable: false,
value: constant(string),
writable: true,
});
};
- 检查
defineProperty
是否可用 - 如果不可用,返回原函数
- 如果可用,使用
defineProperty
为函数添加toString
方法 - 设置
toString
方法的属性描述符:-
configurable
: true,允许重新定义 -
enumerable
: false,不可枚举 -
value
: 使用constant
函数返回固定字符串 -
writable
: true,允许修改
-
应用场景
- 函数调试:
// 为函数添加调试信息
function add(a, b) {
return a + b;
}
const addWithInfo = baseSetToString(
add,
"function add(a, b) { return a + b; }"
);
console.log(addWithInfo.toString()); // 'function add(a, b) { return a + b; }'
- 函数元数据:
// 为函数添加元数据
function processData(data) {
// 处理数据
}
const processWithMeta = baseSetToString(
processData,
"processData: 数据处理函数 v1.0"
);
console.log(processWithMeta.toString()); // 'processData: 数据处理函数 v1.0'
- 函数标识:
// 为函数添加唯一标识
function createUniqueId() {
return Math.random().toString(36).substr(2, 9);
}
const id = createUniqueId();
const identifiedFunction = baseSetToString(
createUniqueId,
`createUniqueId #${id}`
);
console.log(identifiedFunction.toString()); // 'createUniqueId #xxx'
- 兼容性处理:
// 处理不支持 defineProperty 的环境
function safeSetToString(func, string) {
return baseSetToString(func, string);
}
const func = function () {};
const result = safeSetToString(func, "custom function");
console.log(result === func); // 在不支持 defineProperty 的环境中为 true
总结
通过学习 baseSetToString
函数,我们可以看到以下设计原则:
-
兼容性:处理不支持
defineProperty
的环境。 -
可配置性:允许自定义函数的
toString
方法。 -
元数据:为函数添加有用的调试信息。
-
安全性:使用
defineProperty
安全地设置属性。 -
代码复用:将通用的函数元数据设置逻辑抽象出来。
baseSetToString
函数虽然简单,但它在 Lodash 中扮演着重要角色,为内部函数提供了更好的调试信息和元数据支持。
Lodash源码阅读-constant
Lodash 源码阅读-constant
概述
constant
是一个简单的工具函数,用于创建一个始终返回固定值的函数。这个函数在函数式编程中非常有用,特别是在需要提供默认值或占位符函数的场景中。
前置学习
依赖函数
无直接依赖函数。
技术知识
- 闭包:JavaScript 中的闭包概念
- 高阶函数:返回函数的函数
- 函数式编程:纯函数和不变性概念
源码实现
function constant(value) {
return function () {
return value;
};
}
实现思路
constant
函数的实现非常简单,它通过闭包捕获传入的值,并返回一个新的函数。这个新函数在每次调用时都会返回被捕获的原始值。这种实现方式体现了函数式编程中的纯函数特性,因为返回的函数总是返回相同的值,没有任何副作用。
源码解析
函数签名:
function constant(value)
-
value
: 要返回的固定值
实现细节:
function constant(value) {
return function () {
return value;
};
}
- 函数接收一个参数
value
- 返回一个新的函数
- 新函数通过闭包访问
value
- 每次调用新函数都返回相同的
value
应用场景
- 默认值处理:
// 创建一个返回默认配置的函数
const defaultConfig = constant({
timeout: 5000,
retry: 3,
debug: false,
});
const config = defaultConfig();
console.log(config); // { timeout: 5000, retry: 3, debug: false }
总结
通过学习 constant
函数,我们可以看到以下设计原则:
-
简单性:函数实现简单明了,只做一件事。
-
纯函数:返回的函数是纯函数,没有副作用。
-
闭包应用:利用闭包特性保存值。
-
实用性:虽然简单,但在很多场景下非常有用。
Lodash源码阅读-defineProperty
Lodash 源码阅读-defineProperty
概述
defineProperty
是一个内部工具函数,用于安全地获取和使用 Object.defineProperty
方法。它通过尝试获取并测试原生 defineProperty
方法来确保其可用性,如果不可用则返回 undefined。这个函数在 Lodash 中主要用于定义对象属性的特性。
前置学习
依赖函数
- getNative:获取对象的原生方法
技术知识
- Object.defineProperty:定义对象属性的方法
- 属性描述符:对象属性的配置选项
- 错误处理:try-catch 的使用
- 立即执行函数:IIFE 的使用
源码实现
var defineProperty = (function () {
try {
var func = getNative(Object, "defineProperty");
func({}, "", {});
return func;
} catch (e) {}
})();
实现思路
defineProperty
函数的主要目的是安全地获取和使用 Object.defineProperty
方法。它通过以下步骤实现:
- 使用
getNative
获取原生的Object.defineProperty
方法 - 尝试使用该方法定义一个空对象的属性
- 如果成功则返回该方法,如果失败则返回 undefined
这种实现方式确保了返回的是可用的 defineProperty
方法,避免了在不支持的环境中出错。
源码解析
实现细节:
var defineProperty = (function () {
try {
var func = getNative(Object, "defineProperty");
func({}, "", {});
return func;
} catch (e) {}
})();
- 使用立即执行函数(IIFE)创建闭包
- 使用
getNative
获取原生的defineProperty
方法 - 尝试使用该方法定义一个空对象的属性
- 如果成功则返回该方法,如果失败则返回 undefined
- 使用 try-catch 处理可能的错误
应用场景
- 定义不可枚举属性:
// 定义不可枚举的属性
const obj = {};
if (defineProperty) {
defineProperty(obj, "hidden", {
value: "secret",
enumerable: false,
});
}
console.log(Object.keys(obj)); // []
console.log(obj.hidden); // 'secret'
- 定义只读属性:
// 定义只读属性
const config = {};
if (defineProperty) {
defineProperty(config, "apiKey", {
value: "12345",
writable: false,
});
}
config.apiKey = "67890"; // 静默失败
console.log(config.apiKey); // '12345'
- 定义 getter/setter:
// 定义 getter/setter
const person = {};
if (defineProperty) {
let _age = 0;
defineProperty(person, "age", {
get: function () {
return _age;
},
set: function (value) {
_age = value > 0 ? value : 0;
},
});
}
person.age = 25;
console.log(person.age); // 25
person.age = -10;
console.log(person.age); // 0
- 兼容性处理:
// 处理不支持 defineProperty 的环境
function definePropertySafe(obj, key, descriptor) {
if (defineProperty) {
defineProperty(obj, key, descriptor);
} else {
obj[key] = descriptor.value;
}
}
总结
通过学习 defineProperty
函数,我们可以看到以下设计原则:
-
安全性:确保获取的是可用的
defineProperty
方法。 -
健壮性:处理不支持
defineProperty
的环境。 -
兼容性:提供回退机制处理不兼容的情况。
-
错误处理:使用 try-catch 优雅地处理可能的错误。
-
代码复用:将通用的属性定义逻辑抽象出来。
webpack 检出图 第 三 节 ChunkGraph.js
这段代码是 Webpack 构建阶段“模块与输出代码结构之间关系图”的核心实现之一,提供了模块替换、关系查询、chunk 构成等核心功能支持。
🌐 核心概念
-
Module
: 表示一个模块(通常是一个 JS 文件)。 -
Chunk
: 表示一组打包后的模块代码块(可能异步加载)。 -
ChunkGraphModule (cgm)
: 模块在图中的元信息(如所属 chunk、是否是入口模块等)。 -
ChunkGraphChunk (cgc)
: chunk 在图中的元信息(如包含的模块、运行时模块等)。 -
RuntimeModule
: 专用于 runtime 的模块(如 webpack_require 的实现)。 -
Entrypoint
: webpack 的入口定义,表示某个模块是 chunk 的起始点。
🧠 核心方法功能总结
✅ 1. 模块替换逻辑
-
replaceModule(oldModule, newModule)
-
将
oldModule
在所有 chunk 中替换为newModule
,确保 graph 中所有引用和结构正确更新。 -
涉及:
- 普通模块 (
modules
) - 入口模块 (
entryModules
) - 运行时模块 (
runtimeModules
,fullHashModules
,dependentHashModules
)
- 普通模块 (
-
🔍 2. 查询模块是否在某处
-
isModuleInChunk(module, chunk)
:模块是否在指定 chunk 中 -
isModuleInChunkGroup(module, chunkGroup)
:模块是否在某个 chunkGroup(多个 chunk)中 -
isEntryModule(module)
:模块是否是某个 chunk 的入口模块
🔄 3. 获取模块关联的 chunks
-
getModuleChunksIterable(module)
:获取某模块对应的 chunk 集合(可迭代) -
getOrderedModuleChunksIterable(module, sortFn)
:按指定排序方式返回 chunk 集合 -
getModuleChunks(module)
:获取模块的 chunk(缓存版本) -
getNumberOfModuleChunks(module)
:获取模块关联 chunk 的数量
⚙️ 4. 获取模块的运行时环境
-
getModuleRuntimes(module)
:返回模块对应的运行时环境集合
📦 5. 获取 chunk 内模块的信息
-
getNumberOfChunkModules(chunk)
:chunk 中模块的总数 -
getNumberOfChunkFullHashModules(chunk)
:chunk 中用于 full hash 的模块数量 -
getChunkModulesIterable(chunk)
:chunk 中的模块集合 -
getChunkModulesIterableBySourceType(chunk, sourceType)
:按sourceType
获取 chunk 中的模块集合
🧩 设计亮点
- 使用了
_getChunkGraphModule
和_getChunkGraphChunk
来统一获取抽象结构,封装性强。 - 模块和 chunk 的关系被抽象成图状结构,极易扩展、分析依赖。
- 全面缓存(
getFromCache
,getFromUnorderedCache
)优化性能,适合大项目构建场景。 - 支持运行时代码模块(
RuntimeModule
)的替换与追踪,对 HMR 和 runtime 拆分支持好。
/**
* @param {Module} oldModule the replaced module
* @param {Module} newModule the replacing module
* @returns {void}
*/
replaceModule(oldModule, newModule) { // 将旧模块 oldModule 替换为新模块 newModule
const oldCgm = this._getChunkGraphModule(oldModule); // 获取旧模块对应的 ChunkGraphModule 对象
const newCgm = this._getChunkGraphModule(newModule); // 获取新模块对应的 ChunkGraphModule 对象
for (const chunk of oldCgm.chunks) { // 遍历旧模块所在的所有 chunk
const cgc = this._getChunkGraphChunk(chunk); // 获取 chunk 对应的 ChunkGraphChunk 对象
cgc.modules.delete(oldModule); // 从 chunk 的模块集合中移除旧模块
cgc.modules.add(newModule); // 将新模块添加到该 chunk 的模块集合中
newCgm.chunks.add(chunk); // 将 chunk 添加到新模块的 chunk 集合中
}
oldCgm.chunks.clear(); // 清空旧模块的 chunk 集合
if (oldCgm.entryInChunks !== undefined) { // 如果旧模块在某些 chunk 中是入口模块
if (newCgm.entryInChunks === undefined) { // 如果新模块还没有 entryInChunks 属性
newCgm.entryInChunks = new Set(); // 初始化 entryInChunks 集合
}
for (const chunk of oldCgm.entryInChunks) { // 遍历旧模块是入口模块的 chunk 集合
const cgc = this._getChunkGraphChunk(chunk); // 获取 chunk 对应的 ChunkGraphChunk
const old = /** @type {Entrypoint} */ (cgc.entryModules.get(oldModule)); // 获取旧模块对应的入口配置
/** @type {Map<Module, Entrypoint>} */
const newEntryModules = new Map(); // 创建新的 entryModules 映射
for (const [m, cg] of cgc.entryModules) { // 遍历原有的 entryModules 映射
if (m === oldModule) { // 如果是旧模块
newEntryModules.set(newModule, old); // 替换为新模块
} else {
newEntryModules.set(m, cg); // 否则保持原样
}
}
cgc.entryModules = newEntryModules; // 替换 entryModules
newCgm.entryInChunks.add(chunk); // 将该 chunk 添加到新模块的 entryInChunks 集合
}
oldCgm.entryInChunks = undefined; // 清空旧模块的 entryInChunks
}
if (oldCgm.runtimeInChunks !== undefined) { // 如果旧模块被当作 runtime module 使用
if (newCgm.runtimeInChunks === undefined) { // 如果新模块没有 runtimeInChunks 属性
newCgm.runtimeInChunks = new Set(); // 初始化 runtimeInChunks 集合
}
for (const chunk of oldCgm.runtimeInChunks) { // 遍历旧模块所在的 runtime chunk
const cgc = this._getChunkGraphChunk(chunk); // 获取 chunk 对应的 ChunkGraphChunk
cgc.runtimeModules.delete(/** @type {RuntimeModule} */ (oldModule)); // 从 runtimeModules 中移除旧模块
cgc.runtimeModules.add(/** @type {RuntimeModule} */ (newModule)); // 添加新模块
newCgm.runtimeInChunks.add(chunk); // 将 chunk 添加到新模块的 runtimeInChunks
if (
cgc.fullHashModules !== undefined &&
cgc.fullHashModules.has(/** @type {RuntimeModule} */ (oldModule))
) { // 如果 chunk 的 fullHashModules 包含旧模块
cgc.fullHashModules.delete(/** @type {RuntimeModule} */ (oldModule)); // 删除旧模块
cgc.fullHashModules.add(/** @type {RuntimeModule} */ (newModule)); // 添加新模块
}
if (
cgc.dependentHashModules !== undefined &&
cgc.dependentHashModules.has(/** @type {RuntimeModule} */ (oldModule))
) { // 如果 chunk 的 dependentHashModules 包含旧模块
cgc.dependentHashModules.delete(
/** @type {RuntimeModule} */ (oldModule)
);
cgc.dependentHashModules.add(
/** @type {RuntimeModule} */ (newModule)
);
}
}
oldCgm.runtimeInChunks = undefined; // 清空旧模块的 runtimeInChunks
}
}
/**
* @param {Module} module the checked module
* @param {Chunk} chunk the checked chunk
* @returns {boolean} true, if the chunk contains the module
*/
isModuleInChunk(module, chunk) { // 判断模块是否存在于指定 chunk 中
const cgc = this._getChunkGraphChunk(chunk); // 获取 chunk 对应的 ChunkGraphChunk
return cgc.modules.has(module); // 判断模块集合中是否包含该模块
}
/**
* @param {Module} module the checked module
* @param {ChunkGroup} chunkGroup the checked chunk group
* @returns {boolean} true, if the chunk contains the module
*/
isModuleInChunkGroup(module, chunkGroup) { // 判断模块是否存在于指定 ChunkGroup 中
for (const chunk of chunkGroup.chunks) { // 遍历 chunkGroup 中的所有 chunk
if (this.isModuleInChunk(module, chunk)) return true; // 只要有一个 chunk 包含模块则返回 true
}
return false; // 所有 chunk 都不包含模块则返回 false
}
/**
* @param {Module} module the checked module
* @returns {boolean} true, if the module is entry of any chunk
*/
isEntryModule(module) { // 判断模块是否为入口模块
const cgm = this._getChunkGraphModule(module); // 获取 ChunkGraphModule
return cgm.entryInChunks !== undefined; // 判断是否存在 entryInChunks 集合
}
/**
* @param {Module} module the module
* @returns {Iterable<Chunk>} iterable of chunks (do not modify)
*/
getModuleChunksIterable(module) { // 获取模块所在的 chunk 集合(可迭代)
const cgm = this._getChunkGraphModule(module); // 获取 ChunkGraphModule
return cgm.chunks; // 返回 chunks 集合
}
/**
* @param {Module} module the module
* @param {function(Chunk, Chunk): -1|0|1} sortFn sort function
* @returns {Iterable<Chunk>} iterable of chunks (do not modify)
*/
getOrderedModuleChunksIterable(module, sortFn) { // 获取排序后的模块 chunk 集合(可迭代)
const cgm = this._getChunkGraphModule(module); // 获取 ChunkGraphModule
cgm.chunks.sortWith(sortFn); // 使用传入的排序函数进行排序
return cgm.chunks; // 返回排序后的 chunks 集合
}
/**
* @param {Module} module the module
* @returns {Chunk[]} array of chunks (cached, do not modify)
*/
getModuleChunks(module) { // 获取模块所在的 chunk 数组(使用缓存)
const cgm = this._getChunkGraphModule(module); // 获取 ChunkGraphModule
return cgm.chunks.getFromCache(getArray); // 从缓存中获取数组结果
}
/**
* @param {Module} module the module
* @returns {number} the number of chunk which contain the module
*/
getNumberOfModuleChunks(module) { // 获取包含该模块的 chunk 数量
const cgm = this._getChunkGraphModule(module); // 获取 ChunkGraphModule
return cgm.chunks.size; // 返回 chunk 集合的大小
}
/**
* @param {Module} module the module
* @returns {RuntimeSpecSet} runtimes
*/
getModuleRuntimes(module) { // 获取模块关联的运行时环境集合
const cgm = this._getChunkGraphModule(module); // 获取 ChunkGraphModule
return cgm.chunks.getFromUnorderedCache(getModuleRuntimes); // 从缓存中获取运行时集合
}
/**
* @param {Chunk} chunk the chunk
* @returns {number} the number of modules which are contained in this chunk
*/
getNumberOfChunkModules(chunk) { // 获取 chunk 中的模块数量
const cgc = this._getChunkGraphChunk(chunk); // 获取 ChunkGraphChunk
return cgc.modules.size; // 返回模块集合大小
}
/**
* @param {Chunk} chunk the chunk
* @returns {number} the number of full hash modules which are contained in this chunk
*/
getNumberOfChunkFullHashModules(chunk) { // 获取参与 chunk full hash 的模块数量
const cgc = this._getChunkGraphChunk(chunk); // 获取 ChunkGraphChunk
return cgc.fullHashModules === undefined ? 0 : cgc.fullHashModules.size; // 返回集合大小或 0
}
/**
* @param {Chunk} chunk the chunk
* @returns {Iterable<Module>} return the modules for this chunk
*/
getChunkModulesIterable(chunk) { // 获取 chunk 中模块的可迭代集合
const cgc = this._getChunkGraphChunk(chunk); // 获取 ChunkGraphChunk
return cgc.modules; // 返回模块集合
}
/**
* @param {Chunk} chunk the chunk
* @param {string} sourceType source type
* @returns {Iterable<Module> | undefined} return the modules for this chunk
*/
getChunkModulesIterableBySourceType(chunk, sourceType) { // 获取指定 sourceType 的模块集合
const cgc = this._getChunkGraphChunk(chunk); // 获取 ChunkGraphChunk
const modulesWithSourceType = cgc.modules // 从缓存中获取按 sourceType 分类的模块集合
.getFromUnorderedCache(cgc._modulesBySourceType)
.get(sourceType); // 获取指定类型的模块集合
return modulesWithSourceType; // 返回结果
}
HTML新标签与核心 API 实战
解析noble-winrt
4 学习对象的原型链 +使用原型链实现继承
React-router v7 第六章(路由操作)
路由操作
路由的操作是由两个部分组成的:
- loader
- action
在平时工作中大部分都是在做增刪查改(CRUD)
的操作,所以一个界面的接口过多之后就会使逻辑臃肿复杂,难以维护,所以需要使用路由的高级操作来优化代码。
loader
只有GET请求才会触发loader,所以适合用来获取数据
在之前的话我们是 RenderComponent(渲染组件)
-> Fetch(获取数据)
-> RenderView(渲染视图)
有了loader之后是 loader(通过fetch获取数据)
-> useLoaderData(获取数据)
-> RenderComponent(渲染组件)
//router/index.tsx
import { createBrowserRouter } from "react-router";
const router = createBrowserRouter([
{
path: "/",
Component: App,
loader: async () => {
const data = await response.json();
const response = await getUser(data); // [!code highlight] 获取数据
return {
data: response.list,
message: "success",
}
},
},
]);
//App.tsx
import { useLoaderData } from "react-router";
const App = () => {
const { data, message } = useLoaderData(); // [!code highlight] 获取数据
return <div>{data}</div>;
}
action
一般用于表单提交,删除,修改等操作。
useSubmit速查文档 useActionData速查文档
只有POST DELETE PATCH PUT等请求才会触发action,所以适合用来提交表单
//router/index.tsx
import { createBrowserRouter } from "react-router";
const router = createBrowserRouter([
{
// path: '/index',
Component: Layout,
children: [
{
path: 'about',
Component: About,
action: async ({ request }) => {
const formData = await request.formData();
await createUser(formData); // [!code highlight] 创建用户
return {
data: table,
success: true
}
}
},
],
},
]);
//App.tsx
import { useSubmit } from 'react-router';
import { Card, Form, Input, Button } from 'antd';
export default function About() {
const submit = useSubmit();
return <Card>
<Form onFinish={(values) => {
submit(values, { method: 'post'}) // [!code highlight] 提交表单
}}>
<Form.Item name='name' label='姓名'>
<Input />
</Form.Item>
<Form.Item name='age' label='年龄'>
<Input />
</Form.Item>
<Button type='primary' htmlType='submit'>提交</Button>
</Form>
</Card>;
}
状态变更
我们可以配合useNavigation
来管理表单提交的状态
- GET提交会经过以下状态:
idle -> loading -> idle
- POST提交会经过以下状态:
idle -> submitting ->loading -> idle
所以我们可以根据这些状态来控制disabled
loading
等行为
import { useNavigation, useSubmit } from "react-router";
const submit = useSubmit();
const navigation = useNavigation();
return (
<div>
{navigation.state === 'loading' && <div>loading...</div>}
<button disabled={navigation.state === 'submitting'}>提交</button>
</div>
)
每日一题-统计公平数对的数目🟡
给你一个下标从 0 开始、长度为 n
的整数数组 nums
,和两个整数 lower
和 upper
,返回 公平数对的数目 。
如果 (i, j)
数对满足以下情况,则认为它是一个 公平数对 :
-
0 <= i < j < n
,且 lower <= nums[i] + nums[j] <= upper
示例 1:
输入:nums = [0,1,7,4,4,5], lower = 3, upper = 6 输出:6 解释:共计 6 个公平数对:(0,3)、(0,4)、(0,5)、(1,3)、(1,4) 和 (1,5) 。
示例 2:
输入:nums = [1,7,9,2,5], lower = 11, upper = 11 输出:1 解释:只有单个公平数对:(2,3) 。
提示:
1 <= nums.length <= 105
nums.length == n
-109 <= nums[i] <= 109
-109 <= lower <= upper <= 109
CSS考点之权重计算🧑🏻💻
引言💭
在 CSS 中,权重决定了哪条规则会应用到 HTML 元素。当多个规则作用于同一元素时,浏览器会根据权重来判断哪一条规则优先执行。
这也是一个面试高频考题,赶紧码住。✍🏻
什么是 CSS 权重?🤔
CSS 权重是一个数值,用来表示某个选择器的优先级。浏览器根据该优先级来决定哪些规则被应用。CSS 权重的计算是根据选择器的类型(例如:元素、类、ID 选择器等)来确定的。每种选择器类型对应一个特定的权重值,而这些权重值会按照一定的规则叠加。
权重通常用四个数字表示,形式为 (a, b, c, d)
。这四个数字分别代表不同类型选择器的权重:
- a: 内联样式的权重,优先级最高。
- b: ID 选择器的权重。
- c: 类选择器、属性选择器和伪类选择器的权重。
- d: 元素选择器和伪元素选择器的权重。
CSS 权重计算的详细规则📑
-
内联样式(
style="..."
):- 权重:
(1, 0, 0, 0)
,内联样式的优先级最高。 - 示例:
<div style="color: red;">内容</div>
- 权重:
-
ID 选择器:
- 权重:
(0, 1, 0, 0)
,每个 ID 选择器的权重为1
。 - 示例:
#header { color: red; }
,#menu { color: blue; }
- 权重:
-
类选择器、属性选择器和伪类选择器:
- 权重:
(0, 0, 1, 0)
,每个类选择器、属性选择器和伪类选择器的权重为1
。 - 示例:
.menu { color: green; }
,[type="text"] { color: orange; }
,:hover { color: purple; }
- 权重:
-
元素选择器和伪元素选择器:
- 权重:
(0, 0, 0, 1)
,每个元素选择器和伪元素选择器的权重为1
。 - 示例:
div { color: blue; }
,p { color: red; }
,::before { content: ''; }
- 权重:
权重计算示例📌
假设有以下的 CSS 规则:
#header { color: red; }
.menu { color: green; }
div { color: blue; }
对应的 HTML 元素为:
<div id="header" class="menu">Hello</div>
根据权重计算,浏览器会按照以下方式决定哪个规则生效:
-
#header { color: red; }
:- 权重:
(0, 1, 0, 0)
,使用了 ID 选择器。
- 权重:
-
.menu { color: green; }
:- 权重:
(0, 0, 1, 0)
,使用了类选择器。
- 权重:
-
div { color: blue; }
:- 权重:
(0, 0, 0, 1)
,使用了元素选择器。
- 权重:
由于 ID 选择器的权重最高((0, 1, 0, 0)
),最终文本的颜色会是 红色,即 #header
规则生效。
权重递增与选择器的组合🌈
多个选择器组合时,它们的权重会叠加。例如:
#header .menu div { color: purple; }
此时,#header .menu div
的权重是 (0, 1, 1, 1)
,它包含了一个 ID 选择器(#header
)、一个类选择器(.menu
)和一个元素选择器(div
)。它的权重为:
- ID 选择器:
1
- 类选择器:
1
- 元素选择器:
1
因此,最终权重为 (0, 1, 1, 1)
。
内联样式的优先级🎢
内联样式具有最高的优先级,权重为 (1, 0, 0, 0)
。即使在外部样式表中存在其他选择器,内联样式也会覆盖它们。
例如,以下 HTML 元素的内联样式将覆盖任何外部样式:
<div id="header" class="menu" style="color: yellow;">Hello</div>
即使外部 CSS 中有 #header { color: red; }
和 .menu { color: green; }
,内联样式 style="color: yellow;"
仍然会应用,并使文本的颜色变为 黄色。
选择器优先级的具体比较🚀
假设以下 CSS 规则:
#header { color: red; }
.menu { color: green; }
div { color: blue; }
以及 HTML 元素:
<div id="header" class="menu">Hello</div>
在此情况下,CSS 规则的权重:
-
#header { color: red; }
的权重为(0, 1, 0, 0)
。 -
.menu { color: green; }
的权重为(0, 0, 1, 0)
。 -
div { color: blue; }
的权重为(0, 0, 0, 1)
。
因此,最终会应用 #header { color: red; }
,因为它的权重最大,红色会覆盖其他颜色。
结语✒️
在编写 CSS 时,尽量避免使用过多的高权重选择器,尤其是 ID 选择器,以保持样式的可维护性。如果你遇到样式覆盖问题,首先检查选择器的权重,确保它们按正确的顺序排列。🫰🏻
vue与react(自定义)中计算属性对比
一、核心概念对比
特性 | Vue (Computed) | React (useMemo/useCallback) |
---|---|---|
定义方式 | 声明式(基于依赖自动缓存) | 命令式(手动声明依赖数组) |
缓存机制 | 自动缓存(依赖不变则复用) | 手动控制(依赖数组变化时重新计算) |
响应式触发 | 依赖变更自动触发 | 需严格定义依赖数组 |
语法复杂度 | 低(模板中直接使用) | 中(需配合Hooks使用) |
适用场景 | 模板渲染优化、派生状态 | 性能优化、复杂计算避免重复执行 |
二、原理剖析
1. Vue Computed 原理
实现方式:基于响应式系统的依赖追踪
// Vue 3 源码简化版
function computed(getter) {
let value;
let dirty = true; // 标记是否需要重新计算
const runner = effect(getter, {
lazy: true,
scheduler() {
dirty = true; // 依赖变化时标记为脏数据
}
});
return {
get value() {
if (dirty) {
value = runner(); // 重新计算
dirty = false;
}
return value;
}
};
}
关键点:
- 依赖变更时通过
scheduler
标记数据为dirty
- 下次访问时按需重新计算(惰性求值)
2. React useMemo 原理
实现方式:依赖数组的浅比较(Object.is)
// React 源码简化逻辑
function useMemo(factory, deps) {
const hook = getCurrentHook();
const nextDeps = deps || [];
if (hook.memoizedState) {
const [prevValue, prevDeps] = hook.memoizedState;
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevValue; // 依赖未变化时返回缓存值
}
}
const newValue = factory();
hook.memoizedState = [newValue, nextDeps];
return newValue;
}
关键点:
- 每次渲染时对比依赖数组
- 依赖变化时重新执行工厂函数
三、代码实战对比
场景1:基础计算属性
Vue 实现
<template>
<div>
FullName: {{ fullName }} <!-- 自动缓存 -->
</div>
</template>
<script>
export default {
data() {
return { firstName: '张', lastName: '三' };
},
computed: {
fullName() {
return this.firstName + ' ' + this.lastName;
}
}
};
</script>
React 实现
function UserCard({ firstName, lastName }) {
const fullName = useMemo(
() => `${firstName} ${lastName}`,
[firstName, lastName] // 需手动声明依赖
);
return <div>FullName: {fullName}</div>;
}
场景2:复杂数据过滤
Vue 实现
<template>
<ul>
<li v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</li>
</ul>
</template>
<script>
export default {
data() {
return {
users: [
{ id: 1, name: 'Alice', isActive: true },
{ id: 2, name: 'Bob', isActive: false }
]
};
},
computed: {
activeUsers() {
return this.users.filter(u => u.isActive); // 自动缓存结果
}
}
};
</script>
React 实现
function UserList({ users }) {
const activeUsers = useMemo(
() => users.filter(u => u.isActive),
[users] // 注意:如果users引用不变但内容变化,需深度比较
);
return (
<ul>
{activeUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
四、复杂场景处理
场景1:依赖动态变化
Vue 的自动依赖追踪
vueCopy Code
<script>
export default {
data() {
return { a: 1, b: 2, useA: true };
},
computed: {
result() {
return this.useA ? this.a : this.b; // 自动识别依赖切换
}
}
};
</script>
React 的显式依赖管理
function Calculator({ a, b, useA }) {
const result = useMemo(
() => (useA ? a : b),
[useA, useA ? a : b] // 必须明确所有可能依赖
);
return <div>Result: {result}</div>;
}
场景2:计算属性链式调用
Vue 的链式计算
<script>
export default {
data() {
return { price: 100, taxRate: 0.1 };
},
computed: {
tax() {
return this.price * this.taxRate;
},
total() { // 可依赖其他计算属性
return this.price + this.tax;
}
}
};
</script>
React 的逐层缓存
function Invoice({ price, taxRate }) {
const tax = useMemo(() => price * taxRate, [price, taxRate]);
const total = useMemo(() => price + tax, [price, tax]); // 需手动传递依赖
return <div>Total: {total}</div>;
}
五、性能优化对比
Vue 的优势
- 自动缓存:无需手动维护依赖,适合模板中的派生数据
- 细粒度更新:依赖变更时精准触发相关组件更新
React 的优势
- 灵活控制:可结合
useCallback
缓存函数,避免子组件无效渲染 - 跨组件复用:通过自定义Hook共享计算逻辑
// React 自定义Hook复用
function useTotal(price, taxRate) {
return useMemo(() => {
const tax = price * taxRate;
return price + tax;
}, [price, taxRate]);
}
// 多个组件共享同一逻辑
function Cart() {
const total = useTotal(100, 0.1);
return <div>Total: {total}</div>;
}
六、使用场景推荐
框架 | 推荐场景 |
---|---|
Vue | 1. 模板中的复杂表达式优化 2. 需要自动追踪依赖的派生数据 3. 表单联动计算 |
React | 1. 需要手动控制缓存时机 2. 跨组件共享计算逻辑 3. 结合Context的复杂状态派生 |
- Vue Computed:适合声明式UI场景, "省心" 但灵活性较低
- React useMemo:适合精细控制, "强大" 但需手动维护依赖
- 终极选择:Vue适合快速开发,React适合大型应用状态管理
✈️ Colipot Agent + 🔥 MCP Tools = 让你的编程体验直接起飞🚀
前言
上篇文章主要介绍了使用 Mcp ts Sdk
搭建一个 MCP 应用,我们已经知道下面的概念和基本使用!
我们并且在建立 Server
时添加了一个tool
,根据关键字 公理
返回三体黑暗森林法则的两个公理!
往期精彩推荐
- 😡😡😡早知道有这两个 VueRouter 增强插件,我还加什么班!🚀🚀🚀
- 🚀🚀🚀 这六个事半功倍的 Pinia 库,你一定要知道!
- 🚀惊了,这个国产软件居然这么牛,比 uniapp 还全能❤️
- 更多精彩文章欢迎关注我的公众号:萌萌哒草头将军
正文
今天我们的主要目标是:将这个工具注册到 VsCode
中,并可以在 Agent
模式中使用它!
添加 Mcp Server
按 Ctrl+Shift+P
或 Cmd+Shift+P
,输入并选择 MCP: Add Server
,
或者你也可以在 Colipot
中点击工具图标进入:
选择合适的类型
接着会让你选择服务类型,如果你是在本地,推荐使用 Stdio
!
-
Stdio 的优势:
Stdio
传输适合本地开发,避免网络复杂性,适合测试和调试。 -
其他传输方式:虽然
VSCode
还支持SSE(Server-Sent Events)
,但Stdio
更适合本地场景
配置文件
根据自己的服务类型输入启动命令,比我上次我用的 js
,这里会输入: node
连续回车两次,第一次是自动生成的tool唯一标识,第二次会让你选择设置,建议选择workspace settings
,
VSCode
会在根目录下生成.vscode/mcp.json
如下的配置文件:
args
对应的是参数,比如执行文件目标地址!
使用
打开 Copilot Agent
视图,然后使用自然语言调用 MCP
服务器的工具。
我们输入框里输入相关语句,
VsCode
会自动查找对应的tool
最后
通过 VsCode Colipot Agent
提供的能力,我们可以让我们的编辑期更加智能的为我们提供服务!
好了今天的分享就些了,十分感谢大家的支持,如果文章后中有错误的地方,欢迎指正!
往期精彩推荐
- 😡😡😡早知道有这两个 VueRouter 增强插件,我还加什么班!🚀🚀🚀
- 🚀🚀🚀 这六个事半功倍的 Pinia 库,你一定要知道!
- 🚀惊了,这个国产软件居然这么牛,比 uniapp 还全能❤️
- 更多精彩文章欢迎关注我的公众号:萌萌哒草头将军
谷歌浏览器插件 录制菜单路由跳转行为 事件重复解决方案
背景
开发谷歌插件的过程中,监听用户浏览器操作行为,vue3 项目使用hash路由,发现在 background 脚本中通过chrome.tabs.onUpdated和chrome.webNavigation.onHistoryStateUpdated事件监听时,点击切换菜单路由时都会触发一个链接跳转事件,导致监听事件重复。
这里说明一下为什么在chrome.tabs.onUpdated和chrome.webNavigation.onHistoryStateUpdated事件都写了那个链接跳转事件,是因为发现在某些网站上有时候chrome.tabs.onUpdated会触发,有时候chrome.webNavigation.onHistoryStateUpdated会单独触发,为了兼容处理各种情况,所以就在这两个事件里都处理了链接跳转事件,结果引发了文章开头描述的问题
思考过程
引发上述问题可能的原因包括:
- 事件触发条件重叠:当Hash变化时,chrome.tabs.onUpdated可能因为URL变化而触发,而chrome.webNavigation.onHistoryStateUpdated则因为历史状态改变而触发,两者同时发生。
- 事件监听器的重复注册:可能在插件的不同地方多次注册了相同的监听器,导致重复触发。
- 事件传播或页面加载阶段的问题:比如在页面加载的不同阶段,两个事件可能被多次触发。
接下来,我需要考虑如何解决这个问题。可能的解决方案包括:
- 合并事件监听:将两个事件的监听逻辑合并,避免重复处理。例如,将两个事件的回调函数统一到一个处理函数中,并添加防重机制。
- 条件判断过滤:在事件回调中添加条件判断,比如检查URL变化是否是Hash变化,或者是否是用户主动触发的路由跳转,从而决定是否执行逻辑。
- 使用防重或节流:在回调函数中使用防重(如setTimeout和clearTimeout)或节流来避免短时间内多次触发。
- 选择更精确的事件监听:如果Hash路由主要通过hashchange事件触发,可能只需要监听chrome.webNavigation.onHistoryStateUpdated,而chrome.tabs.onUpdated可能包含更多的触发场景,可以考虑移除其中一个监听。(PASS,原因看开头背景说明)
解决方案
1. 合并事件监听逻辑,避免重复处理
// 合并后的处理函数
function handleUrlChange(details) {
if (isHandling) return; // 防止重复触发
isHandling = true;
// 具体处理逻辑(如获取当前URL的Hash值,上报动作)
// 具体处理逻辑
// 延迟重置标志位,避免短时间内重复触发
setTimeout(() => {
isHandling = false;
}, 200); // 根据需求调整延迟时间
}
// 监听 chrome.tabs.onUpdated
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.url && changeInfo.url.includes('#')) {
handleUrlChange({ url: changeInfo.url });
}
});
// 监听 chrome.webNavigation.onHistoryStateUpdated
chrome.webNavigation.onHistoryStateUpdated.addListener((details) => {
handleUrlChange(details);
});
2. 过滤非必要的触发条件
在回调函数中添加条件判断,比如 仅在Hash值变化时执行逻辑,根据自己项目实际情况进行判断。
function handleUrlChange(details) {
const currentUrl = details.url || chrome.tabs.getSelected().url;
const newHash = new URL(currentUrl).hash;
// 获取上一次记录的Hash值(需维护一个变量)
if (lastHash === newHash) return; // 如果Hash未变化,直接返回
lastHash = newHash;
// 执行实际逻辑
console.log('Hash changed:', newHash);
}
关键注意事项
-
防重机制:
- 使用标志位(如
isHandling
)或节流函数(如setTimeout
)防止短时间内重复触发。
- 使用标志位(如
-
状态维护:
- 通过
lastHash
记录上一次的Hash值,避免重复处理相同Hash的事件。
- 通过