JavaScript Proxy與Reflect的實現(xiàn)
在 JavaScript 中如何攔截對象的讀取、修改、刪除操作?Vue3的響應(yīng)式系統(tǒng)如何實現(xiàn)?本文將深入講解 JavaScript 的元編程世界,探索 Proxy 和 Reflect 的奧秘。
前言:從Vue3的響應(yīng)式說起
// Vue3的響應(yīng)式數(shù)據(jù)實現(xiàn)原理
const reactive = (target) => {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key); // 依賴收集
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
trigger(target, key); // 觸發(fā)更新
return result;
}
});
};
Vue3的響應(yīng)式數(shù)據(jù)實現(xiàn)原理的背后,就是 Proxy 和 Reflect 。理解它們,我們就能真正掌握 JavaScript 元編程的能力。
理解Proxy與Reflect
什么是Proxy(代理)?
Proxy 對象用于創(chuàng)建一個對象的代理,從而實現(xiàn)對基本操作的攔截和自定義(如屬性查找、賦值、枚舉、函數(shù)調(diào)用等)。
Proxy工作流程示意圖
┌─────────┐ 操作請求 ┌─────────┐ 轉(zhuǎn)發(fā)操作 ┌─────────┐
│ 客戶端 │───────────→ │ Proxy │───────────→ │ 目標(biāo)對象 │
│ │ │ 代理 │ │ │
│ │←─────────── │ │←─────────── │ │
└─────────┘ 響應(yīng)結(jié)果 └─────────┘ 實際結(jié)果 └─────────┘
↓ ↓
┌─────────────┐ ┌─────────────┐
│ 攔截和自定義 │ │ 原始行為 │
│ (陷阱) │ │ │
└─────────────┘ └─────────────┘
什么是Reflect(反射)?
Reflect 是一個內(nèi)置的對象,它提供攔截 JavaScript 操作的方法。這些方法與 Proxy 的陷阱方法一一對應(yīng)。
為什么需要Reflect?
- 統(tǒng)一的操作API
- 更好的錯誤處理(返回布爾值而非拋出異常)
- 與Proxy陷阱方法一一對應(yīng)
代理基礎(chǔ):創(chuàng)建第一個代理
// 目標(biāo)對象
const target = {
name: '張三',
age: 30
};
// 處理器對象(包含陷阱方法)
const handler = {
// 攔截屬性讀取
get(target, property, receiver) {
// 添加自定義邏輯
if (property === 'age') {
return `${target[property]}歲`;
}
// 默認行為:使用Reflect
return Reflect.get(target, property, receiver);
},
// 攔截屬性設(shè)置
set(target, property, value, receiver) {
// 添加驗證邏輯
if (property === 'age' && (value < 0 || value > 150)) {
console.warn('年齡必須在0-150之間');
return false; // 返回false表示設(shè)置失敗
}
// 默認行為:使用Reflect
return Reflect.set(target, property, value, receiver);
}
};
// 創(chuàng)建代理
const proxy = new Proxy(target, handler);
console.log(proxy.name); // 張三
console.log(proxy.age); // 30歲
console.log(target.age); // 30
13種可攔截的陷阱方法
完整陷阱方法概覽
const completeHandler = {
// 1. 屬性訪問攔截
get(target, prop, receiver) {
console.log(`get: ${prop}`);
return Reflect.get(target, prop, receiver);
},
// 2. 屬性賦值攔截
set(target, prop, value, receiver) {
console.log(`set: ${prop} = ${value}`);
return Reflect.set(target, prop, value, receiver);
},
// 3. in操作符攔截
has(target, prop) {
console.log(`has: ${prop}`);
return Reflect.has(target, prop);
},
// 4. delete操作符攔截
deleteProperty(target, prop) {
console.log(`delete: ${prop}`);
return Reflect.deleteProperty(target, prop);
},
// 5. 構(gòu)造函數(shù)調(diào)用攔截
construct(target, args, newTarget) {
console.log(`construct with args:`, args);
return Reflect.construct(target, args, newTarget);
},
// 6. 函數(shù)調(diào)用攔截
apply(target, thisArg, args) {
console.log(`apply: thisArg=`, thisArg, 'args=', args);
return Reflect.apply(target, thisArg, args);
},
// 7. Object.getOwnPropertyDescriptor攔截
getOwnPropertyDescriptor(target, prop) {
console.log(`getOwnPropertyDescriptor: ${prop}`);
return Reflect.getOwnPropertyDescriptor(target, prop);
},
// 8. Object.defineProperty攔截
defineProperty(target, prop, descriptor) {
console.log(`defineProperty: ${prop}`, descriptor);
return Reflect.defineProperty(target, prop, descriptor);
},
// 9. 原型訪問攔截
getPrototypeOf(target) {
console.log('getPrototypeOf');
return Reflect.getPrototypeOf(target);
},
// 10. 原型設(shè)置攔截
setPrototypeOf(target, prototype) {
console.log(`setPrototypeOf:`, prototype);
return Reflect.setPrototypeOf(target, prototype);
},
// 11. 可擴展性判斷攔截
isExtensible(target) {
console.log('isExtensible');
return Reflect.isExtensible(target);
},
// 12. 防止擴展攔截
preventExtensions(target) {
console.log('preventExtensions');
return Reflect.preventExtensions(target);
},
// 13. 自身屬性枚舉攔截
ownKeys(target) {
console.log('ownKeys');
return Reflect.ownKeys(target);
}
};
陷阱方法詳細解析
1. get陷阱:屬性訪問攔截
get() 會在獲取屬性值的操作中被調(diào)用:
const getHandler = {
get(target, property, receiver) {
// 處理不存在的屬性
if (!(property in target)) {
throw new Error(`屬性不存在: ${property}`);
}
// 處理私有屬性(約定以_開頭的屬性為私有)
if (property.startsWith('_')) {
throw new Error(`不能訪問私有屬性: ${property}`);
}
return Reflect.get(target, property, receiver);
}
};
- 參數(shù)說明:
- target:目標(biāo)對象
- property:引用的目標(biāo)對象上的屬性
- receiver:代理對象
- 返回值說明:返回值無限制
2. set陷阱:屬性賦值攔截
set() 會在設(shè)置屬性值的操作中被調(diào)用:
const setHandler = {
set(target, property, value, receiver) {
// 只讀屬性檢查(約定以$開頭的屬性為只讀)
if (property.startsWith('$')) {
throw new Error(`屬性不可修改: ${property}`);
}
// 執(zhí)行設(shè)置
return Reflect.set(target, property, value, receiver);
}
};
- 參數(shù)說明:
- target:目標(biāo)對象
- property:引用的目標(biāo)對象上的屬性
- value:要給屬性設(shè)置的值
- receiver:代理對象
- 返回值說明:返回 true 表示成功;返回false表示失敗。
3. has陷阱:in操作符攔截
has() 會在 in 操作符中被調(diào)用:
const hasHandler = {
has(target, property) {
// 隱藏某些屬性(不在in操作中暴露)
const hiddenProperties = ['password', 'token', '_internal'];
if (hiddenProperties.includes(property)) {
console.log(`隱藏屬性: ${property}`);
return false;
}
// 虛擬屬性(不存在但返回true)
const virtualProperties = ['isAdmin', 'hasAccess'];
if (virtualProperties.includes(property)) {
console.log(`虛擬屬性: ${property}`);
return true;
}
return Reflect.has(target, property);
}
};
- 參數(shù)說明:
- target:目標(biāo)對象
- value:要給屬性設(shè)置的值
- 返回值說明:返回布爾值表示屬性是否存在,返回非布爾型會被轉(zhuǎn)成布爾型。
注:Object.keys() 不受 has() 影響,仍會返回所有屬性。
4. apply陷阱:函數(shù)調(diào)用攔截
apply() 會在調(diào)用函數(shù)時被調(diào)用:
const applyHandler = {
apply(target, thisArg, argumentsList) {
// 參數(shù)驗證
if (target.name === 'calculate') {
if (argumentsList.length < 2) {
throw new Error('calculate函數(shù)需要至少2個參數(shù)');
}
if (!argumentsList.every(arg => typeof arg === 'number')) {
throw new Error('calculate函數(shù)的所有參數(shù)必須是數(shù)字');
}
}
return Reflect.apply(target, thisArg, argumentsList);
}
};
- 參數(shù)說明:
- target:目標(biāo)對象
- thisArg:調(diào)用函數(shù)時的this參數(shù)
- argumentsList:調(diào)用函數(shù)時的參數(shù)列表
- 返回值說明:返回值無限制
5. construct陷阱:構(gòu)造函數(shù)攔截
construct() 會在調(diào)用函數(shù)時被調(diào)用:
const constructHandler = {
construct(target, argumentsList, newTarget) {
// 單例模式:確保只有一個實例
if (target._instance) {
console.log('返回現(xiàn)有實例');
return target._instance;
}
// 創(chuàng)建實例
const instance = Reflect.construct(target, argumentsList, newTarget);
// 為單例保存實例
if (target.name === 'Singleton') {
target._instance = instance;
}
// 為實例添加額外屬性
instance.createdAt = new Date();
instance._id = Math.random().toString(36).substr(2, 9);
console.log('創(chuàng)建新實例,ID:', instance._id);
return instance;
}
};
- 參數(shù)說明:
- target:目標(biāo)構(gòu)造函數(shù)
- argumentsList:傳給目標(biāo)構(gòu)造函數(shù)的參數(shù)列表
- newTarget:最初被調(diào)用的構(gòu)造函數(shù)
- 返回值說明:必須返回一個對象
6. ownKeys陷阱:影響Object.keys()等遍歷
ownKeys() 會在Object.keys()及類似方法中被調(diào)用:
const ownKeysHandler = {
ownKeys(target) {
// 過濾掉以_開頭的私有屬性
const keys = Reflect.ownKeys(target);
return keys.filter(key => {
if (typeof key === 'string') {
return !key.startsWith('_');
}
return true;
});
}
};
- 參數(shù)說明:
- target:目標(biāo)構(gòu)造函數(shù)
- 返回值說明:必須返回一個包含字符串或符號的可枚舉對象
7. getOwnPropertyDescriptor陷阱:影響Object.getOwnPropertyDescriptor()
getOwnPropertyDescriptor() 會在Object.getOwnPropertyDescriptor()方法中被調(diào)用:
const getOwnPropertyDescriptorHandler = {
getOwnPropertyDescriptor(target, prop) {
// 隱藏私有屬性的描述符
if (prop.startsWith('_')) {
return undefined;
}
return Reflect.getOwnPropertyDescriptor(target, prop);
}
};
- 參數(shù)說明:
- target:目標(biāo)構(gòu)造函數(shù)
- prop:引用的目標(biāo)對象上的字符串屬性
- 返回值說明:必須返回對象,或在屬性不存在時返回 undefined
8. defineProperty陷阱:攔截Object.defineProperty()
defineProperty() 會在Object.defineProperty()方法中被調(diào)用:
const definePropertyHandler = {
defineProperty(target, prop, descriptor) {
// 防止修改只讀屬性
if (prop === 'id' && target.id) {
console.warn('id屬性是只讀的');
return false;
}
return Reflect.defineProperty(target, prop, descriptor);
}
};
- 參數(shù)說明:
- target:目標(biāo)構(gòu)造函數(shù)
- prop:引用的目標(biāo)對象上的字符串屬性
- descriptor:包含可選的enumrable、configurable、value、get 和 set 等定義的對象
- 返回值說明:必須返回布爾值,表示屬性是否成功定義
9. deleteProperty陷阱:攔截delete操作
deleteProperty() 會在 delete 操作中被調(diào)用:
const deletePropertyHandler = {
deleteProperty(target, prop) {
// 防止刪除重要屬性
const protectedProps = ['id', 'createdAt'];
if (protectedProps.includes(prop)) {
console.warn(`不能刪除受保護的屬性: ${prop}`);
return false;
}
return Reflect.deleteProperty(target, prop);
}
};
- 參數(shù)說明:
- target:目標(biāo)構(gòu)造函數(shù)
- property:引用的目標(biāo)對象上的屬性
- 返回值說明:必須返回布爾值,表示屬性是否被成功刪除
10. preventExtensions陷阱:攔截Object.preventExtensions()
preventExtensions() 會在 Object.preventExtensions() 方法中被調(diào)用:
const preventExtensionsHandler = {
preventExtensions(target) {
// 在阻止擴展前添加標(biāo)記
target._frozenAt = new Date();
return Reflect.preventExtensions(target);
}
};
- 參數(shù)說明:
- target:目標(biāo)構(gòu)造函數(shù)
- 返回值說明:必須返回布爾值,表示 target 是否已經(jīng)不可擴展
11. getPrototypeOf陷阱:攔截Object.getPrototypeOf()
getPrototypeOf() 會在 Object.getPrototypeOf() 方法中被調(diào)用:
const getPrototypeOfHandler = {
getPrototypeOf(target) {
return Reflect.getPrototypeOf(target);
}
};
- 參數(shù)說明:
- target:目標(biāo)構(gòu)造函數(shù)
- 返回值說明:必須返回對象或null
12. setPrototypeOf陷阱:攔截Object.setPrototypeOf()
setPrototypeOf() 會在 Object.setPrototypeOf() 方法中被調(diào)用:
const setPrototypeOfHandler = {
setPrototypeOf(target, prototype) {
return Reflect.setPrototypeOf(target, prototype);
}
};
- 參數(shù)說明:
- target:目標(biāo)構(gòu)造函數(shù)
- property:引用的目標(biāo)對象上的屬性
- 返回值說明:必須返回布爾值,表示原型賦值是否成功
13. isExtensible陷阱:攔截Object.isExtensible()
isExtensible() 會在 Object.isExtensible() 方法中被調(diào)用:
const isExtensibleHandler = {
isExtensible(targete) {
return Reflect.isExtensible(targete);
}
};
- 參數(shù)說明:
- target:目標(biāo)構(gòu)造函數(shù)
- 返回值說明:必須返回布爾值,表示 target 是否可擴展
可撤銷代理
什么是可撤銷代理?
可撤銷代理(Revocable Proxy)是一種特殊的代理,它提供了一個 revoke() 方法,調(diào)用該方法后,代理將不再可用,所有對代理的操作都會拋出 TypeError,即:會中斷代理對象和目標(biāo)對象之間的聯(lián)系。
const target = { name: 'zhangsan' };
const handler = {
get() {
return 'foo';
}
}
// 創(chuàng)建可撤銷代理
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.name); // 'zhangsan'
// 撤銷代理
revoke();
// 代理已失效
try {
console.log(proxy.name); // TypeError: Cannot perform 'get' on a proxy that has been revoked
} catch (error) {
console.log('錯誤:', error.message);
}
實現(xiàn)響應(yīng)式數(shù)據(jù)(Vue3原理)
// 簡易版Vue3響應(yīng)式系統(tǒng)
class ReactiveSystem {
constructor() {
this.targetMap = new WeakMap(); // 存儲依賴關(guān)系
this.effectStack = []; // 當(dāng)前正在執(zhí)行的effect
this.batchQueue = new Set(); // 批量更新隊列
this.isBatching = false; // 是否處于批量更新模式
}
// 核心:創(chuàng)建響應(yīng)式對象
reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 獲取原始值
const result = Reflect.get(target, key, receiver);
// 依賴收集
track(target, key);
// 深度響應(yīng)式(如果值是對象,繼續(xù)代理)
if (result && typeof result === 'object') {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
// 獲取舊值
const oldValue = target[key];
// 設(shè)置新值
const result = Reflect.set(target, key, value, receiver);
// 觸發(fā)更新(值確實改變時才觸發(fā))
if (oldValue !== value) {
trigger(target, key);
}
return result;
},
// 處理刪除操作
deleteProperty(target, key) {
const hasKey = key in target;
const result = Reflect.deleteProperty(target, key);
if (hasKey && result) {
trigger(target, key);
}
return result;
}
});
}
// 依賴收集
track(target, key) {
if (this.effectStack.length === 0) return;
let depsMap = this.targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
this.targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
// 收集當(dāng)前正在執(zhí)行的effect
const activeEffect = this.effectStack[this.effectStack.length - 1];
if (activeEffect) {
dep.add(activeEffect);
// effect也需要知道哪些依賴收集了它
activeEffect.deps.push(dep);
}
}
// 觸發(fā)更新
trigger(target, key) {
const depsMap = this.targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
// 如果是批量更新模式,加入隊列
if (this.isBatching) {
dep.forEach(effect => this.batchQueue.add(effect));
} else {
// 立即執(zhí)行所有依賴的effect
dep.forEach(effect => {
if (effect !== this.effectStack[this.effectStack.length - 1]) {
effect.run();
}
});
}
}
}
// 創(chuàng)建effect(副作用)
effect(fn) {
const effect = new ReactiveEffect(fn, this);
effect.run();
// 返回停止函數(shù)
const runner = effect.run.bind(effect);
runner.effect = effect;
runner.stop = () => effect.stop();
return runner;
}
// 開始批量更新
batchStart() {
this.isBatching = true;
}
// 結(jié)束批量更新
batchEnd() {
this.isBatching = false;
// 執(zhí)行隊列中的所有effect
this.batchQueue.forEach(effect => {
if (effect !== this.effectStack[this.effectStack.length - 1]) {
effect.run();
}
});
this.batchQueue.clear();
}
// computed計算屬性
computed(getter) {
let value;
let dirty = true;
const runner = this.effect(() => {
value = getter();
dirty = false;
});
return {
get value() {
if (dirty) {
runner();
}
return value;
}
};
}
// watch偵聽器
watch(source, callback, options = {}) {
let getter;
if (typeof source === 'function') {
getter = source;
} else {
getter = () => this.traverse(source);
}
let oldValue;
const job = () => {
const newValue = runner();
callback(newValue, oldValue);
oldValue = newValue;
};
const runner = this.effect(getter, {
lazy: true,
scheduler: job
});
oldValue = runner();
if (options.immediate) {
job();
}
}
// 深度遍歷對象(用于watch)
traverse(value, seen = new Set()) {
if (typeof value !== 'object' || value === null || seen.has(value)) {
return value;
}
seen.add(value);
for (const key in value) {
this.traverse(value[key], seen);
}
return value;
}
}
// 響應(yīng)式effect類
class ReactiveEffect {
constructor(fn, scheduler) {
this.fn = fn;
this.scheduler = scheduler;
this.deps = [];
this.active = true;
}
run() {
if (!this.active) return;
try {
this.scheduler.effectStack.push(this);
return this.fn();
} finally {
this.scheduler.effectStack.pop();
}
}
stop() {
if (this.active) {
// 從所有依賴中移除自己
this.deps.forEach(dep => dep.delete(this));
this.deps.length = 0;
this.active = false;
}
}
}
實現(xiàn)不可變數(shù)據(jù)(immer.js原理)
// 簡易版immer實現(xiàn)
class Immer {
constructor(baseState) {
this.baseState = baseState;
this.draft = this.createDraft(baseState);
this.isModified = false;
}
// 創(chuàng)建草稿(代理)
createDraft(base) {
// 如果不是對象,直接返回
if (typeof base !== 'object' || base === null) {
return base;
}
// 處理數(shù)組
if (Array.isArray(base)) {
return this.createArrayDraft(base);
}
// 處理對象
return this.createObjectDraft(base);
}
// 創(chuàng)建對象草稿
createObjectDraft(base) {
const draft = Array.isArray(base) ? [] : {};
// 復(fù)制所有屬性
for (const key in base) {
if (base.hasOwnProperty(key)) {
draft[key] = this.createDraft(base[key]);
}
}
// 創(chuàng)建代理
return new Proxy(draft, {
get: (target, prop, receiver) => {
// 特殊屬性處理
if (prop === '__immer_draft__') return true;
if (prop === '__immer_original__') return base;
return Reflect.get(target, prop, receiver);
},
set: (target, prop, value, receiver) => {
this.isModified = true;
// 如果新值是普通值,直接設(shè)置
if (typeof value !== 'object' || value === null) {
return Reflect.set(target, prop, value, receiver);
}
// 如果新值是草稿,獲取其最終狀態(tài)
if (value.__immer_draft__) {
value = this.finalize(value);
}
// 如果新值是對象,創(chuàng)建新的草稿
const newDraft = this.createDraft(value);
return Reflect.set(target, prop, newDraft, receiver);
},
deleteProperty: (target, prop) => {
this.isModified = true;
return Reflect.deleteProperty(target, prop);
}
});
}
// 創(chuàng)建數(shù)組草稿(需要特殊處理)
createArrayDraft(base) {
const draft = this.createObjectDraft(base);
// 代理數(shù)組方法
const arrayMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
arrayMethods.forEach(method => {
const originalMethod = Array.prototype[method];
draft[method] = function (...args) {
this.isModified = true;
return originalMethod.apply(this, args);
}.bind({ isModified: false }); // 這里簡化處理
});
return draft;
}
// 獲取草稿
getDraft() {
return this.draft;
}
// 完成修改,生成新狀態(tài)
produce(producer) {
// 執(zhí)行生產(chǎn)者函數(shù)
producer(this.draft);
// 如果沒有修改,返回原狀態(tài)
if (!this.isModified) {
return this.baseState;
}
// 最終化草稿,生成新狀態(tài)
return this.finalize(this.draft);
}
// 最終化草稿(將草稿轉(zhuǎn)換為普通對象)
finalize(draft) {
// 如果不是草稿,直接返回
if (!draft || !draft.__immer_draft__) {
return draft;
}
const original = draft.__immer_original__;
const result = Array.isArray(original) ? [] : {};
let hasChanges = false;
// 收集所有鍵(包括原對象和新對象)
const allKeys = new Set([
...Object.keys(original || {}),
...Object.keys(draft)
]);
for (const key of allKeys) {
const originalValue = original ? original[key] : undefined;
const draftValue = draft[key];
// 如果屬性被刪除
if (!(key in draft)) {
hasChanges = true;
continue;
}
// 如果值是草稿,遞歸最終化
if (draftValue && draftValue.__immer_draft__) {
const finalized = this.finalize(draftValue);
if (finalized !== originalValue) {
hasChanges = true;
result[key] = finalized;
} else {
result[key] = originalValue;
}
}
// 如果值被修改
else if (draftValue !== originalValue) {
hasChanges = true;
result[key] = draftValue;
}
// 值未修改
else {
result[key] = originalValue;
}
}
return hasChanges ? result : original;
}
}
// 簡化API
function produce(baseState, producer) {
const immer = new Immer(baseState);
return immer.produce(producer);
}
結(jié)語
Proxy和Reflect開啟了JavaScript元編程的新篇章,它們不僅是框架開發(fā)的利器,也是理解現(xiàn)代JavaScript運行時特性的關(guān)鍵。
到此這篇關(guān)于JavaScript Proxy與Reflect的實現(xiàn)的文章就介紹到這了,更多相關(guān)JavaScript Proxy Reflect內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript實現(xiàn)滑動導(dǎo)航欄效果
這篇文章主要為大家詳細介紹了JavaScript實現(xiàn)滑動導(dǎo)航欄效果,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-08-08
Android 自定義view仿微信相機單擊拍照長按錄視頻按鈕
這篇文章主要介紹了Android 自定義view仿微信相機單擊拍照長按錄視頻按鈕,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值 ,需要的朋友可以參考下2019-07-07
Three.js+React實現(xiàn)3D開放世界小游戲
本文使用?Three.js?+?React?+?CANNON?技術(shù)棧,實現(xiàn)通過滑動屏幕控制模型在3D世界里運動的?Low?Poly?低多邊形風(fēng)格小游戲,感興趣的可以了解一下2022-04-04
一文詳解requestAnimationFrame請求動畫幀
requestAnimationFrame是一個用于動畫效果的 API,它使用瀏覽器的刷新率來執(zhí)行回調(diào)函數(shù),通常每秒鐘執(zhí)行60次,這篇文章主要給大家介紹了關(guān)于requestAnimationFrame請求動畫幀的相關(guān)資料,需要的朋友可以參考下2023-12-12
延時重復(fù)執(zhí)行函數(shù) lLoopRun.js
延時重復(fù)執(zhí)行函數(shù) lLoopRun.js...2007-05-05

