进阶篇: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.defineProperty、Proxy
- 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 完整渲染流程
完整生命周期链路:
new Vue()实例化;_init()初始化;$mount挂载;- 编译 template(完整版);
- 生成 render 函数;
mountComponent;- 执行 render 生成 VNode;
- 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 函数,产出render、staticRenderFns。
渲染
首次挂载,$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"></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; - 同时暴露静态方法:
compile、use、mixin、extend、util。
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; - 执行生命周期钩子;
- 初始化
props、setup、methods、data、computed、watch; - 处理
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-for、v-if、v-once、v-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.appendChild或insertBefore插入真实 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._renderProxy:initProxy做渲染时代理,拦截未定义变量抛出提示。
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包含updateAttrs、updateClass、updateDOMListeners、updateDOMProps、updateStyle,更新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,三大核心类:Observer、Dep、Watcher。
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 → _init → initState → initData → observe(data)。
function initData(vm) {
var data = vm.$options.data;
var ob = observe(data);
ob && ob.vmCount++;
}
computed 初始化链路:Vue → _init → initState → initComputed$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 → _init → initState → initWatch → createWatcher → vm.$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"></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。