Vue实例都做了什么?

Vue 实例都做了什么?------从 new Vue() 到销毁的全链路(Vue 2)

这是一篇面向实战与源码思维的"全景文章"。你可以把它当作:Vue 2 实例生命周期 + 响应式机制 + 渲染更新的一次打通复盘。


new Vue(options) 发生了什么?

目标 :把你传入的 options(data/props/methods/computed/watch/template 等)变成一个可响应、可渲染、可通信的组件实例 vm。

高层流程(精简版):

  1. 构造与选项合并

    • 全局构造配置(全局 Vue.mixin/extend 等)与本地 options 做 合并 ,得到实例级的 vm.$options。
  2. 初始化生命周期字段

    • 建立父子关系($parent/$children)、根实例 $root、_isMounted/_isDestroyed 等标记。
  3. 初始化事件系统

    • 准备好组件实例的 自定义事件 能力(vm.$on/$off/$emit/$once)。
  4. 初始化渲染能力

    • 挂上 vm.$slots/$scopedSlots、$createElement(也就是 h()),为后续 render() 做准备。
  5. beforeCreate 钩子 (此时:data/props 还未注入)

  6. initInjections (优先解析 inject)

  7. initState(核心)

    • props → methods → data → computed → watch 按顺序初始化为响应式:

      • 为 data/props 做 依赖收集 的 getter/setter 包装
      • 为 computed 创建 惰性 watcher,带缓存
      • 为 watch 创建 用户 watcher,建立数据 → 回调 的订阅关系
  8. initProvide (准备 provide)

  9. created 钩子 (此时:data/props 已可用 ,但还没挂载 DOM)

记忆点:beforeCreate 前啥都没;created 后数据可用但没有 DOM。


挂载:vm.$mount(el) 如何把数据变成 DOM?

目标 :拿到 render(),跑一遍渲染,得到 VNode ,再通过 patch 变成真实 DOM。

  1. 得到 render 函数

    • 你若提供 render,直接用;
    • 否则以 template(或 el 的外层 HTML)经编译器 转成 render(运行时+编译版才行)。
  2. beforeMount 钩子

  3. 创建"渲染 watcher"(Render Watcher)

    • 定义 updateComponent = () => { vm._update(vm._render()) }
    • 首次执行:_render() 生成 VNode → _update() 调 patch 把 VNode 挂到真实 DOM
  4. mounted 钩子(此时:DOM 已可访问)

关键:渲染 watcher 订阅了所有在 render() 过程中被读取的响应式数据 。数据变 → watcher 被通知 → 重新跑 updateComponent → 触发增量 patch。


响应式:依赖收集与派发更新

Vue 2 基于 Object.defineProperty 做响应式,核心元素有三类:

  • Dep(依赖桶) :每个可被依赖的数据字段都有一个 Dep,记录"谁在用我"(watchers)。

  • Watcher(观察者) :三种常见

    • 渲染 watcher:让视图随数据变化而更新
    • 计算属性 watcher(lazy) :按需求值 + 缓存
    • 用户 watcher :你写在 watch:{} 或 vm.$watch(...) 的那些
  • Scheduler(调度器) :同一个"tick"内多次变更只触发一次批量更新(队列 + 去重),最终在 nextTick 时机统一 flush。

一次更新的链路:

this.xxx = 2

→ 触发该字段的 setter

→ dep.notify()

→ 对应的 watchers 入队

→ 调度器合并、去重

→ 微任务/宏任务 边界的 nextTick 执行 flush

→ 渲染 watcher 再跑 updateComponent

→ VNode diff + patch,最小化 DOM 改动

记忆点:渲染函数里"读过"的响应式数据,才会被订阅。因此"只改没读"的数据不会引起重渲染。


更新:Virtual DOM diff 与最小化 DOM 改动

  • _render() 每次返回一个全新的 VNode 树;

  • _update() 内部用 patch(oldVnode, vnode) 做 diff:

    • 比较节点类型/key,复用能复用的 DOM 节点;
    • 只在必要的地方做增删改;
    • 组件子树按递归策略更新;
    • 高效列表更新依赖 稳定的 key。

实战要点:列表项必须有稳定 key,否则 diff 代价大、还可能引起表单输入错乱。


事件、插槽、父子通信

  • 事件系统 :vm.$emit(子→父)、@xxx/v-on(父监听子);
  • Props/自定义事件:构成了"单向数据流 + 自下而上事件"的通信模式;
  • Slots/Scoped Slots :父将模板片段作为插槽传入,子在自己的渲染上下文中渲染;
  • Provide/Inject :祖先到后代的依赖注入(跨层级传参),注意不是响应式传递(Vue 2 中需要手动处理变化)。

销毁:vm.$destroy()

  1. beforeDestroy 钩子

  2. 解绑父子关系 ,移除自身 vm 在父的 $children 中的引用

  3. Teardown 所有 watchers

    • 渲染 watcher / 用户 watcher / 计算属性 watcher
  4. 移除事件监听

  5. patch(vnode, null) :解绑与清理 DOM/指令/子组件

  6. destroyed 钩子

销毁后:实例不再响应,不要再访问其响应式数据做副作用。


与 Vue 3 的一眼对比(方便心中有数)

  • 响应式 :defineProperty → Proxy(可拦截新增/删除属性、索引访问)
  • API 风格 :Options API → Composition API (setup()/ref/reactive)
  • 调度器 :更细粒度的依赖追踪与调度;scheduler 架构更现代
  • 模板编译 :更多静态提升、块跟踪,更少无谓 diff

(但本文主线仍以 Vue 2 为准)


关键钩子该放什么?

  • beforeCreate:极少用(数据未可用)
  • created :可请求数据(无 DOM 依赖);可注册 watch
  • beforeMount:很少单独用
  • mounted :需要 DOM 时机(测量、第三方 DOM 库)
  • beforeUpdate :读 更新前 的 DOM
  • updated :读 更新后 的 DOM(避免在这里改数据,易引发循环)
  • beforeDestroy :清理定时器、事件、订阅 、手动 unwatch
  • destroyed:一般打点/日志

常见"为什么"的速答

  • 为什么我给对象新增属性不触发更新?
    Vue 2 基于 defineProperty,新增/删除 属性无法被拦截;用 this.$set(obj, key, val)。
  • 数组索引赋值不更新?
    用 this.$set(arr, index, val) 或使用改变长度的变异方法(splice 等)。
  • 为什么我改了很多数据只渲染一次?
    因为有 调度器队列 与 nextTick 合并更新。
  • 列表更新错乱?
    给 v-for 提供 稳定的 key ,不要用索引作为 key(除非真的是静态序列)。

代码

javascript 复制代码
// 1) 选项
const vm = new Vue({
  el: '#app',
  props: ['id'],                 // (在根实例中通常在子组件用)
  data() {
    return { count: 0, list: [] }
  },
  computed: {
    doubled() { return this.count * 2 }
  },
  watch: {
    count: {
      immediate: true,
      handler(n) { console.log('count ->', n) }
    }
  },
  created() {
    // 数据可用,DOM 尚未就绪
    this.fetch()
  },
  mounted() {
    // 可以安全访问 DOM/测量尺寸
    this.$nextTick(() => console.log(this.$el.offsetWidth))
  },
  methods: {
    fetch() { /* 拉数据,填充 list */ },
    inc() { this.count++ }
  },
  render(h) {
    // JSX/模板都可,render 展示"渲染依赖"概念
    return h('div', [
      h('p', `count=${this.count}, doubled=${this.doubled}`),
      h('button', { on: { click: this.inc } }, 'Add')
    ])
  }
})
相关推荐
拆房老料3 小时前
ONLYOFFICE也能像Microsoft Word和WPS一样分别设置中西文字体
前端·html·word·开源软件·wps
VX_bysjlw9854 小时前
数码设备销售网站设计与实现39138-计算机毕设原创(免费领源码+带部署教程)
java·vue.js·spring boot·mysql·tomcat·mybatis·idea
Solis程序员4 小时前
Stripe 幂等表:把「不确定的重试」变成「查表返回」
前端·安全·bootstrap·php·agent
李纲明4 小时前
WordPress 站点变慢:按 TTFB → 缓存 → 查询 → 前端 分层排查(含配置注意)
前端·缓存·性能优化·wordpress·后端开发
程序人生8884 小时前
PDF 转 Word 版式错乱、表格丢失?DocConverter Web 格式互转引擎的保真实践
前端·人工智能·opencv·机器学习·pdf·word
粥里有勺糖5 小时前
安利一下最近用的桌面“Agent” | T3 Code
前端·github·ai编程
xy34535 小时前
Axure 9.0 动态面板基本操作
前端·ui·html·axure·原型·产品设计
不停喝水5 小时前
【前端转全栈java速通课】 项目实战④7-11节 操作数据库-登录-注册-修改密码-注销-mubatis-plus简化crud-
java·前端·数据库
一水鉴天6 小时前
统一对象拓扑(UOT)设计全链定稿:从三件事分离到三继承 之1 20261010(豆包“查看聊天内容”继续)
前端·css
杨杰3986 小时前
我给嵌入式设备写了个 6MB 的 JavaScript 运行时(xuan)
javascript