从输入 URL 到页面:一个 Vue 项目的“奇幻漂流”

🧭 从 URL 到页面:一个 Vue 项目的"奇幻漂流"

这是一段你每天都可能经历的旅程:在浏览器输入一个地址,按下回车,几毫秒后,一个 Vue 单页应用就活生生地出现在屏幕上。这背后发生了什么?
Vue 的响应式系统、虚拟 DOM、编译器和"发布‑订阅"主角们------Observer、Dep、Watcher、Patch------是如何协作的?

让我们像侦探一样,一步步追踪这段旅程,用有趣但不失严谨的方式,把整个技术链路掰开揉碎。

🚀 第一站:浏览器 ------ 资源的"快递小哥"

输入 URL → DNS 解析 → TCP 连接 → 请求 HTML → 接收响应

当你在地址栏敲下 https://my-vue-app.com,浏览器立刻化身快递调度中心:

  1. DNS 查询: 把域名变成 IP 地址(比如 192.0.2.1)。
  2. TCP 握手: 与服务器建立可靠连接。
  3. 发送 HTTP 请求: 告诉服务器"我要你的首页"。
  4. 服务器返回 HTML: 通常一个极简的 index.html,里面只有一个 <div id="app"></div> 和一串 <script src="/js/chunk-vendor.js"> 之类的标签。

这时 Vue 还没现身,只是一个空壳 HTML 被浏览器解析。但关键的 JS 文件已经开始下载------它们才是 Vue 的"灵魂"。

📦 第二站:Vue 实例诞生 ------ "造物主"的仪式

当浏览器加载并执行完打包后的 JS 文件(通常由 Webpack/Vite 生成),Vue 的舞台正式搭好。

javascript 复制代码
// main.js ------ 一切从这里开始
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

这行代码背后,Vue 内部展开了一场精密的初始化交响乐:

🎼 乐章一:合并选项 & 生命周期初始化

  • 将传入的 routerstorerender 等与默认配置合并。
  • 设置内部标志(如 _isMounted),调用 beforeCreate 钩子。

🎼 乐章二:数据响应式 ------ Observer 的"大改造"

beforeCreate钩子执行完,执行initState 接着初始化 injectinitState(propsdatacomputedwatch)provide

javascript 复制代码
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() 返回的对象,递归地把每一个属性变成响应式:

  • Vue 2:用 Object.defineProperty 重写 getter/setter,每个属性配一个专属的 Dep(依赖管理器)。
  • Vue 3:用 Proxy 代理整个对象,更强大(能监听属性添加/删除)。
javascript 复制代码
    // 简化的响应式模型
    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

同时,computedwatch 也会创建对应的 Watcher(观察者)。但此时它们都只是"预备役",还没有真正去订阅数据。

🎼 乐章三:created 钩子触发

现在 datacomputedmethods 都已经可用,但 DOM 还不存在。你可以在 created 里发起异步请求、设置定时器,因为响应式数据已经 ready。

🛠️ 第三站:编译 ------ 模板如何变成"渲染函数"?

Vue 有两种方式获得 render 函数:

  • 你直接提供了(比如单文件组件里的 <script> 导出 render)。
  • 或者 Vue 需要编译模板------这是最通用的方式。 假设我们有一个模板:
html 复制代码
<div id="app">
  <p>{{ message }}</p>
  <button @click="count++">Click me</button>
</div>

Compiler 会做三件事:

  1. 解析(Parse): 把模板字符串转换成 AST(抽象语法树)。AST 就是一个 JS 对象,精准描述了 DOM 结构、指令、文本插值等。
  2. 优化(Optimize): 标记静态节点(比如没有绑定任何动态数据的纯文本)。这一步为后续虚拟 DOM 的 diff 减负。
  3. 代码生成(Codegen): 从 AST 生成一个可执行的 render 函数,类似:
javascript 复制代码
function render() {
    with(this) {
        return _c('div', { attrs: { id: 'app' } }, [
            _c('p', [_v(_s(message))]),
            _c('button', { on: { click: () => count++ } }, [_v('Click me')])
        ])
    }
}

注意:编译阶段不会把 {{ message }} 替换成具体值,也不会为每个指令绑定更新函数。它只产出 render 函数,真正的数据替换要到运行时。

##🎬 第四站:首次渲染 ------ 从数据到真实 DOM 的"首秀"

🎼 乐章四:mountComponent 组件挂载阶段

created执行结束,开始执行 $mount 进入组件挂载阶段。

$mount 现在 datacomputedmethods 都已经可用,但 DOM 还不存在。你可以在 created 里发起异步请求、设置定时器,因为响应式数据已经 ready。

$mount 函数被调用,Vue 创建了一个渲染 Watcher

javascript 复制代码
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 会立即执行一次,开启首次渲染之旅。

1️⃣ _render() ------ 生成虚拟 DOM (VNode)

调用刚才生成的 render 函数。 在 render 执行过程中,this.messagethis.count 被读取 → 触发它们的 getter → 依赖收集开始!

  • 每个响应式属性的 Dep 会检查当前是否有活动的 Watcher(此时就是渲染 Watcher)。
  • 如果有,就把这个渲染 Watcher 添加到自己的订阅列表(subs)中。
javascript 复制代码
// 伪代码:依赖收集
getter() {
  if (Dep.target) {
    dep.depend()  // 把 Dep.target(渲染 Watcher)加入 subs
  }
  return value
}

结果: messagecount 现在"认识"了渲染 Watcher。以后它们变了,就知道该通知谁。 render 最终返回一棵 VNode 树------一个轻量级的 JS 对象,描述了 DOM 结构。

2️⃣ _update() ------ patch 挂载到真实 DOM

调用 __patch__ 函数,首次渲染时 oldVnode 是挂载点(真实 DOM 元素,比如 <div id="app">),vnode 是新 VNode。

patch 会递归地创建真实 DOM 元素,设置属性、事件监听(比如 @click 被绑定到真正的 click 事件),最后把生成的 DOM 插入到页面中。

页面终于显示了! 🎉 随后 mounted 钩子被调用,你可以在里面操作 DOM 了。

🔄 第五站:交互与响应式更新 ------ "自动档"的魔法

用户点击了"Click me"按钮,count++ 被执行。

1️⃣ 数据变化

countsetter 被触发,内部调用 dep.notify()

2️⃣ 派发更新

dep.notify() 会遍历 subs 列表(里面目前有渲染 Watcher),调用每个 Watcher 的 update() 方法。

3️⃣ 异步调度

update() 不会立即重新渲染,而是调用 queueWatcher(this) 把渲染 Watcher 放入一个异步队列 。 Vue 通过 nextTick(微任务或降级宏任务)来批量处理更新,避免同一个 Watcher 被重复添加(去重)。

4️⃣ 重新渲染与 Diff

在下一个 tick,队列被清空:

  • 渲染 Watcher 执行 run() → 再次调用 updateComponent。
  • 重新执行 render() 生成新 VNode(此时 count 已经变成新值,依赖收集会重新建立,旧依赖会被清理)。
  • 调用 _update() 执行 patch(oldVNode, newVNode)Diff 算法登场(Vue 2 双端比较 / Vue 3 快速 diff + 最长递增子序列):
  • 比较新旧 VNode 树,找出最小变化集。
  • 只更新变化的部分(比如按钮文本从 "Click me" 变成 "Click me (1)"),而不重新渲染整个列表。 最终真实 DOM 被高效更新,用户看到了新的数字。 随后 updated 钩子触发。

🗺️ 完整流程图

text 复制代码
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-showv-if 在依赖收集上有什么不同?"

  • v-if:条件为 false 时,该分支根本不渲染 → 不读取内部数据 → 无依赖收集 → 内部数据变化不会触发更新。
  • v-show:只是 CSS 隐藏,DOM 一直存在 → 每次渲染都会读取内部数据 → 依赖始终存在 → 数据变化会触发重新渲染(即使看不见)。

❓ "Observer 在发布‑订阅里是什么角色?"

它是"装修工人"------在初始化时把普通数据改造成带 getter/setterDep 的响应式对象。它不直接参与发布或订阅,但它是整个系统能够运转的基础。

❓ "Vue 3 比 Vue 2 快在哪?"

  • Proxy 代替 Object.defineProperty,可监听属性添加/删除、数组索引等。
  • 编译优化:静态提升、补丁标记、块树 → 让 diff 跳过静态内容。
  • 快速 diff + 最长递增子序列 → 减少 DOM 移动次数。

🎯 总结:从 URL 到像素的"奇幻漂流"

阶段 核心角色 产出
资源加载 浏览器、HTTP HTML + JS
Vue 初始化 ObserverDepWatcher 响应式数据 + 实例
模板编译 Compiler render 函数
首次渲染 渲染 Watcherrenderpatch 真实 DOM
交互更新 setterDep.notify、调度器、patch + diff 最小化 DOM 更新

总结: 从输入 URL 到 Vue 项目渲染,整个链路是:

URL 输入 → 网络加载(HTML 加载) & 解析 JS → Vue实例初始化(响应式数据、编译)→ 首次渲染 Watcher → 执行 render 生成 VNode → patch 创建真实 DOM → 挂载完成 →用户交互 → 数据变化 → 响应式派发 → 重新渲染 → Diff 更新 DOM

这趟旅程中,Vue 的每一个设计都精妙地平衡了声明式编程的优雅与底层性能的极致。希望这次"共探",能让你下次启动 Vue 项目时,看到的不只是一个页面,而是一整套精心编排的幕后舞剧。

相关推荐
码喽7号2 小时前
vue学习四:Axios网络请求
前端·vue.js·学习
像素之间4 小时前
为什么运行时要加set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve
前端·javascript·vue.js
M ? A4 小时前
Vue转React实战:defineProps精准迁移实战
前端·javascript·vue.js·经验分享·react.js·开源·vureact
JokerLee...4 小时前
大屏自适应方案
前端·vue.js·大屏端
PeterMap5 小时前
Vue.js全面解析:从入门到上手,前端新手的首选框架
前端·vue.js
weixin_413838565 小时前
基于区块链的校园二手书交易系统
vue.js·spring·区块链·fabric
veminhe8 小时前
VUE问题
vue.js
M ? A8 小时前
VuReact 编译器核心重构:统一管理组件元数据收集
前端·javascript·vue.js·react.js·重构·开源
M ? A8 小时前
Vue转React最佳工具对比:Vuera、Veaury与VuReact
前端·javascript·vue.js·经验分享·react.js