主题
Vue 2 源码笔记
以 Vue 2.6.14 源码为主线,梳理"数据 → DOM"这条核心链路:响应式系统、依赖收集与 Watcher、computed/watch 原理、响应式的边界、渲染挂载、异步更新调度与 diff 算法。文中源码均引用自 Vue 2.6.14,为便于阅读有删节(省略部分 Flow 类型与开发环境警告分支),关键注释保留原文。
目录
总览:一条数据从 data 到 DOM 的旅程
初始化阶段(同步完成):
new Vue(options)
└─ _init // src/core/instance/init.js
└─ initState // src/core/instance/state.js
├─ initData → observe(data) 把 data 转为响应式
├─ initComputed → new Watcher(lazy) 为每个 computed 建 computed-watcher
└─ initWatch → new Watcher(user) 为每个 watch 建 user-watcher
└─ $mount // 运行时编译器入口
└─ compileToFunctions // template → render 函数(未提供 render 时)
└─ mountComponent // src/core/instance/lifecycle.js
└─ new Watcher(updateComponent) render-watcher
└─ 首次渲染:_render() 生成 VNode → __patch__() 生成真实 DOM
渲染中访问 data/computed → 触发 getter → Dep 收集 render-watcher更新阶段(异步进行):
data 变化 → setter → dep.notify()
└─ watcher.update() → queueWatcher() 去重、入队
└─ nextTick(flushSchedulerQueue) 微任务执行
└─ 队列按 id 排序 → 依次 run()
└─ render-watcher.run → _render → __patch__ → diff 更新 DOM文中涉及的源码文件与职责:
| 文件 | 职责 |
|---|---|
src/core/observer/index.js | observe / Observer / defineReactive / $set / $delete |
src/core/observer/dep.js | Dep 依赖管理器 |
src/core/observer/watcher.js | Watcher(三类实例共用的类) |
src/core/observer/array.js | 数组 7 个变异方法的拦截 |
src/core/observer/scheduler.js | queueWatcher / flushSchedulerQueue 异步调度 |
src/core/util/next-tick.js | nextTick 与定时器降级链 |
src/core/instance/state.js | initState / initComputed / initWatch / $watch |
src/core/instance/lifecycle.js | mountComponent / _update |
src/core/instance/render.js | _render |
src/core/vdom/patch.js | __patch__ / sameVnode / updateChildren |
一、响应式系统
1. observe 与 Observer
initState 中 initData 的最后一件事是 observe(data, true),把整个 data 变成响应式。observe 的职责是:只做判断,判断通过后创建/复用 Observer。
js
// src/core/observer/index.js
export function observe(value, asRootData) {
// 非对象(含 VNode)不处理
if (!isObject(value) || value instanceof VNode) {
return;
}
let ob;
if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
// 已响应式化,直接复用,避免重复劫持
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) && // 冻结(Object.freeze)对象跳过
!value._isVue // Vue 实例自身跳过
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++; // 记录被多少个 Vue 实例当作根 $data($set 警告用)
}
return ob;
}Observer 负责把对象/数组真正转为响应式:
js
export class Observer {
constructor(value) {
this.value = value;
this.dep = new Dep(); // 挂在对象/数组自己身上的 dep
this.vmCount = 0;
def(value, "__ob__", this); // 把自身挂到 value.__ob__(不可枚举)
if (Array.isArray(value)) {
// 数组:先替换原型上的 7 个变异方法(见"四、3. 数组响应式拦截")
if (hasProto) {
protoAugment(value, arrayMethods); // 支持 __proto__:直接换原型
} else {
copyAugment(value, arrayMethods, arrayKeys); // 不支持(IE10-):方法拷到实例上
}
this.observeArray(value); // 再逐项 observe
} else {
this.walk(value); // 对象:逐 key defineReactive
}
}
walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
observeArray(items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]); // 数组元素若是对象/数组,递归转响应式
}
}
}def 是 Object.defineProperty 的封装(enumerable: false),所以 value.__ob__ 能访问但不会被枚举。
两个容易混淆的 dep:
- 属性自己的 dep:
defineReactive里每个 key 闭包一个Dep,拦截该属性的 get/set。 - Observer 的 dep(
ob.dep):挂在整个对象/数组身上。对象的用武之地是$set/$delete(新增/删除属性时手动通知);数组的用武之地是 7 个变异方法(索引访问无法拦截,只能靠方法通知)。
2. defineReactive 与 childOb
defineReactive 是响应式的核心:用 Object.defineProperty 把每个属性转成 getter/setter。
js
export function defineReactive(obj, key, val, customSetter, shallow) {
const dep = new Dep(); // 每个属性一个闭包 dep
const property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return; // 不可配置的属性无法重定义,直接放弃
}
// 兼容属性上已有的 getter/setter(如 props 在 initProps 里先劫持过)
const getter = property && property.get;
const setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
// 关键:对值本身也做响应式。值是对象/数组时 childOb 非空
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend(); // 收集本属性的依赖
if (childOb) {
childOb.dep.depend(); // 同时收集子对象的 ob.dep
if (Array.isArray(value)) {
dependArray(value); // 数组:逐元素收集,元素被整体替换时能通知到
}
}
}
return value;
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val;
// 值未变化直接返回;NaN !== NaN,用自比较兼容
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
if (process.env.NODE_ENV !== "production" && customSetter) {
customSetter();
}
// 只有 getter 没有 setter 的属性不允许赋值(如 props)
if (getter && !setter) return;
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal); // 新值也要转响应式
dep.notify();
},
});
}childOb 的意义:observe(val) 会对属性值再建一层 Observer。childOb.dep.depend() 把当前 Watcher 也收集进子对象的 ob.dep——于是"给嵌套对象新增属性"(this.obj.a = 1 走 $set)时,$set 内部的 ob.dep.notify() 才能通知到外层渲染。没有 childOb,新增属性就永远"失联"了。
3. Dep 依赖管理器
js
// src/core/observer/dep.js
export default class Dep {
static target: ?Watcher;
constructor () {
this.id = uid++
this.subs = [] // 订阅了本属性的 Watcher 集合
}
addSub (sub) {
this.subs.push(sub)
}
removeSub (sub) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this) // 让"当前 Watcher"把本 dep 记进自己的 deps(双向收集)
}
}
notify () {
const subs = this.subs.slice()
// 正常情况下 watcher 在调度队列里已按 id 排序;
// 非异步模式(config.async=false,仅测试用)下这里兜底排序
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep.target = null
const targetStack = []
export function pushTarget (target) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}依赖是双向收集的:
defineReactive 里每个 key 闭包一个 dep ──addSub──▶ watcher(可多个)
▲ │
└────────── watcher.addDep(dep) ◀── 记进 watcher.deps(id 去重)Dep.target 是"当前正在求值的 Watcher"。用栈(targetStack)而不是单变量,是因为求值可以嵌套:渲染时访问 computed → computed-watcher 求值(压栈)→ 其内部又访问 data(再压栈),求值完成后逐层弹出恢复,保证每个 getter 收集到的都是正确的"当前 Watcher"。
二、依赖收集与 Watcher
1. Watcher 的三类实例
Vue 2 里只有一个 Watcher 类,靠构造参数分化出三种用途:
| 类型 | 创建时机 | expOrFn | 关键 options | 数据变化时的行为 |
|---|---|---|---|---|
| render-watcher | mountComponent | updateComponent 函数 | before 钩子 + isRenderWatcher | 触发重新渲染 |
| user-watcher | initWatch → $watch | watch 的 key(字符串) | user: true,可选 deep/sync/immediate | 执行回调函数 |
| computed-watcher | initComputed | computed 的 getter | lazy: true | 仅置 dirty = true,等被访问时重算 |
构造器关键逻辑:
js
// src/core/observer/watcher.js
constructor (vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this // render-watcher 单独记录
}
vm._watchers.push(this) // 所有 watcher 都进 vm._watchers
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // 全局递增 id,调度排序的依据
this.active = true
this.dirty = this.lazy // lazy watcher 初始就是"脏"的
this.deps = [] // 上次求值收集的依赖
this.newDeps = [] // 本次求值收集的依赖
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// expOrFn 是函数直接用;是字符串则 parsePath 解析成 getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn) // 'a.b.c' → 逐级取值函数
if (!this.getter) {
this.getter = noop
// 开发环境警告:watch 只接受点分隔的简单路径
}
}
// 非 lazy 立即求值一次:
// render-watcher 完成首次渲染,user-watcher 完成首次依赖收集
this.value = this.lazy
? undefined
: this.get()
}lazy 与 dirty 的关系:lazy: true 时构造器不调用 get(),且 dirty 初始为 true——computed 第一次被访问时才求值。
2. 依赖收集:get、addDep 与 cleanupDeps
js
get () {
pushTarget(this) // Dep.target = this(入栈)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm) // 求值:读数据 → 触发 getter → dep.depend()
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) {
traverse(value) // deep:递归"摸"一遍所有子属性,完成深层收集
}
popTarget()
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)
}
}
}
cleanupDeps () {
// 求值结束后,把本次没再访问到的旧依赖解除订阅(如 v-if 切换后的分支)
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
// 交换 deps/newDeps,清空 newDeps 供下次求值复用
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}依赖收集三步:求值读取数据 → getter 里 dep.depend() → Dep.target.addDep(dep) → dep.addSub(watcher),双向绑定完成。cleanupDeps 保证每次求值后依赖清单都是"最新访问集"——这正是 v-if 分支切换后,旧分支数据不再触发更新的原因。
三、computed 与 watch 原理
1. computed 原理
三步:initComputed 建 lazy watcher → defineComputed 在 vm 上拦截属性 → 渲染访问时 createComputedGetter 决定是否重算。
js
// src/core/instance/state.js
const computedWatcherOptions = { lazy: true };
function initComputed(vm, computed) {
const watchers = (vm._computedWatchers = Object.create(null));
// SSR 下 computed 退化为普通 getter,不建 watcher(服务端无响应式需求)
const isSSR = isServerRendering();
for (const key in computed) {
const userDef = computed[key];
const getter = typeof userDef === "function" ? userDef : userDef.get;
if (process.env.NODE_ENV !== "production" && getter == null) {
warn(`Getter is missing for computed property "${key}".`, vm);
}
if (!isSSR) {
// 为每个 computed 建一个 lazy watcher
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions,
);
}
// 组件级 computed 在 Vue.extend 时已定义在原型上,这里只处理实例级
if (!(key in vm)) {
defineComputed(vm, key, userDef);
} else if (process.env.NODE_ENV !== "production") {
// 与 data/props/methods 重名时告警
}
}
}
export function defineComputed(target, key, userDef) {
const shouldCache = !isServerRendering();
if (typeof userDef === "function") {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef);
sharedPropertyDefinition.set = noop;
} else {
// 对象写法:get/set 分开
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key) // 默认:带缓存的 getter
: createGetterInvoker(userDef.get) // cache: false:每次访问都重算
: noop;
sharedPropertyDefinition.set = userDef.set || noop;
}
if (
process.env.NODE_ENV !== "production" &&
sharedPropertyDefinition.set === noop
) {
// 没有 setter 时,给赋值行为一个开发警告
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this,
);
};
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate(); // 求值:内部 this.get() 把 computed-watcher 收进数据 dep
}
if (Dep.target) {
watcher.depend(); // 把"当前 watcher"(通常是 render-watcher)转发给数据 dep
}
return watcher.value;
}
};
}computed 的依赖链路:
data.a ──dep──▶ subs: [computed-watcher, render-watcher]
▲ ▲
evaluate() 求值时 渲染访问 computed 时
getter 读 data.a watcher.depend() 转发收集
(Dep.target 是自身)变化时的完整链路:data.a 变化 → dep.notify() 同时叫醒两者 → computed-watcher 是 lazy,update() 只把 dirty 置 true,不渲染;render-watcher 入队 → 重新渲染中访问 computed → dirty 为 true → evaluate() 重算拿到新值。若某次渲染没有访问该 computed,它就保持"脏"状态直到下次被访问——这就是"惰性求值 + 缓存"的本质:读的是 watcher.value,不重新访问依赖就不会重算。
2. watch 原理
调用链:initState → initWatch → createWatcher → vm.$watch → new Watcher(user)。
js
// src/core/instance/state.js
function initWatch(vm, watch) {
for (const key in watch) {
const handler = watch[key];
if (Array.isArray(handler)) {
// watch 的 value 是数组:多个回调,逐个创建
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
createWatcher(vm, key, handler);
}
}
}
function createWatcher(vm, expOrFn, handler, options) {
if (isPlainObject(handler)) {
// 对象声明:{ handler, deep, immediate } → 拆出回调和 options
options = handler;
handler = handler.handler;
}
if (typeof handler === "string") {
handler = vm[handler]; // handler: 'methodName' → 从实例上取方法
}
return vm.$watch(expOrFn, handler, options);
}$watch 定义在 stateMixin 里(入口文件调用,挂在 Vue.prototype 上):
js
Vue.prototype.$watch = function (expOrFn, cb, options) {
const vm = this;
if (isPlainObject(cb)) {
// $watch('a', { handler, deep }) 写法,回炉到 createWatcher
return createWatcher(vm, expOrFn, cb, options);
}
options = options || {};
options.user = true; // 标记为 user-watcher(错误处理走 handleError)
const watcher = new Watcher(vm, expOrFn, cb, options);
if (options.immediate) {
// 立即执行一次回调;pushTarget 使 Dep.target 为空,回调里读数据不会误收集依赖
const info = `callback for immediate watcher "${watcher.expression}"`;
pushTarget();
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info);
popTarget();
}
return function unwatchFn() {
watcher.teardown(); // 返回值用于取消监听
};
};回调的执行在 Watcher.run(),触发条件比"新旧值不同"更宽:
js
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// 对象/数组即使引用相同也要触发——值可能被原地修改
isObject(value) ||
this.deep
) {
const oldValue = this.value
this.value = value
if (this.user) {
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}deep 的实现:get() 的 finally 里 traverse(value) 递归访问所有子属性,把沿途每个 dep 都收进依赖——此后任何深层属性变化都能叫醒它。
3. 两者对比
| computed | watch | |
|---|---|---|
| 本质 | 惰性 watcher(lazy: true) | 用户 watcher(user: true) |
| 用途 | 从已有数据派生新值,模板里当属性用 | 响应变化执行副作用(异步请求、复杂逻辑) |
| 求值时机 | 首次被访问 / 依赖变化后下次被访问 | 创建时立即求值一次;变化后执行回调 |
| 缓存 | 有(dirty 控制) | 无 |
| 依赖收集 | evaluate() 时收集;depend() 转发给渲染 | get() 时收集 |
四、响应式的边界与补充
Object.defineProperty 只能拦截已存在属性的读写。三种情况感知不到:新增属性、删除属性、数组索引赋值。所以需要 $set/$delete 和数组方法拦截来补上。
1. $set
js
// src/core/observer/index.js(Vue.set / vm.$set 同源)
export function set(target, key, val) {
// ① 前置校验:目标是 undefined/null/原始值直接警告
if (
process.env.NODE_ENV !== "production" &&
(isUndef(target) || isPrimitive(target))
) {
warn(
`Cannot set reactive property on undefined, null, or primitive value: ${target}`,
);
}
// ② 数组 + 合法索引:用被拦截的 splice 完成"改值 + 通知"
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key); // 防止索引超长度导致 splice 行为错误
target.splice(key, 1, val);
return val;
}
// ③ 已存在的属性:直接赋值即可(setter 已拦截,会自动通知)
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
const ob = target.__ob__;
// ④ 禁止给 Vue 实例或根 $data 动态加属性(vmCount > 0 说明是根 $data)
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== "production" &&
warn(
"Avoid adding reactive properties to a Vue instance or its root $data " +
"at runtime - declare it upfront in the data option.",
);
return val;
}
// ⑤ 非响应式对象:直接赋值
if (!ob) {
target[key] = val;
return val;
}
// ⑥ 核心:把新属性转响应式,再手动通知依赖
defineReactive(ob.value, key, val);
ob.dep.notify();
return val;
}为什么 ob.dep.notify() 能通知到 render-watcher?因为在"一、2"里 childOb.dep.depend() 已经把子对象的 ob.dep 收进了渲染依赖——给子对象 $set 新属性时,正是这个 dep 在派发更新。
2. $delete
js
export function del(target, key) {
// 前置校验同 set
if (
process.env.NODE_ENV !== "production" &&
(isUndef(target) || isPrimitive(target))
) {
warn(
`Cannot delete reactive property on undefined, null, or primitive value: ${target}`,
);
}
// 数组:走被拦截的 splice
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1);
return;
}
const ob = target.__ob__;
// 同样禁止删除 Vue 实例 / 根 $data 的属性
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== "production" &&
warn(
"Avoid deleting properties on a Vue instance or its root $data " +
"- just set it to null.",
);
return;
}
// 本来就没有的属性,什么都不做
if (!hasOwn(target, key)) {
return;
}
delete target[key];
// 非响应式对象,删完即止
if (!ob) {
return;
}
ob.dep.notify();
}3. 数组响应式拦截
数组没有用 defineProperty 逐索引劫持(性能差且 length 难处理),而是重写 7 个会改变数组自身的变异方法:
js
// src/core/observer/array.js
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto); // 继承原原型的新对象
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]; // 先保存原方法,避免递归调用
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args); // 执行原始逻辑
const ob = this.__ob__;
let inserted;
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2); // splice 从第 3 个参数起才是新元素
break;
}
if (inserted) ob.observeArray(inserted); // 新插入的元素也要转响应式
ob.dep.notify(); // 通知更新
return result;
});
});挂载方式(在 Observer 构造器里):
js
if (hasProto) {
protoAugment(value, arrayMethods); // 支持 __proto__:把数组原型换成 arrayMethods
} else {
copyAugment(value, arrayMethods, arrayKeys); // 不支持(IE10-):方法逐个拷贝到实例
}四个要点:
- 不污染
Array.prototype:拦截方法只存在于arrayMethods上,普通数组不受影响。 - 先保存
original再调用:若直接写arrayProto[method].apply,调到的就是被自己覆盖后的函数,会无限递归(这其实是网上流传最广的一版错误伪代码)。 - 只有
push/unshift/splice会新增元素,需要observeArray让新元素也可响应;pop/shift/sort/reverse不需要。 - 数组索引赋值(
arr[0] = x)与length修改仍然无法拦截——必须用$set或splice。
"数组整体被替换"(this.list = newList)则由 defineReactive 的 childOb + dependArray 处理,与元素级拦截无关。
五、渲染与挂载
1. new Vue 与 _init
js
// src/core/instance/index.js
function Vue(options) {
if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) {
warn("Vue is a constructor and should be called with the `new` keyword");
}
this._init(options);
}_init(src/core/instance/init.js)中与本主线相关的顺序:
js
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // 注入要先于 data/props 解析
initState(vm) // props → methods → data → computed → watch
initProvide(vm)
callHook(vm, 'created')
...
vm.$mount(vm.$options.el) // 挂载入口initState 内部顺序有讲究——data 先于 computed/watch 响应式化,保证后者能用:
js
// src/core/instance/state.js
export function initState(vm) {
vm._watchers = [];
const opts = vm.$options;
if (opts.props) initProps(vm, opts.props);
if (opts.methods) initMethods(vm, opts.methods);
if (opts.data) {
initData(vm); // 内部最后一行:observe(data, true)
} else {
observe((vm._data = {}), true);
}
if (opts.computed) initComputed(vm, opts.computed);
if (opts.watch && opts.watch !== nativeWatch) {
// nativeWatch 是 Firefox 挂在 Object.prototype 上的原生 watch 函数,跳过
initWatch(vm, opts.watch);
}
}2. $mount 的两次定义
完整版(含编译器)构建里,$mount 被定义过两次:先有"运行时版"(runtime/index.js),编译器入口(entry-runtime-with-compiler.js)保存原始引用后重写,在挂载前补上模板编译:
js
// src/platforms/web/entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (el, hydrating) {
el = el && query(el);
const options = this.$options;
// 没有 render 函数时才需要编译模板
if (!options.render) {
let template = options.template;
if (template) {
if (typeof template === "string") {
if (template.charAt(0) === "#") {
template = idToTemplate(template); // '#app' → 取元素 innerHTML
}
} else if (template.nodeType) {
template = template.innerHTML; // 传的是 DOM 节点
} else {
return this; // 非法 template
}
} else if (el) {
template = getOuterHTML(el); // 没传 template 就取挂载点 outerHTML
}
if (template) {
// 编译:模板字符串 → render 函数 + staticRenderFns
const { render, staticRenderFns } = compileToFunctions(
template,
{
outputSourceRange: process.env.NODE_ENV !== "production",
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments,
},
this,
);
options.render = render;
options.staticRenderFns = staticRenderFns;
}
}
return mount.call(this, el, hydrating); // 调回原始 $mount
};模板来源优先级:render 函数 > template 选项 > el 挂载点的 outerHTML。compileToFunctions 内部流程是 parse(模板 → AST)→ optimize(标记静态节点,跳过 diff)→ generate(AST → 代码字符串)→ new Function(代码 → render 函数)。
原始 $mount 只是查询元素并交给 mountComponent:
js
// src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (el, hydrating) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating);
};3. mountComponent 与 updateComponent
mountComponent(src/core/instance/lifecycle.js)做四件事:
js
export function mountComponent(vm, el, hydrating) {
vm.$el = el;
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode; // 无 render 兜底为空节点
// 开发环境:runtime-only 构建使用 template 时报错
}
callHook(vm, "beforeMount"); // ①
let updateComponent;
updateComponent = () => {
vm._update(vm._render(), hydrating); // ② 渲染函数:VNode → patch 到 DOM
};
// ③ 创建 render-watcher(isRenderWatcher = true)
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, "beforeUpdate"); // 每次重渲染前触发
}
},
},
true,
);
hydrating = false;
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, "mounted"); // ④ 根实例在此触发 mounted;子组件在 patch 的 insert 钩子里触发
}
return vm;
}Watcher 构造器末尾的 this.value = this.get() 会立即执行 updateComponent——首次渲染就是这次求值完成的,同时 render 里读到的所有响应式数据都把 render-watcher 收进自己的 dep。
4. _render 与 _update
js
// src/core/instance/render.js
Vue.prototype._render = function () {
const vm = this
const { render, _parentVnode } = vm.$options
...
let vnode
try {
vnode = render.call(vm._renderProxy, vm.$createElement) // 执行 render → VNode 树
} catch (e) {
handleError(e, vm, `render`)
vnode = vm._vnode // 渲染报错时回退到旧 vnode,避免白屏
}
...
vnode.parent = _parentVnode
return vnode
}js
// src/core/instance/lifecycle.js
Vue.prototype._update = function (vnode, hydrating) {
const vm = this
const prevEl = vm.$el
const prevVnode = vm._vnode
...
vm._vnode = vnode
if (!prevVnode) {
// 初次渲染:oldVnode 是真实 DOM
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)
} else {
// 数据更新:两棵 VNode 树 diff
vm.$el = vm.__patch__(prevVnode, vnode)
}
...
}__patch__ 的来源:Vue.prototype.__patch__ = inBrowser ? patch : noop,而 patch 由 createPatchFunction({ nodeOps, modules }) 工厂创建——nodeOps 封装平台 DOM 操作,modules 处理 attrs/class/style/事件等指令(平台相关),核心 diff 算法平台无关。这也是 Vue 能跨 Web/Weex 的原因。
六、更新调度
数据变化 → setter → dep.notify() → 每个 watcher 的 update():
js
// src/core/observer/watcher.js
update () {
if (this.lazy) {
this.dirty = true // computed:只置脏,等下次被访问
} else if (this.sync) {
this.run() // watch 的 sync 选项:同步执行
} else {
queueWatcher(this) // 默认:进入异步队列
}
}1. queueWatcher:去重与入队
js
// src/core/observer/scheduler.js
let has = {}; // watcher.id → true,用于去重
let queue = [];
let waiting = false;
let flushing = false;
let index = 0;
export function queueWatcher(watcher) {
const id = watcher.id;
if (has[id] == null) {
has[id] = true;
if (!flushing) {
queue.push(watcher); // 未在刷新:直接入队尾
} else {
// 正在刷新中:按 id 插入到正确位置(保证执行顺序)
let i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
if (!waiting) {
waiting = true;
nextTick(flushSchedulerQueue); // 只调度一次,同一事件循环共享一次 flush
}
}
}要点:同一事件循环里,一个 watcher 被多次触发只入队一次(has[id]);waiting 保证一次 tick 只注册一个 flush 任务——这就是"数据改多次、DOM 只更新一次"的原因。
2. flushSchedulerQueue:排序与执行
js
function flushSchedulerQueue() {
flushing = true;
let watcher, id;
// 排序的三重含义(源码注释原文):
// 1. 父组件先于子组件更新(父总是先创建,id 更小)
// 2. 组件的 user-watcher 先于 render-watcher(watch 先建,render 后建)
// 3. 父组件 watcher 执行中销毁了子组件时,子组件的 watcher 可以直接跳过
queue.sort((a, b) => a.id - b.id);
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
watcher.before(); // render-watcher 在此触发 beforeUpdate
}
id = watcher.id;
has[id] = null;
watcher.run();
// 开发环境:检测循环更新(watch 回调里改自己监听的值会再次入队)
if (process.env.NODE_ENV !== "production" && has[id] != null) {
circular[id] = (circular[id] || 0) + 1;
if (circular[id] > MAX_UPDATE_COUNT) {
// 100 次后报错中断
warn("You may have an infinite update loop...");
break;
}
}
}
// 刷新完:重置调度状态,再依次触发 activated / updated 钩子
resetSchedulerState();
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
}
function callUpdatedHooks(queue) {
let i = queue.length;
while (i--) {
// 倒序:子组件先于父组件触发 updated
const watcher = queue[i];
const vm = watcher.vm;
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, "updated");
}
}
}3. nextTick 与降级链
flushSchedulerQueue 的异步载体是 nextTick(src/core/util/next-tick.js)。定时器按能力逐级降级:
js
let timerFunc;
if (typeof Promise !== "undefined" && isNative(Promise)) {
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
// iOS UIWebView 的 Promise.then 可能卡住不刷微任务队列,
// 加一个空定时器"逼"浏览器把微任务队列刷掉
if (isIOS) setTimeout(noop);
};
isUsingMicroTask = true;
} else if (
!isIE &&
typeof MutationObserver !== "undefined" &&
(isNative(MutationObserver) ||
// PhantomJS 和 iOS 7.x 的兜底
MutationObserver.toString() === "[object MutationObserverConstructor]")
) {
// 无原生 Promise 时(PhantomJS/iOS7/Android 4.4)用 MutationObserver
// IE11 的 MutationObserver 不可靠(#6466)
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, { characterData: true });
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter); // 变更文本节点 → 触发微任务
};
isUsingMicroTask = true;
} else if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
// setImmediate 是宏任务,但比 setTimeout 快
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}降级链:Promise → MutationObserver → setImmediate → setTimeout(前两者是微任务,后两者是宏任务)。Vue 2.5 曾在事件回调内退化为宏任务(解决 #6566 等事件冒泡问题),2.6 起统一走微任务。
nextTick 本体:
js
const callbacks = [];
let pending = false;
export function nextTick(cb, ctx) {
let _resolve;
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, "nextTick");
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
timerFunc(); // 同一 tick 内只注册一次定时器
}
// 不传回调时返回 Promise(Vue 2.1.0+)
if (!cb && typeof Promise !== "undefined") {
return new Promise((resolve) => {
_resolve = resolve;
});
}
}
function flushCallbacks() {
pending = false;
const copies = callbacks.slice(0); // 拷贝一份再清空:flush 期间新 push 的 cb 留到下一轮
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}pending + callbacks 两个全局状态:同 tick 内多次调用 nextTick 只注册一次 timerFunc,回调按序攒在队列里一次执行。用户手动 this.$nextTick(cb) 与调度器的 nextTick(flushSchedulerQueue) 共享同一个队列——所以"修改数据后立刻用 $nextTick 拿 DOM"能拿到更新后的值。
七、diff 算法
1. patch 入口与 sameVnode
patch(oldVnode, vnode) 先判断 oldVnode 是真实 DOM 还是 VNode:
js
// src/core/vdom/patch.js(删节)
return function patch (oldVnode, vnode, hydrating, removeOnly) {
...
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 新旧根节点可复用 → 就地 patch
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
// 首次渲染:把真实 DOM 包装成 VNode
oldVnode = emptyNodeAt(oldVnode)
}
// 不是 sameVnode 或首渲染:创建全新 DOM 树
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(vnode, insertedVnodeQueue, ...)
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0) // 移除旧树
}
}
return vnode.elm
}sameVnode 判定两个节点是否值得"复用 + 就地更新":
js
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)) || // input 还要求 type 一致
// 异步组件的占位节点
(isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error)))
);
}简化理解:key 相同 + tag 相同 + 都是/都不是注释节点 + data 有无一致。
2. patchVnode
sameVnode 成立后进入 patchVnode——复用 oldVnode.elm,就地更新:
- 复用元素:
vnode.elm = oldVnode.elm。 - 更新节点自身:执行
cbs.update(attrs/class/style/事件/directives 等模块)。 - 处理子节点,四种情况:
- 新旧都有
children→ 进入updateChildren双端比较; - 只有新有 →
addVnodes新增; - 只有旧有 →
removeVnodes移除; - 只有文本 → 新文本与旧文本不同才
setTextContent。
- 新旧都有
3. updateChildren 双端比较
四个指针,从两端向中间靠拢:
oldCh: oldStartIdx ─────────────────▶ oldEndIdx
newCh: newStartIdx ─────────────────▶ newEndIdx五步命中策略(每轮按顺序尝试):
| 步骤 | 条件 | 动作 |
|---|---|---|
| 1 | oldStart 与 newStart 同 | patchVnode,两指针右移 |
| 2 | oldEnd 与 newEnd 同 | patchVnode,两指针左移 |
| 3 | oldStart 与 newEnd 同(节点右移) | patchVnode + DOM 移到 oldEnd 之后;oldStart 右移、newEnd 左移 |
| 4 | oldEnd 与 newStart 同(节点左移) | patchVnode + DOM 移到 oldStart 之前;oldEnd 左移、newStart 右移 |
| 5 | 都不匹配 | 拿 newStart 的 key 在旧区间建索引查找 |
第 5 步的三种分支:
- 找不到 →
createElm新建节点,插到 oldStart 之前; - 找到且 sameVnode → patchVnode,旧节点在数组里置
undefined(占位跳过),DOM 移到 oldStart 之前; - 找到但 key 相同元素不同(same key but different element)→ 当作新元素
createElm新建。
源码(删节):
js
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;
// removeOnly 是 <transition-group> 专用:离场过渡期间保持元素相对位置
const canMove = !removeOnly;
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 {
// 兜底:按 key 建索引(惰性创建,只建一次)
if (isUndef(oldKeyToIdx))
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx); // 没 key 就逐个遍历
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 {
// key 相同但元素不同 → 当新元素处理
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);
}
}key 的意义:第 5 步靠 key 建索引才能 O(1) 定位可复用节点;没有 key 只能 findIdxInOld 逐个遍历,且 sameVnode 判定退化为"tag 相同就复用",同 tag 节点错位时容易误复用、错删——这也是"列表渲染必须加 key"的源码依据。