普通视图

发现新文章,点击刷新页面。
今天 — 2025年4月18日掘金 前端

Vue 配置模块深度剖析(十一)

2025年4月18日 00:06

Vue 配置模块深度剖析

本人掘金号,欢迎点击关注:掘金号地址

本人公众号,欢迎点击关注:公众号地址

一、引言

在前端开发领域,Vue.js 以其简洁易用、高效灵活的特性备受开发者青睐。Vue 的配置模块作为其核心组成部分之一,为开发者提供了丰富的配置选项,使得开发者能够根据不同的需求对 Vue 实例进行定制化设置。深入理解 Vue 的配置模块,不仅有助于开发者更好地掌控 Vue 应用的行为,还能在开发过程中避免许多潜在的问题。本文将从源码级别深入分析 Vue 的配置模块,带你了解其内部实现原理和使用方法。

二、Vue 配置模块概述

2.1 配置模块的作用

Vue 的配置模块允许开发者在创建 Vue 实例时传入一系列配置选项,这些选项可以控制 Vue 实例的各个方面,包括数据绑定、生命周期钩子、组件注册、指令定义等。通过合理配置这些选项,开发者可以实现复杂的业务逻辑和交互效果。

2.2 常见的配置选项

以下是一些常见的 Vue 配置选项及其作用:

  • data:用于定义 Vue 实例的数据对象,这些数据可以在模板中进行绑定和使用。
  • methods:用于定义 Vue 实例的方法,这些方法可以在模板中通过事件绑定调用。
  • computed:用于定义计算属性,计算属性的值会根据其依赖的数据自动更新。
  • watch:用于监听数据的变化,并在数据变化时执行相应的回调函数。
  • createdmountedupdated 等:这些是生命周期钩子函数,允许开发者在 Vue 实例的不同生命周期阶段执行特定的代码。

三、Vue 配置选项的初始化

3.1 配置选项的合并策略

在创建 Vue 实例时,Vue 会将用户传入的配置选项与全局配置选项以及组件的默认配置选项进行合并。合并的过程遵循一定的策略,以确保配置选项的正确性和一致性。

javascript

// 合并策略的定义
const strats = {};

// 对于 data 选项的合并策略
strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    // 如果没有 Vue 实例,直接返回合并后的函数
    if (childVal && typeof childVal !== 'function') {
      // 如果子选项不是函数,抛出错误
      process.env.NODE_ENV !== 'production' && warn(
        'The "data" option should be a function ' +
        'that returns a per-instance value in component ' +
        'definitions.',
        vm
      );
      return parentVal;
    }
    return mergeDataOrFn(parentVal, childVal);
  }
  return mergeDataOrFn(parentVal, childVal, vm);
};

// 合并数据或函数的函数
function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!childVal) {
    // 如果子选项不存在,返回父选项
    return parentVal;
  }
  if (!parentVal) {
    // 如果父选项不存在,返回子选项
    return childVal;
  }
  // 对于函数类型的选项,合并成一个新的函数
  if (typeof childVal === 'function' && typeof parentVal === 'function') {
    return function mergedDataFn () {
      return mergeData(
        typeof childVal === 'function' ? childVal.call(this, this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
      );
    };
  }
  return function mergedInstanceDataFn () {
    return mergeData(
      typeof childVal === 'function' ? childVal.call(this, this) : childVal,
      typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
    );
  };
}

// 合并数据对象的函数
function mergeData (to: Object, from: ?Object): Object {
  if (!from) return to;
  let key, toVal, fromVal;
  const keys = Object.keys(from);
  for (let i = 0; i < keys.length; i++) {
    key = keys[i];
    toVal = to[key];
    fromVal = from[key];
    if (!to.hasOwnProperty(key)) {
      // 如果目标对象没有该属性,直接添加
      set(to, key, fromVal);
    } else if (toVal !== fromVal && isPlainObject(toVal) && isPlainObject(fromVal)) {
      // 如果属性值都是对象,递归合并
      mergeData(toVal, fromVal);
    }
  }
  return to;
}

3.2 配置选项的初始化流程

在创建 Vue 实例时,Vue 会按照一定的流程对配置选项进行初始化。以下是简化的初始化流程:

javascript

function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this;
    // 合并配置选项
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    );
    // 初始化生命周期
    initLifecycle(vm);
    // 初始化事件
    initEvents(vm);
    // 初始化渲染
    initRender(vm);
    // 调用 beforeCreate 钩子
    callHook(vm, 'beforeCreate');
    // 初始化注入
    initInjections(vm);
    // 初始化数据
    initState(vm);
    // 初始化提供
    initProvide(vm);
    // 调用 created 钩子
    callHook(vm, 'created');

    if (vm.$options.el) {
      // 如果有 el 选项,挂载 Vue 实例
      vm.$mount(vm.$options.el);
    }
  };
}

在上述代码中,_init 方法是 Vue 实例的初始化方法。首先,通过 mergeOptions 函数合并配置选项,然后依次初始化生命周期、事件、渲染等。在初始化过程中,会调用相应的生命周期钩子函数,如 beforeCreate 和 created

四、data 配置选项

4.1 data 选项的作用

data 选项用于定义 Vue 实例的数据对象。这些数据可以在模板中进行绑定和使用,当数据发生变化时,Vue 会自动更新与之绑定的 DOM 元素。

4.2 data 选项的使用方式

data 选项可以是一个对象或一个函数。当在组件中使用时,data 必须是一个函数,以确保每个组件实例都有自己独立的数据副本。

javascript

// 使用对象形式的 data 选项
const vm1 = new Vue({
  data: {
    message: 'Hello, Vue!'
  }
});

// 使用函数形式的 data 选项
const MyComponent = Vue.extend({
  data: function () {
    return {
      message: 'Hello, Component!'
    };
  }
});

4.3 data 选项的源码分析

javascript

function initData (vm: Component) {
  let data = vm.$options.data;
  // 如果 data 是函数,调用该函数获取数据对象
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {};
  if (!isPlainObject(data)) {
    // 如果 data 不是普通对象,将其初始化为空对象
    data = {};
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    );
  }
  // 代理数据到 Vue 实例上
  const keys = Object.keys(data);
  const props = vm.$options.props;
  const methods = vm.$options.methods;
  let i = keys.length;
  while (i--) {
    const key = keys[i];
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        // 如果方法名与数据名冲突,发出警告
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        );
      }
    }
    if (props && hasOwn(props, key)) {
      // 如果属性名与数据名冲突,发出警告
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      );
    } else if (!isReserved(key)) {
      // 代理数据到 Vue 实例上
      proxy(vm, `_data`, key);
    }
  }
  // 对数据进行响应式处理
  observe(data, true /* asRootData */);
}

function getData (data: Function, vm: Component): any {
  try {
    // 调用 data 函数获取数据对象
    return data.call(vm, vm);
  } catch (e) {
    // 捕获异常并发出警告
    handleError(e, vm, `data()`);
    return {};
  }
}

function proxy (target: Object, sourceKey: string, key: string) {
  // 定义 getter 和 setter 方法,实现数据代理
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key];
  };
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

在上述代码中,initData 函数用于初始化 data 选项。首先,判断 data 是否为函数,如果是则调用该函数获取数据对象。然后,将数据代理到 Vue 实例上,使得可以通过 vm.key 直接访问 vm._data.key。最后,对数据进行响应式处理,使得数据的变化能够被 Vue 监听到并更新 DOM。

五、methods 配置选项

5.1 methods 选项的作用

methods 选项用于定义 Vue 实例的方法。这些方法可以在模板中通过事件绑定调用,也可以在 JavaScript 代码中直接调用。

5.2 methods 选项的使用方式

javascript

const vm = new Vue({
  data: {
    message: 'Hello, Vue!'
  },
  methods: {
    // 定义一个方法
    reverseMessage: function () {
      this.message = this.message.split('').reverse().join('');
    }
  }
});

5.3 methods 选项的源码分析

javascript

function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props;
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      if (methods[key] == null) {
        // 如果方法为空,发出警告
        warn(
          `Method "${key}" has an undefined value in the component definition. ` +
          `Did you reference the function correctly?`,
          vm
        );
      }
      if (props && hasOwn(props, key)) {
        // 如果属性名与方法名冲突,发出警告
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        );
      }
      if ((key in vm) && isReserved(key)) {
        // 如果方法名是保留字,发出警告
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
          `Avoid defining component methods that start with _ or $.`
        );
      }
    }
    // 将方法绑定到 Vue 实例上
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
  }
}

function bind (fn: Function, ctx: Object): Function {
  // 绑定函数的上下文
  function boundFn (a) {
    const l = arguments.length;
    return l
      ? l > 1
        ? fn.apply(ctx, arguments)
        : fn.call(ctx, a)
      : fn.call(ctx);
  }
  boundFn._length = fn.length;
  return boundFn;
}

在上述代码中,initMethods 函数用于初始化 methods 选项。首先,检查方法名是否与属性名冲突或是否为保留字,然后将方法绑定到 Vue 实例上,确保方法的上下文是 Vue 实例本身。

六、computed 配置选项

6.1 computed 选项的作用

computed 选项用于定义计算属性。计算属性的值会根据其依赖的数据自动更新,并且具有缓存机制,只有当依赖的数据发生变化时,计算属性才会重新计算。

6.2 computed 选项的使用方式

javascript

const vm = new Vue({
  data: {
    firstName: 'John',
    lastName: 'Doe'
  },
  computed: {
    // 定义一个计算属性
    fullName: function () {
      return this.firstName + ' ' + this.lastName;
    }
  }
});

6.3 computed 选项的源码分析

javascript

function initComputed (vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null);
  const isSSR = isServerRendering();

  for (const key in computed) {
    const userDef = computed[key];
    const getter = typeof userDef === 'function' ? userDef : userDef.get;
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      // 如果没有定义 getter 函数,发出警告
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      );
    }

    if (!isSSR) {
      // 在非服务端渲染环境下,创建计算属性的 watcher
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        {
          lazy: true
        }
      );
    }

    if (!(key in vm)) {
      // 定义计算属性的 getter 和 setter
      defineComputed(vm, key, userDef);
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        // 如果计算属性名与数据名冲突,发出警告
        warn(`The computed property "${key}" is already defined in data.`, vm);
      } else if (vm.$options.props && key in vm.$options.props) {
        // 如果计算属性名与属性名冲突,发出警告
        warn(`The computed property "${key}" is already defined as a prop.`, vm);
      }
    }
  }
}

function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering();
  if (typeof userDef === 'function') {
    // 如果 userDef 是函数,定义只读的计算属性
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef);
    sharedPropertyDefinition.set = noop;
  } else {
    // 如果 userDef 是对象,定义可读写的计算属性
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop;
    sharedPropertyDefinition.set = userDef.set || noop;
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    // 如果没有定义 setter 函数,发出警告
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      );
    };
  }
  // 定义计算属性
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      if (watcher.dirty) {
        // 如果计算属性需要重新计算,调用 evaluate 方法
        watcher.evaluate();
      }
      if (Dep.target) {
        // 收集依赖
        watcher.depend();
      }
      return watcher.value;
    }
  };
}

在上述代码中,initComputed 函数用于初始化 computed 选项。首先,为每个计算属性创建一个 Watcher 对象,该对象具有 lazy 选项,表示计算属性是惰性求值的。然后,通过 defineComputed 函数定义计算属性的 getter 和 setter 方法。createComputedGetter 函数用于创建计算属性的 getter 方法,当访问计算属性时,会检查计算属性是否需要重新计算,如果需要则调用 evaluate 方法进行计算,并收集依赖。

七、watch 配置选项

7.1 watch 选项的作用

watch 选项用于监听数据的变化,并在数据变化时执行相应的回调函数。通过 watch 选项,开发者可以实现复杂的业务逻辑,如数据验证、异步操作等。

7.2 watch 选项的使用方式

javascript

const vm = new Vue({
  data: {
    message: 'Hello, Vue!'
  },
  watch: {
    // 监听 message 数据的变化
    message: function (newValue, oldValue) {
      console.log(`Message changed from "${oldValue}" to "${newValue}"`);
    }
  }
});

7.3 watch 选项的源码分析

javascript

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key];
    if (Array.isArray(handler)) {
      // 如果处理函数是数组,为每个处理函数创建一个 watcher
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i]);
      }
    } else {
      // 为处理函数创建一个 watcher
      createWatcher(vm, key, handler);
    }
  }
}

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    // 如果处理函数是对象,提取 handler 和 options
    options = handler;
    handler = handler.handler;
  }
  if (typeof handler === 'string') {
    // 如果处理函数是字符串,从 methods 中获取函数
    handler = vm[handler];
  }
  return vm.$watch(expOrFn, handler, options);
}

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this;
  if (isPlainObject(cb)) {
    // 如果回调函数是对象,递归调用 $watch
    return createWatcher(vm, expOrFn, cb, options);
  }
  options = options || {};
  options.user = true;
  const watcher = new Watcher(vm, expOrFn, cb, options);
  if (options.immediate) {
    // 如果设置了 immediate 选项,立即执行回调函数
    try {
      cb.call(vm, watcher.value);
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`);
    }
  }
  // 返回取消监听的函数
  return function unwatchFn () {
    watcher.teardown();
  };
};

在上述代码中,initWatch 函数用于初始化 watch 选项。对于每个监听项,调用 createWatcher 函数创建一个 Watcher 对象。createWatcher 函数会处理处理函数为数组、对象或字符串的情况。$watch 方法是 Vue 实例的一个方法,用于创建一个 Watcher 对象,并监听数据的变化。如果设置了 immediate 选项,会立即执行回调函数。最后,返回一个取消监听的函数。

八、生命周期钩子配置选项

8.1 生命周期钩子的作用

生命周期钩子是 Vue 实例在不同生命周期阶段自动调用的函数。通过使用生命周期钩子,开发者可以在特定的时间点执行自定义的代码,如初始化数据、挂载 DOM、销毁资源等。

8.2 常见的生命周期钩子

常见的生命周期钩子包括 beforeCreatecreatedbeforeMountmountedbeforeUpdateupdatedbeforeDestroydestroyed 等。

8.3 生命周期钩子的源码分析

javascript

export function callHook (vm: Component, hook: string) {
  // 从配置选项中获取对应的生命周期钩子函数
  const handlers = vm.$options[hook];
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        // 调用生命周期钩子函数
        handlers[i].call(vm);
      } catch (e) {
        // 捕获异常并处理错误
        handleError(e, vm, `${hook} hook`);
      }
    }
  }
  if (vm._hasHookEvent) {
    // 触发自定义的生命周期事件
    vm.$emit('hook:' + hook);
  }
}

function initLifecycle (vm: Component) {
  const options = vm.$options;

  // 找到父实例
  let parent = options.parent;
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent;
    }
    parent.$children.push(vm);
  }

  vm.$parent = parent;
  vm.$root = parent ? parent.$root : vm;

  vm.$children = [];
  vm.$refs = {};

  vm._watcher = null;
  vm._inactive = null;
  vm._directInactive = false;
  vm._isMounted = false;
  vm._isDestroyed = false;
  vm._isBeingDestroyed = false;
}

function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el;
  if (!vm.$options.render) {
    // 如果没有定义 render 函数,创建一个空的 render 函数
    vm.$options.render = createEmptyVNode;
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        // 如果有模板或 el 选项,发出警告
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        );
      } else {
        // 如果没有模板和 el 选项,发出警告
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        );
      }
    }
  }
  // 调用 beforeMount 钩子
  callHook(vm, 'beforeMount');

  let updateComponent;
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name;
      const id = vm._uid;
      const startTag = `vue-perf-start:${id}`;
      const endTag = `vue-perf-end:${id}`;

      mark(startTag);
      const vnode = vm._render();
      mark(endTag);
      measure(`vue ${name} render`, startTag, endTag);

      mark(startTag);
      vm._update(vnode, hydrating);
      mark(endTag);
      measure(`vue ${name} patch`, startTag, endTag);
    };
  } else {
    updateComponent = () => {
      // 渲染虚拟 DOM 并更新真实 DOM
      vm._update(vm._render(), hydrating);
    };
  }

  // 创建一个 watcher 来更新组件
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        // 调用 beforeUpdate 钩子
        callHook(vm, 'beforeUpdate');
      }
    }
  }, true /* isRenderWatcher */);
  hydrating = false;

  // 如果 vm.$vnode 为空,说明是根实例,标记为已挂载
  if (vm.$vnode == null) {
    vm._isMounted = true;
    // 调用 mounted 钩子
    callHook(vm, 'mounted');
  }
  return vm;
}

function destroy (vm: Component) {
  if (vm._isBeingDestroyed) {
    return;
  }
  // 调用 beforeDestroy 钩子
  callHook(vm, 'beforeDestroy');
  vm._isBeingDestroyed = true;
  // 销毁所有子实例
  const parent = vm.$parent;
  if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
    remove(parent.$children, vm);
  }
  // 销毁所有 watcher
  if (vm._watcher) {
    vm._watcher.teardown();
  }
  let i = vm._watchers.length;
  while (i--) {
    vm._watchers[i].teardown();
  }
  // 移除所有事件监听器
  if (vm._events) {
    removeListeners(vm._events);
  }
  // 销毁所有子组件
  if (vm._children) {
    let i = vm._children.length;
    while (i--) {
      vm._children[i].$destroy();
    }
  }
  // 调用 destroyed 钩子
  vm._isDestroyed = true;
  callHook(vm, 'destroyed');
  // 触发销毁事件
  vm.$emit('hook:destroyed');
}

在上述代码中,callHook 函数用于调用生命周期钩子函数。initLifecycle 函数用于初始化 Vue 实例的生命周期相关属性。mountComponent 函数用于挂载 Vue 实例,在挂载过程中会调用 beforeMountmounted 等生命周期钩子。destroy 函数用于销毁 Vue 实例,在销毁过程中会调用 beforeDestroydestroyed 等生命周期钩子。

九、组件配置选项

9.1 组件的定义和注册

在 Vue 中,组件是可复用的 Vue 实例。组件可以通过全局注册或局部注册的方式使用。

javascript

// 全局注册组件
Vue.component('my-component', {
  template: '<div>This is a global component</div>'
});

// 局部注册组件
const ChildComponent = {
  template: '<div>This is a local component</div>'
};

const vm = new Vue({
  components: {
    'child-component': ChildComponent
  }
});

9.2 组件配置选项的源码分析

javascript

function initComponents (Vue: GlobalAPI) {
  // 初始化内置组件
  Vue.component('keep-alive', keepAlive);
}

Vue.prototype._init = function (options?: Object) {
  const vm: Component = this;
  // 合并配置选项
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  );
  // 初始化组件
  initComponents(vm);
  // 其他初始化操作...
};

function createComponentInstanceForVnode (
  vnode: VNode,
  parent: any, 
  parentElm?: ?Node,
  refElm?: ?Node
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  };
  // 解析组件选项
  const inlineTemplate = vnode.data.inlineTemplate;
  if (inlineTemplate) {
    options.render = inlineTemplate.render;
    options.staticRenderFns = inlineTemplate.staticRenderFns;
  }
  // 创建组件实例
  const componentOptions = vnode.componentOptions;
  const Ctor = componentOptions.Ctor;
  const propsData = componentOptions.propsData;
  const listeners = componentOptions.listeners;
  const tag = componentOptions.tag;
  if (propsData && Ctor.options.props) {
    // 处理 props 数据
    propsData = resolveProps(
      Ctor.options.props,
      propsData
    );
  }
  // 创建组件实例
  const child = new Ctor({
    propsData,
    _parentVnode: vnode,
    _parentListeners: listeners,
    _renderChildren: vnode.children,
    _componentTag: tag,
    parent
  });
  return child;
}

在上述代码中,initComponents 函数用于初始化内置组件。在 _init 方法中,会调用 initComponents 函数进行组件的初始化。createComponentInstanceForVnode 函数用于创建组件实例,它会解析组件选项,处理 props 数据,然后创建一个新的组件实例。

十、指令配置选项

10.1 指令的定义和使用

指令是 Vue 提供的一种特殊的属性,用于在模板中绑定一些特殊的行为。开发者可以自定义指令来满足特定的需求。

javascript

// 自定义指令
Vue.directive('focus', {
  // 当指令绑定到元素上时调用
  bind: function (el) {
    // 元素聚焦
    el.focus();
  }
});

// 在模板中使用指令
<template>
  <input v-focus>
</template>

10.2 指令配置选项的源码分析

javascript

function initDirectives (vm: Component) {
  const directives = vm.$options.directives;
  if (directives) {
    for (const key in directives) {
      // 注册指令
      registerDirective(vm, key, directives[key]);
    }
  }
}

function registerDirective (vm: Component, name: string, def: Object | Function) {
  if (typeof def === 'function') {
    // 如果定义是函数,转换为对象形式
    def = { bind: def, update: def };
  }
  // 获取指令定义
  const ctor = vm.$options._base.directive(name);
  if (!ctor) {
    // 如果指令未注册,注册指令
    vm.$options._base.directive(name, def);
  }
}

function bindDirectives (vnode: VNodeWithData) {
  const data = vnode.data;
  if (data.directives) {
    const dirs = data.directives;
    const oldDirs = vnode.oldDirectives;
    const newDirs = {};
    for (const key in dirs) {
      // 处理新的指令
      const def = dirs[key];
      const hook = getHookForDirective(def);
      const el = vnode.elm;
      const arg = def.arg;
      const modifiers = def.modifiers;
      const value = def.value;
      const oldValue = oldDirs && oldDirs[key] ? oldDirs[key].value : undefined;
      if (oldDirs && oldDirs[key]) {
        // 如果有旧的指令,更新指令
        hook.update && hook.update(el, {
          name: key,
          value,
          oldValue,
          arg,
          modifiers
        }, vnode, vnode.oldVnode);
      } else {
        // 如果是新的指令,绑定指令
        hook.bind && hook.bind(el, {
          name: key,
          value,
          oldValue,
          arg,
          modifiers
        }, vnode);
      }
      newDirs[key] = def;
    }
    vnode.oldDirectives = newDirs;
  }
}

在上述代码中,initDirectives 函数用于初始化指令。它会遍历 directives 选项,调用 registerDirective 函数注册指令。registerDirective 函数会处理指令定义为函数的情况,并将指令注册到 Vue 实例中。bindDirectives 函数用于处理指令的绑定和更新,它会根据指令的状态调用相应的钩子函数。

十一、总结与展望

11.1 总结

通过对 Vue 配置模块的深入分析,我们了解到 Vue 提供了丰富的配置选项,涵盖了数据绑定、方法定义、计算属性、监听器、生命周期钩子、组件注册、指令定义等多个方面。这些配置选项使得开发者能够根据不同的需求对 Vue 实例进行定制化设置,实现复杂的业务逻辑和交互效果。

在源码层面,我们分析了配置选项的合并策略、初始化流程以及各个配置选项的具体实现原理。通过了解这些源码,我们可以更好地理解 Vue 的工作机制,避免在开发过程中出现一些常见的问题。

11.2 展望

随着前端技术的不断发展,Vue 也在不断地演进和完善。未来,Vue 的配置模块可能会有以下几个方面的发展:

  • 更简洁的配置语法:为了降低开发者的学习成本和提高开发效率,可能会引入更简洁、更直观的配置语法。

  • 更强大的配置选项:可能会增加一些新的配置选项,以满足更多复杂场景的需求,如更灵活的组件化配置、更高级的性能优化配置等。

  • 更好的与其他技术的集成:随着前端生态系统的不断丰富,Vue 可能会提供更好的与其他技术(如 TypeScript、Vuex、Vue Router 等)的集成配置选项,使得开发者能够更方便地构建大型应用。

总之,Vue 的配置模块在未来将继续发挥重要作用,为开发者提供更强大、更灵活的开发工具和支持。

昨天 — 2025年4月17日掘金 前端

关于Flutter架构的小小探讨

作者 JarvanMo
2025年4月17日 22:39

最近要新开发一个Flutter,需要设计一个架构。我在刚接Flutter的时候,我记得Google官方人员曾说过他们内部使用的是BLoC模式,彼时我也尝试用BLoC开发Flutter应用,而Flutter经过这么多年的进化,现在是否有新的架构?官方是否有推荐架构?我一查,还真就有一个官方架构指南。 通过阅读这份架构指南,我发现Flutter团队建议采用MVVM来组织Flutter应用程序的各个层级,遵循在其他移动框架(如原生Kotlin和Swift)中所采用的既定模式。但当我们讨论应用程序架构和状态管理选项时,我们也不能完全忽视BLoC方法的优势。为什么Flutter团队没有建议使用 BLoC 方法呢?

理解基础知识

BLoC即Business Logic Component,它代表业务逻辑组件。它遵循响应式编程范式,使用Streams来管理状态和事件。BLoC 的主要目标是强制实现单向数据流,使业务逻辑与UI完全分离。

BLoC的几个关键组成部分:

  • 事件:用户操作或外部触发因素。
  • 状态:表示UI当前的状态。
  • 业务逻辑组件(Blocs):通过Streams把事件映射到状态。

BLoC中的的单向数据流

另一方面,MVVM(Model-View-ViewModel)模式提供了一种更简单的状态管理方式。在这种模式中,ViewModel充当了用户界面(View)和数据(Model)之间的中间人。它通过使用 notifyListeners() 方法来通知用户界面状态的变化。

基于mvvm的架构

你有没有注意到它们之间的一些相似之处呢?😅 他们两个都旨在高效地管理状态,并以响应式的方式更新UI,而且它们都能与Flutter组件无缝集成,同时也都支持异步操作,不过BLoC的Streams更自然地适用于复杂的工作流程。

MVVM的几个关键组成部分:

  • 模型(Model):代表app的数据。
  • 视图模型(ViewModel):管理状态和业务逻辑。
  • 视图(View):显示UI并观察数据变化。

MVVM方式的组成部分.png

考虑到它们基本上都遵循着同一个思路,就是把不同的任务分开处理,形成一个条理清晰的分层架构。那咱们就来重点关注一下不同点吧。

主要区别

  1. 数据流
    • BLoC:强制实行单向数据流。事件触发状态变化,然后状态被发送到UI。
    • MVVM:采用双向数据流,在这种模式下,状态更新和UI交互可以更自由地发生。
  2. 样板代码量
    • BLoC:需要更多的样板代码,包括对事件、状态以及 BLoC 类的定义。
    • MVVM:样板代码量极少,可以通过 notifyListeners()ValueNotifier.value 直接进行更新。
  3. 性能
    • BLoC:流在处理复杂的异步操作方面效率极高。
    • MVVM:轻量级且速度快,但在规模更大、更复杂的应用程序中可能会力不从心。
  4. 学习曲线
    • BLoC:由于涉及诸如Streams和流控制器StreamControllers等响应式编程概念,学习曲线较陡峭。
    • MVVM:更容易学习,尤其适合初学者。
  5. 可测试性
    • BLoC:具有出色的可测试性,允许独立测试状态转换。
    • MVVM:可测试性适中,需要花费精力将逻辑与用户界面解耦。
  6. 可扩展性
    • BLoC:对于具有共享状态的大型复杂应用程序来说是理想之选。
    • MVVM:由于在大型应用程序中可能存在紧密耦合的问题,所以最适合中小型应用程序。

总结

不管是BLoC,还是搭配ChangeNotifier或者ValueNotifier用的MVVM模式,都能把Flutter代码弄得更清爽、更好维护。至于到底该选哪个,得看你项目体量多大、有多复杂,还有你所偏好的结构层级。

何时选择 BLoC

如果你正在构建一个大型、复杂的应用程序,并且希望借助严格的架构规则让自己安心,那么就选择 BLoC。BLoC 的事件到状态模式以及不可变性使其更易于测试、调试和跟踪数据流。如果你的团队看重可预测性以及一种定义清晰且可良好扩展的方法,那么BLoC是一个可靠的选择。

何时选择使用Notifier的 MVVM 模式

如果你在寻找易于快速实现且容易理解的方案,那么使用 ChangeNotifierValueNotifier 的 MVVM 模式可能适合你的口味。它非常适合中小型项目,或者当你刚刚起步且不需要BLoC的全部复杂功能时。你在设置上花费的时间会更少,而有更多时间进行快速迭代,不过随着时间推移,你需要保持自律,以免造成混乱。

通过了解每种模式的细微差别,你便能从中挑选出契合团队实际需求,且适配应用程序复杂程度的架构模式。无论最终选定哪种,都意味着你在编写更简洁、高效的 Flutter 代码之路上,迈出了坚实而关键的一步。

对于你的 Flutter 项目,你更喜欢哪种方法呢?请在评论区告诉我吧!

最后,希望大家关注我的公众号OpenFlutter,感恩。

image.png

操作教程|通过DataEase制作MaxKB系统数据大屏

2025年4月17日 22:26

MaxKB(Max Knowledge Brain)是一款强大易用的企业级AI助手,支持RAG检索增强生成、工作流编排、MCP工具调用能力,目前正在被广泛应用于智能客服、企业内部知识库、学术研究与教育等场景。MaxKB可以帮助用户快速搭建面向不同应用场景的AI助手,并且有效减少大模型幻觉,方便企业充分利用DeepSeek等大语言模型的学习和推理能力,在AI应用方向上快速兑现业务价值。

在MaxKB实际的使用过程中,会产生许多应用小助手相关的数据,例如应用访问状况、Token消耗数量、用户高频问题等。DataEase开源BI工具可以帮助用户将这些数据整合汇总,以数据大屏的方式加以展示,还可以方便地将数据大屏分享给他人,共享数据信息。

▲图1 MaxKB系统数据大屏(图中示例数据为演示用MaxKB系统数据)

本文将为您详细介绍通过DataEase开源BI工具制作MaxKB系统数据大屏的具体步骤。

一、调取MaxKB数据库中的数据信息

要调取MaxKB的数据信息,并用这些数据制作出DataEase数据大屏,我们需要对MaxKB的参数配置进行一定调整,使得MaxKB数据所在的PostgreSQL数据库端口可以从外部访问。

  1. 在1Panel开源面板中,找到已经安装好的MaxKB,然后编辑MaxKB的参数配置;

▲图2 MaxKB参数配置

  1. 在MaxKB参数配置的“高级设置”中编辑compose文件,添加命令行:“- ${HOST_IP}:5432:5432”,开放MaxKB数据库端口的外部访问功能。

▲图3 在“高级配置”中编辑compose文件

▲图4 在compose文件中添加命令行

二、连接数据源

在DataEase中添加PostgreSQL数据库为数据源,填写数据源名称、MaxKB所在服务器的IP地址,以及数据库的端口号、名称、用户名、密码等信息。填写完基本信息后,获取数据库的Schema,校验成功后保存即可。

▲图5 创建PostgreSQL数据源

三、制作数据大屏

配置好数据源后,就可以使用数据源中的数据制作MaxKB系统数据大屏了。

  1. 在DataEase中,选择“使用模板新建”的方式,在“应用模板”分类下,选择使用“MaxKB系统数据概览”这个应用模板,新建一个数据大屏;

▲图6 使用模板新建数据大屏

▲图7 选择应用“MaxKB系统数据概览”模板

  1. 填写数据大屏的基本信息,将“系统数据源”设置为MaxKB数据源,点击“保存”按钮即可实现模板数据的替换,完成大屏的制作。

▲图8 模板应用信息填写

制作好的MaxKB系统数据大屏效果如图:

▲图9 MaxKB系统数据大屏(图中示例数据为演示用MaxKB系统数据)

四、分享数据大屏

完成数据大屏的制作后,我们可以通过链接方式将做好的MaxKB系统数据大屏分享给他人,还可以为分享链接设置有效期和密码,有效保障数据安全。

▲图10 MaxKB系统数据大屏分享设置

▲图11 公共链接分享设置

用一个项目揭开AI全栈的神秘面纱——让AI根据你的项目数据生成回答

作者 天天扭码
2025年4月17日 21:27

一、引言

在当今数字化时代,AI 技术已经深入到我们生活的方方面面。而 AI 全栈开发则涉及到从前端界面展示到后端数据处理,再到 AI 模型交互的整个流程。今天,我们将通过一个具体的项目,详细揭开 AI 全栈的神秘面纱,让你对全栈开发有更深入的理解。

二、项目概述

本项目是一个 AI 用户问答聊天机器人应用,主要由前端页面展示、后端数据接口提供以及 AI 服务器响应问题三部分组成。前端负责与用户交互,展示用户数据并接收用户问题;后端提供用户数据接口;AI 服务器则根据用户提供的数据和问题,调用 AI 模型生成回答。

image.png

三、项目结构与准备工作

1. 项目结构

项目主要分为三个部分:前端(frontend)、后端(backend)和 AI 服务器(llm ai server)。

  • 前端:负责展示用户界面,与用户交互。
  • 后端:提供数据接口,管理数据文件。
  • AI 服务器:处理用户的问题,调用 AI 模型生成回答。

2. 数据文件准备 - user.json

这个文件存储了用户的基本信息,是一个 JSON 格式的数据文件。内容如下:

{
    "users": [
        {
            "id": 1,
            "name": "小帅",
            "hometown": "山东"
        },
        {
            "id": 2,
            "name": "小瓜", 
            "hometown": "江西"
        },
        {
            "id": 3,
            "name": "小卢",
            "hometown": "江西"
        }
    ]
}

3. 后端数据接口服务启动

我们使用 json-server 来提供数据接口。首先要安装它:

npm i json-server

安装完成后,我们可以使用以下命令启动服务:

json-server --port 3000 --watch user.json

为了方便,我们可以在 package.json 的 scripts 中添加一个命令:

"scripts": {
    "dev": "json-server --port 3000 --watch user.json"
}

之后,我们就可以使用 npm run dev 来启动服务,服务会监听 http://localhost:3000/users,提供用户数据接口。

四、AI 服务器搭建 - main.js

1. 引入必要的模块

const http = require('http');
const OpenAI = require('openai');
const url = require('url');

这里引入了 Node.js 的 http 模块用于创建服务器,OpenAI 模块用于调用 AI 模型,url 模块用于解析 URL 中的查询字符串。

2. 配置 OpenAI 客户端

const client = new OpenAI({
    apiKey: '你的KEY',
    baseURL: 'https://api.302.ai/v1'
});

这里设置了 OpenAI 的 API 密钥和基础 URL,以便后续调用 AI 模型。

3. 通用 LLM 完成接口函数

const getCompletion = async (prompt, model = "gpt-3.5-turbo") => {
    const messages = [{
        role: "user",
        content: prompt
    }];
    const response = await client.chat.completions.create({
        model: model,
        messages: messages,
        temperature: 0.1,
    });
    return response.choices[0].message.content;
};

这个函数用于向 OpenAI 模型发送请求并获取响应。prompt 是用户的问题,model 是使用的 AI 模型,temperature 控制生成结果的随机性,值越低结果越确定。

4. 测试函数(可选)

const main = async () => {
    const prompt = "用贴吧语气的话骂我";
    const result = await getCompletion(prompt);
    console.log(result);
};

这个函数可以用于测试 getCompletion 函数是否正常工作。

5. 创建 HTTP 服务器

const server = http.createServer(async (req, res) => {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');

    const parsedUrl = url.parse(req.url, true);
    const queryObj = parsedUrl.query;
    const prompt = `
    ${queryObj.data}
    请你根据以上数据回答以下问题
    ${queryObj.question}
    `;
    const result = await getCompletion(prompt);
    let info = {
        result
    };
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/json');
    res.end(JSON.stringify(info));
});

server.listen(1314);

这个服务器监听 1314 端口,处理前端发送的请求。它会从 URL 的查询字符串中获取用户的数据和问题,组合成一个提示(prompt),然后调用 getCompletion 函数获取 AI 模型的回答,并将结果以 JSON 格式返回给前端。

五、前端页面搭建 - index.html

1. 页面结构

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI Users Rag chatbot</title>
    <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css">
</head>
<body>
    <div class="container" action="http://www.baidu.com">
        <div class="row col-md-6 col-md-offset-3">
            <table class="table table-striped" id="user_table">
                <thead>
                    <tr>
                        <th>ID</th>
                        <th>姓名</th>
                        <th>家乡</th>
                    </tr>
                </thead>
                <tbody>
                </tbody>
            </table>
            <div class="row">
                <form name="aiForm">
                    <div class="from-group">
                        <label for="questionInput">提问</label> 
                        <input type="text" class="form-control" id="questionInput" name="question" placeholder="请输入问题" >
                    </div>
                    <button type="submit" class="btn btn-default" name="btn">查询</button>
                </form>
            </div>
            <div class="row" id="message">

            </div>
        </div>
    </div>
    <script>
        // 后续 JavaScript 代码
    </script>
</body>
</html>

这里使用了 Bootstrap 框架来美化页面,创建了一个表格用于展示用户数据,一个表单用于用户输入问题,以及一个区域用于显示 AI 回答。

2. 获取并展示用户数据

const tableBody = document.querySelector('table tbody');
const oForm = document.forms['aiForm'];
let usersData;

fetch('http://localhost:3000/users')
   .then(res => res.json())
   .then(users => {
        usersData = users;
        for (let user of users) {
            const tr = document.createElement('tr');
            for (let key in user) {
                const td = document.createElement('td');
                td.innerText = user[key];
                tr.appendChild(td);
            }
            tableBody.appendChild(tr);
        }
    });

使用 fetch 函数从后端接口 http://localhost:3000/users 获取用户数据,然后将数据展示在表格中。

3. 处理用户提问

oForm.addEventListener('submit', function (e) {
    e.preventDefault();
    const question = oForm.question.value.trim();
    if (!question) {
        alert('请输入问题');
        return;
    }

    fetch(`http://localhost:1314/api?question=${question}&data=${JSON.stringify(usersData)}`)
       .then(res => res.json())
       .then(data => {
            console.log(data);
            document.querySelector('#message').innerText = data.result;
        });
});

当用户提交表单时,阻止表单的默认提交行为,获取用户输入的问题,然后使用 fetch 函数将问题和用户数据发送到 AI 服务器 http://localhost:1314/api,最后将 AI 服务器返回的结果展示在页面上。

六、总结

通过这个项目,我们详细了解了 AI 全栈开发的整个流程。从前端的页面设计和交互,到后端的数据接口提供,再到 AI 服务器的模型调用,每个环节都紧密相连。希望这个项目能帮助你揭开 AI 全栈的神秘面纱,让你对全栈开发有更深入的理解和实践经验。在实际开发中,你可以根据需求对项目进行扩展和优化,例如使用更复杂的 AI 模型、优化前端界面等。

一杯咖啡的时间吃透一道算法题——2.两数相加(使用链表)

作者 天天扭码
2025年4月17日 21:00

一、解题前置知识——链表操作(可跳过)

1. 什么是链表

链表是一种线性数据结构,它由一系列节点组成,每个节点包含两部分:

  • 数据 (data):  存储实际的值。
  • 指针 (next):  指向下一个节点,表示链表中下一个元素的位置。

与数组不同,链表中的元素不必存储在连续的内存位置中。 这使得链表在插入和删除元素时更加灵活,因为不需要移动大量的元素。

2. JavaScript 中的链表实现

应题目要求,我们使用函数的方式来创建链表,链表格式如下

 function ListNode(val, next) {
    this.val = (val===undefined ? 0 : val)
    this.next = (next===undefined ? null : next) 
}

3. 链表的基本操作

以下是一些常见的链表操作,以及它们在 JavaScript 中的实现方法:

  • 创建链表:初始化头节点

    let head = null; // 空链表
    

    为何let head = null 为一个空链表?这是一个很有意思的问题

    笔者认为这里的head是不是空链表主要是看head的使用场景,这里是链表的场景所以称之为空链表。

    let head = null; 表示空链表的原因是:

    1.head 是链表的入口点。

    2.null 表示“没有”或“不存在”。

    3.当 head 为 null 时,链表没有起始节点,因此被认为是空的。

  • 在链表头部添加节点 (prepend):

    function prepend(head, val) {
      const newNode = new ListNode(val);
      newNode.next = head;
      return newNode;
    }
    
    // 示例:
    head = prepend(head, 1);
    head = prepend(head, 2);
    

    1.const newNode = new ListNode(val);

    这行代码使用 ListNode 函数创建一个新的节点。新节点的 val 属性被设置为传入的 val 值,而 next 属性默认初始化为 null (如果 ListNode 函数的定义是像你提供的那样)。 此刻,我们创建了一个孤立的节点,它还没有连接到链表上。

    2.newNode.next = head;

    这行代码将新创建的节点 (newNode) 的 next 指针设置为当前的 head。 简而言之,newNode 的 next 指针现在指向链表的第一个节点 (原来的 head 所指向的节点)。 这实际上将 newNode 放在了链表的 "前面"。

    3.return newNode;

    这行代码返回 newNode。 在调用 prepend 函数时,我们需要更新 head 变量,让它指向这个新的头节点。 这就是为什么在调用时我们使用 head = prepend(head, 1); 这样的语句。

  • 在链表尾部添加节点 (append):

    function append(head, val) {
      const newNode = new ListNode(val);
    
      if (!head) {
        return newNode;
      }
    
      let current = head;
      while (current.next) {
        current = current.next;
      }
      current.next = newNode;
      return head;
    }
    
    // 示例:
    head = append(head, 3);
    

    1.const newNode = new ListNode(val);

    创建一个新的节点,其 val 属性被设置为传入的 val 值,next 属性默认为 null

    2.if (!head) { return newNode; }

    这是一个重要的边界情况处理。 如果 head 是 null,这意味着链表当前是空的。 在这种情况下,新节点 newNode 将成为链表的第一个(也是唯一一个)节点,因此函数直接返回 newNode 作为新的 head

    3.let current = head;

    如果链表不为空,我们就需要找到链表的最后一个节点。 current 变量用于从链表的头部开始遍历。

    4.while (current.next) { current = current.next; }

    这是一个循环,用于遍历链表,直到找到最后一个节点。 current.next 检查当前节点的 next 指针是否为 null。 如果 current.next 不是 null,说明 current 不是最后一个节点,所以我们将 current 移动到下一个节点 current = current.next。 当循环结束时,current 将指向链表的最后一个节点(即 current.next 是 null 的节点)。

    5.current.next = newNode;

    这是添加新节点的核心步骤。 将最后一个节点(current)的 next 指针指向新创建的节点 newNode。 这样,newNode 就成为了链表的新的最后一个节点。

    6.return head;

    函数返回 head。 注意:  只有在链表一开始为空的时候,head 才会被更新!如果链表一开始不为空,我们只是在尾部添加了一个节点,head 仍然指向原来的头节点。

  • 插入节点 (insert):

    function insert(head, val, position) {
      if (position < 0) {
        console.log("Invalid position");
        return head;
      }
    
      if (position === 0) {
        return prepend(head, val);
      }
    
      const newNode = new ListNode(val);
      let current = head;
      let previous = null;
      let index = 0;
    
      while (current && index < position) {
        previous = current;
        current = current.next;
        index++;
      }
    
      if (index < position) {
        console.log("Position out of range");
        return head;
      }
    
      newNode.next = current;
      previous.next = newNode;
      return head;
    }
    
    // 示例:
    head = insert(head, 10, 1);
    

    1.校验位置有效性 (`position < 0`)

    函数首先检查 position 是否小于 0。如果是,说明插入位置无效,函数输出错误信息并返回原始的 head,不做任何修改。这是处理一种异常情况。

    2.处理在头部插入的情况 (`position === 0`)

    如果 position 等于 0,意味着需要在链表的头部插入新节点。 此时,函数直接调用我们之前讨论过的 prepend 函数来完成插入操作,并返回新的 head。 这样做的好处是代码复用,同时简化了 insert 函数的逻辑。

    3.创建新节点 (`const newNode = new ListNode(val);`)

    创建一个值为 val 的新节点。 这个节点将要被插入到链表中的指定位置。

    4.初始化 `current`, `previous` 和 `index`

    current:指向当前正在遍历的节点,初始化为 head

    previous:指向 current 节点的前一个节点,初始化为 null

    index:记录当前遍历的节点的位置,初始化为 0。

    5.遍历链表到目标位置 (`while (current && index < position)`)

    这个 while 循环遍历链表,直到到达要插入的位置。 注意循环的两个条件:

    (1)current:确保 current 不为 null,即没有到达链表的末尾。

    (2)index < position:确保还没有到达目标位置。

    在循环内部:

    previous = current;:将 previous 更新为 currentcurrent = current.next;:将 current 移动到下一个节点。 index++;:增加 index 的计数。

    循环结束后,current 将指向目标位置的节点(或者 null,如果目标位置超出了链表的范围),previous 将指向目标位置的前一个节点。

    6.检查位置是否超出范围 (`index < position`)

    如果在遍历结束后,index 仍然小于 position,这意味着目标位置超出了链表的范围(例如,链表只有 3 个节点,但要插入到位置 5)。 在这种情况下,函数输出错误信息并返回原始的 head,不做任何修改。

    7.插入新节点

    newNode.next = current;:将新节点的 next 指针指向 current。这意味着 newNode 将插入到 current 之前。 previous.next = newNode;:将 previous 节点的 next 指针指向 newNode。这意味着 previous 节点现在指向 newNode,从而将 newNode 插入到链表中。

    8.返回 `head`

    返回链表的 head。 由于只有当 position 为0时head会发生变化,其他时候都是返回原head,函数会正确地返回链表的头部。

  • 删除节点 (deleteNode):

    function deleteNode(head, position) {
      if (!head) {
        return null;
      }
    
      if (position < 0) {
        console.log("Invalid position");
        return head;
      }
    
      if (position === 0) {
        return head.next;
      }
    
      let current = head;
      let previous = null;
      let index = 0;
    
      while (current && index < position) {
        previous = current;
        current = current.next;
        index++;
      }
    
      if (!current) {
        console.log("Node not found at position");
        return head;
      }
    
      previous.next = current.next;
      return head;
    }
    
    // 示例:
    head = deleteNode(head, 1);
    

    1.if (!head) { return null; }

    处理链表为空的情况。 如果 head 是 null,说明链表是空的,没有节点可以删除,函数直接返回 null

    2.if (position < 0) { ... }

    处理无效的位置。 如果 position 是负数,说明位置无效,函数输出错误信息并返回原来的 head,不做任何修改。

    3.if (position === 0) { return head.next; }

    处理删除头节点的情况。 如果 position 是 0,说明要删除的是头节点。 在这种情况下,函数直接返回 head.next,即将链表的第二个节点作为新的头节点。 相当于从链表中移除了原来的头节点。 注意:  这种情况下,原来的头节点并没有被显式地释放内存。

    4.let current = head; let previous = null; let index = 0;

    初始化三个变量:

    current:指向当前正在遍历的节点,初始值为 headprevious:指向 current 的前一个节点,初始值为 null (因为头节点没有前一个节点)。 index:记录当前遍历到的节点的位置,初始值为 0。

    5.while (current && index < position) { ... }

    这个 while 循环遍历链表,直到到达要删除的节点的位置。

    current && index < position:确保 current 不为 null (即没有到达链表尾部) 并且 index 小于目标 position

    在循环内部:

    (1)previous = current;:将 previous 更新为 current

    (2)current = current.next;:将 current 移动到下一个节点。

    (3)index++;:增加 index 计数。

    循环结束后,current 会指向要删除的节点,而 previous 会指向要删除节点的前一个节点。

    6.if (!current) { ... }

    在遍历结束后,需要检查是否找到了要删除的节点。 如果 current 是 null,说明遍历到了链表的末尾,但是仍然没有到达指定的位置 (即 position 大于等于链表的长度),这意味着要删除的节点不存在。 函数输出错误信息并返回原始的 head,不进行任何删除操作。

    7.previous.next = current.next;

    如果找到了要删除的节点,这一步是删除节点的核心操作。 让 previous 节点的 next 指针指向 current 节点的 next 指针。 这相当于将 current 节点从链表中"跳过",从而删除了该节点。 关键:  这一步并没有显式地释放 current 节点占用的内存空间。

    8.return head;

    返回链表的 head。在大多数情况(除了删除的是头节点),head 不会改变。

  • 查找节点 (search):

    function search(head, val) {
      let current = head;
      let index = 0;
    
      while (current) {
        if (current.val === val) {
          return index;
        }
        current = current.next;
        index++;
      }
    
      return -1;
    }
    
    // 示例:
    const index = search(head, 3);
    if (index !== -1) {
      console.log("Node found at index:", index);
    } else {
      console.log("Node not found");
    }
    

    1.let current = head;

    初始化 current 指针,指向链表的头部 head。 current 指针用于遍历链表。

    2.let index = 0;

    初始化 index 变量,用于记录当前遍历到的节点的索引。 链表的第一个节点的索引为 0。

    3.while (current) { ... }

    这个 while 循环遍历链表,直到到达链表的末尾(current 变为 null)。 循环条件 current 用于判断当前节点是否有效。如果 current 为 null,说明已经遍历到了链表的末尾,循环结束。

    4.if (current.val === val) { return index; }

    在循环内部,检查当前节点的值 current.val 是否等于要查找的值 val。 如果相等,说明找到了目标节点,函数立即返回该节点的索引 index

    5.current = current.next;

    如果当前节点的值与目标值不匹配,将 current 指针移动到链表的下一个节点。

    6.index++;

    将 index 变量增加 1,以反映当前遍历到的节点的索引。

    7.return -1;

    如果 while 循环结束,说明已经遍历了整个链表,但没有找到目标节点。 在这种情况下,函数返回 -1,表示目标节点不存在于链表中。

  • 反转链表 (reverseList):

    function reverseList(head) {
      let previous = null;
      let current = head;
      let next = null;
    
      while (current) {
        next = current.next;
        current.next = previous;
        previous = current;
        current = next;
      }
    
      return previous;
    }
    
    //示例
    head = reverseList(head);
    

    1.let previous = null;

    初始化 previous 为 null。 previous 变量将用于存储当前节点 current 的前一个节点。 在反转后的链表中,当前节点的前一个节点实际上是原始链表中的后一个节点。 因为 head 节点会变成 tail 节点,所以 tail 节点的 previous 应该是 null。

    2.let current = head;

    初始化 current 为 head。 current 变量将用于遍历链表。

    3.let next = null;

    初始化 next 为 null。 next 变量用于临时存储 current 的下一个节点,以便在反转 current 的 next 指针后,仍然可以访问链表的剩余部分。

    4.while (current) { ... }

    这个 while 循环遍历链表,直到 current 变为 null,表示已经到达链表的末尾。

    5.next = current.next;

    在修改 current.next 之前,先将 current 的下一个节点保存到 next 变量中。 这是至关重要的一步,因为在下一步中会覆盖 current.next

    6.current.next = previous;

    这是反转链表的核心步骤。 将 current 的 next 指针指向 previous。 这实际上将 current 节点从原始链表中分离出来,并将其插入到反转后的链表的头部。

    7.previous = current;

    将 previous 更新为 current。 在下一次迭代中,current 将成为下一个节点的 previous 节点。

    8.current = next;

    将 current 更新为 next。 移动到链表中的下一个节点。

    9.return previous;

    当 while 循环结束时,current 将为 null,而 previous 将指向反转后的链表的头部。 返回 previous

  • 打印链表 (printList):

    function printList(head) {
      let current = head;
      let str = "";
    
      while (current) {
        str += current.val + " -> ";
        current = current.next;
      }
    
      str += "null";
      console.log(str);
    }
    
    // 示例:
    printList(head);
    

    1.let current = head;

    初始化 current 指针,指向链表的头部 head。 current 指针用于遍历链表,就像你在 search 函数中所做的那样。

    2.let str = "";

    初始化一个空字符串 str。 这个字符串将用于构建链表的表示。

    3.while (current) { ... }

    这个 while 循环遍历链表,直到 current 指针变为 null,表示已经到达链表的末尾。

    4.str += current.val + " -> ";

    在循环内部,将当前节点的值 current.val 和字符串 " -> " 添加到 str 字符串中。 这创建了链表中节点之间的箭头表示。

    5.current = current.next;

    将 current 指针移动到链表的下一个节点。

    6.str += "null";

    在 while 循环结束后,将字符串 "null" 添加到 str 字符串中,表示链表的末尾。 这是链表表示的标准约定。

    7.console.log(str);

    使用 console.log() 函数将构建好的字符串 str 打印到控制台。

4. 链表的优点和缺点

  • 优点:

    • 动态大小:  链表的大小可以在运行时动态调整,不需要预先分配固定大小的内存。
    • 插入和删除效率高:  在已知节点位置的情况下,插入和删除操作的时间复杂度为 O(1)。
  • 缺点:

    • 需要额外的内存空间:  每个节点都需要额外的内存空间来存储指针。
    • 访问效率低:  访问链表中特定位置的节点需要从头节点开始遍历,时间复杂度为 O(n)。

5. 何时使用链表

  • 当需要频繁进行插入和删除操作,并且不需要频繁访问特定位置的元素时。
  • 当无法预先确定数据的大小时。

二、题目描述——2.两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

 

示例 1:

输入: l1 = [2,4,3], l2 = [5,6,4]
输出: [7,0,8]
解释: 342 + 465 = 807.

示例 2:

输入: l1 = [0], l2 = [0]
输出: [0]

示例 3:

输入: l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出: [8,9,9,9,0,0,0,1]

 

提示:

  • 每个链表中的节点数在范围 [1, 100] 内
  • 0 <= Node.val <= 9
  • 题目数据保证列表表示的数字不含前导零

三、解题方案

本人的拼尽全力只能想到两种方案

方案一暴力拆分

将两个链表的数组表示出来,加一起之和再转换回去

方案二不拆分,直接处理

采用进位的方式直接在原链表上进行相加

这里我们选择方案二进行解题

四、具体代码

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var addTwoNumbers = function(l1, l2) {
    let carry = 0; 
    let head = null; 
    let tail = null; 

    let current1 = l1;
    let current2 = l2;

    while (current1 || current2 || carry) {
        const digit1 = current1 ? current1.val : 0;
        const digit2 = current2 ? current2.val : 0;

        const sum = digit1 + digit2 + carry;
        const digit = sum % 10; 
        carry = Math.floor(sum / 10); 

        const newNode = new ListNode(digit);

        if (head === null) {
            head = newNode;
            tail = newNode;
        } else {
            tail.next = newNode;
            tail = newNode;
        }

        current1 = current1 ? current1.next : null;
        current2 = current2 ? current2.next : null;
    }

    return head;
};

详细解释

1. let carry = 0; : 初始化进位 carry 为 0。 进位用于处理两个数字相加超过 9 的情况。

2. let head = null; : 初始化结果链表的头节点 head 为 null。 这是新建链表的标准做法。

3. let tail = null; : 初始化结果链表的尾节点 tail 为 null。 tail 指针方便在链表末尾添加新节点,而无需每次都从头遍历。

4. let current1 = l1; : 初始化 current1 指针,指向链表 l1 的头部。

5. let current2 = l2; : 初始化 current2 指针,指向链表 l2 的头部。

6. while (current1 || current2 || carry) { ... } : 一个 while 循环,只要 l1 和 l2 中还有节点或者 carry 不为 0,就继续循环。 这意味着即使一个链表已经遍历完,但另一个链表还有剩余节点或者有进位,都需要继续处理。

7. const digit1 = current1 ? current1.val : 0; : 获取 l1 当前节点的值。 使用三元运算符来处理链表 l1 已经遍历完的情况。 如果 current1 为 null,则 digit1 为 0。

8. const digit2 = current2 ? current2.val : 0; : 获取 l2 当前节点的值。 使用三元运算符来处理链表 l2 已经遍历完的情况。 如果 current2 为 null,则 digit2 为 0。

9. const sum = digit1 + digit2 + carry; : 计算当前位的和,包括 l1 和 l2 的当前位的数字以及进位 carry

10. const digit = sum % 10; : 计算当前位的结果。通过将 sum 除以 10 取余数来获得。

11. carry = Math.floor(sum / 10); : 计算进位。通过将 sum 除以 10 并向下取整来获得。

12. const newNode = new ListNode(digit); : 创建一个新节点 newNode,其值为 digit

13. if (head === null) { ... } : 检查结果链表是否为空。 如果 head 为 null,说明这是结果链表的第一个节点。

14. head = newNode; : 设置 newNode 为结果链表的头节点。

14. tail = newNode; : 设置 newNode 为结果链表的尾节点。 因为链表只有一个节点,所以头和尾都是同一个节点。

15. else { ... } : 如果结果链表不为空,则执行此操作。

16. tail.next = newNode; : 将 newNode 添加到结果链表的尾部。

17. tail = newNode; : 更新 tail 指针,使其指向新的尾节点。

18. current1 = current1 ? current1.next : null; : 将 current1 移动到 l1 的下一个节点。 如果 l1 已经遍历完,则将 current1 设置为 null

19. current2 = current2 ? current2.next : null; : 将 current2 移动到 l2 的下一个节点。 如果 l2 已经遍历完,则将 current2 设置为 null

20. return head; : 返回结果链表的头节点。

举例解析

image.png

image.png

image.png

image.png

image.png

五、结语

再见!

vue与react中监听的简单对比

2025年4月17日 20:37

一、核心概念对比

Vue监听机制

  • 响应式核心‌:基于Object.defineProperty(Vue2)/Proxy(Vue3)的数据劫持

  • 主要API‌:

    • data + watch侦听器
    • computed计算属性
    • $watch方法
  • 设计哲学‌:声明式编程,自动依赖追踪

React监听机制

  • 响应式核心‌:基于不可变数据和虚拟DOM diff

  • 主要API‌:

    • useState + useEffect
    • useMemo/useCallback
    • 类组件的生命周期方法
  • 设计哲学‌:函数式编程,显式控制

二、代码实现对比(含优化方案)

Vue实现示例

export default {
  data() {
    return {
      count: 0,
      user: { 
        name: 'John',
        profile: { age: 30 }
      }
    }
  },
  watch: {
    // 基础监听
    count(newVal, oldVal) {
      console.log(`Count变化: ${oldVal}${newVal}`);
    },
    // 深度监听优化
    user: {
      handler(val) { /* 处理逻辑 */ },
      deep: true,
      immediate: false  // 避免初始化执行
    },
    // 精确监听嵌套属性
    'user.profile.age': function(newAge) {
      console.log('年龄变化:', newAge);
    }
  },
  computed: {
    // 带缓存的计算属性
    userInfo() {
      return `${this.user.name}-${this.user.profile.age}`;
    }
  }
}

React实现示例

import { useState, useEffect, useMemo, useCallback } from 'react';

function UserComponent() {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState({ 
    name: 'John',
    profile: { age: 30 }
  });

  // 基础监听(类似watch)
  useEffect(() => {
    console.log('count变化:', count);
  }, [count]);  // 明确依赖

  // 深度监听优化方案
  useEffect(() => {
    console.log('user变化:', user);
  }, [user.name, user.profile.age]);  // 精确指定依赖

  // 计算属性优化(类似computed)
  const userInfo = useMemo(() => {
    return `${user.name}-${user.profile.age}`;
  }, [user.name, user.profile.age]);

  // 事件处理优化
  const handleClick = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);

  return (
    <div>
      <p>{userInfo}</p>
      <button onClick={handleClick}>增加</button>
    </div>
  );
}

三、深度对比分析

1. 响应式原理差异

维度 Vue React
数据跟踪 自动依赖收集 手动声明依赖
更新触发 精确到属性级别 组件级别重新渲染
实现方式 编译时转换+运行时劫持 运行时虚拟DOM diff

2. 性能优化对比

优化策略 Vue实现方式 React实现方式
计算缓存 自动缓存的computed 需手动useMemo
函数缓存 方法自动绑定 需手动useCallback
列表渲染 v-for自带key优化 需手动指定key
深度监听 内置deep:true支持 需手动拆解对象属性

3. 开发体验对比

开发场景 Vue优势 React优势
快速原型 更少的样板代码 更灵活的组件组合
复杂状态 自动化的响应式更新 更精确的状态控制
跨平台 更好的uni-app整合 更丰富的React Native生态
TypeScript 需要额外类型定义 一流的TS支持

四、Uniapp开发中的监听实践

1. Vue语法下的特殊处理

// 在uni-app中需要特别注意的watch用法
export default {
  watch: {
    // 监听全局变量变化
    '$store.state.token'(newVal) {
      uni.setStorageSync('token', newVal);
    },
    // 处理平台差异
    someValue: {
      handler(val) {
        // #ifdef H5
        console.log('H5特有处理');
        // #endif
      }
    }
  }
}

2. React语法下的注意事项

function UniAppComponent() {
  const [sysInfo, setSysInfo] = useState({});
  
  useEffect(() => {
    // 获取系统信息
    uni.getSystemInfo({
      success: res => setSysInfo(res)
    });
  }, []);

  // 处理平台特定逻辑
  useEffect(() => {
    // #ifdef MP-WEIXIN
    console.log('微信小程序特有逻辑');
    // #endif
  }, [sysInfo]);
}

SvelteKit 最新中文文档教程(21)—— 最佳实践之图片

作者 冴羽
2025年4月17日 20:37

前言

Svelte,一个语法简洁、入门容易,面向未来的前端框架。

从 Svelte 诞生之初,就备受开发者的喜爱,根据统计,从 2019 年到 2024 年,连续 6 年一直是开发者最感兴趣的前端框架 No.1

image.png

Svelte 以其独特的编译时优化机制著称,具有轻量级高性能易上手等特性,非常适合构建轻量级 Web 项目

为了帮助大家学习 Svelte,我同时搭建了 Svelte 最新的中文文档站点。

如果需要进阶学习,也可以入手我的小册《Svelte 开发指南》,语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!

欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”

图片

图片会对您的应用性能产生重大影响。为了获得最佳效果,您应该通过以下方式优化它们:

  • 生成最优格式如 .avif.webp
  • 为不同的屏幕创建不同尺寸的图片
  • 确保资源可以被有效缓存

手动完成这些工作很繁琐。根据您的需求和偏好,您可以使用多种技术。

Vite 的内置处理

Vite 会自动处理导入的资源以提升性能。这包括通过 CSS url() 函数引用的资源。文件名中会添加哈希值以便缓存,小于 assetsInlineLimit 的资源会被内联。Vite 的资源处理最常用于图片,但对视频、音频等也很有用。

<script>
  import logo from '$lib/assets/logo.png';
</script>

<img alt="项目标志" src={logo} />

@sveltejs/enhanced-img

@sveltejs/enhanced-img 是在 Vite 内置资源处理基础上提供的插件。它提供即插即用的图片处理功能,可以提供更小的文件格式如 avifwebp,自动设置图片的固有 widthheight 以避免布局偏移,为各种设备创建多种尺寸的图片,并出于隐私考虑去除 EXIF 数据。它可以在任何基于 Vite 的项目中使用,包括但不限于 SvelteKit 项目。

[!NOTE] 作为构建插件,@sveltejs/enhanced-img 只能在构建过程中优化位于您机器上的文件。如果您的图片位于其他位置(如从数据库、CMS 或后端服务的路径),请阅读从 CDN 动态加载图片

警告@sveltejs/enhanced-img 包是实验性的。它使用 1.0 之前的版本号,每个小版本发布可能会引入破坏性变更。

设置

安装:

npm install --save-dev @sveltejs/enhanced-img

调整 vite.config.js

import { sveltekit } from '@sveltejs/kit/vite';
+++import { enhancedImages } from '@sveltejs/enhanced-img';+++
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    +++enhancedImages(),+++
    sveltekit()
  ]
});

由于转换图片的计算开销,第一次构建会花费更长时间。但是,构建输出会被缓存在 ./node_modules/.cache/imagetools 中,因此后续的构建会很快。

基本用法

在你的 .svelte 组件中使用 <enhanced:img> 而不是<img>,并通过 Vite 资源导入 路径引用图片文件:

<enhanced:img src="./path/to/your/image.jpg" alt="An alt text" />

在构建时,您的 <enhanced:img> 标签将被 <img> 替换,并由 <picture> 包装,提供多种图片类型和尺寸。在不损失质量的情况下只能对图片进行缩小,这意味着你应该提供所需的最高分辨率图片——系统会为可能请求图片的各种设备类型生成较小的版本。

你应该为 HiDPI 显示器(又称视网膜显示器)提供 2x 分辨率的图片。<enhanced:img> 会自动负责向较小的设备提供较小版本的图片。

如果你想为 <enhanced:img> 添加样式,你应该添加一个 class 并针对它进行设置。

动态选择图像

您也可以手动导入图片资源并将其传递给 <enhanced:img>。当您有一组静态图片并想要动态选择或遍历它们时,这种方法很有用。在这种情况下,您需要同时更新 import 语句和 <img> 元素,如下所示,以表明您想要处理它们。

<script>
  import MyImage from './path/to/your/image.jpg?enhanced';
</script>

<enhanced:img src={MyImage} alt="some alt text" />

你也可以使用 Vite 的 import.meta.glob。请注意,你需要通过自定义查询来指定 enhanced

<script>
  const imageModules = import.meta.glob(
    '/path/to/assets/*.{avif,gif,heif,jpeg,jpg,png,tiff,webp,svg}',
    {
      eager: true,
      query: {
        enhanced: true
      }
    }
  )
</script>

{#each Object.entries(imageModules) as [_path, module]}
  <enhanced:img src={module.default} alt="some alt text" />
{/each}

固有尺寸

widthheight 是可选的,因为它们可以从源图像中推断出来,并且在预处理 <enhanced:img> 标签时会自动添加。有了这些属性,浏览器可以保留正确的空间,防止布局偏移

如果你想使用不同的 widthheight,你可以用 CSS 来设置图像样式。由于预处理器会为你添加 widthheight,如果你想要其中一个尺寸自动计算,那么你需要指定这一点:

<style>
  .hero-image img {
    width: var(--size);
    height: auto;
  }
</style>

srcsetsizes

如果你有一个大图像,比如占据设计宽度的主图,你应该指定 sizes,这样在较小的设备上就会请求较小版本的图像。例如,如果你有一个 1280px 的图像,你可能想要指定类似这样的内容:

<enhanced:img src="./image.png" sizes="min(1280px, 100vw)"/>

如果指定了 sizes<enhanced:img> 将为较小的设备生成小尺寸图片,并填充 srcset 属性。

自动生成的最小图片宽度为 540px。如果你需要更小的图片或想要指定自定义宽度,可以使用 w 查询参数:

<enhanced:img
  src="./image.png?w=1280;640;400"
  sizes="(min-width:1920px) 1280px, (min-width:1080px) 640px, (min-width:768px) 400px"
/>

如果未提供 sizes,则将生成一个 HiDPI/Retina 图像和一个标准分辨率图像。您提供的图像应该是您希望显示分辨率的 2 倍,以便浏览器可以在具有高设备像素比的设备上显示该图像。

每个图像的转换

默认情况下,增强的图像将被转换为更高效的格式。但是,你可能希望应用其他转换,如模糊、质量调整、扁平化或旋转操作。你可以通过附加查询字符串来执行每个图像的转换:

<enhanced:img src="./path/to/your/image.jpg?blur=15" alt="An alt text" />

查看 imagetools 仓库以获取完整的指令列表。.

从 CDN 动态加载图片

在某些情况下,图片在构建时可能无法访问 —— 例如,它们可能存储在内容管理系统或其他地方。

使用内容分发网络(CDN)可以让你动态优化这些图片,并在尺寸方面提供更多灵活性,但可能需要一些设置开销和使用成本。根据缓存策略,在从 CDN 收到 304 响应之前,浏览器可能无法使用资源的缓存副本。构建面向 CDN 的 HTML 允许使用 <img> 标签,因为 CDN 可以根据 User-Agent 头部提供适当的格式,而构建时优化必须生成带有多个源的 <picture> 标签。最后,某些 CDN 可能会延迟生成图片,这可能会对低流量且图片频繁更改的网站的性能产生负面影响。

CDN 通常可以直接使用,无需任何库。然而,有许多支持 Svelte 的库可以让使用变得更简单。@unpic/svelte 是一个支持大量提供商的与 CDN 无关的库。你可能还会发现一些特定的 CDN(如 Cloudinary)有 Svelte 支持。最后,一些支持 Svelte 的内容管理系统(CMS)(如 ContentfulStoryblokContentstack 都内置了图像处理支持。

最佳实践

  • 对于每种图片类型,使用上述讨论过的适当解决方案。你可以在一个项目中混合使用这三种解决方案。例如,你可以使用 Vite 的内置处理来为 <meta> 标签提供图片,使用 @sveltejs/enhanced-img 在主页上显示图片,并使用动态方法显示用户提交的内容。
  • 考虑通过 CDN 提供所有图片服务,无论你使用何种图片优化类型。CDN 通过在全球分发静态资源副本来减少延迟。
  • 原始图片应具有良好的质量/分辨率,并且宽度应该是显示宽度的 2 倍,以便支持 HiDPI 设备。图片处理可以将图片尺寸缩小以在服务较小屏幕时节省带宽,但为了放大图片而创造像素会浪费带宽。
  • 对于远大于移动设备宽度(大约400px)的图片,例如占据页面设计宽度的主图,指定 sizes 以便在较小设备上提供较小的图片。
  • 对于重要图片,例如最大内容绘制(LCP)图片,设置 fetchpriority="high" loading="eager" 以尽早优先加载。
  • 为图片提供容器或样式,使其受到约束,不会在页面加载时跳动影响累积布局偏移(CLS)widthheight 帮助浏览器在图片仍在加载时预留空间,因此 @sveltejs/enhanced-img 将为你添加 widthheight
  • 始终提供良好的 alt 文本。如果你没有这样做,Svelte编译器会发出警告。
  • 不要在 sizes 中使用 emrem 并更改这些度量的默认大小。当在 sizes@media 查询中使用时,emrem 都被定义为用户的默认 font-size。对于像 sizes="(min-width: 768px) min(100vw, 108rem), 64rem" 这样的 sizes 声明,控制图片在页面上布局方式的实际 emrem 如果被 CSS 更改可能会有所不同。例如,不要做类似 html { font-size: 62.5%; } 这样的事情,因为浏览器预加载器预留的空间现在会比创建后的 CSS 对象模型的实际空间更大。

Svelte 中文文档

点击查看中文文档:SvelteKit 图片

系统学习 Svelte,欢迎入手小册《Svelte 开发指南》。语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!

此外我还写过 JavaScript 系列TypeScript 系列React 系列Next.js 系列冴羽答读者问等 14 个系列文章, 全系列文章目录:github.com/mqyqingfeng…

欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”

Flex布局完全指南,Flexbox 在线演示工具推送

作者 序猿杂谈
2025年4月17日 19:58

概述

@See developer.mozilla.org/zh-CN/docs/…

网页布局(layout)是 CSS 的一个重点应用。

flex_01.gif

传统的 CSS 布局方案主要基于盒模型,通过以下三大属性实现:

  1. display - 控制元素显示类型
  2. position - 元素定位方式
  3. float - 浮动布局

这种方案存在明显局限:

  • 实现特殊布局(如垂直居中)非常繁琐
  • 需要大量 hack 和额外标记
  • 布局代码难以维护

特别是对于常见的垂直居中需求,传统方法往往需要复杂的嵌套结构或精确计算,不够直观高效。

2009年,W3C 提出了一种新的方案 —— Flex 布局,它通过简单的属性设置就能实现复杂的页面布局。

相比传统基于display + position + float的盒模型布局,Flex 布局特别适合实现响应式设计,能轻松解决传统布局中棘手的垂直居中等问题,随着浏览器全面支持,现已成为主流的布局方案。

提示:所有现代浏览器均已支持 Flex 布局,旧版Webkit需加-webkit-前缀,参考 这里 >>

Flex 布局是什么?

Flexible Box 模型,通常被称为 flexbox,是一种一维的布局模型。它给 flexbox 的子元素之间提供了强大的空间分布和对齐能力。

任何一个容器都可以指定为 Flex 布局。

.box {
  display: flex;
}

基本概念

使用 Flex 布局的元素称为 Flex 容器(flex container),其直接子元素自动成为 Flex 项目(flex item)

flex_concept.png

容器默认包含两根轴:

  • 主轴(main axis):水平方向,起点为 main start,终点为 main end

  • 交叉轴(cross axis):垂直方向,起点为 cross start,终点为 cross end

项目默认沿主轴排列,占据的主轴空间称为 main size,交叉轴空间称为 cross size

容器的属性

  • flex-direction:控制子元素的排列方向

    • row(默认):水平排列,从左到右
    • row-reverse:水平排列,从右到左
    • column:垂直排列,从上到下
    • column-reverse:垂直排列,从下到上
  • flex-wrap:控制是否换行

    • nowrap(默认):不换行(可能溢出)
    • wrap:换行,新行向下排列
    • wrap-reverse:换行,新行向上排列
  • flex-flowflex-directionflex-wrap 的简写
    例:flex-flow: row wrap

  • justify-content:主轴对齐方式

    • flex-start(默认):左对齐
    • flex-end:右对齐
    • center:居中
    • space-between:两端对齐,子元素间距相等
    • space-around:子元素两侧间距相等(间距=边框间距×2)
  • align-items:交叉轴对齐方式

    • stretch(默认):拉伸填满容器高度
    • flex-start:顶部对齐
    • flex-end:底部对齐
    • center:垂直居中
    • baseline:按第一行文字基线对齐
  • align-content:多行子元素的对齐(单行无效)

    • stretch(默认):拉伸填满交叉轴
    • flex-start:顶部对齐
    • flex-end:底部对齐
    • center:垂直居中
    • space-between:两端对齐,行间距相等
    • space-around:行两侧间距相等

元素的属性

  • order:设置子元素的排列顺序。数值越小,越靠前显示,默认是 0。

  • flex-grow:定义子元素在容器有多余空间时的放大比例。默认是 0,表示不放大;设置为 1 则表示可以占据多余空间。

  • flex-shrink:定义子元素在空间不足时的缩小比例。默认是 1,表示可以缩小。如果设为 0,则不会缩小。

  • flex-basis:指定子元素在分配空间之前占据的主轴空间(即它的初始宽度或高度)。默认是 auto,表示根据内容自动计算大小。

  • flex:是 flex-grow、flex-shrink 和 flex-basis 的简写形式。默认值是 0 1 auto,即不放大、可以缩小、大小由内容决定。

  • align-self:允许单个子元素设置与其他子元素不同的对齐方式,用于覆盖父容器的 align-items 设置。默认值是 auto,表示继承父级的对齐方式。如果没有继承,则表现为 stretch(拉伸填满)。

    可选值:

    • auto:继承父元素的对齐方式(align-items 的值)。

    • flex-start:顶部(或主轴起始)对齐。

    • flex-end:底部(或主轴结束)对齐。

    • baseline:根据文本的基线对齐。

    • stretch:如果没有固定高度,会自动拉伸填满容器高度(默认行为)。

布局演示工具

工具地址:lihongyao.github.io/flexbox-pla…

flex-playground.jpg

为了方便大家掌握 Flex 布局 特性,我写了一个 Flexbox 布局演示工具,点击 这里 >> 前往体验。

如果帮到了大家,还请帮我点个 Star!感谢 !

开发技巧

在开发中,我们经常会遇到如下布局:

flex-layouts.png

布局代码如下:

.wrap {
  /* 核心代码 */
  display: flex;
  flex-flow: row wrap;
  align-content: flex-start;
}
.item {
  height: 50px;
  display: flex;
  justify-content: center;
  align-items: center;
  background: cornflowerblue;
  color: #fff;
  font-weight: bold;
  border-radius: 6px;
  /* 核心代码 */
  flex: 0 0 calc((100% - 3 * 10px) / 4);
}
.item:not(:nth-child(4n)) {
  margin-right: 10px;
}
.item:not(:nth-last-child(-n + 4)) {
  margin-bottom: 10px;
}
<div class="wrap">
  <section class="item">1</section>
  <section class="item">2</section>
  <section class="item">3</section>
  <section class="item">4</section>
  <section class="item">5</section>
  <section class="item">6</section>
  <section class="item">7</section>
  <section class="item">8</section>
  <section class="item">9</section>
  <section class="item">10</section>
  <section class="item">11</section>
  <section class="item">12</section>
  <section class="item">13</section>
  <section class="item">14</section>
  <section class="item">15</section>
  <section class="item">16</section>
  <section class="item">17</section>
  <section class="item">18</section>
  <section class="item">19</section>
  <section class="item">20</section>
  <section class="item">21</section>
  <section class="item">22</section>
</div>

Flex布局的优化方案

  1. 使用 margin:auto 替代对齐属性

    在Flex容器中,通过给子元素设置 margin:auto 可以更简洁地实现水平和垂直居中效果,相比 justify-contentalign-items 的组合更灵活。例如:

    .item { margin: auto; } /* 自动占据剩余空间实现居中 */
    
  2. 合理使用 flex 简写属性

    flex: 1 等价于 flex: 1 1 0%,能高效控制伸缩比例和初始大小。推荐优先使用简写形式优化代码。

  3. 响应式布局优化

    结合 @media 查询动态调整 flex-directionflex-wrap,例如在小屏幕切换为 flex-direction: column

  4. 避免过度伸缩

    对固定尺寸元素设置 flex: noneflex-shrink: 0 防止意外压缩,如图片容器:

    .image-box { flex-shrink: 0; }
    
  5. 利用 align-self 精细化控制

    单独覆盖容器对齐设置,如让某个元素底部对齐:

    .special-item { align-self: flex-end; }
    
  6. 性能优化

    复杂布局中避免嵌套过多Flex容器,减少浏览器重排计算

Sciter.js 新手指南-GUI开发中的窗口使用指南

作者 凯哥1970
2025年4月17日 18:42

本文档旨在帮助前端开发者或从其他语言转向 Sciter.js 的开发者快速了解和使用 Sciter 窗口。

什么是 Sciter 窗口?

在 Sciter.js 中,Window 类的实例代表一个桌面窗口。每个加载了 HTML 文档的窗口都有一个 Window.this 引用,指向当前的窗口对象。

注意: 文档中提到的 window 指的是 Sciter 的 Window 类实例(例如 Window.this),而不是浏览器中的 window 对象。

窗口类型 (type)

创建窗口时,可以通过 type 参数指定窗口的类型,不同的类型适用于不同的场景:

  • Window.FRAME_WINDOW: 默认的标准窗口类型,通常带有标题栏和边框,适合主界面(默认类型)。
  • Window.POPUP_WINDOW: 弹出式窗口,通常用于临时信息显示、菜单、提示等临时性界面等,没有标准的窗口装饰。
  • Window.TOOL_WINDOW: 工具窗口,类似于弹出窗口,但在某些操作系统上可能有不同的行为(例如,不显示在任务栏),适合浮动工具条、调色板等。
  • Window.CHILD_WINDOW: 子窗口,嵌入在父窗口的客户区内,依附于父窗口。。
  • Window.DIALOG_WINDOW: 对话框窗口,通常用于模态交互。
// 创建一个弹出式窗口
let toastWindow = new Window({
  type: Window.POPUP_WINDOW,
  url: __DIR__ + "toast.htm", // 加载内容的 HTML 文件
  width: 200,
  height: 100
});

ps: 非常有用的一个属性,就是窗口框架类型 (frameType) - 创建无边框窗口,比如创建一个 360 之类的软件,我们想完全定制自己的头部的风格,这时就要使用这个方式了。

window.frameType 属性允许你控制窗口的外观,包括创建无边框或自定义形状的窗口,稍后设置窗口为无边框带阴影 如 window.frameType = "solid-with-shadow";

注意: 无边框窗口通常需要你在 HTML/CSS 中自行实现窗口的拖动和关闭等交互。 所以当你创建了无边框窗口 (frameType 不是 "standard"),你需要自己处理窗口的拖动(通常通过在特定元素上监听 mousedown 并调用 Window.this.move())和关闭逻辑。

窗口创建 (new Window)

创建新窗口的基本语法是:

let newWindow = new Window({ params: object });

params 对象可以包含多个属性来配置窗口,常用属性包括:

  • type: 窗口类型 (见上文)。
  • url: 要加载的 HTML 文件的路径或 URL。
  • width, height: 窗口的初始宽度和高度(单位:屏幕像素)。
  • x, y: 窗口的初始位置(单位:屏幕像素)。
  • caption: 窗口标题。
  • state: 初始窗口状态 (见下文)。
  • parent: 父窗口实例,如果设置,子窗口会随父窗口最小化或关闭。
  • parameters: 传递给新窗口的数据,可以在新窗口中通过 Window.this.parameters 访问。
// 创建新窗口
var win = new Window({
  url: "path/to/file.htm",  // 窗口内容 HTML 文件
  width: 800,               // 窗口宽度
  height: 600,              // 窗口高度
  alignment: 5,             // 5 表示在父窗口或屏幕中央对齐
  parameters: {...}         // 传递给窗口的参数
});

窗口状态管理(window.state)

你可以读取或设置窗口的 state 属性来控制其显示状态:

  • Window.WINDOW_SHOWN: 正常显示(默认)。
  • Window.WINDOW_MINIMIZED: 最小化到任务栏或托盘。
  • Window.WINDOW_MAXIMIZED: 最大化。
  • Window.WINDOW_HIDDEN: 隐藏窗口。
  • Window.WINDOW_FULL_SCREEN: 全屏显示。
  • Window.WINDOW_SHOWN_NA (仅写): 显示窗口但不激活它(不获取焦点)。
// 获取当前窗口状态
let currentState = Window.this.state;

// 最小化当前窗口
Window.this.state = Window.WINDOW_MINIMIZED;

// 切换窗口显示/隐藏状态
function toggleWindow() {
  if (Window.this.state === Window.WINDOW_HIDDEN) {
    Window.this.state = Window.WINDOW_SHOWN;
  } else {
    Window.this.state = Window.WINDOW_HIDDEN;
  }
}

窗口控制

Window 对象提供了多种方法来控制窗口的位置、大小和行为:

  • 移动和调整大小:
    • window.move(x, y [, width, height [, "client"]]): 移动窗口并可选地调整大小(坐标单位:物理像素 PPX)。如果提供 "client",则坐标相对于客户区。
    • window.moveTo(monitor, x, y [, width, height [, "client"]]): 移动到指定显示器(坐标单位:设备无关像素 DIPs/CSS 像素)。
  • 获取窗口几何信息:
    • window.box(boxPart, boxOf, relTo): 获取窗口不同部分(边框、客户区、光标等)相对于不同参考点(桌面、显示器、自身)的几何信息(位置、尺寸)。
  • 激活与关闭:
    • window.activate(bringToFront: boolean): 将输入焦点设置到窗口,true 表示同时将其带到最前。
    • window.close([value]): 请求关闭窗口。对于模态对话框,可以传递一个返回值。
  • 更新布局:
    • window.update(): 请求重新计算窗口内元素的布局。
  • 模态对话框:
    • window.modal(JSX | {params} | new Window(...)): 显示模态对话框。可以是预定义的 JSX(如 <alert>),创建新窗口的参数对象,或一个已创建的 Window 实例。该方法会阻塞直到模态窗口关闭,并返回其关闭值。
// 将窗口移动到屏幕 (100, 100) 的位置
Window.this.move(100, 100);

// 获取窗口客户区的宽度和高度 (CSS 像素)
let [w, h] = Window.this.box("dimension", "client", "self", false);

// 显示一个简单的警告框
Window.this.modal(<alert>操作成功!</alert>);

// 关闭当前窗口
document.on("click", "#close-button", () => {
  Window.this.close();
});

窗口之间的通信

Sciter 提供了几种机制进行窗口间或窗口与原生代码的交互:

  • Window.share: 一个在应用程序所有窗口和文档间共享的全局 JavaScript 对象。你可以用它来存储共享状态或数据。
    // 在窗口 A 中设置共享数据
    Window.share.currentUser = { name: "Alice" };
    
    // 在窗口 B 中读取共享数据
    console.log(Window.share.currentUser.name); // 输出 "Alice"
    
    注意: 使用 Window.share 时要小心,确保在窗口关闭前(例如在 unload 事件中)清理不再需要的数据,避免内存泄漏。
  • 参数传递:新建窗口时通过 parameters 字段传递,子窗口可通过 Window.this.parameters 读取。
  • window.xcall(name, ...args): 调用附加到窗口的原生行为 (Native Behavior) 中定义的函数。这是 Sciter 脚本与原生代码交互的主要方式。
  • 事件通信
    • Window.post(event) 向所有窗口广播事件(异步)。
    • Window.send(event) 向所有窗口同步发送事件,遇到第一个消费事件的窗口即停止。
    • window.dispatchEvent(event) 向本窗口同步分发事件。
    • window.postEvent(event) 向本窗口异步分发事件。

方式1, 使用事件通信

// 父窗口向子窗口传递数据
this.postEvent(
    new Event('set-style-dynamic', {
        bubbles: true,
        data: { prop, val }
    })
);

// 其它窗口或组件接收数据
["on set-style-dynamic"](evt){
    const { prop, val } = evt.data;
    const input = this.$(`input:not([index])[prop=${prop}]`);
    if (!input) return;
    input.value = false;
}


方式2, 使用传参

// 父窗口打开子窗口并传参
let child = new Window({
  url: "child.htm",
  parameters: { foo: 123 }
});
// 子窗口读取参数
console.log(Window.this.parameters.foo);

与原生代码交互

window.xcall("functionName", arg1, arg2); // 调用原生行为中的函数

系统交互

  • 文件/文件夹选择对话框:
    • window.selectFile({mode, filter, ...}): 打开系统文件选择对话框。
    • window.selectFolder({caption, path}): 打开系统文件夹选择对话框。
  • 系统托盘图标:
    • window.trayIcon({image, text}): 显示系统托盘图标。
    • window.trayIcon("remove"): 移除托盘图标。
    • window.trayIcon("place"): 获取托盘图标位置。
  • 热键:
    • window.addHotKeyHandler(keyCombination, callback): 注册全局系统热键。
    • window.removeHotKeyHandler(id): 移除热键。
// 注册 Ctrl+F8 热键
try {
  const hotkeyId = Window.this.addHotKeyHandler("Control+F8", () => {
    Window.this.modal(<alert>全局热键 Control+F8 被按下!</alert>);
  });
  // 保存 hotkeyId 以便之后移除
} catch (e) {
  console.error("注册热键失败: ", e);
}

常用事件

  • 窗口关闭拦截

    • 监听 closerequest,可阻止关闭或自定义行为。
    • 示例:
      Window.this.on("closerequest", event => {
        if(event.reason == 0) { // 用户点击关闭按钮
          Window.this.state = Window.WINDOW_MINIMIZED;
          event.preventDefault();
        }
      });
      
      
  • 窗口类型与样式

    • 通过 window-frame 属性或构造参数 type 指定类型,配合 CSS 自定义窗口外观。
    • 示例见 samples.sciter/window/chrome-types/
  • 模态对话框

    • window.modal(<alert>内容</alert>) 弹出消息框
    • window.modal({ ... }) 打开模态窗口
  • 托盘图标与通知

    • window.trayIcon({image, text}) 设置托盘图标
    • 监听 trayiconclick 响应点击
  • 多窗口管理

    • Window.all 获取所有窗口实例
    • Window.share 跨窗口共享数据

最佳实践

  1. 主窗口建议使用 FRAME_WINDOW,弹窗/工具条用 POPUP/TOOL_WINDOW。
  2. 窗口通信优先用事件机制,避免直接操作其它窗口实例。
  3. 处理窗口关闭事件: 使用 window.on("closerequest", ...) 事件可以在用户尝试关闭窗口时执行清理操作或阻止关闭(例如,提示保存未保存的更改)。
  4. 参数传递建议用 parameters 字段,避免全局变量污染。
  5. 模块化设计 :将窗口逻辑、UI 和业务逻辑分离,参考 SDK 文档 samples.app/classic 示例
  6. 使用 Reactor 组件 :对于复杂 UI,使用 Reactor 组件化开发
  7. 窗口生命周期管理 :妥善处理窗口的创建和销毁,避免资源泄漏
  8. 事件委托 :使用事件委托模式处理 UI 事件,提高性能
  9. 响应式设计 :使用 CSS 流布局和弹性布局,适应不同屏幕尺寸 可以参考 sdk/samples.app 和 sdk/samples.sciter/window 目录下的示例获取更多实践经验。

React Native踩坑记录之——屏幕适配

作者 乐影
2025年4月17日 18:14

写在前面

前端开发一定躲不开的一个话题就是屏幕适配,主播最近在写React Native(以下简称RN)的时候感觉写样式很不方便,特别是RN的尺寸是无单位的,也不能使用vw、vh这种视口单位,无法满足在不同屏幕上达到一致的显示效果的需求,话不多说进入正题

解决方案

既然RN中不存在vw、vh,那我们就自己写一个,RN提供了一个Dimension API可以获取到屏幕的宽高

const screenWidth = Dimensions.get('window').width;
function vw(size: number) {
    const scale = screenWidth / 100;
    return PixelRatio.roundToNearestPixel(size * scale);
}

// 使用起来也很简单
StyleSheet.create({
  box: {
    width: vw(100),
    height: vw(100),
    borderRadius: vw(50),
  }
});

类似的我们还可以做设计稿尺寸的转换

const screenWidth = Dimensions.get('window').width;
const DESIGN_WIDTH = 414;
export function vw(designSize: number) {
    const scale = screenWidth / DESIGN_WIDTH;
    return PixelRatio.roundToNearestPixel(designSize * scale);
}

但是当样式多起来总感觉这样写起来不够简便,于是我决定换个方法,直接在创建样式的时候进行尺寸的转换

type NamedStyles<T> = { [P in keyof T]: ViewStyle | TextStyle | ImageStyle };

// 提取样式转换逻辑到单独的函数
function transformStyleValue(style: any) {
    for (const prop in style) {
        const value = style[prop];
        if (typeof value === 'number') {
            style[prop] = vw(value);
        }
        // number + vw 写法支持
        if (typeof value === 'string' && value.includes('vw')) {
            style[prop] = vw(parseFloat(value.replace('vw', '')));
        }
    }
    return style;
}

export const createStyle = <T extends NamedStyles<T> | NamedStyles<any>>(styles: T & NamedStyles<any>, options: CreateStyleOptions = DEFAULT_OPTIONS): T => {
    // ...

    const transformedStyles = { ...styles } as T;
    for (const key in transformedStyles) {
        const style = transformedStyles[key] as any;
        transformedStyles[key] = transformStyleValue(style);
    }

    return StyleSheet.create(transformedStyles);
};

但是这样会有个问题,如果我不希望某个属性响应式,比如字体大小太大或太小会影响视觉体验,我们可以在原函数做出一点点的改变,创建样式表时加入一些配置

type NamedStyles<T> = { [P in keyof T]: ViewStyle | TextStyle | ImageStyle };

// 定义 options 参数的类型
type CreateStyleOptions = {
    exclude: string[];
};

const DEFAULT_OPTIONS: CreateStyleOptions = { exclude: ['fontSize'] };

// 提取样式转换逻辑到单独的函数
function transformStyleValue(style: any, options: CreateStyleOptions) {
    const { exclude } = options;
    for (const prop in style) {
        // 属性排除
        if (exclude.includes(prop)) {
            continue;
        }
        const value = style[prop];
        if (typeof value === 'number') {
            style[prop] = vw(value);
        }
        if (typeof value === 'string' && value.includes('vw')) {
            style[prop] = vw(parseFloat(value.replace('vw', '')));
        }
    }
    return style;
}

export const createStyle = <T extends NamedStyles<T> | NamedStyles<any>>(styles: T & NamedStyles<any>, options: CreateStyleOptions = DEFAULT_OPTIONS): T => {
    // ...

    const transformedStyles = { ...styles } as T;
    for (const key in transformedStyles) {
        const style = transformedStyles[key] as any;
        transformedStyles[key] = transformStyleValue(style, options);
    }
    return StyleSheet.create(transformedStyles);
};

总结

通过上述方案,实现了 RN 的屏幕适配,既保证了大部分元素能够根据屏幕尺寸灵活调整,又避免了关键属性因过度转换而影响用户体验。在实际项目中,可根据具体需求调整设计稿宽度和排除属性列表,以达到最佳的适配效果

如何有更好的方案也欢迎提出来

🤯 Vue 人快上车!用 useContext 实现 Vuex 同款全局状态管理!

作者 JiangJiang
2025年4月17日 18:03

作为一个 Vue 转 React 的前端,我们早就习惯了使用 Vuex 来管理全局状态,什么 state、mutation、action 一套操作下来玩得贼熟练。但在 React 中,我们没有 Vuex 了,要怎样去进行状态管理呢?

🧠 React 的状态管理多麻烦?

如果你刚开始写 React,可能会遇到这些情况:

  • props 一层层传,像接力赛;
  • 兄弟组件想用一个变量,得倒腾 lifting state;

“React 没 Vuex/Pinia,那它是怎么搞全局状态的?”

答案就是:

  • 中小型项目:useContext
  • 中大型项目:Zustand / Redux

其实 React 内建的 useContext 就是天然的状态传递通道,它虽不是完整状态管理方案,但在很多中小项目中已经够用。


🧩 什么是 useContext?

一句话:React 的 useContext 就是你熟悉的 Vue 的 inject + provide

"useContext 允许父组件向其下层无论多深的任何组件提供信息,而无需通过 props 显式传递。"

有点像 Vue 的 inject 吧?那我们直接上个栗子,看下怎么用。

一看就懂的栗子:

import { useState, createContext, useContext } from "react";

const UserContext = createContext(null);

export default function Parent() {
  const [user, setUser] = useState("JiangJiang");

  return (
    <UserContext.Provider value={{ user, setUser }}>
      <Child />
    </UserContext.Provider>
  );
}

function Child() {
  const { user } = useContext(UserContext);

  return (
    <div>
      <p>User: {user}</p>
      <Button>Change User</Button>
    </div>
  );
}

function Button({ children }) {
  const { setUser } = useContext(UserContext);
  return <button onClick={() => setUser("帅比-JiangJiang")}>{children}</button>;
}

在使用 useContext 之前,我们需要先使用 createContext 去创建一个 context

const UserContext = createContext(null);

createContext 只有一个参数,表示默认值,可以传入任何类型的参数。

上面例子的父子组件顺序为:Parent => Child => Button

Button 想要修改 user,通过 useContext 去获取 Parent 组件中的 setUser 方法,我们看看 useContext 的用法:

  • useContext 传入参数是数据所在的 context
  • 通过 xxxContext.Provider 向下树状传递数据,数据传入的 keyvalue
  • 树状结构下的子组件可以通过:useContext(xxxContext) 去解构使用 value 的值。

ainimation1.gif

简单吧?没有复杂的注册、模块化、mutation,不依赖第三方库,靠原生 API 就能共享状态!

⚔️ useContext 和 Vuex 差异

对比点 useContext + Hooks Vuex
使用成本 极低,React 原生支持 较高,需要额外引入
状态更新 useStateuseReducer mutation / action
模块化支持 需手动管理多个 context 官方支持模块化
适用场景 中小项目,状态结构简单 中大型项目,复杂状态交互

对于中小项目,useContext 是完美替代 Vuex 的方案,大项目再考虑更重型的方案。

如果你是 Vue 转 React 的开发者,一定能体会到它那份“轻量又优雅”。

💡 想要更像 Vuex?useContext + useReducer

还记得 Vuex 怎么管理用户状态的吧?有个 state 里存着用户信息,有个 mutationaction 控制 登录/登出

React 也可以用 useReducer 来实现类似的逻辑:把 登录/登出 变成一个个 action,统一管理!

1. 定义 UserContext 和 reducer

import { useReducer, createContext, useContext } from 'react'

const UserContext = createContext(null)

// reducer 逻辑
function handleUser(state, action) {
    switch (action.type) {
        case 'LOGIN':
            return {
                isLogin: true,
                userInfo: action.payload
            }
        case 'LOGOUT':
            return {
                isLogin: false,
                userInfo: null
            }
        default:
            return state
    }
}

// Provider 包装组件
export const UserProvider = ({ children }) => {
    const [state, dispatch] = useReducer(handleUser, {
        isLogin: false,
        userInfo: null
    })

    return <UserContext.Provider value={{ state, dispatch }}>{children}</UserContext.Provider>
}

// 自定义 Hook,方便使用
export const useUser = () => useContext(UserContext);
  • 定义 handleUser 函数控制 登录/登出 数据。
  • 通过 useReducer 创建一个 reducer
  • 创建一个 context 存储 statedispatch 方法。
  • 封装 Provider 包装组件。
  • 创建自定义 Hook useUser,方面调用。

2. 在顶层 App 包裹 Provider,组件中使用 UserContext

export default function App() {
  return (
    <UserProvider>
      <Home />
    </UserProvider>
  );
}

function Home() {
  return (
    <div>
      <h2>欢迎来到首页</h2>
      <Profile />
    </div>
  );
}

function Profile() {
  const { state, dispatch } = useUser();
  const handleLogin = () => {
    const fakeUser = "帅比-JiangJiang";
    dispatch({ type: "LOGIN", payload: fakeUser });
  };

  const handleLogout = () => {
    dispatch({ type: "LOGOUT" });
  };
  return (
    <>
      {state.isLogin ? (
        <>
          <p>你好,{state.userInfo}</p>
          <button onClick={() => dispatch({ type: "LOGOUT" })}>退出登录</button>
        </>
      ) : (
        <>
          <p>你还未登录</p>
          <button onClick={handleLogin}>登录</button>
        </>
      )}
    </>
  );
}

补充完组件逻辑,我们的简单 登录/登出 功能就完成了,我们看下效果:

ainimation2.gif


我们来类比一下 Vuex:

Vuex useReducer + useContext
state useReducerstate
mutation / action dispatch({ type, payload })
mapState / mapActions 自定义 Hook useUser
Vue 的 <App> 根组件里挂载 Store <UserProvider>

同样的登录/登出功能,逻辑一模一样,只是换了个写法。


useContext 这么好用,我们是不是可以无脑的去进行使用呢?

🐢 useContext 的全量更新问题

假设你写了一个像下面这样的 Provider

<UserContext.Provider value={{ state, dispatch }}>
  {children}
</UserContext.Provider>

每当 statedispatch 中的任意一个变化,所有使用 useContext(UserContext) 的组件,都会重新渲染,哪怕组件只用了 userInfo,也会被牵连重渲染。

这就像 Vue 中你修改了一个 state,结果所有用 mapState 的组件都被刷新了,不分青红皂白。

😩 为什么会这样?

因为 useContext 的机制是:只要 Providervalue 发生了引用变化,所有消费者组件就会重新渲染。

“React 的 context 更新是按引用判断,而不是按值比较!”

// 每次渲染都会生成一个新对象(即使内容一样)
const obj = { a: 1 }
const obj2 = { a: 1 }

obj === obj2 // false(尽管内容相同)

同一个 context,用到的组件越多,性能损耗就会越大,比如下面场景:

<App>
  <Header />
  <Sidebar />
  <Profile />
  <Notifications />
</App>

这些组件都通过 useContext(UserContext) 拿到了状态,一旦我们在某个组件里改了 state 的值,就会连带着别的组件全部被重新渲染一次。

🛠️ 优化方案

1. 拆分 context

const UserStateContext = createContext();
const UserDispatchContext = createContext();

export const useUserState = () => useContext(UserStateContext);
export const useUserDispatch = () => useContext(UserDispatchContext);

然后在 Provider 中拆开:

<UserStateContext.Provider value={state}>
  <UserDispatchContext.Provider value={dispatch}>
    {children}
  </UserDispatchContext.Provider>
</UserStateContext.Provider>

👉 好处:只用 state 的组件,不会因 dispatch 变化而重新渲染。

2. value 用 useMemo 缓存

如果你传递的是一个对象,可以用 useMemo 优化:

const contextValue = useMemo(() => ({ state, dispatch }), [state, dispatch]);

<UserContext.Provider value={contextValue}>
  {children}
</UserContext.Provider>

但注意:如果 state 是个对象,任何字段变化都会导致整个对象变新引用。所以这个方案适合中小型 state,不适合超大的嵌套结构。

3. 使用第三方库进行状态管理

React 社区已经有很多更轻便又更高性能的状态管理库,比如:

  • zustand:极简、无 Provider、按需订阅
  • jotai:原子化思维,每个 state 都是一个最小单元
  • valtio:像用 Vue 的响应式一样用 state

中大型复杂项目比较推荐引入这些库。


✅ 总结一下

如果你是 Vue 转 React 的前端,那 useContext 使用起来是真香:

  • 🚀 快速搞定跨组件状态共享
  • 🧩 和 Vuex 类似,结构灵活
  • 💡 搭配 useReducer / useMemo,组合拳更强大
  • 🧘‍♀️ 轻量级、零依赖,写起来丝滑舒服

小项目别犹豫,大项目也能当基础设施用,useContext 真香!

希望这篇文章能帮你快速掌握 useContext,如果你觉得有帮助,别忘了点个赞👍或关注我后续的 重学 React 系列!

await-to-js 源码解读

作者 岭子笑笑
2025年4月17日 17:52

1. 前言

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。 **这是源码共读的第21期,链接:juejin.cn/post/708310…

2. await-to-js库了解

通过await-to-js库,就可以在使用async 和await的时候轻松处理错误信息

如下示例

import to from 'await-to-js';
async function test() {
  // 1. try...catch捕获错误
  try{
    const user = await UserModel.findById(1);
  }catch(e) {
    return cb('No user found');
  }

  // 2使用Promise.catch捕获错误
  const res = await UserModel.findById(1).catch((e) => {
    return cb('No user found');
  });


  // 3. 使用to函数捕获错误
  const [err, user] = await to(UserModel.findById(1));
  if(!user) return cb('No user found');
}

3. 源码解析

源码就封装了一个to方法, 统一处理 Promise 的成功和失败情况

  • 如果 Promise 成功解析(resolved) ,返回 [null, data],其中 data 是解析的值。
  • 如果 Promise 被拒绝(rejected) ,返回 [error, undefined],其中 error 是捕获的错误

这样我们就方便获取err啦~

/**
 * @param { Promise } promise 一个promise传参
 * @param { Object= } errorExt - Additional Information you can pass to the err object  用户传入的额外错误提示
 * @return { Promise }
 */
export function to<T, U = Error> (
  promise: Promise<T>,
  errorExt?: object
): Promise<[U, undefined] | [null, T]> {
  return promise
    .then<[null, T]>((data: T) => [null, data])  // resolved后,err就为null返回
    .catch<[U, undefined]>((err: U) => {
      if (errorExt) {
        const parsedError = Object.assign({}, err, errorExt); // 合并错误信息errorExt
        return [parsedError, undefined]; // 返回[parsedError, undefined]
      }

      return [err, undefined]; // 直接返回[err, undefined]
    });
}

export default to;

4. 总结

虽然是小小的一个封装,但是用处极大

React:渲染帧和事件循环是什么关系

作者 火星思想
2025年4月17日 17:48

React为减缓浏览器单帧渲染压力做了时间切片处理,然后通过宏任务进行调度,其底层基于浏览器的渲染事件循环的原理。浏览器中的渲染帧事件循环是紧密协作但职责分明的两个核心机制,它们的关系可以通过以下要点清晰呈现:


1. 层级关系

事件循环(Event Loop)
├─ 宏任务队列(Task Queue)
├─ 微任务队列(Microtask Queue)
└─ 渲染帧(Frame)
   ├─ 样式计算(Style)
   ├─ 布局(Layout)
   └─ 绘制(Paint)

2. 协作流程

react/packages/scheduler/src/forks/Scheduler.js 中体现的协作逻辑:

function performWorkUntilDeadline() {
  // 阶段1:执行宏任务(如React调度任务)
  flushWork(); 
  
  // 阶段2:清空微任务队列(浏览器内部行为)
  
  // 阶段3:执行渲染管线(浏览器内部行为)
  if(needsPaint) requestPaint(); 
}

标准事件循环迭代

  1. 从宏任务队列取出一个任务执行
  2. 执行所有微任务(直到队列清空)
  3. 执行渲染管线(如果需要)
  4. 进入下一轮循环

3. 关键差异

特性 事件循环 渲染帧
触发频率 持续运行 按屏幕刷新率触发(如60Hz)
核心职责 任务调度与执行 视觉更新计算
可中断性 不可中断(必须执行到微任务清空) 可跳过部分阶段
性能影响 长任务导致事件循环卡顿 复杂样式导致渲染耗时

4. React的协调策略

React调度器通过时间切片主动适配两者:

function workLoop() {
  while (task && !shouldYield()) {
    process(task); // 每次执行不超过5ms
  }
  // 主动让出控制权给浏览器
  if (hasMoreWork) schedulePerformWorkUntilDeadline();
}

这样设计使得:

  1. 事件循环不会被长时间阻塞
  2. 渲染帧能获得稳定的执行机会
  3. 高优先级更新能及时反映到视图

5. 实际案例对比

原生事件循环:

// 案例1:阻塞型微任务
Promise.resolve().then(() => {
  heavyTask(50ms); // 直接导致3帧丢失
});

React调度:

// 案例2:时间切片
function task() {
  chunkedWork(5ms); // 分块执行
  if (remainingWork) {
    scheduleTask(task); // 下一宏任务继续
  }
}

总结关系图

[事件循环] ← 控制权交替 → [渲染帧]
   ↑                       ↑
   │                       │
宏任务/微任务           样式/布局/绘制
   │                       │
   └─── React调度器协调 ───┘

这种设计就像双人接力赛跑

  • 事件循环是持棒的奔跑者(必须持续前进)
  • 渲染帧是交棒检查点(按固定节奏接棒)
  • React调度器是教练(控制交接时机和节奏)

问题

问:如果React代码中有比执行较长时间的微任务呢?也能保持流畅吗?

答:当然是否定。因为React对宏任务做了时间切片,微任务在渲染之前执行,如果时间过长,仍然会阻塞渲染。

ag-grid 自定义组件开发详解

作者 枫荷
2025年4月17日 17:41

需求背景

业务中存在需要自定义select,并实现打开弹框新增数据,如图 image.png

技术背景

  1. react 18
  2. ag 版本 31.1.1 (32版本提供了onValueChange方法,无需getValue,可自行了解)
  3. 组件 antd
  4. 开启了stopEditingWhenCellsLoseFocus(防止用户点击保存按钮,ag还在编辑态,拿不到最新的数据问题,设置失焦自动完成编辑)

核心点

  1. 设置自定义组件cell 为 popup
  2. 点击select Options、modal框时 阻止 mousedown事件(ag失焦判断监听的是 mousedown事件),否则下拉框、弹框会自动关闭

自定义组件需要注意的点

  1. function组件,必须使用 forward 抛出 ICellEditor 方法,其中getValue是必须的
  2. class组件 需实现 ICellEditorComp(建议使用 function 组件,更简单清晰)
  3. stopEdit是同步方法,而setValue是异步方法(回调更新),导致getValue拿不到最新的值,解决方案,使用useRef包裹一下
  4. 下拉框属于popup类型,ag默认有cell尺寸限制,这里根据你的需求选择
  5. react中如果是popup类型,则 col define时必须添加 cellEditorPopup: true,

ICellEditor 介绍

方法名 时机 用途(举例说明 官方注释
isCancelBeforeStart() 编辑器创建前(init() 后立即调用) 根据初始条件决定是否 取消启动编辑(如按下非法键、字段只读等) “If you return true, the editor will not be used and the grid will continue editing.”
isCancelAfterEnd() 编辑器关闭前(getValue() 调用之后) 决定是否丢弃编辑结果,例如用户输入不合法、不满足业务逻辑时取消编辑 “If you return true, then the new value will not be used.”
getValue() 编辑完成时 返回最终要写入单元格的值;比如返回 <Select /> 的选中值 “Return the final value - called by the grid once after editing is complete”
afterGuiAttached() 编辑器 DOM 渲染后 用于设置焦点等 DOM 操作;比如自动聚焦 <input /> “Useful for any logic that requires attachment before executing”
focusIn() 在整行编辑时,编辑器获得焦点时 执行一些进入焦点时的动作(例如高亮) “If doing full line edit, then gets called when focus should be put into the editor”
focusOut() 在整行编辑时,编辑器失去焦点时 用于处理失焦逻辑(如验证、清理) “If doing full line edit, then gets called when focus is leaving the editor”
refresh(params) 当 cellEditor 被重复使用,并接收新的参数时 更新内部状态(比如新 row data、新默认值) “Gets called with the latest cell editor params every time they update”
isPopup() 编辑器启动时调用一次 返回 true 则编辑器以 popup 模式展示,不受 cell 尺寸限制 “If you return true, the editor will appear in a popup”
getPopupPosition() 仅当 isPopup() 返回 true 时调用 返回 "over"(覆盖 cell)或 "under"(浮在 cell 下方) “Return 'over' if the popup should cover the cell, or 'under'…”

代码示例

import React, {
  useState,
  useImperativeHandle,
  forwardRef,
  useEffect,
  useRef,
} from "react";
import { Select, Divider, Button, Modal, Input } from "antd";
import { useMemoizedFn } from "ahooks";
import { CustomCellEditorProps } from "ag-grid-react";
import { ICellEditor } from "ag-grid-enterprise";

const { Option } = Select;

function AntdSelectPopupEditor(
  params: CustomCellEditorProps,
  ref: React.Ref<ICellEditor>,
) {
  // 初始值为ag传入的value
  const [value, setV] = useState(params.value || "");
  // 解决stopEdit时,getValue 拿到的不是最新值的问题
  const valueRef = useRef(value);
  const options = ["Apple", "Banana", "Orange"];
  const containerRef = useRef<HTMLDivElement>(null);
  // modal框
  const [open, setOpen] = useState(false);
  const modalRef = useRef<HTMLDivElement>(null);
  const setValue = useMemoizedFn((value: any) => {
    setV(value);
    valueRef.current = value;
  });

  useImperativeHandle(ref, () => ({
    // 重要:自定义组件必须有
    getValue: () => {
      return valueRef.current;
    },
    // 重要: 告诉 AG Grid:这个编辑器是 popup,不要随便失焦
    isPopup: () => true, 
    afterGuiAttached: () => {
      // 我这边设置select 默认open,不需要这个了
      // setTimeout(() => {
      //   containerRef.current?.querySelector(".ant-select-selector")?.focus?.();
      // });
    },
  }));

  const handleAddOption = () => {
    setOpen(true);
  };

  // 👇关键点:防止点击 dropdown 触发失焦
  useEffect(() => {
    const stopMouseDown = (e: any) => {
      // 如果点击在 dropdown、editor或者modal框内部就阻止默认冒泡
      if (
        (containerRef.current && containerRef.current.contains(e.target)) ||
        e.target.closest(".ant-select-popup-editor-modal")
      ) {
        e.stopPropagation();
      }
    };
    document.addEventListener("mousedown", stopMouseDown, true);
    return () => {
      document.removeEventListener("mousedown", stopMouseDown, true);
    };
  }, []);

  return (
    <div ref={containerRef} style={{ padding: 8, minWidth: 200 }}>
      <Select
        value={value}
        onChange={(v) => {
          setValue(v);
          params.stopEditing();
        }}
        dropdownRender={(menu: any) => (
          <>
            {menu}
            <Divider style={{ margin: "8px 0" }} />
            <div style={{ padding: "8px", textAlign: "center" }}>
              <Button
                type="link"
                onClick={handleAddOption}
              >
                添加新选项
              </Button>
            </div>
          </>
        )}
        getPopupContainer={() => containerRef.current}
        style={{ width: "100%" }}
        // 默认打开,ag编辑组件时编辑时加载,这里直接设置为open即可
        open 
      >
        {options.map((opt) => (
          <Option key={opt} value={opt}>
            {opt}
          </Option>
        ))}
      </Select>
      <Modal
        open={open}
        title="规格选择"
        className="ant-select-popup-editor-modal"
        ref={modalRef}
        onOk={() => {
          params.stopEditing();
          setOpen(false);
        }}
        onCancel={() => setOpen(false)}
      >
        <Input value={value} onChange={(e) => setValue(e.target.value)} />
      </Modal>
    </div>
  );
}

export default forwardRef(AntdSelectPopupEditor);

使用示例

import { ColDef } from "ag-grid-enterprise";
import { AgGridReact } from "ag-grid-react";
import { useEffect, useState } from "react";
import AntdSelectPopupEditor from "./AntdSelectPopupEditor";
// Row Data Interface
interface IRow {
  make: string;
  model: string;
  price: number;
  electric: boolean;
  size: string;
}

// Create new GridExample component
const GridExample = () => {
  // Row Data: The data to be displayed.
  const [rowData, setRowData] = useState<IRow[]>([
    {
      make: "Tesla",
      model: "Model Y",
      price: 64950,
      electric: true,
      size: "1",
    },
    {
      make: "Ford",
      model: "F-Series",
      price: 33850,
      electric: false,
      size: "2",
    },
    {
      make: "Toyota",
      model: "Corolla",
      price: 29600,
      electric: false,
      size: "3",
    },
    { make: "Mercedes", model: "EQA", price: 48890, electric: true, size: "4" },
    { make: "Fiat", model: "500", price: 15774, electric: false, size: "4" },
    { make: "Nissan", model: "Juke", price: 20675, electric: false, size: "5" },
    { make: "Fiat", model: "500", price: 15774, electric: false, size: "6" },
  ]);

  // Column Definitions: Defines & controls grid columns.
  const [colDefs, setColDefs] = useState<ColDef<IRow>[]>([
    { field: "make" },
    { field: "model" },
    { field: "price", editable: true },
    { field: "electric", editable: true },
    {
      field: "size",
      cellEditor: AntdSelectPopupEditor,
      editable: true,
      // 重要
      cellEditorPopup: true,
      cellEditorPopupPosition: "over",
    },
  ]);

  return (
    <div
      className={"ag-theme-quartz"}
      style={{ width: "100%", height: "500px" }}
    >
      <AgGridReact
        rowData={rowData}
        columnDefs={colDefs}
        stopEditingWhenCellsLoseFocus={true}
      />
    </div>
  );
};
export default GridExample;

效果展示

output.gif

封装 useStopPropagation 方便复用

type ClassName = string;
type Container = React.RefObject<any> | ClassName;

function useStopPropagation(contains: Container[]) {
  const stopMouseDown = (e: any) => {
    // 如果点击在 dropdown 或 editor 里,就阻止默认行为
    const isContains = contains.some((container) => {
      if (typeof container === "string") {
        return e.target.closest(
          container.startsWith(".") ? container : `.${container}`,
        );
      }
      return container.current?.contains(e.target);
    });

    if (isContains) {
      e.stopPropagation();
    }
  };

  document.addEventListener("mousedown", stopMouseDown, true);
  return () => {
    document.removeEventListener("mousedown", stopMouseDown, true);
  };
}

export default useStopPropagation;

ts+vue3出乎意料的推导报错

2025年4月17日 17:39

今天在写代码时候,发现一个让我琢磨不透的报错。通过官网得知2种写法效果是一样的。但是偏偏有一种会类型推导报错。代码如下:

export interface AddRoleParams {
  name: string;
  description: string,
  menus: object[]
}

export interface EditRoleParams extends AddRoleParams {
  id: number,
}

export interface ListItem {
  id: number;
  [key: string]: unknown;
}

const b =  ref<EditRoleParams>({ id: 0, name: '', description: '', menus: []})
export function a(params:Ref<ListItem> | Reactive<ListItem>){ 
  console.log(params)
}

a(b)

上面的代码是正常的,如果把b的赋值改成这个

const b: Ref<EditRoleParams> =  ref({ id: 0, name: '', description: '', menus: []})

编辑器会发生报错

但是从官网上来看效果应该是一样的。

不过我也没找到是啥原因。

web通过离线编译protobuf,在线解析proto二进制数据

作者 榴弹丶
2025年4月17日 17:29

一、安装依赖

环境版本

$ node -v
v18.12.0

$ npm -v
8.19.2

为了使用ts类型推断,我这里安装ts-proto库,我用的版本是^2.7.0

$ npm i ts-proto

安装protoc工具

  • 这个是非web或node程序,一个可执行的.exe文件用来编译.proto文件;
  • 点击下方链接,选择你所属电脑的系统对应的工具,我这里是windows32位操作系统。

github.com/protocolbuf…

image.png

  • 下载完成后,解压缩到某个文件夹下,配置系统环境变量路径:

image.png

image.png

  • 查看protoc版本
$ protoc --version

image.png

  • 这里是一个坑点,protoc默认从nodejs的根目录的node_modules文件夹去查找对应执行文件了,具体原因还不了解,可能是因为之前安装了npm i protoc -g导致的,这里我们规避一下,改一种写法:
$ protoc.exe --version
libprotoc 30.2
  • 运行正常,安装步骤完结。

二、编译.proto文件

准备2个.proto 文件,a.protob.proto,其中a.proto使用import关键字引入了b.proto文件的类型

  • 当前你的工程目录为这样:
ts-proto-demo/
├── protos/
│   ├── a.proto
│   └── b.proto
├── package.json
├── tsconfig.json
└── README.md
// a.proto
syntax = "proto3";

package demo;

// 引入b.proto
import "b.proto";

message Post2 {
  string id2 = 1;
  string title2 = 2;
}
message Post {
  string id = 1;
  string title = 2;
  // b的User类型
  User author = 3;
  Post2 post2 = 4;
}

// b.proto
syntax = "proto3";

package demo;

message User {
  string id = 1;
  string name = 2;
}

开始编译

  • 执行protoc可执行文件
protoc.exe \
--plugin=protoc-gen-ts_proto=".\\node_modules\\.bin\\protoc-gen-ts_proto.cmd" \
--proto_path=./protos/ \
--ts_proto_out=. \
./protos/a.proto

--plugin=protoc-gen-ts_proto=".\\node_modules\\.bin\\protoc-gen-ts_proto.cmd"

  • npm安装ts-proto库时,会在node_modules/.bin/文件夹下添加一个名为protoc-gen-ts_proto.cmd的可执行文件;
  • 注意!!!我是windows系统,所以选用.cmd结尾的文件;
  • 注意!!!我使用的是cmd运行脚本,所以--plugin路径使用".\\node_modules\\.bin\\protoc-gen-ts_proto.cmd"写法。

image.png

  • --proto_path=./protos/ 如果你有多个.proto文件需要编译,或者有互相引用的.proto文件,需要设置--proto_path,大致意思是包含proto_path文件夹下的所有.proto文件

  • --ts_proto_out=. 输出*.ts解析脚本目录,没什么好说的

  • ./protos/a.proto 需要编译的.proto文件

执行完成

image.png

三、查看结果

  • 当前你的工程目录为这样:
ts-proto-demo/
├── protos/
│   ├── a.proto
│   └── b.proto
├── a.ts
├── b.ts
├── package.json
├── tsconfig.json
└── README.md

a.ts文件内容:

image.png

image.png

正常我们只需要关心最终输出的类型,应该是Post类型,Post包含了Post2User类型。

四、使用

import {Post} from "./a.ts"
const data: Uint8Array = // ***;
const result = Post.encode(data);
cosole.log(result);

// 你会得到类似下面的结果
/**
{
  id: '1',
  title: "标题1",
  author: {
   id: "3";
   name: "leo";
  },
  post2: {
      id2: '2',
      title2: "标题2",
  }
}
*/

ES6 var + let + const 和 代码块

作者 WEI_Gaot
2025年4月17日 17:28
特性 var let const
作用域 函数作用域 / 全局作用域 块级作用域 块级作用域
提升 提升并初始化为 undefined 提升但不初始化 (TDZ) 提升但不初始化 (TDZ)
重复声明 允许 不允许 不允许
重新赋值 允许 允许 不允许
声明时初始化 可选 可选 必须
全局对象属性 是 (全局作用域)

1. 最基本的区别

var VS let + const

var 能够重复声明变量
var name = 'why'
console.log(name) //why
var name = 'wei'
console.log(name)//wei
let 不能够重复声明相同的变量

会报错

Snipaste_2025-04-17_15-47-20.png

const 不能够重复声明相同的变量

Snipaste_2025-04-17_15-48-54.png

var let VS const

var 和 let 能够改变声明变量, const不能改变声明的变量
var ageVar = 18
console.log(ageVar) //18
ageVar = 19
console.log(ageVar) //19

let ageLet = 88
console.log(ageLet) //88
ageLet = 99
console.log(ageLet) //99

const不能重复声明

const ageConst = 188
console.log(ageConst)
ageConst = 111
console.log(ageConst) //报错

2. 对对象的相同点和不同点

对于var
var obj1 = {
  name: 'why',
  age: 18
}
console.log(obj1)//{ name: 'why', age: 18 }
obj1.name = 'wei'
console.log(obj1)//{ name: 'wei', age: 18 }

 obj1 = {aa: 'aa'}
console.log(obj1)//{ aa: 'aa' }
对于let
let  obj1 = {
  name: 'why',
  age: 18
}
console.log(obj1)//{ name: 'why', age: 18 }
obj1.name = 'wei'
console.log(obj1)//{ name: 'wei', age: 18 }


obj1 = {aa: 'aa'}
console.log(obj1)//{ aa: 'aa' }
对于const
const obj1 = {
  name: 'why',
  age: 18
}
console.log(obj1)//{ name: 'why', age: 18 }
obj1.name = 'wei'
console.log(obj1)//{ name: 'wei', age: 18 }

//会报错❌
obj1 = {aa: 'aa'}
console.log(obj1)//{ aa: 'aa' }

3. let 和 const 的作用域提升

console.log(nameVar)// undefined
var nameVar = 'why'

//error
console.log(nameLet)
let nameLet = 'why'

//error
console.log(nameConst)
const nameConst = 'why' 
  1. 当使用var let const创建变量时,他们的声明的变量都会被创建出来
  2. 但不同的是。var创建的变量是可以被访问的,而let 和 const 创建的的变量是不能够被访问
  3. let和const的变量直到赋值时对变量进行绑定,才能够进行访问
我们得出结论

作用域提升:在上面变量的作用域中,如果这变量可以在声明之前被访问,那么我们称为作用域提升

let 和 const:他们和var一样变量名在声明时被创建出来了,但是不能被访问,不能称为作用域的提升

4. 与 window的关系 + 存放地址

Google的V8不能处理JavaScript中window的,window是在浏览器中进行处理的,包括window的一些属性(Date, Number)

  1. var 在全局作用域中声明的变量:

    • 存储位置: 这些变量被存储为全局对象(在浏览器环境中是 window 对象,在 Node.js 中是 global 对象)的属性
    • 机制: 当 JavaScript 引擎处理全局作用域时,它会为 var 声明的变量在全局对象的属性集合中创建相应的条目。这就是为什么你可以通过 window.yourVarName (浏览器) 或 global.yourVarName (Node.js) 来访问全局 var 变量。它们与全局对象的生命周期绑定。
  2. let 和 const 在全局作用域中声明的变量:

    • 存储位置: 这些变量被存储在一个与全局对象不同的、独立的词法环境(Lexical Environment)中。这个环境通常被称为“脚本作用域” (Script Scope) 或顶层词法环境。它们会成为全局对象的属性。
    • 机制: ES6 引入了块级作用域和 let/const,其设计目标之一是减少对全局对象的污染。因此,规范规定全局的 let 和 const 变量存储在脚本自身的顶级作用域记录中,而不是直接附加到 window 或 global 对象上。这个脚本作用域存在于全局作用域链的最顶层,使得这些变量仍然是全局可访问的(在声明之后且脱离 TDZ),但它们与全局对象解耦了。

简单来说:

  • 全局 var → 住在 window (或 global) 这栋大楼里,成为大楼的一个房间(属性)。
  • 全局 let / const → 也住在这片区域(全局作用域),但它们住在自己独立的房子(脚本作用域/顶层词法环境)里,不属于 window 大楼。

为什么要知道这个区别?

  • 避免全局污染: 使用 let 和 const 可以让你的全局变量不直接干扰 window 对象,减少了意外覆盖 window 内置属性或被其他库覆盖的风险。
  • 理解访问方式: 你不能通过 window.myLetVariable 来访问全局 let 变量,必须直接使用 myLetVariable。
  • 模块化思维: 这更符合现代 JavaScript 的模块化开发模式,每个脚本或模块可以在其顶层作用域中拥有自己的变量,而不会轻易干扰其他模块或全局环境。
var globalVar = 'var on window';
let globalLet = 'let in script scope';
const globalConst = 'const in script scope';

console.log(window.globalVar);   // 'var on window'
console.log(window.globalLet);   // undefined
console.log(window.globalConst); // undefined

console.log(globalVar);          // 'var on window'
console.log(globalLet);          // 'let in script scope'
console.log(globalConst);        // 'const in script scope'

let 和 const 的暂时性死区(TDZ)

在代码中,使用let 和 const 声明变量,在声明变量之前,变量都是不可以访问的, 我们称这种现象为暂时性死区(TDZ)

经常出现在if条件语句函数语句

在 if 中
var foo = 'foo'

if (true) {
  console.log(foo) //foo
}

下面的代码会报错,触发了暂时性死区(TDZ)

var foo = 'foo'

if (true) {
  console.log(foo)

  let foo = 'abc'
}
在 function 中
var foo = 'foo'
function bar () {
  console.log(foo)
}
bar() //foo

下面的代码会报错,触发了暂时性死区(TDZ)

var foo = 'foo'
function bar () {
  console.log(foo)
  let foo = 'abc'
}
bar()

代码块

声明字面量

var obj = {
  name: 'why'
}

什么时候选择var let const

  • var 由于历史的特殊性:作用域提升,window全局对象,没有块级作用域等历史遗留问题 目前不推荐使用var
  • 在开发中推荐使用let const ,我们优先使用const,可以保证数据的安全性,不会被随意的的篡改。当我们明确的知道变量后续需要被重新赋值的时候,再使用let

代码块

{
  var foo = 'foo'
}

ES5中只有两个东西会形成块级作用域

在es5中没有块级作用域
  1. 全局作用域
  2. 函数作用域

函数作用域中通过函数链,可以访问外部的变量

当有函数嵌套时,也是同样的道理,内部的函数可以通过函数链访问到外部的变量,而外部的不能够访问内部的变量

ES6 块级作用域

代码块对var不起作用,其他起作用
{
  var foo = 'foo'
  let bar = 'bar'
  const bar1 = 'bar1'
  
  class Person {}
}
console.log(foo) //foo

console.log(bar) //error
console.log(bar1) //error
var p = new Person() //error
函数在代码块中的特殊表现
{
  function demo () {
    console.log('demo function')
  }
}
demo() //demo function

为什么会这样,不是说代码块中外部无法访问吗❓ 答:不同的浏览器有不同的实行,大部分浏览器为了兼容以前的代码,让function是没有块级作用域的

常见的块级作用域

1. if语句
if (true) {
  var foo = 'foo'
  let bar = 'bar'
  const bar1 = 'bar1'
}

console.log(foo) //foo ✔️
console.log(bar) //error
console.log(bar1) //error
2. switch语句
var color = 'red'

switch(color) {
  case 'red':
    var foo = 'foo'
    let bar = 'bar'
    const bar1 = 'bar1'
}

console.log(foo) //foo 
console.log(bar) //error
console.log(bar1) //error
3. for循环语句

var 在for语句中

for (var i = 0; i < 1; i++) {
  console.log(`hello ${i}`)
}

console.log(i)

//hello 0
//1

let (const)在for语句中

for (let  i = 0; i < 1; i++) {
  console.log(`hello ${i}`)
}

console.log(i) //error

//hello 0

在 Vue 3 中实现右键菜单功能

2025年4月17日 17:27

在 Vue 3 中实现右键菜单功能,可以通过以下步骤实现:

vue
<template>

  <div 

    @contextmenu.prevent="showContextMenu"

    @click="closeContextMenu"

    class="container"

  >

    <!-- 你的页面内容 -->

    <div v-if="showMenu" 

         :style="menuStyle"

         class="context-menu">

      <div 

        v-for="(item, index) in menuItems"

        :key="index"

        class="menu-item"

        @click.stop="handleMenuItem(item)"

      >

        {{ item.label }}

      </div>

    </div>

  </div>

</template>

 

<script setup>

import { ref } from 'vue'

 

const showMenu = ref(false)

const menuStyle = ref({})

const menuItems = ref([

  { label: '复制', action: 'copy' },

  { label: '粘贴', action: 'paste' },

  { label: '剪切', action: 'cut' }

])

 

// 显示右键菜单

const showContextMenu = (event) => {

  event.preventDefault()

  showMenu.value = true

  menuStyle.value = {

    left: `${event.clientX}px`,

    top: `${event.clientY}px`

  }

}

 

// 关闭菜单

const closeContextMenu = () => {

  showMenu.value = false

}

 

// 处理菜单项点击

const handleMenuItem = (item) => {

  console.log('执行操作:', item.action)

  closeContextMenu()

}

</script>

 

<style scoped>

.container {

  position: relative;

  min-height: 100vh;

}

 

.context-menu {

  position: fixed;

  background: white;

  border-radius: 4px;

  box-shadow: 0 2px 8px rgba(0,0,0,0.15);

  z-index: 1000;

}

 

.menu-item {

  padding: 8px 16px;

  cursor: pointer;

  transition: background 0.2s;

}

 

.menu-item:hover {

  background: #f0f0f0;

}

</style>

实现要点说明:

  1. 事件监听
  • 使用 @contextmenu.prevent 监听右键事件并阻止默认菜单
  • 通过 @click 事件关闭菜单(点击容器任意位置)
  1. 菜单定位
  • 使用 event.clientX/Y 获取点击坐标
  • 通过动态样式绑定定位菜单
  1. 菜单控制
  • 使用 showMenu 响应式变量控制显示状态
  • handleMenuItem 处理具体菜单操作
  1. 样式优化
  • 固定定位保证菜单跟随鼠标
  • 添加阴影和圆角提升视觉效果
  • 悬停效果增强交互体验

扩展建议:

  1. 动态菜单项:可以通过 props 接收外部传入的菜单配置
  2. 图标支持:添加图标字段并集成图标组件
  3. 动画效果:添加过渡动画提升体验
  4. 主题支持:通过 CSS 变量实现主题切换
  5. 禁用状态:添加 disabled 属性控制菜单项可用性

如果需要更复杂的交互,可以考虑使用第三方库如 vue-context-menu,但上述实现已能满足大多数基础需求。

LeetCode 题解 | 1.两数之和(最优解)

作者 天天扭码
2025年4月17日 17:28

题目——1.两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。

你可以按任意顺序返回答案。

示例 1:

输入: nums = [2,7,11,15], target = 9
输出: [0,1]
解释: 因为 nums[0] + nums[1] == 9 ,返回 [0, 1]

示例 2:

输入: nums = [3,2,4], target = 6
输出: [1,2]

示例 3:

输入: nums = [3,3], target = 6
输出: [0,1]

 

提示:

  • 2 <= nums.length <= 104
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109
  • 只会存在一个有效答案

二、小白题解(正是本人)

/**

 * @param {number[]nums

 * @param {numbertarget

 * @return {number[]}

 */

var twoSum = function(nums, target) {

    for(let i=0;i<nums.length;i++){

        for(let j=i+1;j<nums.length;j++){

            if(nums[i]+nums[j]===target){

                return [i,j];

            }

        }

    }

};

这个解题思路有很多缺点—— 时间复杂度高没有异常处理看上去像作者自己写的......

总之就是一个暴力题解,我们在最坏的情况下是N(O2)N(O2)的时间复杂度,会将nums中的所有元素组合遍历知道找到符合条件的两个元素的下标,那么有没有更好的解法呢?

三、有的兄弟,肯定有的(时间复杂度上的最优解)

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
function twoSum(nums, target) {
    const numMap = {};
    for (let i = 0; i < nums.length; i++) {
        const complement = target - nums[i];
        if (complement in numMap) {
            return [numMap[complement], i];
        }
        numMap[nums[i]] = i;
    }
    return []; 
}

这个解法用到了哈希表,这里我们并不需要去详细研究哈希表是个什么东西,我们这里可以把哈希表理解为一个容器,里面的数据可以拿出来和我们想要的目标数据对比,每次对比是N(O)N(O)的复杂度。

我们来看题解的主体

 for (let i = 0; i < nums.length; i++) {
        const complement = target - nums[i];
        if (complement in numMap) {
            return [numMap[complement], i];
        }
        numMap[nums[i]] = i;
    }

1.每个元素遍历了几次?

我们只有一个for循环,这意味着每个数组里面的元素只会遍历一遍

2.每个元素遍历经历了什么

存入哈希表之前

1.被用作计算和自己组合起来符合题意的元素的值

const complement = target - nums[i];

2.被存储在哈希表中作为键值存储在哈希表中,同时下表作为对应键值的值

numMap[nums[i]] = i;

存入哈希表之后

3.作为可被寻找的、其他元素的“另一半”,如果确定是,则输出两元素在原数组的下标

 if (complement in numMap) {
            return [numMap[complement], i];
        }

四、下面我们用一个例子来分析最优解的执行过程

nums = [2,7,11,15];
target = 9

具体过程如下(图解)

image.png

image.png

image.png

image.png

五、可执行流程(将代码复杂到编译器运行即可得到动态图解)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>两数之和 - 哈希表解法示意图</title>
    <style>
        body {
            font-family: 'Arial', sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f5f7fa;
        }
        .container {
            background: white;
            border-radius: 10px;
            padding: 20px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h1 {
            color: #2c3e50;
            text-align: center;
        }
        .visualization {
            display: flex;
            flex-direction: column;
            gap: 20px;
            margin-top: 30px;
        }
        .array-container {
            display: flex;
            justify-content: center;
            position: relative;
            height: 100px;
        }
        .array-element {
            width: 60px;
            height: 60px;
            background: #3498db;
            color: white;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: 5px;
            margin: 0 5px;
            position: relative;
            transition: all 0.3s;
            font-weight: bold;
        }
        .array-index {
            position: absolute;
            top: -20px;
            color: #7f8c8d;
            font-size: 12px;
        }
        .hash-table {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
            justify-content: center;
            min-height: 120px;
            background: #ecf0f1;
            padding: 15px;
            border-radius: 5px;
        }
        .hash-entry {
            background: #2ecc71;
            color: white;
            padding: 8px 12px;
            border-radius: 5px;
            display: flex;
            flex-direction: column;
            align-items: center;
            opacity: 0;
            transform: scale(0.8);
            transition: all 0.5s;
        }
        .hash-entry.visible {
            opacity: 1;
            transform: scale(1);
        }
        .hash-key {
            font-weight: bold;
            border-bottom: 1px solid white;
            margin-bottom: 3px;
            padding-bottom: 3px;
        }
        .current {
            box-shadow: 0 0 0 3px #e74c3c;
            transform: scale(1.1);
        }
        .complement {
            background: #e74c3c;
        }
        .found {
            background: #f39c12;
            animation: pulse 0.5s 2;
        }
        @keyframes pulse {
            0% { transform: scale(1); }
            50% { transform: scale(1.1); }
            100% { transform: scale(1); }
        }
        .explanation {
            background: #f8f9fa;
            padding: 15px;
            border-radius: 5px;
            margin-top: 20px;
            border-left: 4px solid #3498db;
        }
        .controls {
            display: flex;
            justify-content: center;
            gap: 10px;
            margin-top: 20px;
        }
        button {
            padding: 8px 15px;
            background: #3498db;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            transition: background 0.3s;
        }
        button:hover {
            background: #2980b9;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>两数之和 - 哈希表解法示意图</h1>
        <div class="visualization">
            <div class="array-container">
                <!-- 数组元素将通过JS动态生成 -->
            </div>
            <div class="hash-table">
                <!-- 哈希表内容将通过JS动态生成 -->
            </div>
        </div>
        <div class="explanation">
            <p><strong>当前步骤说明:</strong> <span id="step-explanation">初始化数组和哈希表</span></p>
            <p><strong>算法原理:</strong> 遍历数组,对于每个元素计算 complement = target - nums[i],检查 complement 是否存在于哈希表中。</p>
        </div>
        <div class="controls">
            <button id="prev-btn">上一步</button>
            <button id="next-btn">下一步</button>
            <button id="reset-btn">重置</button>
        </div>
    </div>

    <script>
        // 示例数据
        const nums = [2, 7, 11, 15];
        const target = 9;
        let currentStep = 0;
        let hashMap = {};
        
        // 初始化可视化
        function initVisualization() {
            const arrayContainer = document.querySelector('.array-container');
            arrayContainer.innerHTML = '';
            
            // 创建数组元素
            nums.forEach((num, index) => {
                const element = document.createElement('div');
                element.className = 'array-element';
                element.textContent = num;
                element.id = `num-${index}`;
                
                const indexLabel = document.createElement('div');
                indexLabel.className = 'array-index';
                indexLabel.textContent = index;
                
                element.appendChild(indexLabel);
                arrayContainer.appendChild(element);
            });
            
            updateHashTable();
            updateExplanation();
        }
        
        // 更新哈希表显示
        function updateHashTable() {
            const hashTable = document.querySelector('.hash-table');
            hashTable.innerHTML = '';
            
            for (const [key, value] of Object.entries(hashMap)) {
                const entry = document.createElement('div');
                entry.className = 'hash-entry visible';
                entry.innerHTML = `
                    <div class="hash-key">${key}</div>
                    <div>→ ${value}</div>
                `;
                hashTable.appendChild(entry);
            }
        }
        
        // 更新步骤说明
        function updateExplanation() {
            const explanation = document.getElementById('step-explanation');
            const currentElement = document.getElementById(`num-${currentStep}`);
            const elements = document.querySelectorAll('.array-element');
            
            // 重置所有元素样式
            elements.forEach(el => {
                el.classList.remove('current', 'complement', 'found');
            });
            
            switch(currentStep) {
                case 0:
                    explanation.textContent = "开始遍历数组,当前元素 nums[0] = 2";
                    currentElement.classList.add('current');
                    break;
                case 1:
                    explanation.textContent = "计算 complement = 9 - 2 = 7,7 不在哈希表中,将 2 存入哈希表";
                    currentElement.classList.add('current');
                    break;
                case 2:
                    explanation.textContent = "移动到 nums[1] = 7,计算 complement = 9 - 7 = 2";
                    currentElement.classList.add('current');
                    document.getElementById('num-0').classList.add('complement');
                    break;
                case 3:
                    explanation.textContent = "发现 2 在哈希表中(索引 0),找到解 [0, 1]!";
                    currentElement.classList.add('current');
                    document.getElementById('num-0').classList.add('found');
                    currentElement.classList.add('found');
                    break;
                default:
                    explanation.textContent = "遍历完成";
            }
        }
        
        // 下一步
        function nextStep() {
            if (currentStep >= 4) return;
            
            switch(currentStep) {
                case 0:
                    // 准备处理第一个元素
                    break;
                case 1:
                    // 处理 nums[0] = 2
                    hashMap[nums[0]] = 0;
                    break;
                case 2:
                    // 处理 nums[1] = 7
                    const complement = target - nums[1];
                    if (complement in hashMap) {
                        // 找到解的情况
                    }
                    break;
                case 3:
                    // 完成
                    break;
            }
            
            currentStep++;
            updateHashTable();
            updateExplanation();
        }
        
        // 上一步
        function prevStep() {
            if (currentStep <= 0) return;
            
            currentStep--;
            
            // 回退哈希表状态
            if (currentStep < 1) {
                hashMap = {};
            } else if (currentStep < 2) {
                hashMap = { [nums[0]]: 0 };
            }
            
            updateHashTable();
            updateExplanation();
        }
        
        // 重置
        function reset() {
            currentStep = 0;
            hashMap = {};
            initVisualization();
        }
        
        // 事件监听
        document.getElementById('next-btn').addEventListener('click', nextStep);
        document.getElementById('prev-btn').addEventListener('click', prevStep);
        document.getElementById('reset-btn').addEventListener('click', reset);
        
        // 初始化
        initVisualization();
    </script>
</body>
</html>

六、结语

再见!

❌
❌