3-4 Vue2-核心源码讲解

141 阅读13分钟

原文链接(格式更好):《3-4 Vue2-核心源码讲解》

Vue2 源码仓库:github.com/vuejs/vue

为什么还要看 vue2 的源码:

因为 vue3 结构比较最新的,并且细节很多,不利于了解核心的东西

源码入口

查找顺序:

  1. github.com/vuejs/vue/b…
    1. 可以看到dev:*的命令都是Rollup打包逻辑,我们重点跟踪dev命令
  1. github.com/vuejs/vue/b…
    1. 该文件主要是处理、生成rollup打包的配置项,dev命令对应的配置为full-dev
'full-dev': {
  // 入口配置
  entry: resolve('web/entry-runtime-with-compiler.ts'),
  dest: resolve('dist/vue.js'),
  format: 'umd',
  env: 'development',
  alias: { he: './entity-decoder' },
  banner
},
  1. github.com/vuejs/vue/b…
    1. 该文件完整代码如下:
import Vue from './runtime-with-compiler' // 关键代码
import * as vca from 'v3'
import { extend } from 'shared/util'

extend(Vue, vca)

import { effect } from 'v3/reactivity/effect'
Vue.effect = effect

export default Vue
  1. github.com/vuejs/vue/b…
    1. 该文件里面主要定义$mount,关键代码如下:
import Vue from './runtime/index'

// .....

export default Vue as GlobalAPI
  1. github.com/vuejs/vue/b…
    1. 该文件也是一些Vue的配置,关键代码如下:
import Vue from 'core/index'

// ...

export default Vue
  1. github.com/vuejs/vue/b…
    1. 该文件是Vue.prototype的配置,关键代码如下:
import Vue from './instance/index'

// ...

export default Vue
  1. github.com/vuejs/vue/b…
    1. 该文件为new Vue时调用的
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
import type { GlobalAPI } from 'types/global-api'

// 构造函数 Vue,这也是为什么我们在使用时用的为:new Vue(...)
// 因为 Vue 本质就是个构造函数
function Vue(options) {
  if (__DEV__ && !(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options) // ⭐️ 初始化的核心代码,执行 _init 方法,传参为:配置项
}

//@ts-expect-error Vue has function type
initMixin(Vue) // ⭐️ 调用 initMixin 函数,将 _init 方法挂载到 Vue.prototype 上
//@ts-expect-error Vue has function type
stateMixin(Vue)
//@ts-expect-error Vue has function type
eventsMixin(Vue)
//@ts-expect-error Vue has function type
lifecycleMixin(Vue)
//@ts-expect-error Vue has function type
renderMixin(Vue)

export default Vue as unknown as GlobalAPI
  1. github.com/vuejs/vue/b…
    1. 该文件为new Vue时执行的_init方法的定义(真正的入口):
// ...

Vue.prototype._init = function (options?: Record<string, any>) {
  // ...
}

// ...

初始化

我们在使用 Vue 时,是这样初始化的

new Vue({
  el: '#app',
  data: {
    count: 1
  },
  methods: {
    addCount() {
      this.count++
    }
  }
})

现在我们开始学习源码后,那要关注下new Vue到底做了什么

function Vue(options) {
  if (__DEV__ && !(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options) // 执行 _init 方法
}

_init 代码解析

完整源码:github.com/vuejs/vue/b…

核心源码:

// ...

// 在 Vue 原型上定义一个 _init 方法,用于初始化实例。
// 这个方法接收一个可选参数 options,类型为记录(Record)类型,键为字符串,值为任意类型
// 通常用来传入组件的选项对象。
Vue.prototype._init = function (options?: Record<string, any>) {
  // 定义一个常量 vm,类型为 Component,指向当前正在创建的 Vue 实例。
  const vm: Component = this
  // 给每个 Vue 实例设置一个唯一的 _uid 标识符,用以区分不同的实例。
  vm._uid = uid++

  let startTag, endTag
  /* istanbul ignore if */
  if (__DEV__ && config.performance && mark) {
    // 如果处于开发环境 (__DEV__) 并且支持性能标记(config.performance && mark)
    // 则进行 Vue 初始化性能检测的相关操作。
    startTag = `vue-perf-start:${vm._uid}`
    endTag = `vue-perf-end:${vm._uid}`
    mark(startTag)
  }

  // 设置 _isVue 属性为 true,表明这是一个 Vue 实例。
  vm._isVue = true
  // 避免 Vue 监听器观察到此实例。
  vm.__v_skip = true
  // 创建一个新的副作用作用域(EffectScope),用于追踪和执行副作用函数,如计算属性、watcher 等。
  // 这里指定为独立作用域,即不受父作用域的影响。
  vm._scope = new EffectScope(true /* detached */)
  // 设置 Vue 实例的副作用作用域的父级为 undefined 
  vm._scope.parent = undefined
  // 以及标识其与 Vue 实例关联。
  vm._scope._vm = true

  if (options && options._isComponent) {
    // 如果是内部组件,则调用 initInternalComponent 进行优化
    initInternalComponent(vm, options as any)
  } else {
    // ⭐️ 否则通过 mergeOptions 合并构造函数的默认选项和用户传入的选项
    // 并将结果赋值给 $options。
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor as any),
      options || {},
      vm
    )
  }
  /* istanbul ignore else */
  if (__DEV__) {
    // 如果在开发环境下,调用 initProxy 函数来设置代理访问实例数据的逻辑
    initProxy(vm)
  } else {
    // 非开发环境下,直接设置 _renderProxy 指向自身。
    vm._renderProxy = vm
  }
  // 将实例自身暴露给实例自身的 _self 属性。
  vm._self = vm
  // 调用内部方法初始化 生命周期
  initLifecycle(vm)
  // 调用内部方法初始化 事件系统
  initEvents(vm)
  // 调用内部方法初始化 渲染相关
  initRender(vm)
  // ⭐️ 调用钩子函数 beforeCreate
  callHook(vm, 'beforeCreate', undefined, false /* setContext */)
  // 在解析[数据/props]之前解析依赖项(injections)。
  initInjections(vm)
  // ⭐️ 初始化实例的状态:包括 data、props、methods、computed、watch
  initState(vm)
  // 在解析[数据/props]之后解析提供项(provide)。
  initProvide(vm)
  // ⭐️ 调用钩子函数 created
  callHook(vm, 'created')

  /* istanbul ignore if */
  if (__DEV__ && config.performance && mark) {
    // 开发环境下完成性能标记,记录 Vue 初始化过程的时间消耗,并给实例设置名称。
    vm._name = formatComponentName(vm, false)
    mark(endTag)
    measure(`vue ${vm._name} init`, startTag, endTag)
  }

  if (vm.$options.el) {
    // ⭐️ 如果实例的选项中包含 el(元素挂载点),则调用 $mount 方法挂载 Vue 实例到指定 DOM 元素。
    vm.$mount(vm.$options.el)
  }
}

// ...

mergeOptions 方法

export function mergeOptions( // 合并[构造函数上的 options]与[实例的 options]
  parent: Record<string, any>, // Vue实例构造函数上的 options
  child: Record<string, any>, // new Vue(...) 传入的 options
  vm?: Component | null // 实例本身
): ComponentOptions {
  // parent = {
  //   components:{},
  //   directives: {},
  //   filters: {},
  //   _base: Vue
  // }
  // child = { el: '#app', data: { count: 1 } }
  if (__DEV__) {
    checkComponents(child) // 检测组件名称是否合法
  }

  if (isFunction(child)) {
    // @ts-expect-error
    child = child.options // 如果 child 是函数,则取 child.options 作为 child
  }

  // 把 props 属性转为对象形式(标准结构)
  // props: ["count", "a-name"] => props: { count: { type: null }, aName: { type: null } }
  // props: { count: "number" } => props: { count: { type: "number" } }
  normalizeProps(child, vm) 
  
  // 把 inject 属性转为对象形式(标准结构)
  normalizeInject(child, vm) 

  // 把 directives 属性转为对象形式(标准结构)
  normalizeDirectives(child) 

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  if (!child._base) {
    if (child.extends) {
      // 当存在 child.extends 属性时,则调用 mergeOptions 实现合并
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      // 当存在 child.mixins 属性时,则循环调用 mergeOptions 实现合并
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options: ComponentOptions = {} as any
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField(key: any) {
    // const defaultStrat = function (parentVal: any, childVal: any): any {
    //   return childVal === undefined ? parentVal : childVal
    // }
    // defaultStrat 逻辑为:优先取 child 的值,没有再取 parent 的值

    // const strats = config.optionMergeStrategies
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

_init 的核心逻辑

  1. 传入的配置项与默认配置项合并
  2. 初始化该实例的生命周期、事件系统、渲染逻辑
  3. 调用该实例的beforeCreated钩子
  4. 初始化该实例的状态:data、props 等
  5. 调用该实例的created钩子
  6. el存在则执行该实例的挂载$mount逻辑

数据观测

vue 是数据驱动的,数据改变则视图也跟着变化。核心方式是Object.defineProperty()实现数据的劫持

vue 的数据观测核心机制是观察者模式

数据是被观察的一方,当数据发生变化时通知所有观察者,这样观察者就能做出响应(比如:重新渲染视图)

我们将观察者称为watcher,关系为data -> watcher

一个数据可以有多个观察者,通过中间对象dep来记录这种依赖关系data -> dep -> watcher

dep的数据结构为{ id: uuid, subs: []},其中的subs用于存放所有观察者

源码分析:

代码为 init.ts 里面的initState方法里面的initData方法

核心代码的展示与组合:

// initData 方法:
// vm:当前实例
function initData(vm: Component) {
  // 获取实例上的 data 值
  let data: any = vm.$options.data
  // 当 data 为函数时,则执行它并获取它的返回值,否则直接用 data
  data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
  if (!isPlainObject(data)) {
    // 当 data 不是对象时,报警告
    data = {}
    __DEV__ &&
      warn(
        'data functions should return an object:\n' +
          'https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
        vm
      )
  }
  // proxy data on instance
  const keys = Object.keys(data) // 获取 data 对象的 key 数组
  const props = vm.$options.props // 获取实例的 props 值
  const methods = vm.$options.methods // 获取实例的 methods 值
  let i = keys.length // data 的 key 数组长度
  while (i--) { // 递减循环,即从数组的后往前循环
    const key = keys[i] // 获取具体的 key
    if (__DEV__) {
      if (methods && hasOwn(methods, key)) {
        // data 的 key 与 methods 的 key重复时,报警告
        warn(`Method "${key}" has already been defined as a data property.`, vm)
      }
    }
    if (props && hasOwn(props, key)) {
      // data 的 key 与 props 的 key重复时,报警告
      __DEV__ &&
        warn(
          `The data property "${key}" is already declared as a prop. ` +
            `Use prop default value instead.`,
          vm
        )
    } else if (!isReserved(key)) {
      /**
      //  * Check if a string starts with $ or _
      //  */
      // export function isReserved(str: string): boolean {
      //   const c = (str + '').charCodeAt(0)
      //   return c === 0x24 || c === 0x5f
      // }
      // ⭐️ 调用 proxy 函数,传参为:
      // 	vm-实例
      // 	'_data'-固定属性键,等同于 data = vm._data
      // 	key-当前 data 的属性键
      // 将 this._data.xx 代理为 this.xx 
      proxy(vm, `_data`, key)
    }
  }
  // ⭐️ 进行 data 数据的观察逻辑,响应式的核心代码
  const ob = observe(data)
  ob && ob.vmCount++
}

// target:vm 实例
// sourceKey:固定值 _data
// key:当前 data 的属性键
export function proxy(target: Object, sourceKey: string, key: string) {
  function noop(a?: any, b?: any, c?: any) {}

  const sharedPropertyDefinition = {
    enumerable: true, // 是否可枚举
    configurable: true, // 是否可更改与删除
    get: noop,
    set: noop
  }
  
  // 定义具体的 get 函数
  sharedPropertyDefinition.get = function proxyGetter() {
    // this 等价于 target
    return this[sourceKey][key] // 等价于 target._data[key]
  }
  // 定义具体的 set 函数
  sharedPropertyDefinition.set = function proxySetter(val) {
    // this 等价于 target
    this[sourceKey][key] = val // 等价于 target._data[key] = val
  }
  // 属性劫持
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

// ⭐️ observe 函数:响应式的核心代码
// value:为实例的 data
// shallow:undefined
// ssrMockReactivity:undefined
// 返回值为:new Observe(...) 后的实例
export function observe(
  value: any,
  shallow?: boolean,
  ssrMockReactivity?: boolean
): Observer | void {
  // 当已经被观察后,则直接返回
  if (value && hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    return value.__ob__
  }

  // 一系列的判断,可以忽略,直接看里面执行的逻辑
  if (
    shouldObserve &&
    (ssrMockReactivity || !isServerRendering()) &&
    (isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value.__v_skip /* ReactiveFlags.SKIP */ &&
    !isRef(value) &&
    !(value instanceof VNode)
  ) {
    // ⭐️ new 调用 Observer 构造函数,参数为:
    // value:为实例的 data
    // shallow:undefined
    // ssrMockReactivity:undefined
    return new Observer(value, shallow, ssrMockReactivity)
  }
}

export class Observer {
  dep: Dep
  vmCount: number // number of vms that have this object as root $data

  // value:为实例的 data
  // shallow:false
	// mock:false
  constructor(public value: any, public shallow = false, public mock = false) {
    // dep = { id: uuid, subs: [] }
    this.dep = mock ? mockDep : new Dep()
    this.vmCount = 0
    
    /**
     * Define a property.
     */
    function def(obj: Object, key: string, val: any, enumerable?: boolean) {
      Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true
      })
    }

    // 在实例的 data 里面新增 __ob__ 属性,其值为 Observer 实例
    // 暂存一份数据
    def(value, '__ob__', this) 
    if (isArray(value)) {
      // 当实例的 data 为数组时
      if (!mock) {
        if (hasProto) {
          /* eslint-disable no-proto */
          ;(value as any).__proto__ = arrayMethods
          /* eslint-enable no-proto */
        } else {
          for (let i = 0, l = arrayKeys.length; i < l; i++) {
            const key = arrayKeys[i]
            def(value, key, arrayMethods[key])
          }
        }
      }
      if (!shallow) {
        this.observeArray(value)
      }
    } else {
      /**
       * Walk through all properties and convert them into
       * getter/setters. This method should only be called when
       * value type is Object.
       */
      const keys = Object.keys(value)
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i]
        // ⭐️ 调用 defineReactive,传参为:
        // 	value:实例的 data
        // 	key:实例的 data 的每个 key
        // 	NO_INITIAL_VALUE = {}
        // 	undefined
        // 	shallow:false
      	// 	mock:false
        defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
      }
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray(value: any[]) {
    for (let i = 0, l = value.length; i < l; i++) {
      observe(value[i], false, this.mock)
    }
  }
}

/**
 * Define a reactive property on an Object.
 */
// ⭐️ 调用 defineReactive,接受到的参数为:
// 	obj:实例的 data
// 	key:实例的 data 的每个 key
// 	val:{}
// 	customSetter: undefined
// 	shallow:false
// 	mock:false
// 	observeEvenIfShallow:undefined
export function defineReactive(
  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,
  mock?: boolean,
  observeEvenIfShallow = false
) {
  // dep = { id: uuid, subs: [] }
  const dep = new Dep()

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

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if (
    (!getter || setter) &&
    (val === NO_INITIAL_VALUE || arguments.length === 2)
  ) {
    val = obj[key] // 取值,等价于 data[key]
  }

  // 将取得的值,再次递归调用 observe
  let childOb = shallow ? val && val.__ob__ : observe(val, false, mock)
  
  // ⭐️ 使用 Object.defineProperty 实现属性拦截
  // 	obj:实例的 data
  // 	key:实例的 data 的每个 key
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val

      // ⭐️ dep.depend() 实现依赖的采集
      if (Dep.target) {
        if (__DEV__) {
          dep.depend({
            target: obj,
            type: TrackOpTypes.GET,
            key
          })
        } else {
          dep.depend()
        }
        if (childOb) {
          childOb.dep.depend() // 孩子的依赖采集
          if (isArray(value)) {
            dependArray(value)
          }
        }
      }
      return isRef(value) && !shallow ? value.value : value
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      if (!hasChanged(value, newVal)) {
        return
      }
      if (__DEV__ && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else if (getter) {
        // #7981: for accessor properties without setter
        return
      } else if (!shallow && isRef(value) && !isRef(newVal)) {
        value.value = newVal
        return
      } else {
        val = newVal
      }
      childOb = shallow ? newVal && newVal.__ob__ : observe(newVal, false, mock)
      
      // ⭐️ dep.notify() 实现观察者的通知
      if (__DEV__) {
        dep.notify({
          type: TriggerOpTypes.SET,
          target: obj,
          key,
          newValue: newVal,
          oldValue: value
        })
      } else {
        dep.notify()
      }
    }
  })

  return dep
}

export default class Dep {
  static target?: DepTarget | null
  id: number
  subs: Array<DepTarget | null>
  // pending subs cleanup
  _pending = false

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

  addSub(sub: DepTarget) {
    this.subs.push(sub)
  }

  removeSub(sub: DepTarget) {
    // #12696 deps with massive amount of subscribers are extremely slow to
    // clean up in Chromium
    // to workaround this, we unset the sub for now, and clear them on
    // next scheduler flush.
    this.subs[this.subs.indexOf(sub)] = null
    if (!this._pending) {
      this._pending = true
      pendingCleanupDeps.push(this)
    }
  }

  depend(info?: DebuggerEventExtraInfo) {
    if (Dep.target) {
      Dep.target.addDep(this) // ⭐️ 新增依赖的观察者
      if (__DEV__ && info && Dep.target.onTrack) {
        Dep.target.onTrack({
          effect: Dep.target,
          ...info
        })
      }
    }
  }

  notify(info?: DebuggerEventExtraInfo) {
    // stabilize the subscriber list first
    const subs = this.subs.filter(s => s) as DepTarget[]
    if (__DEV__ && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      const sub = subs[i]
      if (__DEV__ && info) {
        sub.onTrigger &&
          sub.onTrigger({
            effect: subs[i],
            ...info
          })
      }
      sub.update() // ⭐️ 通知观察者有更新
    }
  }
}

官方文档:v2.cn.vuejs.org/v2/guide/re…

watch 的一些属性

export default {
  data() {
    return {
      user: {
        name: 'xxx',
        age: 30
      }
    }
  },
  created() {
    // 当 immediate 为 false 时,无法触发对应的 watch
    // 当 immediate 为 true 时,可以触发对应的 watch
    this.changeUserName()
  },
  methods: {
    changeUserName() {
      // 当 deep 为 false 时,无法触发对应的 watch
      // 当 deep 为 true 时,可以触发对应的 watch
      this.user.name = 'yyy' 
    }
  },
  watch: {
    user: {
      handler(newVal, oldVal) {
        console.log(`watched ${oldVal} -> ${newVal}`)
      },
      deep: true,
      immediate: true
    }
  }
}

通过 watch 的源码,可以看到deep、immediate的具体实现

immediate核心源代码:

  Vue.prototype.$watch = function (
    expOrFn: string | (() => any),
    cb: any,
    options?: Record<string, any>
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) { // ⭐️ immediate 为 true 时
      const info = `callback for immediate watcher "${watcher.expression}"`
      pushTarget()
      // ⭐️ 执行对应的回调函数并且捕获错误
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
      popTarget()
    }
    return function unwatchFn() {
      watcher.teardown()
    }
  }

deep核心源代码:

function _traverse(val: any, seen: SimpleSet) {
  let i, keys
  const isA = isArray(val)
  if (
    (!isA && !isObject(val)) ||
    val.__v_skip /* ReactiveFlags.SKIP */ ||
    Object.isFrozen(val) ||
    val instanceof VNode
  ) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else if (isRef(val)) {
    _traverse(val.value, seen)
  } else {
    // ⭐️ 针对对象,获取所有的 keys,然后循环递归处理依赖的监听
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

Diff 算法

虚拟 DOM:用来描述真实 DOM 的一个 JS 对象

diff 算法:对比新、旧虚拟 DOM 差异的算法

patch(打补丁):把新旧节点的差异应用到真实 DOM 上的操作

当数据更新后,会重新生成 VDOM,通过 DIFF 算法找到新旧 VDOM 的差异,最后通过 patch 进行页面更新

虚拟 DOM 结构(伪代码):

// html 代码
<div id="app">
  <h1>Hello, Virtual DOM!</h1>
  <p class="paragraph">This is a paragraph.</p>
</div>

// 对应的虚拟 DOM 结构(简化的伪代码)
const vnode = {
  tag: 'div', // 节点的标签名
  props: { id: 'app' }, // 节点的属性
  children: [ // 子节点数组
    {
      tag: 'h1',
      children: ['Hello, Virtual DOM!']
    },
    {
      tag: 'p',
      props: { className: 'paragraph' },
      children: ['This is a paragraph.']
    }
  ]
};

虚拟 DOM 源码:github.com/vuejs/vue/b…

Diff 算法核心

总结:递归、同级、双端

先比较新旧 VDOM 的根节点,然后一层层往下进行同级比较,同级比较时会采用首尾双端来加快对比

diff 流程的源码解析:github.com/vuejs/vue/b…

// ...

export function createPatchFunction(backend) {
  // ...

  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 is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly

    if (__DEV__) {
      checkDuplicateKeys(newCh)
    }

    // ⭐️ 核心 diff 算法:双指针对比
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // 当旧:首下标小于尾下标 && 新:首下标小于尾下标,则表明还未对比完,将继续
      if (isUndef(oldStartVnode)) {
        // 旧-首 节点不存在时,则往右移一个
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        // 旧-尾 节点不存在时,则往左移一个
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 旧-首 节点 与 新-首 节点 相同时:表明首 节点位置未变
        // 调用 patchVnode 函数,进行它俩的子节点对比
        patchVnode(
          oldStartVnode,
          newStartVnode,
          insertedVnodeQueue,
          newCh,
          newStartIdx
        )
        // 旧-首 节点则往右移一个
        oldStartVnode = oldCh[++oldStartIdx]
        // 新-首 节点则往右移一个
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 旧-尾 节点 与 新-尾 节点 相同时:表明尾 节点位置未变
        // 调用 patchVnode 函数,进行它俩的子节点对比
        patchVnode(
          oldEndVnode,
          newEndVnode,
          insertedVnodeQueue,
          newCh,
          newEndIdx
        )
        // 旧-尾 节点则往左移一个
        oldEndVnode = oldCh[--oldEndIdx]
        // 新-尾 节点则往左移一个
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) {
        // 旧-首 节点 与 新-尾 节点 相同时:表明该节点位置往左移了
        // 调用 patchVnode 函数,进行它俩的子节点对比
        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)) {
        // Vnode moved left
        // 旧-尾 节点 与 新-首 节点 相同时:表明该节点位置往右移了
        // 调用 patchVnode 函数,进行它俩的子节点对比
        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)) {
          // 当 新-首 节点的 key 不在 旧节点的 keys 内,则表明要走新增逻辑
          // New element
          createElm(
            newStartVnode,
            insertedVnodeQueue,
            parentElm,
            oldStartVnode.elm,
            false,
            newCh,
            newStartIdx
          )
        } else {
          // 当 新-首 节点的 key 在 旧节点的 keys 内,则表明是位置移动了
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            // 如果相同 key 对应的新旧节点[相同]时
            
            // 调用 patchVnode 函数,进行它俩的子节点对比
            patchVnode(
              vnodeToMove,
              newStartVnode,
              insertedVnodeQueue,
              newCh,
              newStartIdx
            )
            // 旧节点该 key 的值充值为 undefined
            oldCh[idxInOld] = undefined
            canMove &&
              nodeOps.insertBefore(
                parentElm,
                vnodeToMove.elm,
                oldStartVnode.elm
              )
          } else {
            // 如果相同 key 对应的新旧节点[不相同]时,则表明要走新增逻辑
            // same key but different element. treat as new element
            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)
    }
  }

  function patchVnode(
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly?: any
  ) {
    // ....

    updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)

    // ....
  }
  function isUndef(v: any): v is undefined | null {
    return v === undefined || v === null
  }
  
  function isDef<T>(v: T): v is NonNullable<T> {
    return v !== undefined && v !== null
  }

  return function patch(oldVnode, vnode, hydrating, removeOnly) {
  if (isUndef(vnode)) { // 新的 Vnode 没有时
    // 旧的 Vnode 有时,则需要进行销毁操作
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
  }

  let isInitialPatch = false
  const insertedVnodeQueue: any[] = []

  if (isUndef(oldVnode)) {
    // 如果新的 Vnode 存在,旧的 Vnode 不存在,则需要进行创建操作
    // empty mount (likely as component), create new root element
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else { // 新旧 Vnode 都存在时则需要执行的核心逻辑 ⭐️
    // isRealElement:是否为真正存在的元素
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // ⭐️ 不是真正存在的元素 && 新旧 Vnode 相同时
      // ⭐️ patch existing root node
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    } else {
      // 是真正存在的元素 || 新旧 Vnode 不相同时
      if (isRealElement) {
        // 是真正存在的元素时
        // mounting to a real element
        // check if this is server-rendered content and if we can perform
        // a successful hydration.
        if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
          // 服务端渲染
          oldVnode.removeAttribute(SSR_ATTR)
          hydrating = true
        }
        if (isTrue(hydrating)) {
          // 混合渲染
          if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
            invokeInsertHook(vnode, insertedVnodeQueue, true)
            return oldVnode
          } else if (__DEV__) {
            warn(
              'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
            )
          }
        }
        // 既不是服务端渲染,又不是混合渲染
        // 则基于 oldVnode 创建一个空的 Vnode,并替换它
        oldVnode = emptyNodeAt(oldVnode) 
      }

      // replacing existing element
      const oldElm = oldVnode.elm
      const parentElm = nodeOps.parentNode(oldElm)

      // create new node
      createElm(
        vnode,
        insertedVnodeQueue,
        // extremely rare edge case: do not insert if old element is in a
        // leaving transition. Only happens when combining transition +
        // keep-alive + HOCs. (#4590)
        oldElm._leaveCb ? null : parentElm,
        nodeOps.nextSibling(oldElm)
      )

      // update parent placeholder node element, recursively
      if (isDef(vnode.parent)) {
        let ancestor = vnode.parent
        const patchable = isPatchable(vnode)
        while (ancestor) {
          for (let i = 0; i < cbs.destroy.length; ++i) {
            cbs.destroy[i](ancestor)
          }
          ancestor.elm = vnode.elm
          if (patchable) {
            for (let i = 0; i < cbs.create.length; ++i) {
              cbs.create[i](emptyNode, ancestor)
            }
            // #6513
            // invoke insert hooks that may have been merged by create hooks.
            // e.g. for directives that uses the "inserted" hook.
            const insert = ancestor.data.hook.insert
            if (insert.merged) {
              // start at index 1 to avoid re-invoking component mounted hook
              // clone insert hooks to avoid being mutated during iteration.
              // e.g. for customed directives under transition group.
              const cloned = insert.fns.slice(1)
              for (let i = 0; i < cloned.length; i++) {
                cloned[i]()
              }
            }
          } else {
            registerRef(ancestor)
          }
          ancestor = ancestor.parent
        }
      }

      // destroy old node
      if (isDef(parentElm)) {
        removeVnodes([oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode)
      }
    }
  }

  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}

补充知识

Rollup

打包工具,将小的、松散耦合的模块(主要为 js 代码)组合成更大型的应用程序或库,支持 ES6 模块,适用于前端库和组件

与之相应的webpack则适用于应用程序,单应用、多应用等等,支持各种模块加载(css/js/图片等)、按需引入等等功能

// rollup.config.js 
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';

export default {
  input: 'src/index.js', // 指定入口文件
  output: {
    file: 'dist/my-lib.js', // 输出的 bundle 文件路径
    format: 'umd', // 输出格式:UMD,可以在浏览器全局作用域下运行,也支持 CommonJS 和 AMD 模块系统
    name: 'MyLib', // 全局变量名称,用于在非模块化环境中引用该库
  },
  plugins: [
    resolve(), // 解析第三方模块的绝对路径
    commonjs(), // 将 CommonJS 模块转换为 ES6 模块供 Rollup 处理
  ],
};

Vue2 源码调试

  • 下载源码
  • 安装依赖:pnpm i
  • 在需要的地方加上打印
  • 运行:pnpm test:unit将跑所有的单测,这时候控制台就不停的打印,等运行完毕后,再看结果
  • 如果觉得打印的实在太多了,则可以新增一个命令:
    • "test:unit:newvue": "vitest run test/unit/modules/vdom/create-component.spec.ts"
    • 然后命令行运行:pnpm test:unit:newvue
    • 这也会执行new Vue(...)的操作,并且打印的数据更少

Vue2 中直接通过下标修改数组元素不会触发视图原因是?

Vue2 使用 Object.defineProperty 来实现对对象属性的响应式追踪

Object.defineProperty 只能应用于对象属性,而无法直接应用于数组的索引(因为数组索引不是标准意义上的对象属性)

Vue2 对于数组的响应式处理是通过重写数组的几个可变方法(如 push()、pop() 、shift()等)来间接实现的

以下为数组部分方法重写的源代码:

/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */

import { TriggerOpTypes } from '../../v3'
import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Define a property.
 */
export function def(obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original 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)
    // notify change
    if (__DEV__) {
      ob.dep.notify({
        type: TriggerOpTypes.ARRAY_MUTATION,
        target: this,
        key: method
      })
    } else {
      ob.dep.notify()
    }
    return result
  })
})

课后参考

人人都能懂的Vue源码系列(一)—Vue源码目录结构_慕课手记