08-Vue 指令系统实现原理

Vue 指令系统实现原理

深入解析 Vue 指令的注册、解析、执行全流程,剖析 v-model、v-if、v-show 等核心指令的底层实现机制。

一、前言

指令(Directive)是 Vue 提供的特殊属性,带有 v- 前缀,用于在表达式的值改变时,将某些行为应用到 DOM 上。从 v-if 的条件渲染到 v-model 的双向绑定,指令系统贯穿 Vue 开发的方方面面。

理解指令系统的实现原理,不仅能帮助我们更好地使用内置指令,更能指导我们编写出高效、可复用的自定义指令。本文将从源码层面剖析 Vue 指令系统的完整工作机制。

二、核心内容

2.1 指令注册机制

Vue 的指令分为内置指令和自定义指令。内置指令在框架初始化时注册,自定义指令通过 Vue.directive()(Vue2)或 app.directive()(Vue3)注册。

Vue2 指令注册
// 全局注册自定义指令
Vue.directive('focus', {
  inserted(el) {
    el.focus();
  }
});

// 局部注册
export default {
  directives: {
    focus: {
      inserted(el) {
        el.focus();
      }
    }
  }
}

Vue2 的指令注册本质上是在 Vue 构造函数上维护一个 options.directives 对象:

// core/global-api/assets.js
export function initAssetRegisters(Vue) {
  const ASSET_TYPES = ['component', 'directive', 'filter'];
  
  ASSET_TYPES.forEach(type => {
    Vue[type] = function(id, definition) {
      if (!definition) {
        return this.options[type + 's'][id];
      } else {
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition };
        }
        this.options[type + 's'][id] = definition;
        return definition;
      }
    };
  });
}
Vue3 指令注册
// Vue3 全局注册
const app = createApp(App);
app.directive('focus', {
  mounted(el) {
    el.focus();
  }
});

// Vue3 局部注册
export default {
  directives: {
    focus: {
      mounted(el) {
        el.focus();
      }
    }
  }
}

2.2 指令解析过程

指令的解析发生在编译阶段。编译器会识别模板中的 v- 属性,将其转换为渲染函数中的指令对象。

Vue2 指令解析
// 模板中的指令
// <div v-if="show" v-model="value" v-custom:arg.mod="expr"></div>

// 编译后生成的渲染函数大致结构
function render() {
  with(this) {
    return _c('div', {
      directives: [
        { name: "custom", rawName: "v-custom:arg.mod", value: (expr), expression: "expr", arg: "arg", modifiers: {"mod": true} }
      ]
    });
  }
}

Vue2 的编译器在 processElement 阶段处理指令:

// compiler/parser/index.js
function processElement(element, options) {
  processKey(element);
  processRef(element);
  
  // 处理指令
  processAttrs(element);
  
  return element;
}

function processAttrs(el) {
  const list = el.attrsList;
  for (let i = 0, l = list.length; i < l; i++) {
    const name = list[i].name;
    const value = list[i].value;
    
    // 检测 v- 前缀
    if (dirRE.test(name)) {
      // 标记元素有指令
      el.hasBindings = true;
      
      // 解析指令名、参数、修饰符
      const argMatch = name.match(argRE);
      const arg = argMatch && argMatch[1];
      
      // 添加指令到元素描述
      addDirective(el, name, rawName, value, arg);
    }
  }
}
Vue3 指令解析

Vue3 的编译器对指令解析进行了重构,指令在 AST 转换阶段处理:

// compiler-core/transforms/vModel.ts
export const transformModel: DirectiveTransform = (dir, node, context) => {
  const { exp, arg } = dir;
  
  if (!exp) {
    context.onError(createCompilerError(ErrorCodes.X_V_MODEL_NO_EXPRESSION));
  }
  
  // 生成指令运行时参数
  const props = [
    // modelValue: exp
    createObjectProperty(
      'modelValue',
      createCompoundExpression([`$event`])
    ),
    // 'onUpdate:modelValue': $event => exp = $event
    createObjectProperty(
      'onUpdate:modelValue',
      createCompoundExpression([`$event => ((`, exp, `) = $event)`])
    )
  ];
  
  return createDirectiveArgument(props);
};

2.3 指令钩子函数

指令对象包含一组钩子函数,在元素的不同生命周期阶段调用。

Vue2 指令钩子
钩子调用时机
bind指令第一次绑定到元素时
inserted被绑定元素插入父节点时
update组件更新时(子组件可能未更新)
componentUpdated组件及子组件全部更新后
unbind指令与元素解绑时
Vue.directive('demo', {
  bind(el, binding, vnode) {
    // 初始化设置
    el.style.color = binding.value;
  },
  inserted(el, binding, vnode) {
    // DOM 插入后的操作
    console.log('元素已插入:', el.parentNode);
  },
  update(el, binding, vnode, oldVnode) {
    // 更新时
    if (binding.value !== binding.oldValue) {
      el.style.color = binding.value;
    }
  },
  componentUpdated(el, binding, vnode, oldVnode) {
    // 全部更新完成
  },
  unbind(el, binding, vnode) {
    // 清理工作
  }
});
Vue3 指令钩子
钩子调用时机
created指令绑定到元素之前
beforeMount元素挂载前
mounted元素挂载后
beforeUpdate组件更新前
updated组件更新后
beforeUnmount元素卸载前
unmounted元素卸载后
const myDirective = {
  created(el, binding, vnode, prevVnode) {
    // 创建时的逻辑
  },
  beforeMount(el, binding, vnode, prevVnode) {
    // 挂载前
  },
  mounted(el, binding, vnode, prevVnode) {
    // 挂载后
  },
  beforeUpdate(el, binding, vnode, prevVnode) {
    // 更新前
  },
  updated(el, binding, vnode, prevVnode) {
    // 更新后
  },
  beforeUnmount(el, binding, vnode, prevVnode) {
    // 卸载前清理
  },
  unmounted(el, binding, vnode, prevVnode) {
    // 卸载完成
  }
}
钩子参数详解
// binding 对象结构
{
  arg: 'foo',           // 指令参数 v-directive:foo
  dir: { /* 指令定义对象 */ },
  instance: component,  // 组件实例
  modifiers: { bar: true }, // 修饰符 v-directive:foo.bar
  oldArg: ...,          // 之前的参数(仅在 update 钩子中)
  oldValue: ...,        // 之前的值(仅在 update 钩子中)
  value: 'baz'          // 指令值 v-directive="'baz'"
}

2.4 v-model 实现原理

v-model 是 Vue 中最复杂的内置指令,它实现了表单元素与数据之间的双向绑定。

Vue2 v-model 实现
// 模板
// <input v-model="message">

// 编译后等效于
// <input :value="message" @input="message = $event.target.value">

// compiler/codegen/index.js
function genDirectives(el, state) {
  const dirs = el.directives;
  if (!dirs) return;
  
  let res = 'directives:[';
  let hasRuntime = false;
  let needRuntime = false;
  
  for (let i = 0, l = dirs.length; i < l; i++) {
    const dir = dirs[i];
    needRuntime = true;
    
    const gen = state.directives[dir.name];
    if (gen) {
      // 编译时处理
      hasRuntime = true;
      res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
        dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
      }${dir.arg ? `,arg:${dir.arg}` : ''}${
        dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
      }},`;
    }
  }
  
  return res + ']';
}

// 运行时 v-model 指令实现
const model = {
  inserted(el, binding, vnode) {
    if (vnode.tag === 'select') {
      setSelected(el, binding, vnode.context);
    } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
      el._vModifiers = binding.modifiers;
      
      if (!binding.modifiers.lazy) {
        // 处理输入法组合事件
        el.addEventListener('compositionstart', onCompositionStart);
        el.addEventListener('compositionend', onCompositionEnd);
        
        // 监听 input 事件
        el.addEventListener('input', onInput);
      }
    }
  },
  
  // 组件的 v-model
  componentUpdated(el, binding, vnode) {
    if (vnode.tag === 'select') {
      setSelected(el, binding, vnode.context);
    }
  }
};
Vue3 v-model 实现

Vue3 对 v-model 进行了重大改进,支持多个 v-model、自定义修饰符和自定义 modelValue 名称。

// compiler-core/transforms/vModel.ts
export const transformModel: DirectiveTransform = (dir, node, context) => {
  const { exp, arg } = dir;
  
  // 处理参数(自定义 modelValue 名称)
  const rawExp = exp.loc.source;
  const eventName = arg
    ? `onUpdate:${arg.content}`
    : 'onUpdate:modelValue';
  
  // 生成 props
  const props = [
    createObjectProperty(
      arg ? arg.content : 'modelValue',
      createCompoundExpression([rawExp])
    ),
    createObjectProperty(
      eventName,
      createCompoundExpression([
        '$event => ((',
        rawExp,
        ') = $event)'
      ])
    )
  ];
  
  // 处理修饰符
  if (dir.modifiers) {
    const modifiers = dir.modifiers;
    // .trim, .number, .lazy 等修饰符处理
    if (modifiers.trim) {
      // 添加 trim 处理
    }
    if (modifiers.number) {
      // 添加 number 转换
    }
  }
  
  return {
    props,
    needRuntime: false
  };
};

组件上的 v-model 实现:

<!-- 父组件 -->
<template>
  <Child v-model:title="pageTitle" v-model="content" />
</template>

<!-- 子组件 -->
<template>
  <input
    :value="title"
    @input="$emit('update:title', $event.target.value)"
  />
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

<script>
export default {
  props: ['title', 'modelValue'],
  emits: ['update:title', 'update:modelValue']
}
</script>

2.5 v-if 与 v-show 实现差异

v-ifv-show 虽然都控制元素的显示/隐藏,但实现机制完全不同。

v-if 实现原理

v-if 是"真正的"条件渲染,它会根据条件销毁和重建元素及组件:

// 编译器将 v-if 转换为三元表达式
// <div v-if="ok">Yes</div>
// <div v-else>No</div>

// 编译结果
function render() {
  with(this) {
    return ok
      ? _c('div', [_v("Yes")])
      : _c('div', [_v("No")]);
  }
}

Vue3 中 v-if 的编译处理:

// compiler-core/transforms/vIf.ts
export const transformIf = createStructuralDirectiveTransform(
  /^(if|else|else-if)$/,
  (node, dir, context) => {
    return processIf(node, dir, context, (ifNode, branch, isRoot) => {
      // 创建条件分支节点
      return () => {
        if (isRoot) {
          // 根条件节点
          ifNode.codegenNode = createConditionalExpression(
            branch.condition,
            branch.children,
            createConstantExpression(false)
          );
        }
      };
    });
  }
);
v-show 实现原理

v-show 通过 CSS display 属性控制显示,元素始终存在于 DOM 中:

// platforms/web/runtime/directives/show.js
const vShow = {
  beforeMount(el, { value }, { transition }) {
    el._vod = el.style.display === 'none' ? '' : el.style.display;
    
    if (transition && value) {
      transition.beforeEnter(el);
    } else {
      setDisplay(el, value);
    }
  },
  
  mounted(el, { value }, { transition }) {
    if (transition && value) {
      transition.enter(el);
    }
  },
  
  updated(el, { value, oldValue }, { transition }) {
    if (!value === !oldValue) return;
    
    if (transition) {
      if (value) {
        transition.beforeEnter(el);
        setDisplay(el, true);
        transition.enter(el);
      } else {
        transition.leave(el, () => {
          setDisplay(el, false);
        });
      }
    } else {
      setDisplay(el, value);
    }
  },
  
  beforeUnmount(el, { value }) {
    setDisplay(el, value);
  }
};

function setDisplay(el, value) {
  el.style.display = value ? el._vod : 'none';
}
两者对比
特性v-ifv-show
渲染方式条件渲染(销毁/重建)CSS 切换(display)
初始渲染开销较高(需要编译条件分支)较低(始终渲染)
切换开销较高(组件销毁重建)较低(仅 CSS 切换)
适用场景运行时条件很少改变需要频繁切换显示状态
与 transition 配合进入/离开过渡显示/隐藏过渡

2.6 自定义指令实现

自定义指令是扩展 Vue 功能的重要手段。下面通过几个实用示例展示自定义指令的实现。

示例 1:权限控制指令
// directives/permission.js
import { useUserStore } from '@/stores/user';

export const vPermission = {
  mounted(el, binding) {
    const { value } = binding;
    const userStore = useUserStore();
    const permissions = userStore.permissions;
    
    if (value && value instanceof Array) {
      const hasPermission = permissions.some(p => value.includes(p));
      
      if (!hasPermission) {
        el.parentNode && el.parentNode.removeChild(el);
      }
    }
  }
};

// 使用
// <button v-permission="['admin', 'editor']">管理按钮</button>
示例 2:防抖指令
// directives/debounce.js
export const vDebounce = {
  mounted(el, binding) {
    const { value, arg } = binding;
    const delay = parseInt(arg) || 300;
    
    let timer = null;
    
    el._debounceHandler = (e) => {
      clearTimeout(timer);
      timer = setTimeout(() => {
        value(e);
      }, delay);
    };
    
    el.addEventListener('input', el._debounceHandler);
  },
  
  unmounted(el) {
    el.removeEventListener('input', el._debounceHandler);
    el._debounceHandler = null;
  }
};

// 使用
// <input v-debounce:500="handleInput" />
示例 3:拖拽指令
// directives/draggable.js
export const vDraggable = {
  mounted(el, binding) {
    const { value = {} } = binding;
    const { onDrag, onDragEnd, onDragStart } = value;
    
    let isDragging = false;
    let startX, startY, initialLeft, initialTop;
    
    el.style.position = 'absolute';
    el.style.cursor = 'move';
    
    const onMouseDown = (e) => {
      isDragging = true;
      startX = e.clientX;
      startY = e.clientY;
      initialLeft = el.offsetLeft;
      initialTop = el.offsetTop;
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('mouseup', onMouseUp);
      
      onDragStart && onDragStart({
        left: initialLeft,
        top: initialTop
      });
    };
    
    const onMouseMove = (e) => {
      if (!isDragging) return;
      
      const dx = e.clientX - startX;
      const dy = e.clientY - startY;
      
      el.style.left = `${initialLeft + dx}px`;
      el.style.top = `${initialTop + dy}px`;
      
      onDrag && onDrag({
        left: initialLeft + dx,
        top: initialTop + dy,
        dx,
        dy
      });
    };
    
    const onMouseUp = () => {
      isDragging = false;
      document.removeEventListener('mousemove', onMouseMove);
      document.removeEventListener('mouseup', onMouseUp);
      
      onDragEnd && onDragEnd({
        left: parseInt(el.style.left),
        top: parseInt(el.style.top)
      });
    };
    
    el.addEventListener('mousedown', onMouseDown);
    
    // 存储清理函数
    el._draggableCleanup = () => {
      el.removeEventListener('mousedown', onMouseDown);
    };
  },
  
  unmounted(el) {
    el._draggableCleanup && el._draggableCleanup();
  }
};

// 使用
// <div v-draggable="{ onDragEnd: handleDragEnd }">拖拽我</div>

三、Mermaid 图表

指令系统整体架构

运行时阶段

编译阶段

指令注册表

内置指令
v-if / v-show / v-model / v-for / v-bind / v-on

自定义指令
v-focus / v-permission / v-debounce

模板字符串

模板解析器

AST 抽象语法树

指令转换器

代码生成器

渲染函数

虚拟 DOM

Patch 算法

指令钩子调度

created

beforeMount

mounted

beforeUpdate

updated

beforeUnmount

unmounted

v-model 双向绑定流程

响应式系统 组件实例 v-model 指令 输入框 用户 响应式系统 组件实例 v-model 指令 输入框 用户 数据变化路径 输入内容 触发 input 事件 更新数据 (value = newValue) 触发 setter 通知依赖更新 重新渲染 更新 DOM value 数据修改 触发 setter 通知更新 更新 DOM value 显示新内容

四、代码示例

示例 1:完整的自定义指令注册与使用

// main.js
import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);

// 全局注册自定义指令
app.directive('highlight', {
  mounted(el, binding) {
    const color = binding.value || 'yellow';
    el.style.backgroundColor = color;
    el.style.transition = 'background-color 0.3s';
  },
  updated(el, binding) {
    if (binding.value !== binding.oldValue) {
      el.style.backgroundColor = binding.value || 'yellow';
    }
  }
});

// 点击外部关闭指令
app.directive('click-outside', {
  mounted(el, binding) {
    el._clickOutside = (event) => {
      if (!(el === event.target || el.contains(event.target))) {
        binding.value(event);
      }
    };
    document.addEventListener('click', el._clickOutside);
  },
  unmounted(el) {
    document.removeEventListener('click', el._clickOutside);
    el._clickOutside = null;
  }
});

app.mount('#app');
<!-- App.vue -->
<template>
  <div>
    <!-- 高亮指令 -->
    <p v-highlight="highlightColor">这段文字会被高亮显示</p>
    <button @click="changeColor">切换颜色</button>
    
    <!-- 点击外部关闭 -->
    <div v-click-outside="closeDropdown" class="dropdown">
      <button @click="toggleDropdown">下拉菜单</button>
      <div v-show="isOpen" class="dropdown-menu">
        <div>选项 1</div>
        <div>选项 2</div>
        <div>选项 3</div>
      </div>
    </div>
  </div>
</template>

<script>
import { ref } from 'vue';

export default {
  setup() {
    const highlightColor = ref('yellow');
    const isOpen = ref(false);
    
    const changeColor = () => {
      highlightColor.value = highlightColor.value === 'yellow' ? 'lightgreen' : 'yellow';
    };
    
    const toggleDropdown = () => {
      isOpen.value = !isOpen.value;
    };
    
    const closeDropdown = () => {
      isOpen.value = false;
    };
    
    return {
      highlightColor,
      isOpen,
      changeColor,
      toggleDropdown,
      closeDropdown
    };
  }
};
</script>

<style>
.dropdown {
  position: relative;
  display: inline-block;
}
.dropdown-menu {
  position: absolute;
  top: 100%;
  left: 0;
  border: 1px solid #ddd;
  background: white;
  padding: 10px;
  min-width: 120px;
}
</style>

示例 2:v-model 自定义组件实现

<!-- CustomInput.vue -->
<template>
  <div class="custom-input">
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
      @blur="$emit('blur')"
    />
    <span class="clear-btn" @click="clear" v-show="modelValue">×</span>
  </div>
</template>

<script>
export default {
  name: 'CustomInput',
  props: {
    modelValue: {
      type: String,
      default: ''
    }
  },
  emits: ['update:modelValue', 'blur'],
  
  setup(props, { emit }) {
    const clear = () => {
      emit('update:modelValue', '');
    };
    
    return { clear };
  }
};
</script>

<style scoped>
.custom-input {
  position: relative;
  display: inline-block;
}
.custom-input input {
  padding-right: 24px;
}
.clear-btn {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  cursor: pointer;
  color: #999;
}
</style>
<!-- Parent.vue -->
<template>
  <div>
    <CustomInput v-model="text" @blur="handleBlur" />
    <p>当前值: {{ text }}</p>
  </div>
</template>

<script>
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';

export default {
  components: { CustomInput },
  setup() {
    const text = ref('');
    
    const handleBlur = () => {
      console.log('输入框失去焦点');
    };
    
    return { text, handleBlur };
  }
};
</script>

示例 3:指令与 Composition API 结合

// composables/useDirective.js
import { getCurrentInstance, onMounted, onUnmounted } from 'vue';

/**
 * 在组合式函数中创建指令效果
 */
export function useEventListener(target, event, handler) {
  onMounted(() => {
    target.addEventListener(event, handler);
  });
  
  onUnmounted(() => {
    target.removeEventListener(event, handler);
  });
}

/**
 * 自动聚焦的组合式函数
 */
export function useFocus(elRef) {
  onMounted(() => {
    elRef.value?.focus();
  });
}

/**
 * 滚动加载更多
 */
export function useInfiniteScroll(elRef, callback, options = {}) {
  const { threshold = 100, immediate = false } = options;
  let isLoading = false;
  
  const handler = async () => {
    const el = elRef.value;
    if (!el || isLoading) return;
    
    const scrollBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
    
    if (scrollBottom < threshold) {
      isLoading = true;
      await callback();
      isLoading = false;
    }
  };
  
  onMounted(() => {
    const el = elRef.value;
    if (el) {
      el.addEventListener('scroll', handler);
      if (immediate) handler();
    }
  });
  
  onUnmounted(() => {
    elRef.value?.removeEventListener('scroll', handler);
  });
}

// 使用示例
import { ref } from 'vue';
import { useFocus, useInfiniteScroll } from './composables/useDirective.js';

export default {
  setup() {
    const inputRef = ref(null);
    const listRef = ref(null);
    const items = ref([]);
    
    // 自动聚焦
    useFocus(inputRef);
    
    // 无限滚动
    const loadMore = async () => {
      const newItems = await fetchMoreItems();
      items.value.push(...newItems);
    };
    
    useInfiniteScroll(listRef, loadMore, { immediate: true });
    
    return {
      inputRef,
      listRef,
      items
    };
  }
};

五、常见问题

Q1:自定义指令的钩子函数中如何访问组件实例?

在 Vue3 中,可以通过 binding.instance 访问组件实例。但需要注意,这不是响应式的,且不应该在指令中过度依赖组件实例的状态。

const directive = {
  mounted(el, binding) {
    const instance = binding.instance;
    console.log('组件实例:', instance);
    // 可以访问组件的 methods 或 data
    instance.someMethod();
  }
};

Q2:v-if 和 v-show 应该如何选择?

  • 使用 v-if:当条件不常改变,或需要条件性渲染大量内容时。v-if 在条件为 false 时不渲染元素,初始渲染开销较小。
  • 使用 v-show:当需要频繁切换显示状态时。v-show 只是切换 CSS display 属性,切换开销更小。

Q3:Vue3 中为什么移除了 bindunbind 钩子?

Vue3 将指令钩子与组件生命周期对齐,使用 beforeMount/mounted 替代 bind/inserted,使用 beforeUnmount/unmounted 替代 unbind。这样命名更加一致,也更符合指令在组件生命周期中的实际执行时机。

Q4:如何在同一元素上使用多个 v-model?

Vue3 支持在同一组件上使用多个 v-model,通过参数区分:

<template>
  <UserForm
    v-model:name="user.name"
    v-model:email="user.email"
    v-model:age="user.age"
  />
</template>

Q5:自定义指令的修饰符如何工作?

修饰符在 binding.modifiers 中以对象形式提供:

// v-debounce:500.trim
const directive = {
  mounted(el, binding) {
    console.log(binding.modifiers); // { trim: true }
    console.log(binding.arg); // "500"
  }
};

六、总结

Vue 的指令系统是一个精心设计的扩展机制,它将 DOM 操作与响应式数据绑定解耦,让开发者可以用声明式的方式操作视图。

  1. 注册机制:指令通过全局或局部方式注册,在编译阶段被识别并转换为渲染函数的一部分
  2. 解析过程:编译器将 v- 属性解析为指令对象,包含名称、参数、修饰符和表达式
  3. 钩子执行:指令在元素的生命周期各阶段执行对应的钩子函数,实现细粒度的 DOM 控制
  4. 内置指令v-model 实现双向绑定的核心机制,v-ifv-show 分别采用条件渲染和 CSS 切换策略
  5. 自定义扩展:通过自定义指令可以封装可复用的 DOM 操作逻辑,与 Composition API 结合使用效果更佳

掌握指令系统的实现原理,是成为 Vue 高级开发者的重要一步。

七、思考题

  1. 分析 Vue3 中 v-model 的编译输出,对比 .lazy.number.trim 三个修饰符的实现差异。

  2. 实现一个 v-resize 指令,监听元素尺寸变化并在变化时调用绑定函数。考虑使用 ResizeObserver API。

  3. 为什么 v-for 的优先级高于 v-if?如果反过来会有什么影响?查看源码分析这一设计决策。

  4. 在 Vue3 中,如何实现一个支持动画的 v-show 自定义版本?参考内置 v-showtransition 组件的交互方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李铁蛋zs

投喂博主,解锁更多实用前端技巧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值