进阶篇:Vue2.x 原理深度解析

Vue 是一套构建用户界面的 JavaScript 框架,基于标准 HTML、CSS、JavaScript 构建,提供声明式、组件化的编程范式,用来高效开发 Web 界面。

Vue 两大核心能力:

  • 声明式渲染:在 HTML 基础上扩展模板语法,声明描述 JavaScript 状态与最终输出 HTML 之间的映射关系。
  • 响应式:自动追踪 JavaScript 状态的变化,状态发生变更后自动更新 DOM 视图。

    核心概念

响应式系统

响应式是 Vue 最标志性的特性,组件状态由响应式 JavaScript 对象构成;修改对象数据,视图会自动随之更新。 响应式本质是一套处理变化的编程范式。

let a = 1
let b = update() // b = 2
function update() {
    b = a + 1
}
a = 2

原生 JavaScript 无法实现:当 a 赋值改变,自动执行逻辑更新 b

Vue2 通过劫持对象属性读写实现该能力。 JS 劫持对象属性有两套方案:Object.definePropertyProxy

  • Vue2 选用 Object.defineProperty,目的是兼容低版本浏览器;* Vue3使用Proxy,ref仍然依赖defineProperty实现。

Object.defineProperty()可以为对象新增或者修改属性,同时配置属性描述符:

Object.defineProperty(obj, prop, descriptor)

descriptor 属性描述符:

  • configurable:布尔值,控制属性描述符本身是否可修改;
  • enumerable:布尔值,控制属性是否可以被枚举遍历;
  • value:数据描述符,属性值;
  • writable:数据描述符,属性是否允许被修改;
  • get:存取描述符,读取属性触发执行的函数;
  • set:存取描述符,修改属性触发执行的函数。
const obj = {
    a: 1,
    b: null
}

function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            return val
        },
        set(newVal) {
            obj.b = newVal + 1
            console.log(obj.b)
            return newVal
        }
    })
}

defineReactive(obj, 'a', obj.a)
obj.a = 10

备注:Vue2 完全基于 Object.defineProperty 实现响应式,IE8 及更低版本浏览器不支持该 API,因此 Vue2 不支持 IE8。

Virtual DOM(虚拟DOM)

Virtual DOM 本质是使用普通 JavaScript 对象(VNode 虚拟节点)来描述 DOM 树结构,是对真实 DOM 的抽象层。应用状态变更,先作用于虚拟 DOM,再最终映射到真实 DOM。

真实 DOM:

<div class="a" id="b">我是内容</div>

对应的VNode对象:

{
    tag: 'div',
    attrs: {
        class: 'a',
        id: 'b'
    },
    text: '我是内容',
    children: []
}

Vue2 的 Virtual DOM 借鉴 Snabbdom 库实现,模板编译后输出渲染函数,结合响应式系统、指令完成视图更新。

核心原因:DOM 本身 API 复杂,频繁操作真实 DOM 性能开销巨大;JavaScript 执行运算速度很快。

数据发生改变,对比新旧两份虚拟节点,通过 DOM-Diff 计算出最小变更,只更新需要改动的 DOM 节点,减少 DOM 操作开销。

  • Vue1.x 使用 DocumentFragment
  • Vue2.x 引入 Virtual DOM,相对 Vue 1.x 渲染速度提升约 2-4 倍,内存占用降低。

DOM-Diff

DOM-Diff 在 Vue 内部叫 patch(打补丁)。 以新VNode作为基准,修改旧VNode,让旧VNode变成和新VNode一致。旧VNode是更新前页面对应的虚拟节点,新VNode是数据变化后待渲染的虚拟节点。

patch 做三类操作:

  • 创建节点:新VNode存在,旧VNode没有 → 创建DOM节点;
  • 删除节点:旧VNode存在,新VNode没有 → 删除DOM节点;
  • 更新节点:新旧VNode都存在 → 更新节点属性、子节点。

DOM-Diff 采用同级比较、深度优先遍历,不会跨层级对比节点,时间复杂度 O(n)。 执行逻辑:

  • 遍历新虚拟DOM子节点,在旧虚拟DOM中寻找对应节点;
  • 找到就移动复用;找不到就新建插入;
  • 遍历结束,旧虚拟DOM剩余未匹配节点全部删除。

核心算法优化:

patchVnode(oldVnode, vnode)
    ├── 1. 如果新旧 VNode 引用相同 → 直接返回
    ├── 2. 更新节点属性(props、class、style、事件等)
    ├── 3. 如果都有文本节点且文本内容不同 → 更新文本
    ├── 4. 如果都有子节点
    │   └── updateChildren() → 双端 diff 算法
    │       ├── 头头比较(sameVnode)
    │       ├── 尾尾比较(sameVnode)
    │       ├── 头尾比较(sameVnode)
    │       ├── 尾头比较(sameVnode)
    │       └── 遍历查找 → 找到则复用,否则新建
    ├── 5. 只有新节点有子节点 → 添加子节点
    └── 6. 只有旧节点有子节点 → 删除子节点

观察者模式

观察者模式包含被观察者Subject和多个观察者Observer。当被观察者状态发生改变,主动通知全部观察者执行更新逻辑。

被观察者维护观察者列表,提供添加、移除、通知的方法;状态变更执行通知,所有观察者收到消息执行自身逻辑。

优点:天然适合响应式场景,数据变化自动通知订阅方。

缺点:耦合度较高,对比发布订阅模式缺少独立事件中心。

// 被观察者
class Subject {
    constructor() {
        this.obs = []
    }
    add(ob) {
        this.obs.push(ob)
    }
    remove(ob) {
        this.obs = this.obs.filter(o => o.name !== ob.name)
    }
    notify(message) {
        this.obs.forEach(ob => ob.notified(message))
    }
}

// 观察者
class Observer {
    constructor(name) {
        this.name = name
    }
    notified(message) {
        console.log(`Hello, ${this.name}. This is ${message}!`)
    }
}

const subject = new Subject()
const observerLi = new Observer('Li')
const observerZhao = new Observer('Zhao')
subject.add(observerLi)
subject.add(observerZhao)
subject.notify('Subject message 01')
subject.remove(observerLi)
subject.notify('Subject message 02')

Vue2 响应式体系就是观察者模式的落地实现:Observer 处理对象数据,劫持 get/set;Dep 作为被观察者,收集依赖(Watcher);Watcher 作为观察者,负责执行更新。

Vue 完整渲染流程

完整生命周期链路:

  1. new Vue() 实例化;
  2. _init() 初始化;
  3. $mount 挂载;
  4. 编译 template(完整版);
  5. 生成 render 函数;
  6. mountComponent
  7. 执行 render 生成 VNode;
  8. patch 更新真实 DOM。

初始化与挂载

调用 new Vue() 创建 Vue 实例,内部执行 _init() 做实例初始化:

  • 初始化生命周期、事件、props、methods、data、computed、watch;
  • 通过 Object.defineProperty 设置 get/set,完成响应式劫持。

初始化完成后,调用 $mount 执行挂载。

备注:如果是运行时编译版本,模板 template 需要编译为 render 渲染函数;如果是 runtime-only 版本,则直接使用用户传入的 render 函数。

编译

编译把模板字符串转换成可执行 render 函数,分为三个阶段:

  • parse 解析:解析 template 模板,处理指令、class、style,输出 AST 抽象语法树;
  • optimize 优化:标记静态节点;diff 打补丁的时候直接跳过静态节点,减少对比开销;
  • generate 生成:把 AST 转换成 render 函数字符串,再通过 new Function 转为可执行 JS 函数,产出 renderstaticRenderFns

渲染

首次挂载,$mount 内部调用 mountComponent,核心逻辑如下:

  • 执行 render 函数,生成 VNode;
  • render 读取响应式数据,触发属性 getter,完成依赖收集(将当前渲染 Watcher 存入 Dep 的订阅数组);
  • 执行 patch,将 VNode 渲染为真实 DOM。

当响应式数据被修改时:

  • 修改响应式数据,触发 setter,Dep 通知内部所有 Watcher 执行 update
  • Watcher 不会立即执行更新,而是推入队列queueWatcher),在下一个事件循环(microtask) 中统一执行;
  • nextTick(flushSchedulerQueue) 批量执行所有 Watcher 的 run()
    • 执行 render 生成新 VNode(复用已有 Watcher);
    • 执行 patch(DOM-Diff)完成视图更新;
  • Vue 内部使用队列异步批量处理 Watcher 更新,同一事件循环中多次修改数据,只会触发一次渲染,避免频繁 DOM 操作。

完整流程图

new Vue()
    ↓
_init()
    ├── 初始化生命周期、事件
    ├── 初始化 props、methods、data、computed、watch
    └── 响应式劫持(Object.defineProperty)
    ↓
$mount()
    ↓
compile(完整版)或 直接使用 render(runtime-only)
    ├── parse:template → AST
    ├── optimize:标记静态节点
    └── generate:AST → render 函数字符串
    ↓
mountComponent()
    ├── 新建渲染 Watcher
    │   ├── 立即执行 updateComponent()
    │   │   ├── 执行 render() → 读取响应式数据
    │   │   │   └── 触发 getter → 依赖收集(将当前 Watcher 存入 Dep)
    │   │   └── 执行 patch() → 对比 VNode → 更新真实 DOM
    │   └── 完成首次渲染
    └── 触发 mounted 钩子
    ↓
数据变化
    ├── 触发 setter → dep.notify()
    ├── Watcher.update() → 推入队列(queueWatcher)
    ├── nextTick(flushSchedulerQueue)
    │   ├── 批量执行 Watcher.run()
    │   │   ├── 执行 render() → 生成新 VNode(复用已有 Watcher)
    │   │   └── 执行 patch() → DOM-Diff → 更新真实 DOM
    │   └── 触发 updated 钩子
    └── 完成更新

源码解读

基于阅读 Vue2.7.14 源码的笔记,说明关键调用链路、核心源码片段,帮助理解内部执行逻辑。

<!-- Vue.js v2.7.14 -->
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
<script id="content" type="x-template">
    <div @click="onClickAlert">
      <p>{{vueVersion}}</p>
      <p>{{message}}</p>
    </div>
</script>
<script>
    const app = new Vue({
        template: '#content',
        props: {
            name: String
        },
        propsData: {
            name: 'Lizhao'
        },
        setup: function () {
            return {
                counter: 100
            }
        },
        provide: {
            createTime: new Date(2022, 10, 11, 9, 0, 0).toString()
        },
        computed: {
            vueVersion () {
                return `Vue@${this.version}`
            }
        },
        watch: {
            message: [
                function () { console.log('message is changed! 01') },
                function () { console.log('message is changed! 02') },
            ]
        },
        data: function () {
            const { name } = this
            return {
                version: Vue.version,
                message: `Hello, ${name}. This is ${new Date()}.`
            }
        },
        methods: {
            onClickAlert () {
                this.message = `Hello, ${this.name}. This is ${new Date()}.`
            }
        }
    })
    app.$mount('#app')
</script>

new Vue()

每一个Vue应用都从new Vue()开始,根据传入options选项生成Vue实例。

const app = new Vue({
    template: '#content',
    data: {
        message: `Hello, Lizhao. This is ${new Date()}.`
    }
})

_init 实例初始化

Vue本身是一个构造函数,原型上挂载各类实例方法:

  • _init$set$watch$once$emit$update$destroy
  • 实例属性如$data$props
  • 同时暴露静态方法:compileusemixinextendutil
function Vue(options) {
    // ...
    this._init(options);
}

initMixin$1(Vue);

function initMixin$1(Vue) {
    Vue.prototype._init = function (options) {};
}

Vue.prototype.$mount = function (el, hydrating) {
    el = el && inBrowser ? query(el) : undefined;
    return mountComponent(this, el, hydrating);
};

_init内部完整流程:

  • 处理options选项;
  • 初始化内部对象:$refs_events$attrs$listeners_provided
  • 执行生命周期钩子;
  • 初始化propssetupmethodsdatacomputedwatch
  • 处理inject/provide
  • 最后调用$mount
function initMixin$1(Vue) {
    Vue.prototype._init = function (options) {
        //...
        initProxy(vm);
        vm._self = vm;
        initLifecycle(vm);
        initEvents(vm);
        initRender(vm);
        callHook$1(vm, 'beforeCreate', undefined, false /* setContext */);
        initInjections(vm); // resolve injections before data/props
        initState(vm);
        initProvide(vm); // resolve provide after data/props
        callHook$1(vm, 'created');
        // ...
        if (vm.$options.el) {
            vm.$mount(vm.$options.el);
        }
    };
}

initState是状态初始化入口,负责 props、setup、methods、data、computed、watch:

function initState(vm) {
    var opts = vm.$options;
    if (opts.props)
        initProps$1(vm, opts.props);
    initSetup(vm);
    if (opts.methods)
        initMethods(vm, opts.methods);
    if (opts.data) {
        initData(vm);
    } else {
        var ob = observe((vm._data = {}));
        ob && ob.vmCount++;
    }
    if (opts.computed)
        initComputed$1(vm, opts.computed);
    if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch);
    }
}

$mount 挂载

$mount两个核心逻辑:

  • compileToFunctions:把template模板编译得到render函数;
  • mountComponent:执行render,生成VNode,挂载到DOM节点。
Vue.prototype.$mount = function (el, hydrating) {
    // ...
    return mountComponent(this, el, hydrating);
};

var mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (el, hydrating) {
    // ...
    var _a = compileToFunctions(template, {
        // ...
    }, this), render = _a.render, staticRenderFns = _a.staticRenderFns;
    options.render = render;
    // ...
    return mount.call(this, el, hydrating);
};

compileToFunctions 调用栈

compileToFunctions(template, options)
    └── createCompileToFunctionFn() 返回的编译函数
        └── compile(template, options)  ← createCompiler 生成的编译函数
            └── baseCompile(template, options)
                ├── parse(template, options)
                │   ├── 解析模板字符串为 AST
                │   ├── 解析标签、属性、指令、插值表达式
                │   └── 构建 AST 节点树(含父子关系)
                ├── optimize(AST, options)
                │   └── 标记静态节点和静态根节点
                └── generate(AST, options)
                    ├── 遍历 AST,生成 render 函数字符串
                    ├── 生成 staticRenderFns 数组(静态节点渲染函数)
                    └── 返回 { render, staticRenderFns }

生成 render 函数的主要流程:

  • 获取模板字符串
    • 若传入 template 选项,直接使用模板字符串;
    • 若传入 el 选项,通过 DOM 节点的 outerHTML 属性获取 HTML 字符串;
    • 若同时存在,template 优先级高于 el
  • parse 解析
    • 调用 parse 函数,将模板字符串解析为 AST(抽象语法树)
    • 解析内容包括:HTML 标签结构、属性绑定、事件绑定、指令(v-forv-ifv-oncev-model 等)、插值表达式({{ }})、slot 插槽等;
    • 构建 AST 节点树,维护节点间的父子层级关系。
  • optimize 优化
    • 调用 optimize 函数,遍历 AST 标记静态节点静态根节点
    • 在后续 diff 更新时,跳过静态节点,减少对比开销。
  • generate 生成
    • 调用 generate 函数,遍历 AST 生成 render 函数字符串
    • 同时生成 staticRenderFns 数组(用于静态节点的独立渲染);
    • 最终通过 new Function 将字符串转换为可执行的 render 函数,内部通过 with(this) 绑定 Vue 实例上下文。
var _a = createCompiler(baseOptions);
var compileToFunctions = _a.compileToFunctions;
var createCompiler = createCompilerCreator(function baseCompile(template, options) {
    var ast = parse(template.trim(), options);
    // ...
    var code = generate(ast, options);
    return {
        ast: ast,
        render: code.render,
        staticRenderFns: code.staticRenderFns
    };
});
function createCompilerCreator(baseCompile) {
    return function createCompiler(baseOptions) {
        function compile(template, options) {
            // ...
            var compiled = baseCompile(template.trim(), finalOptions);
            {
                detectErrors(compiled.ast, warn);
            }
            compiled.errors = errors;
            compiled.tips = tips;
            return compiled;
        }
        return {
            compile: compile,
            compileToFunctions: createCompileToFunctionFn(compile)
        };
    };
}
function createCompileToFunctionFn(compile) {
    var cache = Object.create(null);
    return function compileToFunctions(template, options, vm) {
        // ...
        var compiled = compile(template, options);
        // ...
        var res = {};
        var fnGenErrors = [];
        res.render = createFunction(compiled.render, fnGenErrors);
        res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
            return createFunction(code, fnGenErrors);
        });
        // ...
        return (cache[key] = res);
    };
}

function generate(ast, options) {
    var state = new CodegenState(options);
    var code = ast ? ast.tag === 'script' ? 'null' : genElement(ast, state) : '_c("div")';
    return {
        render: "with(this){return ".concat(code, "}"),
        staticRenderFns: state.staticRenderFns
    };
}

mountComponent 调用栈

mountComponent(vm, el, hydrating)
    ├── callHook(vm, 'beforeMount')
    ├── 定义 updateComponent
    │   └── vm._update(vm._render(), hydrating)
    │       ├── Vue.prototype._render()
    │       │   └── render.call(vm, vm.$createElement)
    │       │       ├── 读取响应式数据 → 触发 getter → 依赖收集
    │       │       └── 返回 VNode 树
    │       └── Vue.prototype._update()
    │           └── vm.__patch__(prevVnode, vnode)
    │               ├── 首次渲染(prevVnode 不存在)
    │               │   └── createElm(vnode)
    │               │       ├── createElement(tag) → 创建 DOM 元素
    │               │       ├── 递归 createChildren() → 创建子节点
    │               │       └── insert(parentElm, elm)
    │               │           └── nodeOps.appendChild() / nodeOps.insertBefore()
    │               └── 更新渲染(prevVnode 存在)
    │                   └── patchVnode(oldVnode, vnode)
    │                       ├── 更新节点属性、事件、样式
    │                       ├── updateChildren() → 双端 diff 子节点
    │                       │   ├── sameVnode 判断 → 复用/更新/创建/删除
    │                       │   └── patchVnode 递归处理子节点
    │                       └── 返回更新的 DOM 节点
    └── new Watcher(vm, updateComponent, ...)
        ├── 标记为渲染 Watcher
        ├── 立即执行 updateComponent() → 完成首次渲染
        └── 后续数据变化 → 异步执行 updateComponent() → 更新渲染

挂载主要流程:

  • new Watcher(vm, updateComponent) 创建渲染 Watcher,并立即执行 updateComponent,完成首次渲染;
  • updateComponent 内部执行 vm._render(),调用 render 函数生成 VNode 树,过程中读取响应式数据触发 getter,完成依赖收集
  • 随后执行 vm._update(vnode),内部调用 vm.__patch__ 更新 DOM
  • __patch__ 区分首次渲染与更新渲染:
    • 首次渲染:调用 createElm 递归创建 DOM 节点,通过 nodeOps.appendChildinsertBefore 插入真实 DOM
    • 更新渲染:调用 patchVnode 对比新旧 VNode,通过 updateChildren 双端 diff 算法最小化 DOM 操作
  • 后续响应式数据变化时,setter 触发 dep.notify(),Watcher 推入异步队列,最终重新执行 updateComponent,走更新渲染路径
function mountComponent(vm, el, hydrating) {
    // ...
    updateComponent = function () {
        vm._update(vm._render(), hydrating);
    };
    // ...
    new Watcher(vm, updateComponent, noop, watcherOptions, true);
    // ...
}

var Watcher = (function () {
    function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
        // ...
        if (isFunction(expOrFn)) {
            this.getter = expOrFn;
        } else {
            this.getter = parsePath(expOrFn);
            // ..
        }
        this.value = this.lazy ? undefined : this.get();
    }
    // ...
    return Watcher;
}());

function lifecycleMixin(Vue) {
    Vue.prototype._update = function (vnode, hydrating) {
        // ...
        if (!prevVnode) {
            vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
        } else {
            vm.$el = vm.__patch__(prevVnode, vnode);
        }
        // ...
    };
}

var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules$1 });
Vue.prototype.__patch__ = inBrowser ? patch : noop;
function createPatchFunction(backend) {
    // ...
    function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
        // ...
        if (isDef(tag)) {
            // ...
            insert(parentElm, vnode.elm, refElm);
            // ...
        } else if (isTrue(vnode.isComment)) {
            vnode.elm = nodeOps.createComment(vnode.text);
            insert(parentElm, vnode.elm, refElm);
        } else {
            vnode.elm = nodeOps.createTextNode(vnode.text);
            insert(parentElm, vnode.elm, refElm);
        }
    }
    // ...
    return function patch(oldVnode, vnode, hydrating, removeOnly) {
        // ...
        if (isUndef(oldVnode)) {
            isInitialPatch = true;
            createElm(vnode, insertedVnodeQueue);
        } else {
            var isRealElement = isDef(oldVnode.nodeType);
            if (!isRealElement && sameVnode(oldVnode, vnode)) {
                patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
            } else {
                // ...
                createElm(vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm));
                // ...
            }
        }
        // ...
    };
}

function insert(parent, elm, ref) {
    if (isDef(parent)) {
        if (isDef(ref)) {
            if (nodeOps.parentNode(ref) === parent) {
                nodeOps.insertBefore(parent, elm, ref);
            }
        } else {
            nodeOps.appendChild(parent, elm);
        }
    }
}
var nodeOps = Object.freeze({
    // ...
    insertBefore: insertBefore,
    removeChild: removeChild,
    appendChild: appendChild,
    // ...
});
function appendChild(node, child) {
    node.appendChild(child);
}

VNode 虚拟节点

vm._render() 执行render函数,调用$createElement生成VNode:

mountComponent
    └── updateComponent
        └── vm._render (Vue.prototype._render)
        └── render.call(vm._renderProxy, vm.$createElement)
            └── vm.$createElement
                └── createElement
                    └── new VNode()
function renderMixin(Vue) {
    // ...
    Vue.prototype._render = function () {
        // ...
        var vnode;
        try {
            setCurrentInstance(vm);
            currentRenderingInstance = vm;
            vnode = render.call(vm._renderProxy, vm.$createElement);
        }
        // ...
        return vnode;
    };
}

编译输出的render函数字符串示例:

render.toString()
# function anonymous() {with(this){return _c('div',{on:{"click":onClickAlert}},[_c('p',[_v(_s(vueVersion))]),_v(" "),_c('p',[_v(_s(message))])])}}
function anonymous() {
    with(this){
        return _c('div',{on:{"click":onClickAlert}},[
            _c('p',[_v(_s(vueVersion))]),
            _v(" "),
            _c('p',[_v(_s(message))])
        ])
    }
}
  • with(this):改变作用域指向vm._renderProxy
  • _c:创建普通元素VNode;
  • _v:创建文本VNode;
  • _s:序列化数据。

vm._renderProxyinitProxy做渲染时代理,拦截未定义变量抛出提示。

initProxy = function initProxy(vm) {
    if (hasProxy_1) {
        // ...
        vm._renderProxy = new Proxy(vm, handlers);
    } else {
        vm._renderProxy = vm;
    }
};

_c:对_createElement的封装:

function initRender(vm) {
    // ...
    vm._c = function (a, b, c, d) { return createElement$1(vm, a, b, c, d, false); };
    vm.$createElement = function (a, b, c, d) { return createElement$1(vm, a, b, c, d, true); };
    // ...
}

function createElement$1(context, tag, data, children, normalizationType, alwaysNormalize) {
    // ...
    return _createElement(context, tag, data, children, normalizationType);
}

function _createElement(context, tag, data, children, normalizationType) {
    // ...
    var vnode, ns;
    if (typeof tag === 'string') {
        var Ctor = void 0;
        ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
        if (config.isReservedTag(tag)) {
            // ...
            vnode = new VNode(config.parsePlatformTagName(tag), data, children, undefined, undefined, context);
        } else if ((!data || !data.pre) &&
                   // ...
                   vnode = createComponent(Ctor, data, context, children, tag);
                   } else {
            // ...
            vnode = new VNode(tag, data, children, undefined, undefined, context);
        }
    } else {
        vnode = createComponent(tag, data, context, children);
    }
    // ...
}

VNode 构造函数:

var VNode = /** @class */ (function () {
    function VNode(tag, data, children, text, elm, context, componentOptions, asyncFactory) {
        this.tag = tag;
        this.data = data;
        this.children = children;
        this.text = text;
        this.elm = elm;
        this.ns = undefined;
        this.context = context;
        this.fnContext = undefined;
        this.fnOptions = undefined;
        this.fnScopeId = undefined;
        this.key = data && data.key;
        this.componentOptions = componentOptions;
        this.componentInstance = undefined;
        this.parent = undefined;
        this.raw = false;
        this.isStatic = false;
        this.isRootInsert = true;
        this.isComment = false;
        this.isCloned = false;
        this.isOnce = false;
        this.asyncFactory = asyncFactory;
        this.asyncMeta = undefined;
        this.isAsyncPlaceholder = false;
    }
    Object.defineProperty(VNode.prototype, "child", {
        get: function () {
            return this.componentInstance;
        },
        enumerable: false,
        configurable: true
    });
    return VNode;
}());

DOM-Diff(patch)

DOM-Diff 通过对比变化前后的虚拟节点,计算出需要更新的节点,以达到尽可能少操作 DOM 的目的。

DOM-Diff 在 patch 函数中实现。

mountComponent
    └── vm._update()
        └── Vue.prototype._update()
            └── __patch__()
                └── Vue.prototype.__patch__
                    └── patch()
                        └── createPatchFunction()  ← 返回平台特定的 patch 函数
var patch = createPatchFunction({
    nodeOps: nodeOps,
    modules: modules$1
});

function sameVnode(a, b) {
    return (a.key === b.key &&
        a.asyncFactory === b.asyncFactory &&
        ((a.tag === b.tag &&
                a.isComment === b.isComment &&
                isDef(a.data) === isDef(b.data) &&
                sameInputType(a, b)) ||
            (isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error))));
}

function createPatchFunction(backend) {
    function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {}
    return function patch(oldVnode, vnode, hydrating, removeOnly) {};
}

patch

patch()
    ├── vnode 不存在,oldVnode 存在
    │   └── invokeDestroyHook(oldVnode)  → 销毁旧节点
    ├── oldVnode 不存在
    │   └── createElm(vnode)  → 创建新节点
    └── oldVnode 存在且不是真实节点
        ├── sameVnode(oldVnode, vnode) === true
        │   └── patchVnode(oldVnode, vnode)
        │       ├── oldVnode === vnode → 不处理
        │       ├── 均为静态节点 → 不处理
        │       ├── vnode 有 text → 替换文本
        │       ├── 新旧都有 children → updateChildren()
        │       ├── 只有新节点有 children → addVnodes()
        │       ├── 只有旧节点有 children → removeVnodes()
        │       └── 均无 children → 清空文本
        └── sameVnode(oldVnode, vnode) === false
            └── 删除旧节点,创建新节点
function patch(oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
        if (isDef(oldVnode))
            invokeDestroyHook(oldVnode);
        return;
    }
    var isInitialPatch = false;
    var insertedVnodeQueue = [];
    if (isUndef(oldVnode)) {
        isInitialPatch = true;
        createElm(vnode, insertedVnodeQueue);
    } else {
        var isRealElement = isDef(oldVnode.nodeType);
        if (!isRealElement && sameVnode(oldVnode, vnode)) {
            patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
        } else {
            // ...
            createElm(vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm));
            // ...
        }
    }
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
    return vnode.elm;
};

patchVnode

patch()
    ├── vnode 不存在,oldVnode 存在
    │   └── invokeDestroyHook(oldVnode)  → 销毁旧节点
    ├── oldVnode 不存在
    │   └── createElm(vnode)  → 创建新节点
    └── oldVnode 存在且不是真实节点
        ├── sameVnode(oldVnode, vnode) === true
        │   └── patchVnode(oldVnode, vnode)
        │       ├── oldVnode === vnode → 不处理
        │       ├── 均为静态节点 → 不处理
        │       ├── vnode 有 text → 替换文本
        │       ├── 新旧都有 children → updateChildren()
        │       ├── 只有新节点有 children → addVnodes()
        │       ├── 只有旧节点有 children → removeVnodes()
        │       └── 均无 children → 清空文本
        └── sameVnode(oldVnode, vnode) === false
            └── 删除旧节点,创建新节点
function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
    if (oldVnode === vnode) {
        return;
    }
    // ...
    var oldCh = oldVnode.children;
    var ch = vnode.children;
    // ...
    if (isDef(data) && isPatchable(vnode)) {
        for (i = 0; i < cbs.update.length; ++i)
            cbs.update[i](oldVnode, vnode);
        if (isDef((i = data.hook)) && isDef((i = i.update)))
            i(oldVnode, vnode);
    }
    if (isUndef(vnode.text)) {
        if (isDef(oldCh) && isDef(ch)) {
            if (oldCh !== ch) {
                updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
            }
        } else if (isDef(ch)) {
            checkDuplicateKeys(ch);
            if (isDef(oldVnode.text)) {
                nodeOps.setTextContent(elm, '');
            }
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
        } else if (isDef(oldCh)) {
            removeVnodes(oldCh, 0, oldCh.length - 1);
        } else if (isDef(oldVnode.text)) {
            nodeOps.setTextContent(elm, '');
        }
    } else if (oldVnode.text !== vnode.text) {
        nodeOps.setTextContent(elm, vnode.text);
    }
    // ...
}

备注cbs.update包含updateAttrsupdateClassupdateDOMListenersupdateDOMPropsupdateStyle,更新DOM属性、样式、事件。

updateChildren

双端diff核心,使用首尾指针移动对比,尽可能原地复用DOM。

updateChildren()
    ├── 初始化四指针:oldStartIdx、oldEndIdx、newStartIdx、newEndIdx
    └── while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)
        ├── oldStartVnode 为 undefined → oldStartIdx++,进入下一轮循环
        ├── oldEndVnode 为 undefined → oldEndIdx--,进入下一轮循环
        ├── sameVnode(旧头,新头) 匹配成功
        │   └── patchVnode(),双指针同时向内收缩,进入下一轮循环
        ├── sameVnode(旧尾,新尾) 匹配成功
        │   └── patchVnode(),双尾指针向内收缩,进入下一轮循环
        ├── sameVnode(旧头,新尾) 匹配成功
        │   └── patchVnode(),移动DOM,指针收缩,进入下一轮循环
        ├── sameVnode(旧尾,新头) 匹配成功
        │   └── patchVnode(),移动DOM,指针收缩,进入下一轮循环
        └── 四次比对全部失败
            ├── 构建 oldKeyToIdx key-index 哈希映射表
            ├── 查找 newStartVnode 在旧数组中的位置 idxInOld
            ├── idxInOld 不存在 → createElm() 创建新DOM
            └── idxInOld 存在
                ├── sameVnode 可复用 → patchVnode(),标记旧位置undefined,移动DOM
                └── sameVnode 不可复用 → createElm() 创建新DOM
            └── newStartIdx++,进入下一轮循环
    ├── while循环结束
    │   ├── oldStartIdx > oldEndIdx → addVnodes() 批量新增剩余新节点
    │   └── newStartIdx > newEndIdx → removeVnodes() 批量删除旧数组多余节点
    └── 函数结束
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0;
    let newStartIdx = 0;
    let oldEndIdx = oldCh.length - 1;
    let oldStartVnode = oldCh[0];
    let oldEndVnode = oldCh[oldEndIdx];
    let newEndIdx = newCh.length - 1;
    let newStartVnode = newCh[0];
    let newEndVnode = newCh[newEndIdx];
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm;

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (isUndef(oldStartVnode)) {
            oldStartVnode = oldCh[++oldStartIdx];
        } else if (isUndef(oldEndVnode)) {
            oldEndVnode = oldCh[--oldEndIdx];
        } else if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
        } else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndIdx];
        } else if (sameVnode(oldStartVnode, newEndVnode)) {
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
            canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
        } else if (sameVnode(oldEndVnode, newStartVnode)) {
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
            canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
        } else {
            if (isUndef(oldKeyToIdx)) {
                oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
            }
            idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
            if (isUndef(idxInOld)) {
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
            } else {
                vnodeToMove = oldCh[idxInOld];
                if (sameVnode(vnodeToMove, newStartVnode)) {
                    patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
                    oldCh[idxInOld] = undefined;
                    canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
                } else {
                    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
                }
            }
            newStartVnode = newCh[++newStartIdx];
        }
    }

    if (oldStartIdx > oldEndIdx) {
        refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
        addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
    } else if (newStartIdx > newEndIdx) {
        removeVnodes(oldCh, oldStartIdx, oldEndIdx);
    }
}

响应式系统源码

核心函数defineReactive,三大核心类:ObserverDepWatcher

defineReactive

defineReactive为对象属性设置get/set,每一个属性拥有独立Dep实例。读取触发get做依赖收集;修改触发set调用dep.notify()通知Watcher更新。只有属性被读取才会收集依赖,如果模板没有使用该属性,不会收集进subs。

function defineReactive(obj, key, val, customSetter, shallow, mock) {
    var dep = new Dep();
    var property = Object.getOwnPropertyDescriptor(obj, key);
    // ...
    // 默认对子属性也进行响应式绑定
    var childOb = !shallow && observe(val, false, mock);
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            // 当读取属性时,将 Dep.target 添加到 Dep 的依赖收集中
            var value = getter ? getter.call(obj) : val;
            // Dep.target 主要是在 Watcher 的 get 方法中赋值的,其他地方也会设置
            if (Dep.target) {
                dep.depend({
                    target: obj,
                    type: "get" /* TrackOpTypes.GET */,
                    key: key
                });
                if (childOb) {
                    childOb.dep.depend();
                    if (isArray(value)) {
                        dependArray(value);
                    }
                }
            }
            return isRef(value) && !shallow ? value.value : value;
        },
        set: function reactiveSetter(newVal) {
            // ...
            childOb = !shallow && observe(newVal, false, mock);
            dep.notify({
                type: "set" /* TriggerOpTypes.SET */,
                target: obj,
                key: key,
                newValue: newVal,
                oldValue: value
            });
        }
    });
}

Observer

Observer遍历对象所有属性,调用defineReactive;数组会重写变异方法。

var Observer = (function () {
    function Observer(value, shallow, mock) {
        // ...
        if (isArray(value)) {} else {
            // ...
            var keys = Object.keys(value);
            for (var i = 0; i < keys.length; i++) {
                var key = keys[i];
                defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock);            }
        }
    }
    // ...
    return Observer;
}());
function observe(value, shallow, ssrMockReactivity) {
    // ...
    if (/** ... **/) {
        return new Observer(value, shallow, ssrMockReactivity);
    }
}

Dep

保存Watcher订阅列表,实现depend收集、notify通知。

var Dep = (function () {
    function Dep() {
        this._pending = false;
        this.id = uid$2++;
        this.subs = [];
    }
    Dep.prototype.addSub = function (sub) {
        this.subs.push(sub);
    };
    Dep.prototype.removeSub = function (sub) {
        this.subs[this.subs.indexOf(sub)] = null;
        if (!this._pending) {
            this._pending = true;
            pendingCleanupDeps.push(this);
        }
    };
    Dep.prototype.depend = function (info) {
        if (Dep.target) {
            Dep.target.addDep(this);
            if (info && Dep.target.onTrack) {
                Dep.target.onTrack(__assign({ effect: Dep.target }, info));
            }
        }
    };
    Dep.prototype.notify = function (info) {
        if (!config.async) {
            subs.sort(function (a, b) { return a.id - b.id; });
        }
        for (var i = 0, l = subs.length; i < l; i++) {
            var sub = subs[i];
            if (info) {
                sub.onTrigger &&
                    sub.onTrigger(__assign({ effect: subs[i] }, info));
            }
            sub.update();
        }
    };
    return Dep;
}());

Dep.target = null;
var targetStack = [];
function pushTarget(target) {
    targetStack.push(target);
    Dep.target = target;
}
function popTarget() {
    targetStack.pop();
    Dep.target = targetStack[targetStack.length - 1];
}

Watcher

观察者分为:渲染Watcher、computed Watcher、用户watch Watcher。

var Watcher = /** @class */ (function () {
    function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
        this.value = this.lazy ? undefined : this.get();
    }
    Watcher.prototype.get = function () {
        pushTarget(this);
        var value;
        var vm = this.vm;
        try {
            value = this.getter.call(vm, vm);
        } finally {
            popTarget();
            this.cleanupDeps();
        }
        return value;
    };
    Watcher.prototype.addDep = function (dep) { };
    Watcher.prototype.cleanupDeps = function (dep) { };
    Watcher.prototype.update = function () {
        if (this.lazy) {
            this.dirty = true;
        } else if (this.sync) {
            this.run();
        } else {
            queueWatcher(this);
        }
    };
    Watcher.prototype.run = function () {
        if (this.active) {
            var value = this.get();
        }
    };
    return Watcher;
}());

响应式整体执行流程(以渲染Watcher举例)

  • defineReactive劫持data所有属性get、set;
  • 实例化渲染Watcher,执行get,把自身赋值给Dep.target
  • 执行render函数读取data,触发getter,执行dep.depend()收集Watcher进入subs;
  • 修改data触发setter,执行dep.notify()
  • 通知所有Watcher执行update,进入队列,执行run,再次执行render,得到新VNode,执行patch更新DOM。

data、computed、watch 初始化链路

data 初始化链路:Vue_initinitStateinitDataobserve(data)

function initData(vm) {
    var data = vm.$options.data;
    var ob = observe(data);
    ob && ob.vmCount++;
}

computed 初始化链路:Vue_initinitStateinitComputed$1,创建lazy:true的Watcher。

function initComputed$1(vm, computed) {
    var watchers = (vm._computedWatchers = Object.create(null));
    for (var key in computed) {
        var userDef = computed[key];
        var getter = isFunction(userDef) ? userDef : userDef.get;
        watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions);
    }
}

watch 初始化链路:Vue_initinitStateinitWatchcreateWatchervm.$watch → new Watcher。

function initWatch(vm, watch) {
    for (var key in watch) {
        var handler = watch[key];
        if (isArray(handler)) {
            for (var i = 0; i < handler.length; i++) {
                createWatcher(vm, key, handler[i]);
            }
        } else {
            createWatcher(vm, key, handler);
        }
    }
}

function stateMixin(Vue) {
    Vue.prototype.$watch = function (expOrFn, cb, options) {
        var watcher = new Watcher(vm, expOrFn, cb, options);
    };
}

一个极简的 Vue

响应式系统应该实现以下三点:

  • 将传参 data 处理成响应式;
  • 一个将模板解析为 DOM 的渲染函数;
  • data 改变时,触发视图改变,即自动调用渲染函数。
// index.js
function MyVue(options) {
    this.$options = options
    this.template = null
    this.methods = {}
    this.init()
}
MyVue.prototype.init = function () {
    const vm = this
    vm.template = document.querySelector(vm.$options.template)
    vm.methods = vm.$options.methods
    const data = vm.$options.data()
    const keys = Object.keys(data)
    keys.forEach(k => {
        defineReactive(vm, k, data[k])
    })
}
MyVue.prototype.$mount = function (selector) {
    const vm = this
    new Watcher(this, () => {
        render(selector, vm)
    })
}
window.MyVue = MyVue
<div id="app"></div>
<script src="./index.js"></script>
<script id="content" type="x-template">
  <div @click="onClickMessage">
        {{message}}
    </div>
</script>
<script>
    const app = new MyVue({
        template: '#content',
        data: function () {
            return {
                message: `Hello, Lizhao. This is ${new Date()}.`
            }
        },
        methods: {
            onClickMessage () {
                this.message = `Hello, Lizhao. This is ${new Date()}.`
            }
        }
    })
    app.$mount('#app')
</script>
function defineReactive(obj, key, val) {
    var dep = new Dep();
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            if (Dep.target) {
                dep.depend();
            }
            return val
        },
        set: function reactiveSetter(newVal) {
            val = newVal
            dep.notify();
        }
    });
    return dep;
}
function render (selector, inst) {
    const dom = parseToDom(inst.template.innerHTML, inst);
    const $app = document.querySelector(selector);
    $app.innerHTML = '';
    $app.appendChild(dom)
}
function parseToDom (html, inst) {
    const dom = new DOMParser().parseFromString(html, 'text/html').body.childNodes[0]
    dom.innerText = dom.innerText.replace(/{{.*}}/, function (str) {
        const key = str.replace(/{{(.*)}}/, '$1')
        return inst[key]
    })
    const methodName = dom.getAttribute('@click')
    dom.addEventListener('click', function () {
        inst.methods[methodName].bind(inst)()
    }, false)  
    return dom
}
var Dep = (function () {
    function Dep() {
        this.subs = [];
    }
    Dep.prototype.addSub = function (sub) {
        this.subs.push(sub);
    };
    Dep.prototype.depend = function () {
        if (Dep.target) {
            this.addSub(Dep.target)
        }
    }
    Dep.prototype.removeSub = function (sub) {
        this.subs = this.subs.filter((item) => sub !== item)
    };
    Dep.prototype.notify = function () {
        this.subs.forEach((sub) => { sub.update() })
    };
    return Dep;
}());
Dep.target = null;

var Watcher = (function () {
    function Watcher(vm, expOrFn) {
        this.deps = []
        this.vm = vm
        this.getter = expOrFn
        this.value = this.get()
    }
    Watcher.prototype.get = function () {
        Dep.target = this
        const value = this.getter.call(this.vm)
        Dep.target = null
        return value;
    };

    Watcher.prototype.addDep = function (dep) {
        this.deps.push(dep)
    };
    Watcher.prototype.update = function () {
        const value = this.getter.call(this.vm)
        return value
    };
    return Watcher;
}());

常见问题

为什么 Vue 的响应式是观察者模式?

虽然 Vue 内部使用了 Dep 和 Watcher,但它的实现更接近观察者模式,因为:

  • Dep(被观察者)直接持有 Watcher(观察者)的引用;
  • 数据变化时,Dep 直接调用 Watcher.update(),没有中间的事件总线。

组件层面则不同:父子组件通过 $on / $emit 通信;若使用全局 Event Bus 或 Vuex 做跨组件传递,则属于发布-订阅模式。

观察者模式和发布-订阅模式有什么不同?

观察者模式:目标(Subject)直接通知观察者(Observer),两者互相知道对方,属于松耦合(但仍有直接依赖)。

发布-订阅模式:发布者(Publisher)和订阅者(Subscriber)互不相识,通过事件中心(Event Bus)通信,属于完全解耦。

两者主要差异:

  • 中间层:观察者模式无中间层,Subject 直接管理 Observer;发布-订阅模式有 Event Bus、Broker 等调度中心。
  • 耦合度:观察者模式中 Subject 持有 Observer 引用;发布-订阅模式中发布者与订阅者互不可见。
  • 灵活性:观察者模式较低,所有观察者收到同一通知;发布-订阅模式较高,支持按事件类型(event name)精确订阅。

常见场景:观察者模式用于数据变化驱动视图更新(Vue 响应式、MVC 中的 Model-View);发布-订阅模式用于跨组件通信(Event Bus、DOM 事件、Node.js EventEmitter)。

为什么说 Vue 没有完全遵循 MVVM?

MVVM(Model-View-ViewModel)由三部分组成:

  • Model:数据与业务逻辑;
  • View:UI 视图;
  • ViewModel:中间绑定层。

严格 MVVM 规范要求 View 和 Model 不允许直接通信,全部经过 ViewModel 中转。

Vue 提供 $refs,代码可以直接操作 DOM 视图,因此 Vue 没有完全严格遵循 MVVM 模型。

为什么使用 Virtual DOM?

  • 跨平台:VNode 为纯 JS 对象,不依赖浏览器 DOM,支持浏览器、SSR、Weex 等多环境。
  • 减少 DOM 开销:JS 计算成本远低于 DOM 操作;diff 算法计算最小变更,减少真实 DOM 操作。

注意:首次渲染需要创建 VNode 对象,首次渲染并不会变快;优势体现在频繁更新场景

key 在 v-for 虚拟节点中的作用

key 用于 diff 过程识别 VNode 是否是同一个节点。

  • 设置 key:依靠 key 精准判断,移动/删除节点,减少错误复用;
  • 不设置 key:使用就地复用策略,尽量修改现有 DOM,列表顺序变化容易产生 UI 错乱 bug。

注意:同父元素下子节点 key 必须唯一,不要直接使用数组 index 作为 key。

参考资料

Vue2 官方文档

Vue3 文档:响应式深度讲解

© lizhao all right reserved,powered by Gitbook文件修订时间: 2026-09-03 01:55:15

results matching ""

    No results matching ""