WeBand
119.15M · 2026-04-13
让我们像侦探一样,一步步追踪这段旅程,用有趣但不失严谨的方式,把整个技术链路掰开揉碎。
当你在地址栏敲下 ,浏览器立刻化身快递调度中心:
192.0.2.1)。<div id="app"></div> 和一串 <script src="/js/chunk-vendor.js"> 之类的标签。当浏览器加载并执行完打包后的 JS 文件(通常由 Webpack/Vite 生成),Vue 的舞台正式搭好。
// main.js —— 一切从这里开始
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
这行代码背后,Vue 内部展开了一场精密的初始化交响乐:
router、store、render 等与默认配置合并。_isMounted),调用 beforeCreate 钩子。Observer 的“大改造”beforeCreate钩子执行完,执行initState 接着初始化 inject→initState(props→data→computed→watch)→provide
function initState(vm) {
initProps(vm, opts.props);
initMethods(vm, opts.methods); // 处理 methods
initData(vm); // 调用 observe() 将 data 转为响应式
initComputed(vm, opts.computed);// 处理 computed
initWatch(vm, opts.watch); // 处理 watch
}
响应式data这是最精彩的部分。Vue 会遍历 data() 返回的对象,递归地把每一个属性变成响应式:
Object.defineProperty 重写 getter/setter,每个属性配一个专属的 Dep(依赖管理器)。Proxy 代理整个对象,更强大(能属性添加/删除)。 // 简化的响应式模型
data() {
return { count: 0, user: { name: 'Alice' } }
}
// ↓ 响应式数据 内部主要实现
// 1. Observer(观察者)- 数据劫持
/**核心工作:
* - 为对象添加 __ob__ 属性,指向 Observer 实例
* - 对数组:重写 push/pop/shift/unshift/splice/sort/reverse 方法
* - 对对象:调用 defineReactive 将每个属性转换为 getter/setter
*/
class Observer {
constructor(value, shallow = false, mock = false) {
this.value = value;
this.shallow = shallow;
this.dep = new Dep(); // 每个 Observer 持有一个 Dep
this.vmCount = 0;
def(value, '__ob__', this); // 在对象上标记 __ob__
if (isArray(value)) {
// 数组:拦截变异方法
this.observeArray(value);
} else {
// 对象:遍历每个属性,转换为 getter/setter
const keys = Object.keys(value);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock);
}
}
}
}
function defineReactive(obj, key, val) {
observe(val); // 递归处理嵌套对象
const dep = new Dep(); // 每个属性有自己的依赖管理器
Object.defineProperty(obj, key, {
get() {
if (Dep.target) { // 当前正在执行的 Watcher
dep.addSub(Dep.target); // 依赖收集
}
return val;
},
set(newVal) {
if (newVal !== val) {
val = newVal;
observe(newVal); // 新值如果是对象,也需要转为响应式
dep.notify(); // 派发更新,通知所有 Watcher
}
}
});
}
// Dep 类 是一个依赖收集器,充当发布-订阅模式的调度中心:
class Dep {
constructor() { this.subs = []; }
addSub(watcher) { this.subs.push(watcher); }
notify() { this.subs.forEach(w => w.update()); }
}
// ↓ 经过 Observer
count 拥有了 getter/setter + 一个 Dep
user 对象也被递归改造,name 同样拥有 getter/setter + Dep
同时,computed 和 watch 也会创建对应的 Watcher(观察者)。但此时它们都只是“预备役”,还没有真正去订阅数据。
现在 data、computed、methods 都已经可用,但 DOM 还不存在。你可以在 created 里发起异步请求、设置定时器,因为响应式数据已经 ready。
Vue 有两种方式获得 render 函数:
<script> 导出 render)。<div id="app">
<p>{{ message }}</p>
<button @click="count++">Click me</button>
</div>
Compiler 会做三件事:
render 函数,类似:function render() {
with(this) {
return _c('div', { attrs: { id: 'app' } }, [
_c('p', [_v(_s(message))]),
_c('button', { on: { click: () => count++ } }, [_v('Click me')])
])
}
}
## 第四站:首次渲染 —— 从数据到真实 DOM 的“首秀”
mountComponent 组件挂载阶段created执行结束,开始执行 $mount 进入组件挂载阶段。
$mount
现在 data、computed、methods 都已经可用,但 DOM 还不存在。你可以在 created 里发起异步请求、设置定时器,因为响应式数据已经 ready。
$mount 函数被调用,Vue 创建了一个渲染 Watcher:
Vue.prototype.$mount = function (el, hydrating) {
// ...
return mountComponent(this, el, hydrating)
}
function mountComponent(vm, el, hydrating) {
vm.$el = el;
callHook$1(vm, 'beforeMount');
// 创建更新函数
const updateComponent = () => {
vm._update(vm._render(), hydrating); // render 生成 vnode,update 更新 DOM
};
// 创建渲染 Watcher !!!!!!!!!!!!!在这呢~
new Watcher(vm, updateComponent, noop, {
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook$1(vm, 'beforeUpdate');
}
}
}, true)
if (vm.$vnode == null) {
vm._isMounted = true;
callHook$1(vm, 'mounted');
}
return vm;
}
class Watcher {
constructor(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm;
this.deps = []; // 当前依赖的 Dep 列表
this.newDeps = []; // 新一轮收集的 Dep 列表
this.depIds = new Set(); // 避免重复添加
this.getter = expOrFn; // 获取值的函数(渲染函数或表达式)
this.value = this.lazy ? undefined : this.get();
}
get() {
pushTarget(this); // 将自己设为 Dep.target
let value;
try {
value = this.getter.call(this.vm, this.vm); // 执行 getter,触发依赖收集
} finally {
popTarget(); // 恢复上一个 Dep.target
this.cleanupDeps(); // 清理不再需要的依赖
}
return value;
}
addDep(dep) {
const id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this); // 双向绑定:Watcher 订阅 Dep
}
}
}
update() {
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this); // 异步队列更新
}
}
}
updateComponent 内部就是:vm._update(vm._render(), ...)。
渲染 Watcher 会立即执行一次,开启首次渲染之旅。
调用刚才生成的 render 函数。
在 render 执行过程中,this.message 和 this.count 被读取 → 触发它们的 getter → 依赖收集开始!
Dep 会检查当前是否有活动的 Watcher(此时就是渲染 Watcher)。subs)中。// 伪代码:依赖收集
getter() {
if (Dep.target) {
dep.depend() // 把 Dep.target(渲染 Watcher)加入 subs
}
return value
}
结果:message 和 count 现在“认识”了渲染 Watcher。以后它们变了,就知道该通知谁。
render 最终返回一棵 VNode 树——一个轻量级的 JS 对象,描述了 DOM 结构。
调用 __patch__ 函数,首次渲染时 oldVnode 是挂载点(真实 DOM 元素,比如 <div id="app">),vnode 是新 VNode。
patch 会递归地创建真实 DOM 元素,设置属性、事件(比如 @click 被绑定到真正的 click 事件),最后把生成的 DOM 插入到页面中。
页面终于显示了!
随后 mounted 钩子被调用,你可以在里面操作 DOM 了。
用户点击了“Click me”按钮,count++ 被执行。
count 的 setter 被触发,内部调用 dep.notify()。
dep.notify() 会遍历 subs 列表(里面目前有渲染 Watcher),调用每个 Watcher 的 update() 方法。
update() 不会立即重新渲染,而是调用 queueWatcher(this) 把渲染 Watcher 放入一个异步队列。
Vue 通过 nextTick(微任务或降级宏任务)来批量处理更新,避免同一个 Watcher 被重复添加(去重)。
在下一个 tick,队列被清空:
run() → 再次调用 updateComponent。render() 生成新 VNode(此时 count 已经变成新值,依赖收集会重新建立,旧依赖会被清理)。_update() 执行 patch(oldVNode, newVNode)。
Diff 算法登场(Vue 2 双端比较 / Vue 3 快速 diff + 最长递增子序列):updated 钩子触发。URL 输入
↓
DNS 解析 → TCP 连接
↓
HTML 加载 & 解析 JS
↓
new Vue()
├─ 合并选项
├─ beforeCreate(inject → props → )
├─ initInjections → initState(methods → data → computed → watch)
├─── Observer 转换 data(响应式 + Dep)
├─── 初始化 computed / watch(创建 Watcher)
├─ created
└─ $mount
├─ 编译模板 → render 函数(如果没提供)
├─ 创建渲染 Watcher(Vue 2) / Effect(Vue 3)
│ ├─ 执行 _render() → 读取响应式数据 → 依赖收集(数据→Dep→Watcher) → 生成VNode
│ └─ 执行 _update() → patch → 真实 DOM
└─ mounted
↓
用户交互(修改数据)
├─ setter → dep.notify()
├─ 渲染 Watcher 被推入异步队列
├─ nextTick 执行队列
│ ├─ 重新执行 _render() → 新 VNode
│ └─ patch(oldVNode, newVNode) → Diff → 更新 DOM
└─ updated
不会。渲染 Watcher 只收集本次渲染实际访问到的数据。如果 v-if 为 false 导致某个分支从未进入,那分支里的数据就不会被收集。当条件变为 true 时,下一次渲染会自动订阅它们。
v-show 和 v-if 在依赖收集上有什么不同?”v-if:条件为 false 时,该分支根本不渲染 → 不读取内部数据 → 无依赖收集 → 内部数据变化不会触发更新。v-show:只是 CSS 隐藏,DOM 一直存在 → 每次渲染都会读取内部数据 → 依赖始终存在 → 数据变化会触发重新渲染(即使看不见)。它是“装修工人”——在初始化时把普通数据改造成带 getter/setter 和 Dep 的响应式对象。它不直接参与发布或订阅,但它是整个系统能够运转的基础。
Proxy 代替 Object.defineProperty,可属性添加/删除、数组索引等。| 阶段 | 核心角色 | 产出 |
|---|---|---|
| 资源加载 | 浏览器、HTTP | HTML + JS |
| Vue 初始化 | Observer、Dep、Watcher | 响应式数据 + 实例 |
| 模板编译 | Compiler | render 函数 |
| 首次渲染 | 渲染 Watcher、render、patch | 真实 DOM |
| 交互更新 | setter、Dep.notify、调度器、patch + diff | 最小化 DOM 更新 |
总结: 从输入 URL 到 Vue 项目渲染,整个链路是:
这趟旅程中,Vue 的每一个设计都精妙地平衡了声明式编程的优雅与底层性能的极致。希望这次“共探”,能让你下次启动 Vue 项目时,看到的不只是一个页面,而是一整套精心编排的幕后舞剧。
AI 周刊【2026.04.06-04.12】:Anthropic 藏起最强模型、AI 社会矛盾激化、"欢乐马"登顶
樱花漫画官方下载入口安卓最新版-优质漫画平台正版免费下载安装
2026-04-13
2026-04-13