# Alpine.js Magics

> [Alpine.js Magics](https://www.ftls.xyz/docs/alpine.js/alpinejs-3-magics/)
> Penned by [恐咖兵糖](https://www.ftls.xyz/) on 0001-01-01


## 概述

Alpine.js 提供了一系列以 `$` 开头的魔法属性（Magics），这些属性可以在任何 JavaScript 表达式中使用，用于访问组件内部状态、操作 DOM、调度事件等。魔法属性是 Alpine.js 响应式系统的核心组成部分，能够极大地增强组件的交互能力。

---

## $el

### 说明

`$el` 是一个特殊属性，返回当前 Alpine.js 组件的根元素引用。通过 `$el`，可以在 JavaScript 代码中直接访问和管理该元素，实现与 DOM 的深度交互。

### 适用范围

- 获取当前组件的根 DOM 元素
- 程序化操作元素（如添加类名、修改样式）
- 与第三方库集成时需要获取元素引用
- 实现复杂的 DOM 操作逻辑

### 语法

```html
<div x-data="{ count: 0 }" x-init="$el.style.backgroundColor = '#f0f0f0'">
    <p>计数: <span x-text="count"></span></p>
    <button @click="count++; $el.classList.add('active')">增加</button>
</div>

<!-- 使用 $el 获取元素属性 -->
<div x-data="{
    getElementInfo() {
        return {
            tag: $el.tagName,
            id: $el.id,
            class: $el.className
        }
    }
}">
    <button @click="console.log(getElementInfo())">获取元素信息</button>
</div>
```

### 注意事项

- `$el` 指向的是带有 `x-data` 指令的根元素，而非子元素
- 在 `x-init` 中使用时，元素已经完成初始化，可以安全访问
- `$el` 是原生 DOM 元素，可以调用所有标准的 DOM 方法
- 多次渲染时，`$el` 会始终指向当前组件的根元素

---

## $refs

### 说明

`$refs` 是一个对象，用于存储通过 `x-ref` 指令标记的 DOM 元素引用。通过 `$refs`，可以轻松访问组件内的任意元素，实现精确的 DOM 操作。

### 适用范围

- 获取多个子元素的引用
- 与需要 DOM 引用的第三方库集成
- 实现元素的聚焦、滚动、测量等操作
- 访问动态创建的元素

### 语法

```html
<div x-data="{
    scrollToTop() {
        $refs.content.scrollTop = 0
    },
    getText() {
        return $refs.message.textContent
    }
}">
    <h1 x-ref="title">标题</h1>
    <div x-ref="content" style="height: 200px; overflow-y: auto;">
        <p x-ref="message">内容区域</p>
    </div>

    <button @click="scrollToTop()">滚动到顶部</button>
    <button @click="$refs.title.style.color = 'red'">改变标题颜色</button>
    <button @click="alert(getText())">显示消息内容</button>
</div>

<!-- 在模板中使用 $refs -->
<template x-for="item in items" :key="item.id">
    <div>
        <span x-text="item.name"></span>
        <button @click="$refs['item-' + item.id].focus()">聚焦</button>
        <input :x-ref="'item-' + item.id" type="text">
    </div>
</template>
```

### 注意事项

- `x-ref` 可以在同一个组件的多个元素上使用，生成唯一的引用键
- `$refs` 对象中的键名就是 `x-ref` 属性的值
- 支持动态 ref 名称，使用字符串拼接的方式访问
- 如果 `x-ref` 指定的元素不存在，对应的 `$refs` 属性为 `undefined`
- `$refs` 只包含直接子元素的引用，不会向上或向下跨越组件边界

---

## $store

### 说明

`$store` 用于访问全局状态存储。通过 `Alpine.store()` 方法定义的全局状态，可以在任何组件中通过 `$store` 访问和修改，实现跨组件的状态共享。

### 适用范围

- 多组件共享状态
- 全局配置管理
- 用户认证状态
- 购物车、主题设置等全局数据
- 组件间的数据通信

### 语法

```html
<!-- 定义全局 store -->
<script>
    document.addEventListener('alpine:init', () => {
        Alpine.store('user', {
            name: '张三',
            isLoggedIn: false,
            login() {
                this.isLoggedIn = true
            },
            logout() {
                this.isLoggedIn = false
            }
        })

        Alpine.store('theme', {
            mode: 'light',
            toggle() {
                this.mode = this.mode === 'light' ? 'dark' : 'light'
            }
        })
    })
</script>

<!-- 在组件中使用 $store -->
<div x-data>
    <p>用户名: <span x-text="$store.user.name"></span></p>
    <p>登录状态: <span x-text="$store.user.isLoggedIn ? '已登录' : '未登录'"></span></p>
    <button @click="$store.user.login()">登录</button>
    <button @click="$store.user.logout()">退出</button>

    <p>当前主题: <span x-text="$store.theme.mode"></span></p>
    <button @click="$store.theme.toggle()">切换主题</button>
</div>

<!-- 在计算属性中使用 -->
<div x-data="{
    greeting() {
        return '你好，' + $store.user.name
    }
}">
    <p x-text="greeting()"></p>
</div>
```

### 注意事项

- 全局 store 需要在 Alpine.js 初始化前定义，使用 `alpine:init` 事件
- store 中的数据是响应式的，修改后所有引用的地方都会自动更新
- store 可以包含方法，用于封装业务逻辑
- 多个 store 之间可以相互引用
- store 的设计类似于 Vuex/Redux，但更轻量

---

## $watch

### 说明

`$watch` 用于监听组件数据的变化。当被监听的数据发生改变时，会触发指定的回调函数。它是实现数据响应式逻辑的重要工具，类似于 Vue 的 `watch`。

### 适用范围

- 监听单个数据的变化
- 数据变化时执行副作用操作
- 与第三方库同步状态
- 实现日志记录或调试
- 表单验证逻辑

### 语法

```html
<div x-data="{
    count: 0,
    message: '',
    init() {
        // 监听 count 变化
        this.$watch('count', (value) => {
            console.log('count 变化了:', value)
            this.message = '计数已更新为 ' + value
        })

        // 监听对象属性的变化
        this.$watch('user.name', (value) => {
            console.log('用户名变化:', value)
        })
    },
    user: { name: '张三', age: 25 }
}">
    <p>计数: <span x-text="count"></span></p>
    <p x-text="message"></p>
    <button @click="count++">增加</button>
    <button @click="count--">减少</button>
    <button @click="user.name = '李四'">改变用户名</button>
</div>

<!-- 监听深度变化 -->
<div x-data="{
    options: { theme: 'light', fontSize: 14 },
    init() {
        this.$watch('options', (value) => {
            console.log('选项变化:', value)
        }, { deep: true })
    }
}">
    <button @click="options.theme = options.theme === 'light' ? 'dark' : 'light'">
        切换主题
    </button>
</div>
```

### 注意事项

- `$watch` 需要在 `x-init` 或组件初始化函数中使用
- 回调函数接收两个参数：新值和旧值
- 默认只监听直接属性变化，使用 `{ deep: true }` 监听深层变化
- 监听器会在首次赋值时触发一次（可配置）
- 避免在监听器中修改自身，可能导致无限循环
- 组件销毁时监听器会自动移除

---

## $dispatch

### 说明

`$dispatch` 用于创建和派发自定义 DOM 事件。它允许组件之间通过事件机制进行通信，实现松耦合的组件交互。

### 适用范围

- 组件间通信
- 父子组件事件传递
- 通知外部系统状态变化
- 触发自定义行为
- 与原生事件系统集成

### 语法

```html
<!-- 基础使用 -->
<div x-data="{
    submit() {
        $dispatch('form-submit', { name: '张三', age: 25 })
    }
}">
    <button @click="submit()">提交</button>
</div>

<!-- 监听自定义事件 -->
<div x-data="{
    handleSubmit(event) {
        console.log('收到事件数据:', event.detail)
    }
}" @form-submit="handleSubmit($event)">
    <p>等待事件...</p>
</div>

<!-- 配合 x-on 使用 -->
<div x-data="{ count: 0 }">
    <button @click="$dispatch('increment', { value: 1 })">增加</button>

    <div @increment="count += $event.detail.value">
        计数: <span x-text="count"></span>
    </div>
</div>

<!-- 事件冒泡 -->
<div @custom-event="console.log('父元素收到事件')">
    <button @click="$dispatch('custom-event')">触发事件</button>
</div>

<!-- 派发到父组件 -->
<div x-data="{
    notifyParent() {
        $dispatch('notify', { message: 'Hello from child' })
    }
}" @notify="console.log($event.detail)">
    <button @click="notifyParent()">通知父组件</button>
</div>
```

### 注意事项

- `$dispatch` 派发的事件默认会向上冒泡
- 事件数据存储在 `event.detail` 中
- 事件名称建议使用有意义的命名，如 `form-submit`、`data-updated` 等
- 可以使用 `@` 简写形式监听自定义事件
- 事件数据必须是可序列化的 JSON 数据
- 配合 `x-model` 可以实现子组件向父组件传递数据

---

## $nextTick

### 说明

`$nextTick` 是一个 Promise 兼容的函数，用于在 DOM 更新完成后执行代码。由于 Alpine.js 的响应式更新是异步的，$nextTick 确保代码在 DOM 重新渲染后执行。

### 适用范围

- DOM 更新后的操作
- 获取更新后的元素尺寸或位置
- 与需要 DOM 就绪的第三方库集成
- 确保数据渲染后再执行逻辑
- 表单验证后的焦点设置

### 语法

```html
<div x-data="{
    show: false,
    message: '',
    async toggle() {
        this.show = !this.show
        await this.$nextTick()
        console.log('DOM 已更新')

        // 获取更新后的元素
        const box = this.$refs.box
        console.log('元素尺寸:', box.offsetHeight)
    },
    async updateAndFocus() {
        this.message = '新消息'
        await this.$nextTick()
        this.$refs.input.focus()
    }
}">
    <button @click="toggle()">切换显示</button>
    <div x-show="show" x-ref="box" style="padding: 20px; background: #eee;">
        内容区域
    </div>

    <input x-ref="input" x-model="message">
    <button @click="updateAndFocus()">更新并聚焦</button>
</div>

<!-- 在列表渲染后操作 -->
<div x-data="{
    items: ['a', 'b', 'c'],
    addItem() {
        this.items.push('d')
        this.$nextTick(() => {
            console.log('新列表已渲染')
            // 操作新渲染的元素
        })
    }
}">
    <ul>
        <template x-for="item in items" :key="item">
            <li x-text="item"></li>
        </template>
    </ul>
    <button @click="addItem()">添加项目</button>
</div>
```

### 注意事项

- `$nextTick` 返回一个 Promise，可以配合 `await` 使用
- 回调函数会在当前执行栈完成后，下一次 DOM 更新前执行
- 需要处理兼容性问题时，可以使用 `.then()` 形式
- 对于复杂的异步操作，可能需要多次调用 `$nextTick`
- 配合 `x-transition` 时，需要考虑过渡动画的时间

---

## $root

### 说明

`$root` 返回当前组件的根元素，与 `$el` 类似，但在嵌套组件场景中有特殊用途。它可以访问最顶层的 Alpine.js 组件根元素。

### 适用范围

- 嵌套组件中访问根组件
- 跨层级的数据访问
- 组件封装时暴露接口
- 获取根元素的属性或方法

### 语法

```html
<!-- 嵌套组件 -->
<div x-data="{
    name: '根组件',
    rootMethod() {
        console.log('根组件方法')
    }
}">
    <div x-data="{
        childName: '子组件',
        accessRoot() {
            console.log('根组件名称:', $root.name)
            $root.rootMethod()
        }
    }">
        <p x-text="name"></p>
        <p x-text="childName"></p>
        <button @click="accessRoot()">访问根组件</button>
    </div>
</div>

<!-- 使用 $root 访问根组件的数据 -->
<div x-data="{
    sharedData: '共享数据',
    value: ''
}">
    <div x-data="{
        updateValue(val) {
            this.value = val
            $root.sharedData = '已更新'
        }
    }">
        <button @click="updateValue('新值')">更新根组件数据</button>
    </div>
    <p x-text="value"></p>
    <p x-text="sharedData"></p>
</div>
```

### 注意事项

- 在单层组件中，`$root` 和 `$el` 返回相同的元素
- 嵌套组件时，`$root` 指向最近的上层带 `x-data` 的元素
- `$root` 只能访问同一家谱链上的组件，不能跨分支访问
- 过度使用 `$root` 可能导致组件耦合度过高
- 优先使用 `x-data` 的数据继承机制，减少对 `$root` 的依赖

---

## $data

### 说明

`$data` 返回当前组件的完整数据对象。通过 `$data`，可以直接访问和操作组件的所有响应式数据，包括计算属性和方法。

### 适用范围

- 调试时查看组件数据
- 在控制台输出组件状态
- 批量操作数据
- 获取数据的快照

### 语法

```html
<div x-data="{
    name: '张三',
    age: 25,
    hobbies: ['阅读', '编程', '运动'],
    user: { city: '北京' },
    greet() {
        return '你好，' + this.name
    },
    logData() {
        console.log('当前数据:', this.$data)
        console.log('姓名:', this.$data.name)
    }
}">
    <p>姓名: <span x-text="name"></span></p>
    <p>年龄: <span x-text="age"></span></p>
    <p>方法调用: <span x-text="greet()"></span></p>

    <button @click="logData()">输出数据到控制台</button>
    <button @click="$data.age = 30">直接修改年龄</button>
    <button @click="$data.hobbies.push('旅行')">添加爱好</button>
</div>

<!-- 在外部访问数据（配合 Alpine.store） -->
<script>
    document.addEventListener('alpine:init', () => {
        Alpine.data('userProfile', () => ({
            name: '李四',
            email: 'li@example.com',
            getProfile() {
                return { ...this.$data }
            }
        }))
    })
</script>

<div x-data="userProfile()">
    <button @click="console.log(getProfile())">获取资料</button>
</div>
```

### 注意事项

- `$data` 返回的是原始数据对象的引用，直接修改会触发响应式更新
- `$data` 包含所有在 `x-data` 中定义的数据、方法和计算属性
- 使用 `$data` 修改数据与直接修改效果相同
- 调试时可以使用 `$data` 查看组件完整状态
- 在组件销毁后，`$data` 可能不再可用

---

## $id

### 说明

`$id` 用于生成唯一的 ID 字符串。它基于组件内部计数器为每个唯一名称生成确定性的 ID，确保同一组件或页面中的 ID 保持一致。

### 适用范围

- 生成可访问性相关的唯一 ID
- 表单标签与输入框的关联
- 生成 ARIA 属性值
- 动态创建需要唯一标识的元素
- 组件内部的 ID 管理

### 语法

```html
<!-- 基础使用 -->
<div x-data x-id="['text-input', 'text-label']">
    <label :for="$id('text-input')" x-text="$id('text-label')"></label>
    <input type="text" :id="$id('text-input')">
</div>

<!-- 多个 ID -->
<div x-data x-id="['modal-title', 'modal-content', 'modal-close']">
    <div :id="$id('modal-title')">模态框标题</div>
    <div :id="$id('modal-content')">模态框内容</div>
    <button :id="$id('modal-close')">关闭</button>
</div>

<!-- 循环中的 ID -->
<div x-data="{ items: [1, 2, 3] }" x-id="['item-label', 'item-input']">
    <template x-for="item in items" :key="item">
        <div>
            <label :for="$id('item-input')">项目 <span x-text="item"></span></label>
            <input :id="$id('item-input')" type="text">
        </div>
    </template>
</div>

<!-- 与 $refs 结合 -->
<div x-data x-id="['dropdown', 'dropdown-menu']">
    <button :aria-expanded="false" :aria-controls="$id('dropdown-menu')">
        菜单
    </button>
    <div :id="$id('dropdown-menu')">
        <!-- 菜单项 -->
    </div>
</div>
```

### 注意事项

- `$id` 需要在 `x-id` 指令中声明需要生成的 ID 名称
- 相同名称在同一组件中会生成相同的 ID
- ID 是确定性的，页面刷新后会重新计算
- 主要用于提高网页的可访问性（a11y）
- 如果省略 `x-id` 声明，仍然可以使用 `$id()` 生成随机 ID
- `$id` 生成的 ID 格式为 `名称-序号`，如 `text-input-1`

---

## 总结

Alpine.js 的魔法属性为开发者提供了强大的工具集，能够在不使用复杂 JavaScript 代码的情况下实现丰富的交互功能。这些魔法属性覆盖了 DOM 访问、状态管理、事件处理、数据监听等常见场景，使得 Alpine.js 成为构建轻量级交互页面的理想选择。熟练掌握这些魔法属性，能够显著提升开发效率和代码可维护性。