手把手带你阅读vue2源码

手把手带你阅读Vue2源码

前言

Vue.js 作为目前最流行的前端框架之一,其源码设计精妙,架构清晰。通过阅读Vue2的源码,我们不仅能深入理解其工作原理,还能学习到很多优秀的设计模式和编程思想。本文将带你从零开始,深入Vue2的核心源码,理解其实现原理。

Vue2源码结构

首先,让我们了解一下Vue2的源码目录结构:

csharp 复制代码
src/
├── core/                    # 核心代码
│   ├── instance/           # Vue实例相关
│   │   ├── index.ts        # Vue构造函数
│   │   ├── init.ts         # 实例初始化
│   │   ├── state.ts        # 状态管理(data、props、computed等)
│   │   ├── render.ts       # 渲染相关
│   │   ├── lifecycle.ts    # 生命周期
│   │   └── ...
│   ├── observer/           # 响应式系统
│   │   ├── index.ts        # Observer类
│   │   ├── dep.ts          # 依赖收集
│   │   ├── watcher.ts      # 观察者
│   │   ├── scheduler.ts    # 调度器
│   │   └── array.ts        # 数组响应式处理
│   ├── vdom/               # 虚拟DOM
│   └── util/               # 工具函数
├── compiler/               # 模板编译
└── platforms/              # 平台相关代码

一、Vue构造函数与实例初始化

1.1 Vue构造函数

Vue的入口在 src/core/instance/index.ts

typescript 复制代码
function Vue(options) {
  if (__DEV__ && !(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

// 通过Mixin模式扩展Vue原型
initMixin(Vue)      // 初始化相关
stateMixin(Vue)     // 状态相关
eventsMixin(Vue)    // 事件相关
lifecycleMixin(Vue) // 生命周期相关
renderMixin(Vue)    // 渲染相关

Vue采用了Mixin模式来组织代码,将不同功能的代码分别通过不同的Mixin注入到Vue原型上,这样既保持了代码的模块化,又避免了单一文件过大。

1.2 实例初始化流程

当执行 new Vue(options) 时,会调用 _init 方法,定义在 src/core/instance/init.ts

typescript 复制代码
Vue.prototype._init = function (options?: Record<string, any>) {
  const vm: Component = this
  
  // 1. 唯一标识
  vm._uid = uid++
  
  // 2. 标记为Vue实例
  vm._isVue = true
  
  // 3. 合并选项
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor as any),
    options || {},
    vm
  )
  
  // 4. 初始化代理(开发环境)
  if (__DEV__) {
    initProxy(vm)
  } else {
    vm._renderProxy = vm
  }
  
  // 5. 初始化生命周期
  initLifecycle(vm)
  
  // 6. 初始化事件
  initEvents(vm)
  
  // 7. 初始化渲染
  initRender(vm)
  
  // 8. 调用beforeCreate钩子
  callHook(vm, 'beforeCreate')
  
  // 9. 初始化injections
  initInjections(vm)
  
  // 10. 初始化状态(data、props、methods、computed、watch)
  initState(vm)
  
  // 11. 初始化provide
  initProvide(vm)
  
  // 12. 调用created钩子
  callHook(vm, 'created')
  
  // 13. 如果有el选项,自动挂载
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}

关键点:

  • 初始化顺序很重要:beforeCreateinjectionsstateprovidecreated
  • 这样设计是为了确保在 created 钩子中能够访问到 dataprops 等状态
  • injectionsstate 之前初始化,providestate 之后初始化,符合依赖注入的设计

二、响应式系统核心原理

Vue2的响应式系统是其核心特性,基于 Object.defineProperty 实现。主要包含三个核心类:ObserverDepWatcher

2.1 Observer(观察者)

Observer 类负责将普通对象转换为响应式对象,定义在 src/core/observer/index.ts

typescript 复制代码
export class Observer {
  dep: Dep
  vmCount: number

  constructor(public value: any, public shallow = false, public mock = false) {
    this.dep = mock ? mockDep : new Dep()
    this.vmCount = 0
    
    // 在对象上添加 __ob__ 属性,指向Observer实例
    def(value, '__ob__', this)
    
    if (isArray(value)) {
      // 数组:重写数组方法
      if (hasProto) {
        value.__proto__ = arrayMethods
      } else {
        for (let i = 0, l = arrayKeys.length; i < l; i++) {
          def(value, arrayKeys[i], arrayMethods[arrayKeys[i]])
        }
      }
      this.observeArray(value)
    } else {
      // 对象:遍历属性,转换为getter/setter
      const keys = Object.keys(value)
      for (let i = 0; i < keys.length; i++) {
        defineReactive(value, keys[i], NO_INITIAL_VALUE, undefined, shallow, mock)
      }
    }
  }
}

核心机制:

  • 为对象添加 __ob__ 属性,存储Observer实例
  • 数组通过重写原型方法实现响应式
  • 对象通过 defineReactive 将属性转换为getter/setter

2.2 defineReactive(定义响应式属性)

defineReactive 是响应式系统的核心函数:

typescript 复制代码
export function defineReactive(
  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,
  mock?: boolean
) {
  const dep = new Dep() // 每个属性都有独立的Dep实例

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  const getter = property && property.get
  const setter = property && property.set

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      
      // 依赖收集:如果当前有Watcher,将其添加到Dep中
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      
      // 值没有变化,直接返回
      if (!hasChanged(value, newVal)) {
        return
      }
      
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      
      // 新值也需要变成响应式
      childOb = observe(newVal, false, mock)
      
      // 通知所有依赖更新
      dep.notify()
    }
  })
}

工作流程:

  1. getter :当访问属性时,如果当前有 Dep.target(Watcher),调用 dep.depend() 收集依赖
  2. setter :当修改属性时,调用 dep.notify() 通知所有依赖更新

2.3 Dep(依赖收集器)

Dep 类负责管理依赖,定义在 src/core/observer/dep.ts

typescript 复制代码
export default class Dep {
  static target?: DepTarget | null
  id: number
  subs: Array<DepTarget | null>

  constructor() {
    this.id = uid++
    this.subs = []
  }

  // 添加订阅者
  addSub(sub: DepTarget) {
    this.subs.push(sub)
  }

  // 移除订阅者
  removeSub(sub: DepTarget) {
    this.subs[this.subs.indexOf(sub)] = null
  }

  // 依赖收集:将当前Watcher添加到subs中
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  // 通知所有订阅者更新
  notify() {
    const subs = this.subs.filter(s => s) as DepTarget[]
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// 全局唯一的当前Watcher
Dep.target = null
const targetStack: Array<DepTarget | null | undefined> = []

export function pushTarget(target?: DepTarget | null) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget() {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

设计亮点:

  • Dep.target 是全局唯一的,同一时间只能有一个Watcher在收集依赖
  • 使用栈结构 targetStack 处理嵌套Watcher的情况

2.4 Watcher(观察者)

Watcher 是连接响应式数据和视图更新的桥梁,定义在 src/core/observer/watcher.ts

typescript 复制代码
export default class Watcher implements DepTarget {
  vm?: Component | null
  expression: string
  cb: Function
  id: number
  deps: Array<Dep>
  newDeps: Array<Dep>
  getter: Function
  value: any

  constructor(
    vm: Component | null,
    expOrFn: string | (() => any),
    cb: Function,
    options?: WatcherOptions | null,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    
    // 将表达式转换为getter函数
    if (isFunction(expOrFn)) {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    
    this.value = this.lazy ? undefined : this.get()
  }

  // 获取值,触发依赖收集
  get() {
    pushTarget(this) // 设置当前Watcher为Dep.target
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm) // 执行getter,触发属性的get
    } catch (e: any) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } finally {
      if (this.deep) {
        traverse(value) // 深度遍历,收集所有嵌套属性的依赖
      }
      popTarget() // 恢复之前的Watcher
      this.cleanupDeps() // 清理不再需要的依赖
    }
    return value
  }

  // 依赖更新时调用
  update() {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run() // 同步执行
    } else {
      queueWatcher(this) // 异步执行,加入队列
    }
  }

  // 执行回调
  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) {
          this.cb.call(this.vm, value, oldValue)
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
}

Watcher类型:

  • Render Watcher:每个组件实例都有一个,用于更新视图
  • User Watcher :用户通过 $watchwatch 选项创建的
  • Computed Watcher :计算属性的Watcher,具有 lazy 特性

2.5 响应式系统流程图

scss 复制代码
访问属性 (getter)
    ↓
Dep.target 存在?
    ↓ 是
dep.depend() → watcher.addDep(dep) → dep.addSub(watcher)
    ↓
依赖收集完成

修改属性 (setter)
    ↓
dep.notify()
    ↓
遍历 subs,调用 watcher.update()
    ↓
queueWatcher(watcher)
    ↓
nextTick(flushSchedulerQueue)
    ↓
watcher.run() → watcher.get() → 重新渲染/执行回调

三、渲染系统

3.1 render函数

每个Vue组件都有一个render函数,定义在 src/core/instance/render.ts

typescript 复制代码
Vue.prototype._render = function (): VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options

  // 设置父vnode
  vm.$vnode = _parentVnode
  
  // 执行render函数,生成vnode
  let vnode
  try {
    setCurrentInstance(vm)
    currentRenderingInstance = vm
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e: any) {
    handleError(e, vm, `render`)
    vnode = vm._vnode
  } finally {
    currentRenderingInstance = prevRenderInst
    setCurrentInstance(prevInst)
  }
  
  // 确保返回单个根节点
  if (isArray(vnode) && vnode.length === 1) {
    vnode = vnode[0]
  }
  
  if (!(vnode instanceof VNode)) {
    vnode = createEmptyVNode()
  }
  
  vnode.parent = _parentVnode
  return vnode
}

关键点:

  • render 函数接收 createElement 作为参数
  • vm._renderProxy 在开发环境下是Proxy,用于警告未定义的属性
  • render函数执行时会访问响应式数据,触发依赖收集

3.2 渲染Watcher

在组件挂载时,会创建一个Render Watcher:

typescript 复制代码
export function mountComponent(
  vm: Component,
  el: Element | null | undefined,
  hydrating?: boolean
): Component {
  // ...
  
  let updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }

  new Watcher(
    vm,
    updateComponent,
    noop,
    {
      before() {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, 'beforeUpdate')
        }
      }
    },
    true /* isRenderWatcher */
  )
  
  // ...
}

当响应式数据变化时:

  1. 触发属性的setter
  2. 调用 dep.notify()
  3. Render Watcher的 update() 被调用
  4. 加入更新队列,在 nextTick 中执行
  5. 执行 updateComponent,重新渲染

四、调度器(Scheduler)

Vue使用调度器来批量处理更新,避免频繁的DOM操作,定义在 src/core/observer/scheduler.ts

typescript 复制代码
const queue: Array<Watcher> = []
let has: { [key: number]: true | undefined | null } = {}
let waiting = false
let flushing = false

export function queueWatcher(watcher: Watcher) {
  const id = watcher.id
  
  // 去重:同一个Watcher只添加一次
  if (has[id] != null) {
    return
  }
  
  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)
  }
  
  // 确保只调用一次nextTick
  if (!waiting) {
    waiting = true
    nextTick(flushSchedulerQueue)
  }
}

function flushSchedulerQueue() {
  currentFlushTimestamp = getNow()
  flushing = true
  
  // 排序:确保父组件先于子组件更新
  queue.sort((a, b) => a.id - b.id)
  
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before() // 调用beforeUpdate钩子
    }
    has[id] = null
    watcher.run() // 执行更新
  }
  
  resetSchedulerState()
  callUpdatedHooks(updatedQueue)
}

设计优势:

  1. 批量更新:同一事件循环中的多次数据变化,只会触发一次更新
  2. 去重:同一个Watcher不会重复添加
  3. 排序:确保父组件先于子组件更新,避免不必要的子组件更新

五、数组的响应式处理

由于 Object.defineProperty 无法监听数组索引的变化,Vue2通过重写数组方法来实现响应式:

typescript 复制代码
// src/core/observer/array.ts
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)
        break
    }
    if (inserted) ob.observeArray(inserted)
    ob.dep.notify() // 通知更新
    return result
  })
})

实现原理:

  • 创建新对象继承 Array.prototype
  • 重写会改变数组的方法
  • 在方法中调用 ob.dep.notify() 通知更新
  • 对新添加的元素进行响应式处理

六、总结

通过阅读Vue2源码,我们可以学到:

  1. Mixin模式:通过Mixin组织代码,保持模块化
  2. 观察者模式:Observer、Dep、Watcher的经典实现
  3. 依赖收集:通过getter/setter实现自动依赖收集
  4. 批量更新:使用调度器优化性能
  5. 数组处理:通过重写原型方法实现数组响应式

Vue2的响应式系统虽然基于 Object.defineProperty,存在一些局限性(如无法监听新增属性、数组索引等),但其设计思想仍然值得学习。Vue3改用Proxy实现,解决了这些问题,但核心的依赖收集和更新机制仍然类似。

希望这篇文章能帮助你更好地理解Vue2的工作原理。如果你对某个部分有疑问,建议直接阅读源码,结合调试工具,会有更深入的理解。


参考资源:

相关推荐
华洛2 小时前
经验贴:Agent实战落地踩坑六大经验教训,保姆教程。
前端·javascript·产品
计算机学姐2 小时前
基于SpringBoot的新闻管理系统【协同过滤推荐算法+可视化统计】
java·vue.js·spring boot·后端·spring·mybatis·推荐算法
luckyzlb2 小时前
03-node.js & webpack
前端·webpack·node.js
一 乐2 小时前
远程在线诊疗|在线诊疗|基于java和小程序的在线诊疗系统小程序设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·小程序
左耳咚2 小时前
如何解析 zip 文件
前端·javascript·面试
程序员小寒2 小时前
前端高频面试题之Vue(初、中级篇)
前端·javascript·vue.js
陈辛chenxin2 小时前
软件测试大赛Web测试赛道工程化ai提示词大全
前端·可用性测试·测试覆盖率
沿着路走到底2 小时前
python 判断与循环
java·前端·python
Code知行合壹2 小时前
AJAX和Promise
前端·ajax