直接看人話總結git
承接上一章angularjs
該文章項目地址github
文章地址ajax
angular 版本:8.0.0-rc.4typescript
歡迎看看個人類angular框架bootstrap
試讀angular源碼第一章:開場與platformBrowserDynamicapi
試讀angular源碼第二章:引導模塊bootstrapModule瀏覽器
試讀angular源碼第四章:angular模塊及JIT編譯模塊app
angularjs 時代,經過觸發 $scope.$apply
$scope.$digest
來通知進行髒檢查並更新視圖。
從 angularjs 的行爲中,谷歌大佬們發現,全部的視圖變動都來自於下面幾種行爲:
onclick
, onmouseover
, onkeyup
setInterval
, setTimeout
, setImmediate
ajax
,fetch
,Promise.then
angular 便把在 Dart 中實踐過的 zone 的技術在 JavaScript 中實現了一次。
Dart 中的異步操做是沒法被當前代碼 try/cacth
的,而在 Dart 中你能夠給執行對象指定一個 zone,相似提供一個上下文執行環境 。
而在這個執行環境內,你就能夠所有能夠捕獲、攔截或修改一些代碼行爲,好比全部未被處理的異常。
用人話說,zone 就是一個相似 JavaScript 中的執行上下文,提供了一個環境或者過程。
每個異步方法的執行都在 zone 都被當作爲一個Task,並在Task的基礎上,zone 爲開發者提供了執行先後的鉤子函數,來得到執行先後的信息。
大概講下 Zone
:
zone.js/lib/zone.ts
const Zone: ZoneType = (function(global: any) {
...
class Zone implements AmbientZone {
...
}
...
let _currentZoneFrame: _ZoneFrame = {parent: null, zone: new Zone(null, null)};
...
return global['Zone'] = Zone;
})(global);
複製代碼
Zone
是個自執行函數
執行的時候會建立一個 parent
和 zoneSpec
都是 null
,而且 name
是 <root>
的 Zone
實例,因此 Zone
是一顆有惟一根節點的樹
執行的末尾經過把 class Zone
賦值給頂層變量的 Zone
屬性。
瞭解下 zone 的代理 ZoneDelegate
zoneSpec
是 fork
子 zone 的時候傳入的配置對象
當 Zone
初始化本身的代理 ZoneDelegate
時,會把 Zone
實例 和父級的 zoneSpec
傳入
在 Zone
初始化時,會同步初始化一個代理 ZoneDelegate
:
zone.js/lib/zone.ts
class Zone implements AmbientZone {
...
constructor(parent: Zone|null, zoneSpec: ZoneSpec|null) {
// 註釋:zoneSpec 就是 fork的時候那個配置對象
this._parent = parent;
this._name = zoneSpec ? zoneSpec.name || 'unnamed' : '<root>';
this._properties = zoneSpec && zoneSpec.properties || {};
// 註釋:實現 zone 代理
this._zoneDelegate =
new ZoneDelegate(this, this._parent && this._parent._zoneDelegate, zoneSpec);
}
...
}
複製代碼
ZoneDelegate
的構造函數把經過 fork
方法建立 Zone
時傳入的配置和鉤子函數初始化到 ZoneDelegate
代理實例上:
zone.js/lib/zone.ts
class ZoneDelegate implements AmbientZoneDelegate {
...
constructor(zone: Zone, parentDelegate: ZoneDelegate|null, zoneSpec: ZoneSpec|null) {
...
// 註釋:zoneSpec 就是 fork的時候那個配置對象
this._forkZS = zoneSpec && (zoneSpec && zoneSpec.onFork ? zoneSpec : parentDelegate!._forkZS);
this._forkDlgt = zoneSpec && (zoneSpec.onFork ? parentDelegate : parentDelegate!._forkDlgt);
this._forkCurrZone = zoneSpec && (zoneSpec.onFork ? this.zone : parentDelegate!.zone);
...
}
fork(targetZone: Zone, zoneSpec: ZoneSpec): AmbientZone {
// 註釋:若是有 onFork 鉤子就執行
return this._forkZS ? this._forkZS.onFork!(this._forkDlgt!, this.zone, targetZone, zoneSpec) :
new Zone(targetZone, zoneSpec);
}
...
}
複製代碼
最後,實際上代理執行鉤子的時候,好比 zone.fork
的時候的配置對象內有鉤子函數,那麼就會調用鉤子函數來執行
每一個 Zone
都會有一個 ZoneDelegate
代理實例,主要爲 Zone
調用傳入的回調函數,創建、調用回調函數中的異步任務,捕捉異步任務的錯誤
zone 經過 monkey patch 的方式,暴力將瀏覽器內的異步API進行封裝並替換掉,這一塊就在這裏。
這樣當在 Zone 的上下文內運行時,並能夠經過 Zone.current
來通知 angular 進行到了哪裏並進行變動檢測(其實也就是所謂的觸發髒檢查)(至於若是觸發的咱們放到下章再講)
這部分在打包 zonejs 的時候就將這幾個替換API操做的文件根據平臺打包到一塊兒了,舉個瀏覽器的例子:
zone.js/lib/browser/browser.ts
Zone.__load_patch('timers', (global: any) => {
const set = 'set';
const clear = 'clear';
patchTimer(global, set, clear, 'Timeout');
patchTimer(global, set, clear, 'Interval');
patchTimer(global, set, clear, 'Immediate');
});
複製代碼
具體加載定時器補丁的方法:
zone.js/lib/common/timers.ts
export function patchTimer(window: any, setName: string, cancelName: string, nameSuffix: string) {
let setNative: Function|null = null;
let clearNative: Function|null = null;
...
setNative =
patchMethod(window, setName, (delegate: Function) => function(self: any, args: any[]) {
...
});
...
}
複製代碼
經過 patchMethod
將原生API替換爲被 zone 封裝過的API來得到與 Zone
通訊並觸發鉤子函數的能力:
zone.js/lib/common/utils.ts
export function patchMethod(
target: any, name: string,
patchFn: (delegate: Function, delegateName: string, name: string) => (self: any, args: any[]) =>
any): Function|null {
let proto = target;
while (proto && !proto.hasOwnProperty(name)) {
proto = ObjectGetPrototypeOf(proto);
}
if (!proto && target[name]) {
// somehow we did not find it, but we can see it. This happens on IE for Window properties.
proto = target;
}
const delegateName = zoneSymbol(name);
let delegate: Function|null = null;
if (proto && !(delegate = proto[delegateName])) {
delegate = proto[delegateName] = proto[name];
// check whether proto[name] is writable
// some property is readonly in safari, such as HtmlCanvasElement.prototype.toBlob
const desc = proto && ObjectGetOwnPropertyDescriptor(proto, name);
if (isPropertyWritable(desc)) {
const patchDelegate = patchFn(delegate!, delegateName, name);
proto[name] = function() {
return patchDelegate(this, arguments as any);
};
attachOriginToPatched(proto[name], delegate);
if (shouldCopySymbolProperties) {
copySymbolProperties(delegate, proto[name]);
}
}
}
return delegate;
}
複製代碼
最後用一個全局變量 patches: {[key: string]: any}
存儲打過的補丁,能夠用來判斷 zone 是否已經上過補丁等。
zone.js/lib/zone.ts
const patches: {[key: string]: any} = {};
class Zone implements AmbientZone {
// 註釋:經過該方法緩存猴子補丁
static __load_patch(name: string, fn: _PatchFn): void {
if (patches.hasOwnProperty(name)) {
if (checkDuplicate) {
throw Error('Already loaded patch: ' + name);
}
} else if (!global['__Zone_disable_' + name]) {
const perfName = 'Zone:' + name;
mark(perfName);
patches[name] = fn(global, Zone, _api);
performanceMeasure(perfName, perfName);
}
}
}
複製代碼
在 zone 中,每種異步都被稱爲任務 :Task
type TaskType = 'microTask'|'macroTask'|'eventTask';
type TaskState = 'notScheduled'|'scheduling'|'scheduled'|'running'|'canceling'|'unknown';
interface Task {
type: TaskType;
state: TaskState;
source: string;
invoke: Function;
callback: Function;
data?: TaskData;
scheduleFn?: (task: Task) => void;
cancelFn?: (task: Task) => void;
readonly zone: Zone;
runCount: number;
cancelScheduleRequest(): void;
}
複製代碼
Task
分爲三種:
Promise,MutationObserver、process.nextTick
setTimeout, setInterval, setImmediate, I/O, UI rendering
只有這三種,因此像 DOM0 級別事件如 img.onload=()=>{}
,在 angular裏面是沒法觸發髒檢查的。
Task
的狀態則有 'notScheduled'|'scheduling'|'scheduled'|'running'|'canceling'|'unknown';
而設置執行運行時的鉤子則須要在 zone.fork
時設置配置,看這裏:
zone.js/lib/zone.ts
interface ZoneSpec {
...
/** * Allows interception of task scheduling. */
onScheduleTask?:
(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task) => Task;
onInvokeTask?:
(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task,
applyThis: any, applyArgs?: any[]) => any;
/** * Allows interception of task cancellation. */
onCancelTask?:
(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task) => any;
/** * Notifies of changes to the task queue empty status. */
onHasTask?:
(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
hasTaskState: HasTaskState) => void;
}
複製代碼
onScheduleTask
建立異步任務onInvokeTask
執行異步任務onCancelTask
取消異步任務onHasTask
通知任務隊列空狀態的更改經過設置這幾種鉤子,angular 就能知道某些異步任務執行的哪一步,也能夠經過鉤子去觸發髒檢查
angular 啓動 zonejs 是在上文說過的 bootstrapModule
階段:
angular/packages/core/src/application_ref.ts
@Injectable()
export class PlatformRef {
...
bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>, options?: BootstrapOptions):
Promise<NgModuleRef<M>> {
// Note: We need to create the NgZone _before_ we instantiate the module,
// as instantiating the module creates some providers eagerly.
// So we create a mini parent injector that just contains the new NgZone and
// pass that as parent to the NgModuleFactory.
const ngZoneOption = options ? options.ngZone : undefined;
const ngZone = getNgZone(ngZoneOption);
const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}];
// Attention: Don't use ApplicationRef.run here,
// as we want to be sure that all possible constructor calls are inside `ngZone.run`!
// 註釋:會被 onInvoke 執行
return ngZone.run(() => {
...
});
}
...
}
複製代碼
在實例化模塊工廠以前,經過 getNgZone
獲取了一個 NgZone
實例:
angular/packages/core/src/application_ref.ts
function getNgZone(ngZoneOption?: NgZone | 'zone.js' | 'noop'): NgZone {
let ngZone: NgZone;
if (ngZoneOption === 'noop') {
ngZone = new NoopNgZone();
} else {
ngZone = (ngZoneOption === 'zone.js' ? undefined : ngZoneOption) ||
new NgZone({enableLongStackTrace: isDevMode()});
}
return ngZone;
}
複製代碼
angular/packages/core/src/zone/ng_zone.ts
export class NgZone {
...
constructor({enableLongStackTrace = false}) {
if (typeof Zone == 'undefined') {
throw new Error(`In this configuration Angular requires Zone.js`);
}
Zone.assertZonePatched();
const self = this as any as NgZonePrivate;
self._nesting = 0;
self._outer = self._inner = Zone.current;
if ((Zone as any)['wtfZoneSpec']) {
self._inner = self._inner.fork((Zone as any)['wtfZoneSpec']);
}
if ((Zone as any)['TaskTrackingZoneSpec']) {
self._inner = self._inner.fork(new ((Zone as any)['TaskTrackingZoneSpec'] as any));
}
if (enableLongStackTrace && (Zone as any)['longStackTraceZoneSpec']) {
self._inner = self._inner.fork((Zone as any)['longStackTraceZoneSpec']);
}
forkInnerZoneWithAngularBehavior(self);
}
...
run<T>(fn: (...args: any[]) => T, applyThis?: any, applyArgs?: any[]): T { return (this as any as NgZonePrivate)._inner.run(fn, applyThis, applyArgs) as T; } } 複製代碼
assertZonePatched
,確認下 zone 是否已經打過補丁(是否替換過原生 API 至於爲何咱們往下面再說)zone.js/lib/zone.ts
class Zone implements AmbientZone {
static __symbol__: (name: string) => string = __symbol__;
static assertZonePatched() {
if (global['Promise'] !== patches['ZoneAwarePromise']) {
throw new Error(
'Zone.js has detected that ZoneAwarePromise `(window|global).Promise` ' +
'has been overwritten.\n' +
'Most likely cause is that a Promise polyfill has been loaded ' +
'after Zone.js (Polyfilling Promise api is not necessary when zone.js is loaded. ' +
'If you must load one, do so before loading zone.js.)');
}
}
}
複製代碼
_nesting
爲 Zone
執行棧的層數(這個放後面說)
angular/packages/core/src/zone/ng_zone.ts
class NgZone {
constructor({enableLongStackTrace = false}) {
if (typeof Zone == 'undefined') {
throw new Error(`In this configuration Angular requires Zone.js`);
}
// 註釋:確認是否已經上過zone補丁
Zone.assertZonePatched();
const self = this as any as NgZonePrivate;
self._nesting = 0;
// 註釋:此時是 root zone
self._outer = self._inner = Zone.current;
...
}
}
複製代碼
_outer
和 _inner
爲當前全局的 zone Zone.current
,
zone.js/lib/zone.ts
interface ZoneType {
/** * @returns {Zone} Returns the current [Zone]. The only way to change * the current zone is by invoking a run() method, which will update the current zone for the * duration of the run method callback. */
current: Zone;
}
複製代碼
Zone.current
是 zone 上的一個靜態屬性,用來保存全局此刻正在使用的 zone,只能經過 zone.run
來 更改
forkInnerZoneWithAngularBehavior
從當前的 zone(其實此時就是根<root>Zone
) fork 出一份 angular zone,並設置鉤子angular/packages/core/src/zone/ng_zone.ts
// 註釋:zone 是 `Zone.current` 此時是 root zone
function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
zone._inner = zone._inner.fork({
name: 'angular',
properties: <any>{'isAngularZone': true},
onInvokeTask: (delegate: ZoneDelegate, current: Zone, target: Zone, task: Task, applyThis: any,
applyArgs: any): any => {
try {
onEnter(zone);
return delegate.invokeTask(target, task, applyThis, applyArgs);
} finally {
onLeave(zone);
}
},
// 註釋:啓動 ngZone的 run
onInvoke: (delegate: ZoneDelegate, current: Zone, target: Zone, callback: Function,
applyThis: any, applyArgs: any[], source: string): any => {
try {
onEnter(zone);
return delegate.invoke(target, callback, applyThis, applyArgs, source);
} finally {
onLeave(zone);
}
},
onHasTask:
(delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) => {
delegate.hasTask(target, hasTaskState);
if (current === target) {
// We are only interested in hasTask events which originate from our zone
// (A child hasTask event is not interesting to us)
if (hasTaskState.change == 'microTask') {
zone.hasPendingMicrotasks = hasTaskState.microTask;
checkStable(zone);
} else if (hasTaskState.change == 'macroTask') {
zone.hasPendingMacrotasks = hasTaskState.macroTask;
}
}
},
onHandleError: (delegate: ZoneDelegate, current: Zone, target: Zone, error: any): boolean => {
delegate.handleError(target, error);
zone.runOutsideAngular(() => zone.onError.emit(error));
return false;
}
});
}
複製代碼
在上面,getNgZone
的時候會 new NgZone
,
而在 NgZone
構造函數的結尾,forkInnerZoneWithAngularBehavior
中執行了 zone._inner.fork
:
angular/packages/core/src/zone/ng_zone.ts
export class NgZone {
constructor({enableLongStackTrace = false}) {
...
forkInnerZoneWithAngularBehavior(self);
}
}
function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
zone._inner = zone._inner.fork({
name: 'angular',
...
});
}
複製代碼
zone.fork
主要是建立一個子 Zone
實例,而 fork
方法主要調用構造函數中實例化的 ZoneDelegate
實例的 fork
方法:
zone.js/lib/zone.ts
class Zone implements AmbientZone {
constructor(parent: Zone|null, zoneSpec: ZoneSpec|null) {
this._parent = parent;
this._name = zoneSpec ? zoneSpec.name || 'unnamed' : '<root>';
this._properties = zoneSpec && zoneSpec.properties || {};
this._zoneDelegate =
new ZoneDelegate(this, this._parent && this._parent._zoneDelegate, zoneSpec);
}
// 註釋:fork 出子zone, zoneSpec 就是那一大堆配置
public fork(zoneSpec: ZoneSpec): AmbientZone {
if (!zoneSpec) throw new Error('ZoneSpec required!');
return this._zoneDelegate.fork(this, zoneSpec);
}
}
複製代碼
每一個 Zone
都會有一個 ZoneDelegate
代理實例,主要爲 Zone
調用傳入的回調函數,創建、調用回調函數中的異步任務,捕捉異步任務的錯誤
這裏經過調用 ZoneDelegate
實例的 fork
方法從根 Zone
建立了一個 Zone
:
zone.js/lib/zone.ts
class ZoneDelegate implements AmbientZoneDelegate {
...
fork(targetZone: Zone, zoneSpec: ZoneSpec): AmbientZone {
// 註釋:若是有 onFork 鉤子就執行
return this._forkZS ? this._forkZS.onFork!(this._forkDlgt!, this.zone, targetZone, zoneSpec) :
new Zone(targetZone, zoneSpec);
}
...
}
複製代碼
因此,當初始化 ngZone
的時候,這個 zone._inner
就是 Zone.current
,也就是 let _currentZoneFrame: _ZoneFrame = {parent: null, zone: new Zone(null, null)};
時候建立的 new Zone(null, null)
root zone。
所以此時的 zone._inner
就是 Zone.current
其實也是 <root> Zone
因此angular zone
是從 <root>Zone
fork 出的子 zone。
當初始化好 Zone
和 ZoneDelegate
,angular 調用了 ngZone.run
angular/packages/core/src/zone/ng_zone.ts
export class NgZone {
run<T>(fn: (...args: any[]) => T, applyThis?: any, applyArgs?: any[]): T { return (this as any as NgZonePrivate)._inner.run(fn, applyThis, applyArgs) as T; } } 複製代碼
ngZone.run
又調用了 zone.run
zone.js/lib/zone.ts
interface _ZoneFrame {
parent: _ZoneFrame|null;
zone: Zone;
}
let _currentZoneFrame: _ZoneFrame = {parent: null, zone: new Zone(null, null)};
class Zone implements AmbientZone {
...
// 註釋:經過this._zoneDelegate.invoke執行一個函數
public run(callback: Function, applyThis?: any, applyArgs?: any[], source?: string): any;
public run<T>(callback: (...args: any[]) => T, applyThis?: any, applyArgs?: any[], source?: string): T { _currentZoneFrame = {parent: _currentZoneFrame, zone: this}; try { return this._zoneDelegate.invoke(this, callback, applyThis, applyArgs, source); } finally { _currentZoneFrame = _currentZoneFrame.parent!; } } ... } 複製代碼
_currentZoneFrame
是一個全局對象,保存了當前系統中的 zone 幀鏈
在初始化的時候,會建立一個 parent: null, zone: new Zone
的根 _currentZoneFrame
,所以根 Zone
就是在這裏被建立的
它有兩個屬性:
parent
指向了父 zoneFrame
zone
指向了當前激活的zone對象因此 _currentZoneFrame
並非固定不變的。
ngZone.run
又觸發了 this._zoneDelegate.invoke
zone 是經過 this._zoneDelegate.invoke
執行一個方法的:
angular/packages/core/src/zone/ng_zone.ts
class ZoneDelegate implements AmbientZoneDelegate {
private _invokeZS: ZoneSpec|null;
...
constructor(zone: Zone, parentDelegate: ZoneDelegate|null, zoneSpec: ZoneSpec|null) {
this._invokeZS = zoneSpec && (zoneSpec.onInvoke ? zoneSpec : parentDelegate!._invokeZS);
}
...
invoke(
targetZone: Zone, callback: Function, applyThis: any, applyArgs?: any[], source?: string): any {
return this._invokeZS ? this._invokeZS.onInvoke!
(this._invokeDlgt!, this._invokeCurrZone!, targetZone, callback,
applyThis, applyArgs, source) :
callback.apply(applyThis, applyArgs);
}
...
}
複製代碼
invoke
方法接受4個參數:
targetZone: Zone
當前調用 ZoneDelegate
的 Zone
實例callback: Function
回調函數,其實就是 zone.run(callback)
傳入的那個函數,實例化模塊組件的函數applyThis: any
須要綁定的 this
applyArgs?: any[]
回調函數的參數source?: string
資源暫時不知道幹嗎的invoke
方法的做用就是:若是 this._invokeZS
存在而且有 onInvoke
鉤子就用 this._invokeZS.onInvoke
執行回調,不然僅僅調用回調函數。
因此回到一開始實例化 ngZone
的最後 forkInnerZoneWithAngularBehavior
的代碼:
angular/packages/core/src/zone/ng_zone.ts
function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
zone._inner = zone._inner.fork({
name: 'angular',
....
onInvoke: (delegate: ZoneDelegate, current: Zone, target: Zone, callback: Function,
applyThis: any, applyArgs: any[], source: string): any => {
try {
onEnter(zone);
return delegate.invoke(target, callback, applyThis, applyArgs, source);
} finally {
onLeave(zone);
}
},
...
});
}
複製代碼
因此當 onInvoke
鉤子調用了 run
的回調的時候,會前後觸發 onEnter(zone);
onLeave(zone);
:
angular/packages/core/src/zone/ng_zone.ts
function onEnter(zone: NgZonePrivate) {
zone._nesting++;
if (zone.isStable) {
zone.isStable = false;
zone.onUnstable.emit(null);
}
}
function onLeave(zone: NgZonePrivate) {
zone._nesting--;
checkStable(zone);
}
複製代碼
當進入執行棧時,ngZone._nesting
++
離開 --
鉤子函數 onInvoke
又調用了 delegate: ZoneDelegate
既 angular zone
的父級 <Zone>zone
的 invoke
方法:
zone.js/lib/zone.ts
class ZoneDelegate implements AmbientZoneDelegate {
...
invoke(
targetZone: Zone, callback: Function, applyThis: any, applyArgs?: any[],
source?: string): any {
return this._invokeZS ? this._invokeZS.onInvoke!
(this._invokeDlgt!, this._invokeCurrZone!, targetZone, callback,
applyThis, applyArgs, source) :
callback.apply(applyThis, applyArgs);
}
...
}
複製代碼
可是由於 <root>Zone
沒有 ZoneDelegate
,因此只是執行了 callback.apply(applyThis, applyArgs);
那麼爲何這麼作呢,讓 onInvoke
遞歸調用 delegate.invoke
?
由於 Zone
實例實際上是個樹形結構
我猜想 angular 想讓執行時通過層層傳遞觸發每個父級 Zone
的代理對象並觸發相應的鉤子函數調起對應的操做,最後交由根代理對象來執行真正的回調函數。
因此稍微總結下:
ngZone.run
其實就是調用了建立的 angular zone 的 run
方法zone.run
又調用了 fork angular 的時候傳入的配置 onInvoke
鉤子onInvoke
鉤子又執行了傳入 run
的回調即:onInvoke
鉤子執行了建立模塊和組件的函數onInvoke
執行回調時切入切面,會經過 onEnter(zone);
onLeave(zone);
來用 EventEmitter
通知 angular到此爲止,初始化好了 zone 的運行環境
用人話總結下:
Zone
建立 <root>Zone
bootstrapModuleFactory
在引導根模塊時,先會用 getNgZone
從 <root>Zone
出一個 angular Zone
,並設置幾個鉤子ngZone.run
並傳入實例化模塊工廠和組件的回調函數ngZone.run
調用 angular Zone
的 run
方法angular Zone
的 run
方法調用 fork
出 angular Zone
時傳入的配置中的 onInvoke
執行angular Zone
的 onInvoke
觸發進入/離開切面的操做,並調起父級 zone 的代理的 onInvoke
鉤子函數Zone
到祖 Zone
遞歸執行 onInvoke
鉤子,觸發對應的切面函數<root>Zone
執行實例化模塊工廠和組件的回調函數