阅读视图

发现新文章,点击刷新页面。

专访苹果副总裁:一个小红书博主,该用什么样的苹果工具?

2026 年 1 月 29 日, 苹果将上线 Apple Creator Studio——这是一项全新的订阅服务,汇总了所有苹果创造力/生产力软件。

价格则低至每月一杯咖啡。

在产品发布前夕,苹果全球产品营销副总裁 Bob Borchers 及全球 App 产品营销高级总监 Brent Chiu-Watson 接受了爱范儿的专访。

▲ Bob Borchers,Apple 全球产品营销副总裁

通过这次采访,我们试图找到一个答案:

当一个音乐人同时也是视频制作者、平面设计师和小企业主时,苹果该给他什么样的工具?

创作者的新物种

如果你是一位小红书博主,大抵对这个工作流不会陌生:

想好一个选题,先在文档上写脚本,再用设备拍摄,接下来用一系列后期软件调色、剪辑、配乐,最后设计一个吸睛的封面,跟着视频一并传到平台,发布。

在 Bob Borchers 眼中,这就是典型的现代创作者画像:

他们不再局限于单一领域。一个音乐人不只是写歌,他们还要制作音轨、设计专辑封面、拍摄 MV、创作周边产品。

创作者经济不是新概念,但它的形态正在剧烈变化。

十年前,音乐人的工作流程是线性的:写歌→录音→找厂牌→发行。每个人都在自己的赛道里深耕,工具也是垂直的、专业的。

▲ 图|AudioDope

但今天,这条生产链已经彻底扁平化。

拥有数百万粉丝的爱尔兰音乐人艾莉·谢尔洛克(Allie Sherlock),经常会在都柏林格拉夫顿街表演,然后把视频上传到 YouTube。但她要做的事情,远不止于音乐创作:她用 Logic Pro 制作原创音乐,用 Final Cut Pro 剪辑街头表演视频,用 Pixelmator Pro 设计专辑封面和周边,还会用 Keynote 制作宣传材料,用 Pages 制作自己的商品目录——一个人,五种角色。

▲ 图|Youtube @Ellie Sherlock

作为一名媒体编辑,写作只是我工作的一部分,很多情况下,我同样需要拍视频、做剪辑、设计封面——这就是当下创作者的常态,相信你我已经司空见惯,但放在过去是难以想象的。

传统的创作工具链是割裂的。

做音乐用 Logic Pro,剪视频用 Final Cut Pro,修图用 Photoshop,做设计用 Illustrator……每个软件都有自己的学习曲线、文件格式、付费计划,他们分属不同的大公司,你需要在各种地方交钱。

Apple Creator Studio,想做的就是 all in one。

全家桶的哲学

Apple Creator Studio 是什么?实际上就是一整套的创作者服务——

包含了 Final Cut Pro(视频编辑)、Logic Pro(音乐制作)、Pixelmator Pro(图像编辑)、Pages、Numbers、Keynote,以及 Motion、Compressor、MainStage 等配套应用。这些应用及其高级内容全部打包,每月 38 元,一年 380 元,最多可以家庭 6 人共享。学生和教育工作者的单价更低,每月 18 元,一年 180 元。

这个价格背后是一笔账:如果单独购买这些专业软件,Final Cut Pro 售价 1998 元,Logic Pro 售价 1298 元,Pixelmator Pro 售价 328 元,加上其他工具,总价超过 4000 元。而订阅 Apple Creator Studio,4000 元足够一个中国创作者连续使用超过 10 年。

苹果产品营销总监 Brent Chiu-Watson 解释了这套系统的工作逻辑:

我们相信,技术应该让创意自由流动,在你需要的时候,以最合适的形式出现。

再比如,Final Cut Pro 内置了 Logic Pro 的节拍检测引擎。你导入音乐后,系统会自动分析节奏,在时间线上标记每一个拍点。剪辑视频时,画面会自动吸附到这些拍点,不需要手动数节拍。

更深层的整合发生在技术层面。

所有应用共享 Apple 设备的端侧 AI 能力,且具备高度一致性:超分辨率和自动裁剪功能可以跨 Pixelmator Pro、Keynote、Pages 和 Numbers 使用。比如在 Keynote 里调整图片构图时,自动裁剪就会给出三种优化方案。

Brent 强调了这种一致性:

用户不需要学习每个应用的 AI 功能在哪里、怎么用。同样的能力,会出现在每一个需要它的地方。

这种整合需要巨大的工程投入。所有应用必须使用统一的图像处理框架、统一的 AI 模型调用接口、统一的交互逻辑。而苹果能做到这一点,因为它控制了从芯片到操作系统、再到应用层的整个技术栈。

在苹果看来,把 AI 功能部署在设备端运行能带来高速、一致、安全的体验,但这并不意味着不会开放云端 AI 的能力。Brent Chiu-Watson 告诉爱范儿:

市场变化很快,我们会持续观察用户需求。如果某些场景需要不同的技术方案,我们也会考虑。

从芯片到软件的全栈优化,从 Final Cut Pro 到 Logic Pro,从 Pixelmator 到 Keynote,看上去各不相及,但它们都跑在同一颗芯片上,共享同一套技术框架,为同一份创造力服务。

为心智打造工具

自乔布斯创立苹果以来,「赋能创造力」始终是其核心哲学,甚至写入到了苹果公司的愿景里:

为心智打造工具,推动人类进步。

To make a contribution to the world by making tools for the mind that advance humankind.

1984 年第一台 Macintosh 诞生时,它就被定位为「为创作者而生」的产品:具备极强的审美意识和当时极其稀缺的图文混排能力,甚至搭载了 HyperCard 工具,让没有编程基础的人也能通过可视化方式开发程序或网页。

▲ 图|Norman Seeff 拍摄的乔布斯与比尔 · 阿特金森

在过去 20 多年里,苹果通过持续收购,将 Final Cut Pro、Logic Pro、Pixelmator 等专业创造力工具纳入门下。这种「一以贯之」的哲学体现为:将创作者所需的工具进行整合

在 iPod 和 iPhone 问世之前,苹果公司最主要的商业模式,就是让用户通过购买苹果的软硬件产品,来获得一套完整的创造力工具箱。

时至今日,苹果生态中的创作者,其数量级和覆盖度已不可同日而语,新时代的创作者们跨场景、跨门类、跨边界,同时调用多种创作工具是家常便饭。

于是,苹果开始成为「创造力工具」的整合者和加速者:只要购买一台 Apple 设备,花 38 元开通订阅,就能解锁价值 4000 多块的一整天创造力工具,马上获得专业生产力——在软件买断制的时代,如此低的准入门槛是难以想象的。

Bob Borchers 解释了这个定价策略:

我们的目标是尽可能广泛地激励和加速创造力。我们希望给他们工具和能力,让他们更高效地做正在做的事,也能探索以前没想过的事。

从这个角度来看,Apple Creator Studio 并不只是想打价格战,而是基于对创作者市场的长期判断。

今天,我们成为一名创作者的门槛如此之低,分发渠道如此广泛,那创造力工具的门槛,理应也需要一降再降。更低的创造力门槛,意味着释放更多的创造力,而这些创造力最终会反哺整个生态——今天的学生创作者,可能就是明天的制作人、音乐家、设计师。

值得一提的是,Apple Creator Studio 采用订阅制,但所有应用依然可以单独购买, Bob Borchers 解释:

我们知道有些创作者对某个应用有非常具体的需求,这就是为什么我们继续提供一次性购买选项,并且会持续更新这些版本。

订阅用户看重完整性和便利性,买断用户看重确定性和所有权。苹果的策略是两者兼顾,订阅版和买断版功能基本一致,只有极少数高级功能是订阅独占。在 Bob Borchers 看来,未来的 Apple Creator Studio 仍大有可为:

这只是开始,Apple Creator Studio 会随着时间不断增强,加入新内容和新功能。

1984 年,Macintosh 发布时,苹果也不知道设计师会用它来做什么;2001 年 Final Cut Pro 发布时,苹果也不知道独立电影人会用它来挑战好莱坞;如今,打开 iPad 或 Mac,随时随地都能成为专业级的创作人——工具的意义,从来不在于定义创作的边界,而是移除创作的障碍。

对于这个时代的创作者来说,「购买苹果设备、订阅苹果服务、解锁创作能力」正演变为一种新的认知范式。当创作者进化时,工具也必须进化,而工具的终极形态,永远在未来。

每个月 38 块的 Apple Creator Studio,就是战未来。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


3D字体TextGeometry

1. 引入字体加载器

由于 TextGeometry 不是核心库的一部分,我们需要单独引入:

import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js';
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry.js';

2. 加载 JSON 字体并创建

Three.js 不直接读取 .ttf,它读取的是专用的 JSON 字体文件。

import * as THREE from 'three';
import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js';
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry.js';
import local_font from '../../assets/font/helvetiker_bold.typeface.json';

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222222); // 深灰色背景

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const loader = new FontLoader();
console.log('开始加载字体...');

let font = loader.parse(local_font);
const textGeometry = new TextGeometry('Hello Three.js!', {
    font: font,
    size: 1,
    depth: 0.5,
    curveSegments: 12,
    bevelEnabled: false,
});

textGeometry.center();

const textMaterial = new THREE.MeshNormalMaterial();
let textMesh = new THREE.Mesh(textGeometry, textMaterial);
textMesh.position.set(0, 0, 0); // 确保在原点
scene.add(textMesh);
console.log('文字网格已添加到场景');

function animate() {
    requestAnimationFrame(animate);

    if (textMesh) {
        textMesh.rotation.y += 0.01;
    }

    renderer.render(scene, camera);
}
animate();
console.log('动画循环已启动');

📂 核心代码与完整示例:    my-three-app

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多Three.js开发干货

markstream-vue实战踩坑笔记

最近新开始做了一个ai对话的项目,打算重构一下老项目的markdown渲染和交互。老项目使用marked对md进行渲染成html,然后使用v-html渲染,搞到后面发现太多技术债了,所以打算换一个实现方法。经过一番对比,发现 markstream-vue 的star是最多的,去看了一下官方文档发现特性也很多。所以这里决定就用这个组件了。

需求说明

这里实现的一个项目是ai对话的应用,特点是可能markdown里要混着各种业务组件,以及一些md的节点可以点击交互,比如列表项或者链接点击代替用户自动发送信息。

安装使用

安装就不多浪费口水了,官方文档写的非常详细,我们先看看案例markdown,以及使用库提供的markdown解析成节点:

import { getMarkdown, MarkdownRender, parseMarkdownToStructure } from 'markstream-vue'

const md = `
<thinking>
这是推理内容
</thinking>

请选择办理方式
- 线上办理
- 线下办理

<a data-send="其他办理方式">其他办理方式</a>
`
console.log(JSON.stringify(parseMarkdownToStructure(md, getMarkdown()), null, 2))

image.png

对list_item和link进行自定义

如果只是渲染上面的md,很简单:

<template>
  <MarkdownRender :content="md" />
</template>

但是我需要实现对list_item和link进行自定义渲染,那就需要使用到 setCustomComponents 以及对renderer设置custom-id

参考官方的思考block渲染组件:github.com/Simon-He95/…

我们先试试thinking的渲染:

<script setup lang="ts">
import { MarkdownRender, setCustomComponents } from 'markstream-vue'
import ThinkingNode from './ThinkingNode.vue'
import 'markstream-vue/index.css'

const md = `
<thinking>
这是推理内容
</thinking>


请选择办理方式
- 线上办理
- 线下办理

<a data-send="其他办理方式">其他办理方式</a>
`
setCustomComponents('doc', {
  thinking: ThinkingNode,
})
</script>

<template>
  <MarkdownRender custom-id="doc" :content="md" :custom-html-tags="['thinking']" />
</template>

image.png

没啥问题,接下来对link节点自定义,我们只需要直接去官方仓库copy出来改就好了

github.com/Simon-He95/…

因为我们不需要花里胡哨的特效,这里对源代码进行删减,并且对点击事件进行调整

// LinkNode.vue
<script setup lang="ts">
import { HtmlInlineNode, ImageNode, StrikethroughNode, StrongNode, TextNode } from 'markstream-vue'
import { computed, useAttrs } from 'vue'
import { extractAnchorAttributes } from './utils.ts'

export interface LinkNodeNode {
  type: 'link'
  href: string
  title: string | null
  text: string
  children: { type: string, raw: string }[]
  raw: string
  loading?: boolean
}

// 定义链接节点
export interface LinkNodeProps {
  node: LinkNodeNode
  indexKey?: number | string
  customId?: string
  showTooltip?: boolean
  color?: string
}

// 接收props — 把动画/颜色相关配置暴露为props,并通过CSS变量注入样式
const props = withDefaults(defineProps<LinkNodeProps>(), {
  showTooltip: true,
})

const emits = defineEmits<{
  send: [text: string]
}>()

const cssVars = computed(() => {
  return {
    '--link-color': props.color ?? '#0366d6',
  } as Record<string, string>
})

// Available node components for child rendering
const nodeComponents = {
  text: TextNode,
  strong: StrongNode,
  strikethrough: StrikethroughNode,
  image: ImageNode,
  html_inline: HtmlInlineNode,
}

// forward any non-prop attributes (e.g. custom-id) to the rendered element
const attrs = useAttrs()

const linkAttrs = computed(() => extractAnchorAttributes(props.node.raw))

const linkHref = computed(() => {
  const href = props.node.href
  if (href?.startsWith('javascript:') || !href) {
    // 如果 href 以 javascript: 开头,一律返回 void(0) 防止执行恶意代码
    return 'javascript:void(0)'
  }
  return href
})

const linkTarget = computed(() => {
  if (linkAttrs.value?.target) {
    return linkAttrs.value?.target
  }
  if (!props.node.href) {
    return '_self'
  }
  return '_blank'
})

const title = computed(() => String(props.node.title ?? props.node.href ?? ''))

function onClick() {
  if (linkAttrs.value?.['data-send']) {
    emits('send', linkAttrs.value?.['data-send'])
  }
}
</script>

<template>
  <a
    v-if="!node.loading"
    class="link-node"
    :href="linkHref"
    :target="linkTarget"
    rel="noopener noreferrer"
    :title="showTooltip ? '' : title"
    :aria-label="`Link: ${title}`"
    :aria-hidden="node.loading ? 'true' : 'false'"
    v-bind="attrs"
    :style="cssVars"
    @click="onClick"
  >
    <component
      :is="nodeComponents[child.type]"
      v-for="(child, index) in node.children"
      :key="`${indexKey || 'emphasis'}-${index}`"
      :node="child"
      :custom-id="props.customId"
      :index-key="`${indexKey || 'link-text'}-${index}`"
    />
  </a>
  <span v-else class="link-loading inline-flex items-baseline gap-1.5" :aria-hidden="!node.loading ? 'true' : 'false'" v-bind="attrs" :style="cssVars">
    <span class="link-text-wrapper relative inline-flex">
      <span class="link-text leading-[normal]">
        <span class="link-text leading-[normal]">{{ node.text }}</span>
      </span>
    </span>
  </span>
</template>

<style scoped>
.link-node {
  color: var(--link-color, #0366d6);
  text-decoration: none;
}

.link-loading .link-text-wrapper {
  position: relative;
}

.link-loading .link-text {
  position: relative;
  z-index: 2;
}
</style>
// utils.ts
export function extractAnchorAttributes(str: string) {
  // 1. 基础类型检查
  if (!str)
    return null

  // 去除首尾空格,方便处理
  const trimmed = str.trim()

  // 2. 快速判断:必须以 <a 开头(忽略大小写),且后面必须跟空白字符
  // 这一步只做检测,不提取,避免复杂的正则回溯
  if (!/^<a\s/i.test(trimmed)) {
    return null
  }

  // 3. 提取 <a ... > 中间的内容
  // 既然我们已经确信它是 <a 开头,直接找第一个 > 的位置即可
  // 这种使用 indexOf + slice 的方式是 O(n) 的,绝对没有 ReDoS 风险
  const closeTagIndex = trimmed.indexOf('>')
  if (closeTagIndex === -1)
    return null

  // 截取 <a 和 > 中间的字符串
  // <a href="..."> -> 截取 " href="..."
  // 我们不需要精确去掉 <a 后的空格,因为后面的属性正则会自动处理空格
  const attrString = trimmed.slice(2, closeTagIndex)

  // 4. 解析属性键值对
  const attributes: Record<string, string> = {}

  // 优化后的正则:
  // ([^\s=]+)  匹配属性名 (非空格、非等号)
  // \s*=\s*    匹配等号及周围空格
  // (["'])     捕获引号
  // (.*?)      捕获值 (非贪婪)
  // \2         匹配闭合引号
  const attrRegex = /([^\s=]+)\s*=\s*(["'])(.*?)\2/g

  // 5. 使用 matchAll 替代 while 赋值循环
  // matchAll 返回一个迭代器,完美解决 ESLint no-cond-assign 问题
  const matches = attrString.matchAll(attrRegex)

  for (const match of matches) {
    if (!match[1])
      continue
    // match[1] 是 key, match[3] 是 value
    attributes[match[1]] = match[3] || ''
  }

  return attributes
}

因为我的业务需要将一些属性加到标签的属性上,这里我让哈基米3pro帮我写了一段正则,从raw的html上面提取属性。

很完美,可以正常捕获点击了,但是如果像上面那样配置thinking节点一样配置,我们就没办法拿到事件了,这里我们要取个巧,在外面将这个link组件包多一层,拿到emit:

<script setup lang="ts">
import { h } from 'vue'
import { MarkdownRender, setCustomComponents } from 'markstream-vue'
import ThinkingNode from './ThinkingNode.vue'
import LinkNode from './LinkNode.vue'
import 'markstream-vue/index.css'
import type { LinkNodeNode } from './LinkNode.vue'

const md = `
<thinking>
这是推理内容
</thinking>


请选择办理方式
- 线上办理
- 线下办理

<a data-send="其他办理方式">其他办理方式</a>
`
setCustomComponents('doc', {
  thinking: ThinkingNode,
  link: (props: { node: LinkNodeNode }) => h(LinkNode, {
    ...props,
    onSend(text) {
      console.log(text)
    },
  }),
})
</script>

<template>
  <MarkdownRender custom-id="doc" :content="md" :custom-html-tags="['thinking']" />
</template>

list_item也是类似的操作,不过有点区别是需要自定义list节点,没办法单独自定义list_item,不过方法也是类似的,把源代码copy出来改就可以:

// ListNode.vue
<script setup lang="ts">
import ListItemNode from './ListItemNode.vue'

// 节点子元素类型
interface NodeChild {
  type: string
  raw: string
  [key: string]: unknown
}

// 列表项类型
interface ListItem {
  type: 'list_item'
  children: NodeChild[]
  raw: string
}

const { node, customId, indexKey, typewriter } = defineProps<{
  node: {
    type: 'list'
    ordered: boolean
    start?: number
    items: ListItem[]
    raw: string
  }
  customId?: string
  indexKey?: number | string
  typewriter?: boolean
}>()

defineEmits(['copy'])
</script>

<template>
  <component
    :is="node.ordered ? 'ol' : 'ul'"
    class="list-node"
    :class="{ 'list-decimal': node.ordered, 'list-disc': !node.ordered }"
  >
    <ListItemNode
      v-for="(item, index) in node.items"
      :key="`${indexKey || 'list'}-${index}`"
      v-memo="[item]"
      :item="item"
      :custom-id="customId"
      :index-key="`${indexKey || 'list'}-${index}`"
      :typewriter="typewriter"
      :value="node.ordered ? (node.start ?? 1) + index : undefined"
      @copy="$emit('copy', $event)"
    />
  </component>
</template>

<style scoped>
.list-node {
  @apply my-5 pl-[calc(13/8*1em)];
}
.list-decimal {
  list-style-type: decimal;
}
.list-disc {
  list-style-type: disc;
  @apply max-lg:my-[calc(4/3*1em)] max-lg:pl-[calc(14/9*1em)];
}
</style>
<script setup lang="ts">
import { computed, watchEffect } from 'vue'
import { MarkdownRender } from 'markstream-vue'

// 节点子元素类型
interface NodeChild {
  type: string
  raw: string
  [key: string]: unknown
}

// 列表项类型
interface ListItem {
  type: 'list_item'
  children: NodeChild[]
  raw: string
}

const props = defineProps<{
  item: ListItem
  indexKey?: number | string
  value?: number
  customId?: string
  /** Forwarded flag to enable/disable non-code node enter transition */
  typewriter?: boolean
}>()

defineEmits<{
  copy: [text: string]
}>()

const liValueAttr = computed(() =>
  props.value == null ? {} : { value: props.value },
)

function onClick() {
  console.log(props.item.children[0]?.raw)
}
</script>

<template>
  <li
    class="my-2 list-item pl-1.5"
    dir="auto"
    v-bind="liValueAttr"
    @click.capture="onClick"
  >
    <MarkdownRender :nodes="item.children" />
  </li>
</template>

<style scoped>
ol > .list-item::marker {
  color: var(--list-item-counter-marker, #64748b);
  line-height: 1.6;
}
ul > .list-item::marker {
  color: var(--list-item-marker, #cbd5e1);
}

/* 大列表滚动到视口时,嵌套 NodeRenderer 需要立即绘制内容,避免空白 */
.list-item :deep(.markdown-renderer) {
  content-visibility: visible;
  contain-intrinsic-size: 0px 0px;
  contain: none;
}
</style>

vmr组件

除了这些原生的html节点之外,还有一个叫vmr组件的东西,可以渲染一些自定义的组件,使用文档可以看:

markstream-vue-docs.simonhe.me/zh/guide/co…

我们要接受emit也是同样的道理

const vmrComponents = {
// 你的一些业务组件
}
setCustomComponents(customIdComputed.value, {
  // ...
  vmr_container: ({ node }: { node: any }) => {
    const component = props.vmrComponents?.[node.name] || (() => h('div', {}, 'unknown component'))
    return h(component, {
      node,
      onEvent(value: VmrComponentEvent) {
        emits('event', value)
      },
    })
  },
})

因为我是将marksteam组件封装多了一层,作为公共组件,所以我就将所有业务相关的事件封装到一个emit事件里面了,然后使用ts对事件做区分,实现类型提示:

export type MarkdownComponentEvent = EventA | EventB

interface EventA {
  eventName: 'a'
  eventValue: {
    // ...
  }
}

interface EventB {
  eventName: 'b'
  eventValue: {
    // ...
  }
}

十问2026中国电影

本文来自微信公众号:壹娱观察,作者:路柯,原文标题:《十问2026中国电影:500亿票房还能达成吗?100亿影片还会有吗?》,题图来自:视觉中国


没有人能否认2025年电影市场给出的“意外惊喜”有多巨大——518.32亿总票房成功重返高位,其中《哪吒之魔童闹海》以154亿元的单片成绩刷新华语影史纪录、成为今年全球票房TOP1且跃升至全球影史票房TOP5。


图源:灯塔专业版


这一成绩不仅印证了观众观影需求的强劲反弹,更展现了头部IP对市场大盘的强大拉动能力。


高光数据背后的结构性问题同样不容忽视。超级爆款的虹吸效应显著,导致头部资源过度集中,大量中小成本影片面临排片挤压、票房惨淡的生存困境,百花齐放的市场环境似乎没有了,市场生态的均衡性亟待改善。更致命的是,不少倾注心力的工业化大片纷纷遭遇票房折戟,情绪、口碑、尺度、系列化如何拉动观影成了玄学,市场上出现了“片名完全决定成败”等哭笑不得的现象。


“一家独大、众星黯淡”的格局,也为行业可持续发展埋下了隐患。


2025年票房排行(前20)图源:灯塔专业版


与此同时,资本的哨声也在释放信号。改变了市场格局的阿里影业直接“变身”为大麦娱乐,电影进一步明确线下场景枢纽作用;儒意收购万达电影之后终于拿出连环炮弹,除了重磅片单安抚军心之外,加强院线场景优势、投潮玩、跨界游戏等举动频发,意图让市场看到“超级娱乐空间”的蓝图展开;华谊兄弟的资本状况再度引发高关注,王忠磊、王晓蓉夫妇在短视频上蠢蠢欲动,连连引发大佬何时直播带货自救的热议……似乎每一年都在见证历史,但每一次的历史性事件背后都不难听到一声叹息。


步入2026年,中国电影行业将在复苏的基础上迎接更深层次的考验。从票房能否延续500亿规模的业绩压力,到传统影企转型与新兴势力崛起的格局重塑,再到IP谷子经济的潮水涌向、AI技术落地带来的创作变革,诸多不确定性持续交织叠加……如何在变化中寻找确定性,无疑要成为2026年贯穿全年的核心命题。


总票房超过500亿,2026年能够再创造吗?还会有单片破100亿吗?


2025年春节,当《哪吒2》最终将华语片票房冠军的天花板史无前例地托高到150亿之时,业内一片欢欣鼓舞,紧随其后就是另外一种声音:《哪吒2》单片票房如此之高时,一个可能发生的事实将是,这一年的其他电影可能会遭遇票房总体量不高的事实。


又一个反驳的观点是,如果不是《哪吒2》票房150亿,那么,2025年的整体票房绝不会达到500亿。


这其实反映了电影市场的一个悖论,害怕一部或者几部影片占据过高票房比,但又害怕全年的整体票房不如意。


所以,对2026年的一个最重要期待是,这一年的整体票房会延续甚至高于2025年的500亿吗?如果答案为是,那么完成这一重要KPI的就需要,一部单片继续完成《哪吒2》票房的伟业,或者3部影片完成《战狼2》超50亿票房的成绩。当然还有另外一个情况,那就是多部影片达成30亿+的成绩。


抱歉,目前国产电影片单卖相并没有几部能有这样的野望,值得注意的是如《澎湖海战》这般直贴当下民族情绪的影片能否如愿爆发,去年是“抗日”,今年无疑与《澎湖海战》早早释出的海报宣传语有关。与此同时,好在2026年好莱坞电影将是大年,其中能否有单片实现30亿+任务,或许是2026年整体目标的最大变量。


最多钱的王长田,能在2026年出手收购吗?


曾几何时的“中国五大”或者“中国六大”的说法早已成为了历史,但这其中自始至终都有一家常青树,那就是王长田和其带领的光线传媒。截至目前,光线传媒市值高达548亿,截止2025三季度,广义上的现金储备高达42亿之多,是电影行业中的市值王和现金王。


《哪吒2》刷新中国票房冠军,让王长田有着史无前例的勇气,完成其30-50年计划要打造的“中国神话宇宙”计划。


过往一段时间里,光线传媒是整个电影大厂中最抠门的一家,这指代着对电影投资的谨慎甚至内部员工福利,但在涉及公司战略层面上的并购上,光线从来没有手软过。2016年光线以现金+股票的组合方式,成功拿下了猫眼电影;同样也是在这数年期间,光线扫货般地投资了数十家动画电影制作公司。


光线传媒2025年Q3财报


以上这两笔动作,让光线分别拿到了两张船票,那么,2026年光线会有收购动作,还是依旧保守的存在银行里?


如果有,那么,路线也不复杂,凡事有助于动画IP的全产业链运用动作,都可以是未来光线出手并购的方向。


除此之外,王长田能否更大胆一点,去链接更多的多元娱乐视角,也是存在可能。


电影市场都希望“当下一哥”是时候出手就出手吧。


博纳会成为下一个华谊兄弟吗?传统电影公司靠近生死线?


2025年是光线传媒的高光时刻,也是另一家电影大厂的悲情时刻。


去年春节,博纳电影寄予厚望的《蛟龙行动》彻底不灵了,春节档上映时最终票房仅为2.74亿元,而选择重映之后总票房也没能超过4亿元。


这部《红海行动》续集大作的失败,着实有着多重警示意义。其一代表着过去几年大行其道的主旋律,在这一刻进入到了一个逼仄道路之中,其二则是,与电影票房失败同步进行的是博纳这家公司财务状况的恶化。


所以,当市场问到博纳会是下一个华谊兄弟吗?或许可以是情况并没有那么糟糕,但的确有滑落为一家常年失败公司的风险。据数据统计,自2022年博纳完成A股上市后,博纳亏损已超26亿,耗资不菲的《蛟龙行动》无疑进一步加重了这家公司的亏损。


最大的行业警示意义在于,一部大片的失败就让博纳这家传统大厂财务状况不佳,那更危险的地方或许在于,那些中小电影制作公司,在大环境不佳的情况下,状况只能更加不好,所谓行业的斩杀线已经越发脆弱。


中国工业化大片陨落,会伤到信心吗?


市场对于2025年一开始的预期并不弱,毕竟有如《蛟龙行动》《封神2》《东极岛》《刺杀小说家2》等一系列工业化大片待映,结果这几部电影纷纷遭遇了陨落,甚至三分之二连4亿的坎都没迈过。


这些中国式大片的陨落会影响到信心吗?当然会。


在电影已然是高风险游戏的当下,投资方自然会收缩对大片的投资,创作者也进一步会束手束脚。这也导致2026年工业化大片屈指可数,堆砌明星阵容亦或再度成为大片主流。


中国电影某种程度的工业化信心受损,或许不是要不要工业化的错,而是这些片子在或故事或表达层面的纰漏,都拖了工业大片的后腿。所以,摆在中国电影面前的那一道,讲好故事还是第一防线,之于2026年,作出更多故事性的试验,比急于强调自身工业化更紧急,更何况好莱坞在这一年格外凶猛。


动画大年之后,2026年类型化靠什么接住?


从年初《哪吒2》到暑期档《浪浪山小妖怪》《罗小黑战记2》再到年末《疯狂动物城2》,2025年成为当之无愧的动画大年,那么,新的这一年,动画电影依然会成为行业的一抹亮色吗?


虽然市场不再会立马出现第二个《哪吒2》,但华语动画电影的路径,早已丰富颇多、走向成熟,在神话题材之外,现实题材、东方表达、世界观开创等都可以成为动画电影的未来,另一面,除去光线系和追光系之外,也有更多的动画制作团队展露头角。


动画电影正在摆脱对单一爆款的依赖,用体系化、类型化发展接住大年红利,通过技术创新拓宽表达边界,以题材细分覆盖全龄受众,构建“续作扛鼎+新作突围”的健康生态。


那么,2026还有什么类型化能顺利引爆市场呢?恐怖、惊悚类型的升级或许是一大看点,毕竟这几年总有几部超低成本的恐怖片成功撬动票仓,而今年开年《闪灵》的4K焕新上映显然打开了口子,感官配合尺度刺激,才是走进电影院的好理由。


另一个需要关注的是香港电影的荣光。2025年《捕风追影》的成功就是一例,但在2026香港电影片单上《寒战1994》《九龙城寨之终章》《怒火漫延》等过往成功系列作品的连番上映,将进一步冲刺票仓。


当下电影票房的最理想保底牌到底是什么——顶流?情怀?尺度?圈层?


电影市场有绝对的成功公式吗?那当然是没有。


但是,有些“保底”其实值得观望。顶流依旧好用,肖战、易烊千玺便是今年的绝好例子,但是片方不可贪;情怀发挥重要作用,《疯狂动物城2》显然是今年市场的最佳案例,年末的《寻秦记》虽然没有创造奇迹,依旧关注度不俗,不少中年男性还是打开钱包;围绕“尺度”做文章继续奏效,《匿杀》拿着系列尺度的标签,笑傲档期;圈层狂欢,显然是意外惊喜,《鬼灭之刃:无限列车》到来,充分显示了二次元人群的扛鼎作用……


2026中国电影市场,需要珍贵“小而美”的力量,其背后是足以粉丝影响路人的爆发性体现,当大众注意力越来越分层,拥有保底牌的电影是否就该从满足众多角落里的“小美好”出发,拿出极致产品,才是走向大众的理想通路。


关注票房,不如关注票房之外的谷子经济?


随着《浪浪山小妖怪》票房大丰收之后,无论是其衍生品还是与茶饮品牌的联名,都让其在票房之外收获了更大确定性的收入,这事的意义在于证明了属于中国电影的“谷子经济”已然成型。“谷子”(Goods音译)不再是观影的附加品,而是成为电影IP的核心盈利环节。


2026年谷子经济将呈现三大趋势:一是开发前置化,电影创作阶段就同步规划衍生品;二是品类多元化,从玩偶、卡牌延伸到文旅场景、VR互动;三是生态闭环化,通过主题展、沉浸式影院等场景,实现“观影-消费-互动”的全链路变现。


艾媒咨询《2024-2025年中国谷子经济市场分析》


对于动画IP而言,衍生品收入甚至可能反哺续作开发,形成“内容-衍生-内容”的良性循环,这应当成为电影人去不断提升非票房收入的一条康庄大道。


成本估算,控本继续,进一步思考3~5亿票房的赚钱术?


从2025年票房呈现来看,3~5亿票房成为了集中区间,其中有黑马奔腾,也有不少“将军”掉马,中国电影到了不能贪大钱、把握稳量收入的清醒认识时刻。


3~5亿票房相当于成本在于1亿上下,放在当下,需要被视作中等影片,这其实也提醒了未来中国电影市场一个理想结构那就是,得想尽办法让中等影片赚到钱,先理清楚自己的“底”该如何保住,基于此做好成本规划,别急于贪心想着“我就能博大”。


图源:猫眼专业版


AI电影,会在中国市场奏响吗?


在去年年底的海南国际电影节上,卡梅隆谈及到了AI之于电影的能量,他说,“AI或许能生成一部看起来像阿凡达的电影,但在那样的作品真正诞生之前,AI是无法凭空创造出它的”。


另一方面,已经有颇多电影都打着AIGC创作上院线的噱头,比如4月24日,70分钟长片《海上女王郑一嫂》在新加坡上映,作为全球首部政府批准走进院线公映的AIGC大电影,而号称中国首部院线AIGC动画电影《团圆令》已于2025年12月20日在北京举行点映礼……


AI之于电影业的爆炸影响,在全世界而言,似乎就差那么一部叫好又叫座的AI院线电影了。其实也可以沿用那句话:市场低估了AI对电影的远期影响,也高估了AI对电影的当下影响。


所以有理由期待的是,2026年会是AI技术在行业落地的一个关键年份,这有关技术带来的降本增效,也有关AI完全创作出一部令市场称赞的电影作品。


中国电影人也都在响应和回答AI了,但请注意,谁都没法一口气撑起来,不如好好从“降本”开始落地。


新一轮电影扛起者,还没轮到90后吗?


电影市场的另一个老旧层面无疑表现在头部创作者的年龄层,从2025年来看,成功跑出来的年轻一代创作者只有申奥一人,而申奥放在其他行业里,能被称为1986年的“中登”了。


显然,中国电影市场进入了新一轮“中登”时代,似乎是陈思诚领军、申奥收尾,其中衔接着郭帆、贾玲、韩寒、文牧野等一批80后创作者,各自在创作上有着截然不同的擅长“手艺”,但他们的确算得上当下电影市场卖座的金字招牌。


不得不承认,这批80后导演也都或多或少享受过电影上行期最好的红利,而在这个残酷的下行周期,还在霸榜的他们之于中国电影市场是一件好事吗?为何90后创作者就断层了呢?路径哪里出了问题?是因为观影人群逐渐中老年化,所以,创作者还是这一波才能稳住吗?


这或许是对于2026电影市场的最需要紧急回应的叩问。


掌握较高话语权的“中登”们,能否在保证收益的前提下,跳出舒适区,推动内容品类的多元化创新,而不是一遍又一遍的诉说着自己的爱好、主张着自己的价值观,毕竟,也只有你们拥有大胆试验的最大可能了。


结语


2026年的中国电影市场,是否能延续500亿票房的高歌仍旧变数连连,即便如此,也有声音在喊百亿真人电影近在眼前,毕竟也有武侠电影的最高配置与周星驰出山等利好消息。然而,注意力转移、企业转型、技术变革、市场冷热等带来的不确定性仍让人害怕中国电影市场面临又一次的崩塌。


2025年让电影市场好好缓了一口气,其暴露出的结构性问题将是2026年最大的挑战。


要让电影投资从高风险理财到确定性收益,是重构中国电影发展的关键性要素之一,背后则是成本控制与多元变现的双重思衡,在此之下,每个电影人也没有太多踌躇时刻了,面对现实、厘清幻想,遵循“内容为王、效率至上”的底层逻辑,拿出自己的手艺,脚踏实地往前走吧。


本文来自微信公众号:壹娱观察,作者:壹叔团队

下载虎嗅APP,第一时间获取深度独到的商业科技资讯,连接更多创新人群与线下活动

Vue 的 nextTick:破解异步更新的玄机

一、先看现象:为什么数据变了,DOM 却没更新?

<template>
  <div>
    <div ref="message">{{ msg }}</div>
    <button @click="changeMessage">点击我</button>
  </div>
</template>

<script>
export default {
  data() {
    return { msg: '初始消息' }
  },
  methods: {
    changeMessage() {
      this.msg = '新消息'
      console.log('数据已更新:', this.msg)
      console.log('DOM内容:', this.$refs.message?.textContent) // 还是'初始消息'!
    }
  }
}
</script>

执行结果:

数据已更新: 新消息
DOM内容: 初始消息  ← 问题在这里!

数据明明已经改了,为什么 DOM 还是旧值?这就是 nextTick 要解决的问题。

二、核心原理:Vue 的异步更新队列

Vue 的 DOM 更新是异步的。当你修改数据时,Vue 不会立即更新 DOM,而是:

  1. 开启一个队列,缓冲同一事件循环中的所有数据变更
  2. 移除重复的 watcher,避免不必要的计算
  3. 下一个事件循环中,刷新队列并执行实际 DOM 更新
// Vue 内部的简化逻辑
let queue = []
let waiting = false

function queueWatcher(watcher) {
  // 1. 去重
  if (!queue.includes(watcher)) {
    queue.push(watcher)
  }
  
  // 2. 异步执行
  if (!waiting) {
    waiting = true
    nextTick(flushQueue)
  }
}

function flushQueue() {
  queue.forEach(watcher => watcher.run())
  queue = []
  waiting = false
}

三、nextTick 的本质:微任务调度器

nextTick 的核心任务:在 DOM 更新完成后执行回调

// Vue 2.x 中的 nextTick 实现(简化版)
let callbacks = []
let pending = false

function nextTick(cb) {
  callbacks.push(cb)
  
  if (!pending) {
    pending = true
    
    // 优先级:Promise > MutationObserver > setImmediate > setTimeout
    if (typeof Promise !== 'undefined') {
      Promise.resolve().then(flushCallbacks)
    } else if (typeof MutationObserver !== 'undefined') {
      // 用 MutationObserver 模拟微任务
    } else {
      setTimeout(flushCallbacks, 0)
    }
  }
}

function flushCallbacks() {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  copies.forEach(cb => cb())
}

四、四大核心使用场景

场景1:获取更新后的 DOM

<script>
export default {
  methods: {
    async updateAndLog() {
      this.msg = '更新后的消息'
      
      // ❌ 错误:此时 DOM 还未更新
      console.log('同步获取:', this.$refs.message.textContent)
      
      // ✅ 正确:使用 nextTick
      this.$nextTick(() => {
        console.log('nextTick获取:', this.$refs.message.textContent)
      })
      
      // ✅ 更优雅:async/await 版本
      this.msg = '另一个消息'
      await this.$nextTick()
      console.log('await获取:', this.$refs.message.textContent)
    }
  }
}
</script>

场景2:操作第三方 DOM 库

<template>
  <div ref="chartContainer"></div>
</template>

<script>
import echarts from 'echarts'

export default {
  data() {
    return { data: [] }
  },
  
  async mounted() {
    // ❌ 错误:容器可能还未渲染
    // this.chart = echarts.init(this.$refs.chartContainer)
    
    // ✅ 正确:确保 DOM 就绪
    this.$nextTick(() => {
      this.chart = echarts.init(this.$refs.chartContainer)
      this.renderChart()
    })
  },
  
  methods: {
    async updateChart(newData) {
      this.data = newData
      
      // 等待 Vue 更新 DOM 和图表数据
      await this.$nextTick()
      
      // 此时可以安全操作图表实例
      this.chart.setOption({
        series: [{ data: this.data }]
      })
    }
  }
}
</script>

场景3:解决计算属性依赖问题

<script>
export default {
  data() {
    return {
      list: [1, 2, 3],
      newItem: ''
    }
  },
  
  computed: {
    filteredList() {
      // 依赖 list 的变化
      return this.list.filter(item => item > 1)
    }
  },
  
  methods: {
    async addItem(item) {
      this.list.push(item)
      
      // ❌ filteredList 可能还未计算完成
      console.log('列表长度:', this.filteredList.length)
      
      // ✅ 确保计算属性已更新
      this.$nextTick(() => {
        console.log('正确的长度:', this.filteredList.length)
      })
    }
  }
}
</script>

场景4:优化批量更新性能

// 批量操作示例
async function batchUpdate(items) {
  // 开始批量更新
  this.updating = true
  
  // 所有数据变更都在同一个事件循环中
  items.forEach(item => {
    this.dataList.push(processItem(item))
  })
  
  // 只触发一次 DOM 更新
  await this.$nextTick()
  
  // 此时 DOM 已更新完成
  this.updating = false
  this.showCompletionMessage()
  
  // 继续其他操作
  await this.$nextTick()
  this.triggerAnimation()
}

五、性能陷阱与最佳实践

陷阱1:嵌套的 nextTick

// ❌ 性能浪费:创建多个微任务
this.$nextTick(() => {
  // 操作1
  this.$nextTick(() => {
    // 操作2
    this.$nextTick(() => {
      // 操作3
    })
  })
})

// ✅ 优化:合并到同一个回调中
this.$nextTick(() => {
  // 操作1
  // 操作2  
  // 操作3
})

陷阱2:与宏任务混用

// ❌ 顺序不可控
this.msg = '更新'
setTimeout(() => {
  console.log(this.$refs.message.textContent)
}, 0)

// ✅ 明确使用 nextTick
this.msg = '更新'
this.$nextTick(() => {
  console.log(this.$refs.message.textContent)
})

最佳实践:使用 async/await

methods: {
  async reliableUpdate() {
    // 1. 更新数据
    this.data = await fetchData()
    
    // 2. 等待 DOM 更新
    await this.$nextTick()
    
    // 3. 操作更新后的 DOM
    this.scrollToBottom()
    
    // 4. 如果需要,再次等待
    await this.$nextTick()
    this.triggerAnimation()
    
    return '更新完成'
  }
}

六、Vue 3 的变化与优化

Vue 3 的 nextTick 更加精简高效:

// Vue 3 中的使用
import { nextTick } from 'vue'

// 方式1:回调函数
nextTick(() => {
  console.log('DOM 已更新')
})

// 方式2:Promise
await nextTick()
console.log('DOM 已更新')

// 方式3:Composition API
setup() {
  const handleClick = async () => {
    state.value = '新值'
    await nextTick()
    // 操作 DOM
  }
  
  return { handleClick }
}

Vue 3 的优化:

  • 使用 Promise.resolve().then() 作为默认策略
  • 移除兼容性代码,更小的体积
  • 更好的 TypeScript 支持

七、源码级理解

// Vue 2.x nextTick 核心逻辑
export function nextTick(cb, ctx) {
  let _resolve
  
  // 1. 将回调推入队列
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  
  // 2. 如果未在等待,开始异步执行
  if (!pending) {
    pending = true
    timerFunc() // 触发异步更新
  }
  
  // 3. 支持 Promise 链式调用
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

八、实战:手写简易 nextTick

class MyVue {
  constructor() {
    this.callbacks = []
    this.pending = false
  }
  
  $nextTick(cb) {
    // 返回 Promise 支持 async/await
    return new Promise(resolve => {
      const wrappedCallback = () => {
        if (cb) cb()
        resolve()
      }
      
      this.callbacks.push(wrappedCallback)
      
      if (!this.pending) {
        this.pending = true
        
        // 优先使用微任务
        if (typeof Promise !== 'undefined') {
          Promise.resolve().then(() => this.flushCallbacks())
        } else {
          setTimeout(() => this.flushCallbacks(), 0)
        }
      }
    })
  }
  
  flushCallbacks() {
    this.pending = false
    const copies = this.callbacks.slice(0)
    this.callbacks.length = 0
    copies.forEach(cb => cb())
  }
  
  // 模拟数据更新
  async setData(key, value) {
    this[key] = value
    await this.$nextTick()
    console.log(`DOM 已更新: ${key} = ${value}`)
  }
}

总结

nextTick 的三层理解:

  1. 表象层:在 DOM 更新后执行代码
  2. 原理层:Vue 异步更新队列的调度器
  3. 实现层:基于 JavaScript 事件循环的微任务管理器

使用原则:

  1. 需要访问更新后的 DOM 时,必须用 nextTick
  2. 操作第三方库前,先等 Vue 更新完成
  3. 批量操作后,用 nextTick 统一处理副作用
  4. 优先使用 async/await 语法,更清晰直观

一句话概括:

nextTick 是 Vue 给你的一个承诺:"等我把 DOM 更新完,再执行你的代码"。

记住这个承诺,你就能完美掌控 Vue 的更新时机。


思考题: 如果连续修改同一个数据 1000 次,Vue 会触发多少次 DOM 更新? (答案:得益于 nextTick 的队列机制,只会触发 1 次)

Vue 技巧揭秘:一个事件触发多个方法,你竟然还不知道?

解锁 v-on 的高级用法,让你的 Vue 代码更加优雅高效!

前言

在 Vue 开发中,v-on(或 @)是我们最常用的指令之一。但你是否曾经遇到过这样的场景:一个按钮点击后需要执行多个操作?比如点击"提交"按钮时,既要验证表单,又要发送请求,还要显示加载状态。

你会怎么处理?嵌套调用?写一个包装函数?其实,Vue 早就为我们提供了更优雅的解决方案!

一、答案是肯定的:可以绑定多个方法!

让我们直接看答案:Vue 的 v-on 确实可以绑定多个方法,而且有不止一种实现方式。

先来看一个最常见的需求场景:

<template>
  <div>
    <!-- 常见的“不优雅”做法 -->
    <button @click="handleSubmit">
      提交订单
    </button>
    
    <!-- 更优雅的多方法绑定 -->
    <button @click="validateForm(), submitData(), logActivity()">
      智能提交
    </button>
  </div>
</template>

<script>
export default {
  methods: {
    handleSubmit() {
      // 传统方式:把所有逻辑写在一个方法里
      this.validateForm()
      this.submitData()
      this.logActivity()
    },
    
    validateForm() {
      console.log('验证表单...')
    },
    
    submitData() {
      console.log('提交数据...')
    },
    
    logActivity() {
      console.log('记录用户行为...')
    }
  }
}
</script>

二、四种实现方式详解

方式一:直接调用多个方法(最简洁)

<template>
  <button @click="method1(), method2(), method3()">
    点击执行三个方法
  </button>
</template>

特点:

  • ✅ 最直观,直接在模板中调用
  • ✅ 可以传递参数
  • ❌ 模板会显得有点"拥挤"

示例:

<template>
  <div>
    <!-- 传递参数 -->
    <button @click="
      logClick('按钮被点击了'),
      incrementCounter(1),
      sendAnalytics('button_click')
    ">
      带参数的多方法调用
    </button>
    
    <!-- 访问事件对象 -->
    <button @click="
      handleClick1($event),
      handleClick2($event),
      preventDefaults($event)
    ">
      使用事件对象
    </button>
  </div>
</template>

<script>
export default {
  methods: {
    logClick(message) {
      console.log(message)
    },
    incrementCounter(amount) {
      this.count += amount
    },
    sendAnalytics(eventName) {
      // 发送分析数据
    },
    preventDefaults(event) {
      event.preventDefault()
    }
  }
}
</script>

方式二:调用一个包装函数(最传统)

<template>
  <button @click="handleAllMethods">
    包装函数方式
  </button>
</template>

<script>
export default {
  methods: {
    handleAllMethods() {
      this.method1()
      this.method2()
      this.method3()
    },
    method1() { /* ... */ },
    method2() { /* ... */ },
    method3() { /* ... */ }
  }
}
</script>

适用场景:

  • 方法之间有复杂的执行顺序
  • 需要条件判断
  • 需要错误处理

示例:

<script>
export default {
  methods: {
    async handleComplexClick() {
      // 1. 先验证
      const isValid = this.validateForm()
      if (!isValid) return
      
      // 2. 显示加载
      this.showLoading = true
      
      try {
        // 3. 执行多个异步操作
        await Promise.all([
          this.submitData(),
          this.logActivity(),
          this.updateCache()
        ])
        
        // 4. 显示成功提示
        this.showSuccess()
      } catch (error) {
        // 5. 错误处理
        this.handleError(error)
      } finally {
        // 6. 隐藏加载
        this.showLoading = false
      }
    }
  }
}
</script>

方式三:使用对象语法(最灵活)

<template>
  <button v-on="{ click: [method1, method2, method3] }">
    对象语法(数组形式)
  </button>
  
  <!-- 或者 -->
  <button v-on="eventHandlers">
    对象语法(响应式对象)
  </button>
</template>

<script>
export default {
  data() {
    return {
      eventHandlers: {
        click: this.handleClick,
        mouseenter: this.handleMouseEnter,
        mouseleave: this.handleMouseLeave
      }
    }
  },
  methods: {
    handleClick() {
      this.method1()
      this.method2()
    },
    method1() { /* ... */ },
    method2() { /* ... */ }
  }
}
</script>

特点:

  • ✅ 可以动态绑定事件处理器
  • ✅ 支持多个不同事件类型
  • ✅ 适合需要动态切换事件处理逻辑的场景

方式四:修饰符组合(最 Vue)

<template>
  <!-- 结合修饰符 -->
  <button @click.stop.prevent="method1(), method2()">
    带修饰符的多方法
  </button>
  
  <!-- 键盘事件多方法 -->
  <input 
    @keyup.enter="
      validateInput(),
      submitForm(),
      clearInput()
    "
    @keyup.esc="cancelEdit(), resetForm()"
  >
</template>

三、实战案例:一个完整的表单组件

让我们来看一个实际的业务场景:

<template>
  <div class="smart-form">
    <form @submit.prevent="handleSmartSubmit">
      <input v-model="formData.email" placeholder="邮箱">
      <input v-model="formData.password" type="password" placeholder="密码">
      
      <button 
        type="submit"
        :disabled="isSubmitting"
        @click="
          $event.stopPropagation(),
          validateBeforeSubmit(),
          trackButtonClick('submit_button')
        "
        @mouseenter="showTooltip = true"
        @mouseleave="showTooltip = false"
      >
        {{ isSubmitting ? '提交中...' : '智能提交' }}
      </button>
      
      <div v-if="showTooltip" class="tooltip">
        点击后执行:验证 → 提交 → 记录 → 分析
      </div>
    </form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      formData: {
        email: '',
        password: ''
      },
      isSubmitting: false,
      showTooltip: false
    }
  },
  
  methods: {
    async handleSmartSubmit() {
      if (this.isSubmitting) return
      
      this.isSubmitting = true
      
      try {
        // 并行执行多个操作
        await Promise.all([
          this.submitToServer(),
          this.logUserActivity(),
          this.updateLocalStorage()
        ])
        
        // 串行执行后续操作
        this.showSuccessMessage()
        this.redirectToDashboard()
        this.sendAnalytics('form_submit_success')
        
      } catch (error) {
        this.handleError(error)
        this.sendAnalytics('form_submit_error', { error: error.message })
      } finally {
        this.isSubmitting = false
      }
    },
    
    validateBeforeSubmit() {
      if (!this.formData.email) {
        throw new Error('邮箱不能为空')
      }
      // 更多验证逻辑...
    },
    
    trackButtonClick(buttonName) {
      console.log(`按钮被点击: ${buttonName}`)
      // 实际项目中这里可能是发送到分析平台
    },
    
    async submitToServer() {
      // API 调用
      const response = await fetch('/api/submit', {
        method: 'POST',
        body: JSON.stringify(this.formData)
      })
      return response.json()
    },
    
    logUserActivity() {
      // 记录用户行为
      console.log('用户提交表单', this.formData)
    },
    
    updateLocalStorage() {
      // 保存到本地
      localStorage.setItem('lastSubmit', new Date().toISOString())
    },
    
    showSuccessMessage() {
      this.$emit('success', '提交成功!')
    },
    
    redirectToDashboard() {
      setTimeout(() => {
        this.$router.push('/dashboard')
      }, 1500)
    },
    
    sendAnalytics(eventName, data = {}) {
      // 发送分析数据
      console.log(`分析事件: ${eventName}`, data)
    },
    
    handleError(error) {
      console.error('提交错误:', error)
      this.$emit('error', error.message)
    }
  }
}
</script>

<style scoped>
.smart-form {
  max-width: 400px;
  margin: 0 auto;
}

.tooltip {
  background: #f0f0f0;
  padding: 8px;
  border-radius: 4px;
  margin-top: 8px;
  font-size: 12px;
  color: #666;
}
</style>

四、最佳实践和注意事项

1. 保持模板简洁

<!-- 不推荐:模板过于复杂 -->
<button @click="
  validateForm($event, formData, true),
  submitForm(formData, config),
  logEvent('submit', { time: Date.now() }),
  showLoading(),
  redirectAfter(3000)
">
  提交
</button>

<!-- 推荐:复杂逻辑放在方法中 -->
<button @click="handleComplexSubmit">
  提交
</button>

2. 错误处理很重要

<script>
export default {
  methods: {
    safeMultiMethods() {
      try {
        this.method1()
        this.method2()
        this.method3()
      } catch (error) {
        console.error('方法执行失败:', error)
        this.handleGracefully(error)
      }
    }
  }
}
</script>

3. 考虑执行顺序

<template>
  <!-- 注意:方法按顺序执行 -->
  <button @click="
    firstMethod(),  // 先执行
    secondMethod(), // 然后执行
    thirdMethod()   // 最后执行
  ">
    顺序执行
  </button>
</template>

4. 异步方法处理

<script>
export default {
  methods: {
    async handleAsyncMethods() {
      // 并行执行
      await Promise.all([
        this.asyncMethod1(),
        this.asyncMethod2()
      ])
      
      // 串行执行
      await this.asyncMethod3()
      await this.asyncMethod4()
    }
  }
}
</script>

五、性能考虑

1. 避免在模板中执行复杂计算

<!-- 不推荐 -->
<button @click="
  heavyCalculation(data),
  processResults(result)
">
  执行
</button>

<!-- 推荐 -->
<button @click="handleHeavyOperations">
  执行
</button>

2. 使用 computed 属性减少重复调用

<script>
export default {
  computed: {
    // 缓存计算结果
    processedData() {
      return this.heavyCalculation(this.data)
    }
  },
  
  methods: {
    handleClick() {
      // 直接使用缓存结果
      this.processResults(this.processedData)
      this.logAction()
    }
  }
}
</script>

六、Vue 3 中的变化

在 Vue 3 的 Composition API 中,用法基本保持一致:

<template>
  <button @click="method1(), method2()">
    Vue 3 多方法
  </button>
</template>

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

const count = ref(0)

const method1 = () => {
  console.log('方法1')
  count.value++
}

const method2 = () => {
  console.log('方法2')
}
</script>

总结

v-on 绑定多个方法的四种方式:

  1. 直接调用多个方法 - 适合简单场景
  2. 包装函数 - 适合复杂逻辑和复用
  3. 对象语法 - 适合动态事件处理
  4. 修饰符组合 - 适合需要事件修饰的场景

选择建议:

  • 简单逻辑:使用方式一(直接调用)
  • 复杂业务:使用方式二(包装函数)
  • 动态需求:使用方式三(对象语法)
  • 事件控制:使用方式四(修饰符组合)

记住,没有绝对的最佳方式,只有最适合当前场景的方式。关键是保持代码的可读性和可维护性。

希望这篇文章能帮助你更好地使用 Vue 的事件处理机制!如果有更多问题或技巧分享,欢迎在评论区讨论。


Vue 中 computed 和 watch 的深度解析:别再用错了!

大家好!今天我们来聊聊 Vue.js 中两个非常重要的概念:computed 和 watch。很多 Vue 初学者甚至有一定经验的开发者,对这两个功能的使用场景和区别仍然存在困惑。

“什么时候用 computed?什么时候用 watch?” 这可能是 Vue 开发者最常遇到的问题之一。

一、先看一个真实场景

假设我们正在开发一个电商网站,需要计算购物车总价:

// 购物车数据
data() {
  return {
    cartItems: [
      { name'商品A'price100quantity2 },
      { name'商品B'price200quantity1 }
    ],
    discount0.9 // 9折优惠
  }
}

现在我们需要计算:

    1. 商品总价(单价×数量之和)
    1. 折后总价

该怎么实现呢?

二、Computed(计算属性):用于派生数据

computed 的核心思想:基于现有数据计算出一个新的数据值

computed: {
  // 计算商品总价
  totalPrice() {
    return this.cartItems.reduce((sum, item) => {
      return sum + item.price * item.quantity
    }, 0)
  },
  
  // 计算折后价
  finalPrice() {
    return this.totalPrice * this.discount
  }
}

computed 的特点:

  1. 1. 声明式编程:你只需要告诉 Vue "我需要什么数据",Vue 会自动处理依赖和更新
  2. 2. 缓存机制:只有当依赖的数据发生变化时,才会重新计算
  3. 3. 同步计算:适合执行同步操作
  4. 4. 返回一个新值:必须返回一个值

三、Watch(侦听器):用于观察数据变化

watch 的核心思想:当某个数据变化时,执行特定的操作

假设我们希望在购物车商品变化时,自动保存到本地存储:

watch: {
  // 深度监听购物车变化
  cartItems: {
    handler(newVal, oldVal) {
      // 保存到本地存储
      localStorage.setItem('cart'JSON.stringify(newVal))
      // 可以发送到服务器
      this.saveCartToServer(newVal)
    },
    deeptrue// 深度监听
    immediatetrue // 立即执行一次
  },
  
  // 监听总价变化
  totalPrice(newPrice) {
    console.log(`总价变为:${newPrice}`)
    if (newPrice > 1000) {
      this.showDiscountTip() // 显示优惠提示
    }
  }
}

watch 的特点:

  1. 1. 命令式编程:你告诉 Vue "当这个数据变化时,执行这些代码"
  2. 2. 无缓存:每次变化都会执行
  3. 3. 可以执行异步操作:适合 API 调用、复杂业务逻辑
  4. 4. 不返回值:主要目的是执行副作用操作

四、核心区别对比表

特性 computed watch
目的 派生新数据 响应数据变化
缓存 ✅ 有缓存 ❌ 无缓存
返回值 ✅ 必须返回值 ❌ 不返回值
异步 ❌ 不支持异步 ✅ 支持异步
语法 函数形式 对象或函数形式
使用场景 模板中的计算逻辑 数据变化时的副作用

五、什么时候用 computed?什么时候用 watch?

使用 computed 的场景:

  1. 1. 模板中需要复杂表达式时

    <!-- 不推荐 -->
    <div>{{ cartItems.reduce((sum, item) => sum + item.price, 0) }}</div>
    
    <!-- 推荐 -->
    <div>{{ totalPrice }}</div>
    
  2. 2. 一个数据依赖多个数据时

    computed: {
      fullName() {
        return this.firstName + ' ' + this.lastName
      }
    }
    
  3. 3. 需要缓存优化性能时

    // 复杂计算只会在依赖变化时执行
    computed: {
      filteredList() {
        // 假设这是很耗时的筛选操作
        return this.hugeList.filter(item => item.active)
      }
    }
    

使用 watch 的场景:

  1. 1. 数据变化时需要执行异步操作

    watch: {
      searchQuery(newQuery) {
        // 防抖搜索
        clearTimeout(this.timer)
        this.timer = setTimeout(() => {
          this.searchAPI(newQuery)
        }, 500)
      }
    }
    
  2. 2. 数据变化时需要执行复杂业务逻辑

    watch: {
      userLevel(newLevel, oldLevel) {
        if (newLevel === 'vip' && oldLevel !== 'vip') {
          this.showVIPWelcome()
          this.sendVIPNotification()
        }
      }
    }
    
  3. 3. 需要观察对象内部变化时

    watch: {
      formData: {
        handler() {
          this.validateForm()
        },
        deeptrue
      }
    }
    

六、常见误区和最佳实践

误区1:用 watch 实现本该用 computed 的功能

// ❌ 不推荐:用 watch 计算全名
data() {
  return {
    firstName: '张',
    lastName: '三',
    fullName: ''
  }
},
watch: {
  firstName() {
    this.fullName = this.firstName + this.lastName
  },
  lastName() {
    this.fullName = this.firstName + this.lastName
  }
}

// ✅ 推荐:用 computed 计算全名
computed: {
  fullName() {
    return this.firstName + ' ' + this.lastName
  }
}

误区2:在 computed 中执行副作用操作

// ❌ 不推荐:在 computed 中修改数据
computed: {
  processedData() {
    // 不要这样做!
    this.someOtherData = 'changed'
    return this.data.map(item => item * 2)
  }
}

// ✅ 推荐:用 watch 执行副作用
watch: {
  data(newData) {
    this.someOtherData = 'changed'
  }
}

七、性能考量

computed 的缓存机制是 Vue 性能优化的重要手段:

computed: {
  // 假设这是一个计算量很大的函数
  expensiveCalculation() {
    console.log('重新计算!')
    // 复杂计算...
    return result
  }
}

在模板中多次使用:

<div>{{ expensiveCalculation }}</div>
<div>{{ expensiveCalculation }}</div>
<div>{{ expensiveCalculation }}</div>

只会输出一次 "重新计算!",因为 computed 会缓存结果。

八、组合式 API 中的使用

在 Vue 3 的 Composition API 中,使用方式略有不同:

import { ref, computed, watch } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const doubleCount = computed(() => count.value * 2)
    
    watch(count, (newValue, oldValue) => {
      console.log(`count从${oldValue}变为${newValue}`)
    })
    
    return { count, doubleCount }
  }
}

总结

记住这个简单的决策流程

  1. 1. 需要基于现有数据计算一个新值吗?  → 用 computed
  2. 2. 需要在数据变化时执行某些操作吗?  → 用 watch
  3. 3. 这个计算需要在模板中简洁表达吗?  → 用 computed
  4. 4. 需要处理异步操作或复杂业务逻辑吗?  → 用 watch

黄金法则:能用 computed 实现的,优先使用 computed;只有在需要"副作用"操作时,才使用 watch。

希望这篇文章能帮助你更好地理解和使用 Vue 中的 computed 和 watch!在实际开发中灵活运用这两个特性,能让你的代码更加清晰、高效。

华人健康:股东赛富投资拟减持不超2%公司股份

36氪获悉,华人健康公告,股东苏州赛富、黄山赛富、腾元投资、长菁投资(合称“赛富投资”)合计持有公司5.03%股份,计划减持不超过800万股(占公司总股本的2%),减持原因为资金安排需要,减持方式为集中竞价交易和大宗交易,减持期间为公告披露之日起十五个交易日后的三个月内(即2026年2月5日至2026年4月30日)。

苏州固锝:定增申请获深交所审核通过

36氪获悉,苏州固锝公告,公司于2026年1月14日收到深交所出具的《关于苏州固锝电子股份有限公司申请向特定对象发行股票的审核中心意见告知函》,深交所发行上市审核机构对公司向特定对象发行股票的申请文件进行了审核,认为公司符合发行条件、上市条件和信息披露要求。公司本次向特定对象发行股票事项尚需获得中国证监会同意注册后方可实施。

霍尼韦尔:旗下量子计算公司Quantinuum计划在美提交IPO申请

霍尼韦尔宣布,其控股子公司Quantinuum计划向美国证券交易委员会提交一份草案,涉及Quantinuum普通股拟议的首次公开发行事宜。本次拟发行股份数量及价格区间尚未确定。该发行计划需视市场及其他条件而定,并须待美国证券交易委员会完成审查程序后方可实施。(界面)

值得买:公司业务暂不涉及GEO业务,AI相关收入占比很小

36氪获悉,值得买公告,公司股票交易异常波动,连续三个交易日内日收盘价格涨幅偏离值累计超过30%,同时连续十个交易日内日收盘价格涨幅偏离值累计超过100%。公司业务暂不涉及GEO业务,AI相关业务和产品处于投入初始阶段,2025年前三季度实现AI相关收入占公司整体营业收入比例很小,对公司整体经营情况没有重大影响。公司、公司控股股东和实际控制人不存在应披露而未披露的重大事项。

百傲化学:股东拟合计减持不超6%公司股份

36氪获悉,百傲化学公告,控股股东大连通运投资有限公司因自身资金需求,计划减持不超过2,118.67万股,即不超过公司总股本的3%;股东大连光曜致新舒鸿企业管理咨询合伙企业(有限合伙)因自身资金需求,计划减持不超过2,118.67万股,即不超过公司总股本的3%。

天龙集团:尚未开展定义所描述的GEO业务,不直接从事AI业务

36氪获悉,天龙集团公告,公司股票于2025年12月30日至2026年1月14日连续10个交易日内日收盘价格涨幅偏离值累计超过100%,根据相关规定,属于股票交易严重异常波动的情形。近期公司关注到有媒体将公司列为GEO概念股。目前,公司相关子公司的主营业务为数字营销业务,尚未开展定义所描述的GEO业务,公司不直接从事AI业务,公司未因AI工具产生额外的收入。

可口可乐公司设立首席数字官

可口可乐公司1月14日宣布一系列领导层调整,所有任命将于2026年3月31日生效。现任执行副总裁兼首席运营官亨里克·布劳恩(Henrique Braun)将接替詹姆斯·昆西(James Quincey)为首席执行官,昆西将继续担任执行董事长。此外,可口可乐公司将设立首席数字官这一新职位。现任欧亚及中东业务部总裁塞德夫·萨林甘·萨欣(Sedef Salingan Sahin)将出任此职,直接向布劳恩汇报。(界面)

钧达股份:拟3000万元参股星翼芯能,成立CPI膜、CPI膜与晶硅电池结合产品的生产制造合资企业

36氪获悉,钧达股份公告,公司拟以现金出资3000万元,认购上海星翼芯能科技有限公司新增注册资本46.1539万元,获得目标公司16.6667%的股权。目标公司及其下属公司与钧达股份及其下属公司双方成立CPI膜、CPI膜与晶硅电池结合产品的生产制造合资企业。本次股权合作的推进,旨在把握全球低轨卫星组网及太空算力产业的发展机遇,有利于充分发挥双方在光伏产业化能力、钙钛矿技术积淀、太空场景适配能力及航天资源整合领域的核心优势,实现优势互补、互惠共赢。

9天6板利欧股份:目前经营情况正常,不存在公司应披露而未披露的重大事项

36氪获悉,利欧股份公告,公司股票价格于2026年1月13日、2026年1月14日连续二个交易日收盘价格涨幅偏离值累计超过20%,属于股票交易异常波动的情况。经核实,公司前期披露的信息不存在需要更正、补充之处,近期经营情况正常,内外部经营环境未发生重大变化,公司及控股股东、实际控制人不存在关于公司的应披露而未披露的重大事项,或处于筹划阶段的重大事项。

美国银行:2025全年净利润305亿美元,同比增19%

美银第四财季扣除利息支出后的营收为283.7亿美元预估为277.8亿美元;净利息收入157.5亿美元,市场预期154.8亿美元。美银第四财季不含DVA的交易收入为45.3亿美元,市场预期43.3亿美元。平均股本回报率10.5%。美银第四财季净利润76亿美元,上年同期68亿美元;2025全年净利润305亿美元,同比增19%。(财联社)
❌