INT 33h AX=000Ch replaces polling with callbacks. The program gives the driver the far address of a routine and a mask of events; from then on, each time one of those events happens, the driver calls the routine from inside its own interrupt handler, while the main program is interrupted wherever it is.

Mask bitEvent
0the pointer moved
1 / 2left button pressed / released
3 / 4right button pressed / released
5 / 6middle button pressed / released

The handler is called with a far CALL and must return with RETF, not IRET. On entry AX holds the bits of the events that happened (same layout as the mask), BX the button state, CX and DX the position, and SI and DI the raw mickey counts. DS is whatever the driver had, so the handler reaches its own data through CS (in a .COM program CS = DS of the program) or loads DS itself and restores it. It runs at interrupt time: keep it short, do not call DOS, and usually just record the event for the main loop to pick up.

prints: handler installed / handler removed (two lines). The example does not wait for clicks; a real program would watch CLICKS1 in its main loop between the two calls.
XOR AX, AX ; reset (and detect) first
INT 33h
PUSH CS
POP ES ; ES:DX = the handler
MOV DX, OFFSET EVENT1
MOV CX, 0006h ; call on: left press (bit 1), left release (bit 2)
MOV AX, 000Ch ; mouse: install the event handler
INT 33h
MOV DX, OFFSET ON1
MOV AH, 09h ; DOS: print the $-string at DS:DX
INT 21h
; ... the program runs; the driver calls EVENT1 on each left click ...
MOV AX, 000Ch ; remove it again: an empty mask
XOR CX, CX
INT 33h
MOV DX, OFFSET OFF1
MOV AH, 09h ; DOS: print the $-string at DS:DX
INT 21h
EVENT1 PROC FAR ; called by the driver: AX = events, BX = buttons, CX/DX = position
INC CS:CLICKS1 ; DS is not ours here: go through CS
RET ; a far return (RETF), not IRET
EVENT1 ENDP
CLICKS1 DW 0
ON1 DB "handler installed", 13, 10, "$"
OFF1 DB "handler removed", 13, 10, "$"

Pitfalls

The driver stays resident after the program exits; a handler left installed is called in memory that now belongs to someone else, and the next mouse movement crashes the machine. Remove it (CX = 0) or reset the driver (INT 33h AX=0000h clears the handler) before exiting, including on error paths and Ctrl+C. AX=0014h swaps in a new handler and returns the old one, so a program can chain to a previous handler and restore it at the end. The handler interrupts the main program at any instruction, so data shared between them must be updated in a way the main loop cannot see half-done: a single word, or with CLI around the read.