Skip to content

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.jsobserve / Observer / defineReactive / $set / $delete
src/core/observer/dep.jsDep 依赖管理器
src/core/observer/watcher.jsWatcher(三类实例共用的类)
src/core/observer/array.js数组 7 个变异方法的拦截
src/core/observer/scheduler.jsqueueWatcher / flushSchedulerQueue 异步调度
src/core/util/next-tick.jsnextTick 与定时器降级链
src/core/instance/state.jsinitState / initComputed / initWatch / $watch
src/core/instance/lifecycle.jsmountComponent / _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-watchermountComponentupdateComponent 函数before 钩子 + isRenderWatcher触发重新渲染
user-watcherinitWatch → $watchwatch 的 key(字符串)user: true,可选 deep/sync/immediate执行回调函数
computed-watcherinitComputedcomputed 的 getterlazy: 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. 两者对比 ​

computedwatch
本质惰性 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-):方法逐个拷贝到实例
}

四个要点:

  1. 不污染 Array.prototype:拦截方法只存在于 arrayMethods 上,普通数组不受影响。
  2. 先保存 original 再调用:若直接写 arrayProto[method].apply,调到的就是被自己覆盖后的函数,会无限递归(这其实是网上流传最广的一版错误伪代码)。
  3. 只有 push/unshift/splice 会新增元素,需要 observeArray 让新元素也可响应;pop/shift/sort/reverse 不需要。
  4. 数组索引赋值(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,就地更新:

  1. 复用元素:vnode.elm = oldVnode.elm。
  2. 更新节点自身:执行 cbs.update(attrs/class/style/事件/directives 等模块)。
  3. 处理子节点,四种情况:
    • 新旧都有 children → 进入 updateChildren 双端比较;
    • 只有新有 → addVnodes 新增;
    • 只有旧有 → removeVnodes 移除;
    • 只有文本 → 新文本与旧文本不同才 setTextContent。

3. updateChildren 双端比较 ​

四个指针,从两端向中间靠拢:

oldCh:  oldStartIdx ─────────────────▶ oldEndIdx
newCh:  newStartIdx ─────────────────▶ newEndIdx

五步命中策略(每轮按顺序尝试):

步骤条件动作
1oldStart 与 newStart 同patchVnode,两指针右移
2oldEnd 与 newEnd 同patchVnode,两指针左移
3oldStart 与 newEnd 同(节点右移)patchVnode + DOM 移到 oldEnd 之后;oldStart 右移、newEnd 左移
4oldEnd 与 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"的源码依据。