Vue2.6x源码解析(二):组件初始化

1,040 阅读12分钟

系列文章:

1,组件构造函数

根据Vue官方文档可知:

每个 Vue 项目都是通过用 Vue 构造函数创建一个新的 Vue 实例开始的:一个 Vue 应用由一个通过 new Vue 创建的根 Vue 实例,以及可选的嵌套的、可复用的组件树组成。所以我们应该知道所有的 Vue 组件都是 Vue 实例,并且接受相同的选项对象options (根实例特有的一些选项除外)

我们来打印两个Vue实例:

image-20230412142932306.png

根据打印结果可以发现:

1,通过new Vue()创建的Vue实例和组件实例是不同的对象,由不同的构造函数创建。

2,vueComponent组件实例的protoptye原型对象指向了Vue实例,并且通过原型链还可以指向Vue.prototype原型对象。

image-20230412143408576.png

根据上面的分析我们可以得出:

  • 一个Vue单页应用就是一个 Vue 的根实例。
  • 一个Vue组件就是一个 vueComponent 组件实例。

只不过 VueComponent 的构造器和 Vue 的构造器的内容是基本一样的。

通过new Vue创建的Vue实例可以传递特殊的选项,而VueComponent组件(实例) 只能传递常规的选项参数。

前面我们知道了组件实例由VueComponent组件构造函数创建,下面我们分析VueComponent构造函数的由来。

我们首先查看extend方法源码:

# 继承方法
Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }
    // 验证组件的name是否合法
    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      validateComponentName(name)
    }
    # 创建了一个VueComponent组件构造函数 (继承自Vue构造函数)
    const Sub = function VueComponent (options) {
      // 调用相同的Init初始化方法,组件跟Vue一样的初始化过程
      this._init(options)
    }
    # 原型链继承
    Sub.prototype = Object.create(Super.prototype)
    // 指定原型的构造器为构造函数自身
    Sub.prototype.constructor = Sub
    // 组件构造器id标识
    Sub.cid = cid++
    // 将父类的options 选项继承到子类中
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    // 存储Vue构造器
    Sub['super'] = Super
​
    // For props and computed properties, we define the proxy getters on
    // the Vue instances at extension time, on the extended prototype. This
    // avoids Object.defineProperty calls for each instance created.
    if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }
    // 然后为VueComponent组件构造函数设置相同的全局API
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use
​
    // 设置组件的components/directives/filters属性, 存储组件私有的子组件/指令/过滤器
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    if (name) {
      Sub.options.components[name] = Sub
    }
​
    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)
​
    // cache constructor
    cachedCtors[SuperId] = Sub
    
    # 返回一个VueComponent组件构造器
    return Sub
  }

根据上面的源码我们可以看出:extend方法的主要作用就是定义了一个组件的VueComponent构造函数,然后返回构造函数。

我们再去看看extend方法在哪里被调用:

// src/core/vdom/create-component.js
# 创建组件
function createComponent () {
    # 获取基础构造器
    const baseCtor = context.$options._base
​
    if (isObject(Ctor)) {
        # 创建组件构造器:从基础构造器继承
        Ctor = baseCtor.extend(Ctor)
    }
    ...
}

我们先看看基础构造器baseCtor的由来:

// src/core/global-api/index.js
function initGlobalAPI (Vue: GlobalAPI) {
    ...
    # 设置了一个基础构造器; 标识“基本”构造函数以扩展所有普通对象【就是组件对象】
    Vue.options._base = Vue
}

可以看出基础构造器baseCtor其实就是Vue构造函数。

然后在创建组件的createComponent方法里面,调用了extend方法,创建了组件的构造函数VueComponent

到这里我们可以明确的知道,组件的构造函数继承至Vue构造函数。所以在Vue2中,组件的初始化和Vue初始化是一样的,即Vue实例可以当作组件实例来使用,而在Vue3中是不可以的。

总结: 这里我们先讲解组件构造函数VueComponent的由来,才能更好的理解下面组件的初始化过程。

注意:组件的创建过程远不止这么简单,有很多的内容铺垫以及边际情况处理,但那不是我们的重点,我们的重点只需要放在组件执行Init方法的过程,因为它是组件初始化的核心,我们可以从中理解很多组件语法的原理。

2,组件初始化

组件的初始化一样是调用Init方法,因为new Vue一般不会传递data/props/methods这些选项,所以我们放在组件这里来讲。

Vue.prototype._init = function (options) {
  const vm: Component = this
  vm._isVue = true
  if (options && options._isComponent) {
    initInternalComponent(vm, options)
  }
  
  # 组件初始化
  initLifecycle(vm) // 初始化实例属性:初始化一些Vue上的实例属性($parent、$root、$refs、_isMounted、
  initEvents(vm) // 初始化事件:是指将父组件在模板中使用的v-on 注册的事件添加到子组件的事件系统(Vue.js的事件系统)中。
  initRender(vm) // 初始化一些渲染相关属性:(_vnode、$slots、$scopedSlots、_c、$createElement)
  # 触发beforeCreate钩子
  callHook(vm, 'beforeCreate')
  initInjections(vm) // 初始化inject
  initState(vm) // 初始化props、methods 、data 、computed 和watch
  initProvide(vm) // 初始化provide
  # 触发created钩子
  callHook(vm, 'created')
    
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}

在这里我们就可以看出组件的生命周期其实就是组件在初始化过程中调用的一些钩子函数,并没有想象中的那么抽象难理解,而且组件的初始化顺序都是精心安排的,下面我们开始逐个解析初始化内容。

(一)initLifecycle

initLifecycle方法就是初始化组件的一些实例属性:

# 初始化实例属性
export function initLifecycle (vm: Component) {
  const options = vm.$options
​
  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }
  
  # 初始化组件实例属性
  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm
​
  vm.$children = []
  vm.$refs = {}
  # _watcher是存储组件renderWatcher,用于组件更新渲染
  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  // 组件挂载状态
  vm._isMounted = false
  // 卸载状态
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

(二)initEvents

initEvents初始化事件:这里是指用$on方法将父组件在模板中使用的v-on注册的监听事件添加到对应子组件的_events事件系统中。

# 初始化事件
export function initEvents (vm: Component) {
  # vm._events用来存储事件
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // init parent attached events // 初始化父组件附加的事件
  // vm.$options._parentListeners对象:实际上内部存储都是事件
  // {
  //   fn1: function () {}, 
  //   fn2: function () {}
  // }
  const listeners = vm.$options._parentListeners
  if (listeners) {
    // 将父组件向子组件注册的事件:添加到子组件实例中
    // updateComponentListeners 的逻辑很简单,
    // 只需要循环vm.$options._parentListeners 并使用vm.$on方法 把事件都注册到this._events 中即可
    updateComponentListeners(vm, listeners)
  }
}

(三)initRender

initRender:初始化渲染相关的实例属性。

# 初始化一些渲染相关的属性
export function initRender (vm: Component) {
  # 存储render渲染函数:生成的最新vnode
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees // 静态节点树
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  // 初始化插槽
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  # 创建虚拟dom的函数
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
​
  const parentData = parentVnode && parentVnode.data
​
  defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
  defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}

3,initState【重点】

initState:初始化组件状态,这部分内容非常重要,我们把它升级到【标题】级别来详细讲解。

# 初始化状态
export function initState (vm: Component) {
  # 首先在vm 上新增一个属性 _watchers ,用来保存当前组件中所有的watcher实例。
  // 无论是使用watch选项创建的watcher实例,还是组件级别的watcher实例,都会添加到vm._watchers 中。
  vm._watchers = []
  // 取出组件的options选项
  const opts = vm.$options
  # 开始初始化状态【重点】
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)  // 只是将这些方法挂载到组件实例上
  // 将data内所有key转换为响应式数据
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  // 初始化computed、watch
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initState函数初始化组件状态非常重要,源码中首先在vm上新增一个属性 _watchers,它是用来保存当前组件中所有的watcher实例。无论是使用watch方法注册的watcher实例,还是组件自身的watcher实例,都会存储到vm._watchers中,而组件实例的watcherrenderWatcher是在$mount方法中推入到watchers数组中的,也是最后才添加的

另外只有用户传入了哪些选项,这些选项才会被初始化,没有用到的状态则不用初始化,并且初始化的顺序也是有讲究的,这也是为什么我们在data中可以使用props,在computed中可以使用props/data,然后watch是最后才被初始化的,所以watch选项可以对已经初始化的props/data/computed进行监听。

下面我们就开始解析每个状态的具体初始化过程。

initProps

props的实现原理大体上是这样的:父组件提供数据,子组件通过props字段选择自己需要哪些内容。Vue.js内部通过子组件的props选项将需要的数据筛选出来之后添加到子组件的上下文中。

注意:筛选props(规格化)的操作没有在这里,在执行initProps时,propsOptions参数是已经筛选之后的props

# 1,初始化props
function initProps (vm: Component, propsOptions: Object) {
  // vm.$options.propsData保存的是通过父组件传入或用户通过propsData 传入的真实props 数据
  const propsData = vm.$options.propsData || {}
  // 变量props和vm._props指向同一个对象,也就是所有设置到props 变量中的属性最终都会保存到vm._props 中
  // 也就是说vm._props中就是定义的响应式数据
  const props = vm._props = {}
  // 存储propsKey
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  if (!isRoot) {
    // toggleObserving函数的作用是控制defineReactive调用时所传入的value参数是否需要转换成响应式
    // 控制shouldObserve变量
    toggleObserving(false)
  }
  # 循环当前组件的props
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    
    # 将props定义成响应式数据
    defineReactive(props, key, value)
    # 添加访问代理
    if (!(key in vm)) {
      // 和data数据一样,添加代理 使得通过vm.x 可以访问到vm._props.x属性
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

initProps的内容比较简单,这里有一个proxy代理函数需要我们特别理解一下。

proxy代理函数是一个在目标实例上定义属性的方法,并且访问和操作这些属性都被代理到了源对象。

# 代理(目标对象, 源对象, key)
// 该函数的作用是:在目标对象即vm实例上设置一个属性名key的属性。
// 这个key属性的修改和获取操作:实际上针对的是源对象的操作 【this.x 实际上访问的this._props.x】
export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  # 在vm实例上定义访问器属性key【这些属性key来自data/props选项】,我们在组件中通过this.x访问的变量,就是在这里定义的
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

测试数据:

// father.vue
<child :name="name" :childName="childName" @clear="handleClear"></child>
// child.vue
props: {
    // 只接收name
    name: String
}

打印结果:

image-20221103174136495.png

image-20221103174324726.png

initMethods

初始化方法比较简单,就是循环methods对象,将方法全部挂载到vm实例上,在挂载中会做一些方法名重复/合法校验,并给出相应的警告提示。

# 2,初始化方法
function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  # 循环挂载
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      if (typeof methods[key] !== 'function') {
        warn(
          `Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
          `Did you reference the function correctly?`,
          vm
        )
      }
      // 如果methods 中的某个方法已经在props 中声明过了,会在控制台发出警告
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      // 这里isReserved 函数的作用是判断字符串是否是以 $ 或 _ 开头。
      // 如果methods 中的某个方法已经存在于vm 中,并且方法名是以 $ 或 _ 开头的,也会在控制台发出警告
      if ((key in vm) && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
          `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    # 挂载到vm组件实例上
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}
initData

初始化Data的过程也比较简单,简单来说就是首先获取组件中的data对象,然后循环对象,在vm组件实例上定义对应的key属性,通过proxy代理指向vm._data对象,即通过vm.x 可以访问到vm._data.x属性,最后调用observe方法将data转换为响应式数据。

observe方法内幕会在后面讲解Observer/Dep/Watcher中详细介绍。

# 3,初始化组件的data
function initData (vm: Component) {
  let data = vm.$options.data
  # 判断data是不是函数,如果是就直接调用函数,返回data对象;否则直接使用data对象
  # data中的数据最终会保存到vm._data中
  data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
  // isPlainObject:判断data是不是对象类型[object Object]
  if (!isPlainObject(data)) {
    // 不是对象类型就把data设置为一个空对象,并报异常提示
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  # 获取data对象上的变量列表
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  # 递减循环:进行data属性代理
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      // isReserved:检查字符串是否以$或开头_ , 不是的才进行代理,非法名称key不进行代理
      # 属性代理,使得通过vm.x 可以访问到vm._data 中的x 属性
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  # 调用observe函数将data 转换成响应式数据 【data初始化完成】
  observe(data, true /* asRootData */)
}

测试数据:

data() {
  return {
    name: "hello world",
    childName: "tom",
  }
}

打印结果:

image-20221104091742282.png

打印结果与源码分析结果一致:我们在data中定义的key变量,通过proxy方法在vm定义了对应的访问器属性,并且代理指向了vm._data对象,真正的响应式数据。

initComputed

本节将详细介绍计算属性computed的内部原理。

简单来说,computed 是定义在vm 上的一个特殊的getter方法。之所以说特殊,是因为在vm上定义getter方法时,get 并不是用户提供的函数,而是Vue.js内部的一个代理函数。在代理函数中可以结合Watcher实现缓存与收集依赖等功能。

# 4,初始化计算属性:computed 是定义在vm 上的一个特殊的getter方法
function initComputed (vm: Component, computed: Object) {
  # 在组件实例上定义了一个_computedWatchers属性,来存储本组件的所有计算属性watcher
  const watchers = vm._computedWatchers = Object.create(null)
  // 计算属性在SSR环境中,只是一个普通的getter方法
  const isSSR = isServerRendering()
  # 循环计算属性对象
  for (const key in computed) {
    const userDef = computed[key]
    // 判断用户定义的计算属性是不是函数,如果是直接赋值给getter,否则获取userDef中的get函数
    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) {
      // 在web环境中 (即非SSR环境),为计算属性创建对应的内部观察器watcher
      // 计算属性创建的watcher回调函数cb为noop空函数
      # 创建计算属性的watcher
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
​
    // 判断计算属性的key在vm实例中有没有存在,存在给出对应的警告提示且不会定义该计算属性
    if (!(key in vm)) {
      # 在vm实例上定义计算属性
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      } else if (vm.$options.methods && key in vm.$options.methods) {
        warn(`The computed property "${key}" is already defined as a method.`, vm)
      }
    }
  }
}

注意上面创建watcher时的options

// 在实例化Watcher 时,通过参数告诉Watcher 类应该生成一个供计算属性使用的watcher实例
const computedWatcherOptions = { lazy: true }

继续查看defineComputed方法:

# 定义计算属性的方法
export function defineComputed (target: any, key: string, userDef: Object | Function) {
  // 应该缓存
  const shouldCache = !isServerRendering()
  // 判断用户定义的计算属性:如果是函数,说明computed只有getter ,直接设置get就行,并把set设置为空函数
  # 设置计算属性的getter/setter
  // 传入的userDef是一个函数
  if (typeof userDef === 'function') {
    # 重点:计算属性与普通访问器属性不同点就是 计算属性的getter可以缓存,不用每次都重新计算
    // createComputedGetter创建计算属性的getter
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    // 说明传入的userDef是一个对象
    // 定义getter
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    // 存在set,就定义setter,否则set就是一个空函数
    sharedPropertyDefinition.set = userDef.set || noop
  }
  if (process.env.NODE_ENV !== 'production' && sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  # 根据sharedPropertyDefinition配置项,在vm实例上定义计算属性(其实就是一个访问器属性)
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

扩展: 在服务器渲染环境,shouldCache变量会设置为false,将会调用createGetterInvoker方法创建一个普通的getter

function createGetterInvoker(fn) {
  // 返回一个普通的getter,没有缓存功能,每次访问计算属性都会调用传入的get函数
  return function computedGetter () {
    return fn.call(this, this)
  }
}

在计算属性定义到vm实例之前,我们还得继续查看createComputedGetter方法:

# 创建计算属性getter的方法(重点)
function createComputedGetter (key) {
  # 返回的computedGetter函数:会被赋值给计算属性的getter,
  // 每当计算属性被读取时,computedGetter函数都会被执行。都会判断drity,是否需要重新计算结果,如果没有变化则会直接返回value
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // drity属性:标识计算属性的结果是否有变化,ture则说明有变化,重新计算结果
      if (watcher.dirty) {
        # 重新计算
        watcher.evaluate()
      }
      // 这段代码的目的在于将读取计算属性的那个Watcher 添加到计算属性所依赖的所有状态的依赖列表中。
      // 换句话说,就是让读取计算属性的那个Watcher 持续观察计算属性所依赖的状态的变化。
      if (Dep.target) {
        watcher.depend()
      }
      // 返回最新的计算属性值
      return watcher.value
    }
  }
}

测试数据:

computed: {
  myName() {
    return this.value + 10;
  }
},

打印结果:

image-20230406155228810.png

initWatch

初始化状态的最后一步是初始化watch。只有当用户设置了watch选项并且watch选项不等于浏览器原生的watch时,才进行初始化watch的操作,将watch放在最后,是为了可以监听前面已经初始化完成的props/data/computed,这些都是Vue精心设计的,下面我们开始解析watch的内部实现:

# 5,初始化watch
function initWatch (vm: Component, watch: Object) {
  // 循环watch选项对象
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

继续查看createWatcher方法源码:

# 创建watcher:内部就是调用vm.$watch方法
function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  # 普通对象,将对象.handle方法赋值给handler
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  // 方法名
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  # 内部就是调用vm.$watch方法
  return vm.$watch(expOrFn, handler, options)
}

首先会判断watchhandler参数是不是对象,如果是对象,会将handler.handler赋值给handler,比如:

// watch是对象时
// handler = obj.handler
watch: {
    obj: {
        handler() {}
    }
}

如果是方法名,就会从vm实例中寻找对应方法。**如果是函数,将直接传递给watch方法,最终返回watch方法,** 最终返回`watch`方法调用结果。

我们再继续查看$watch方法源码:

# 监听方法,原理实际上就是创建Watcher实例,vm.$watch其实是对Watcher的一种封装
  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    # 如果是直接调用的$watcher方法,需要经过createWatcher处理一下
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    # 设置user为true,表示为用户所创建的watcher
    options.user = true
    // 实例化一个watcher:观察目标更新执行回调
    const watcher = new Watcher(vm, expOrFn, cb, options)
    # 重点:如果选项immediate:true; 则在watch初始化时即立刻调用一次回调
    if (options.immediate) {
      const info = `callback for immediate watcher "${watcher.expression}"`
      pushTarget()
      # 立即执行一次回调函数cb
      /// invokeWithErrorHandling方法专门处理用户传入的回调,有错误处理
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
      popTarget()
    }
    // $watch方法返回一个unwatchFn方法,为取消监听使用,本质也是调用watcher的内部方法
    return function unwatchFn () {
      watcher.teardown()
    }
  }

注意:immediate:true这次回调执行,是同步回调,在初始化的时候立即执行。

总结来讲:watch监听就是内部调用了$watch方法,创建一个watcher来观察目标依赖,在目标依赖变化后,重新执行用户传入的回调函数。 这里的执行也是将回调函数推入queue队列,异步执行更新,后面会详细讲解vue的异步更新队列。

测试数据:

watch: {
    obj: {
        deep: true,
        handler(val) {
          console.log(val);
        },
    }
}

打印结果:

image-20221104104804493.png

总结:组件的生命周期,事件注册,data转换为响应式数据,watch/computed注册等都在初始化过程中完成,是非常重要的过程,初始化完成后就是组件的渲染和挂载,下面我们就开始详细讲解$mount的具体内容。

4,组件加载 vm.$mount

通过前面我们已经知道,组件的初始化内容就是执行init方法的过程:

function VueComponent(options) {
    this.init()
}
// 创建组件实例
const vm = new VueComponent(options)

初始化完成之后,就$mount方法开始组件的加载。

但是注意:组件的加载并不是执行的init方法里面的$mount

init() {
    ....
    # 组件的加载不执行这里的$mount,只有Vue实例的加载才会执行,具体的执行位置这里暂不提及
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
}

下面我们继续查看$mount方法:

// platforms/web/runtime/index.js
// 实际调用的就是patch方法
Vue.prototype.__patch__ = inBrowser ? patch : noop
​
// 这里是纯运行时的$mount方法,直接调用mountComponent方法加载组件
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  # 加载组件
  return mountComponent(this, el, hydrating)
}

根据源码中可以看出$mount方法重点是返回了一个mountComponent的方法调用。

注意: 上面还有一个重点Vue.prototype.__patch__方法的值就是patch函数,后面组件渲染的时候会用到。

我们继续查看mountComponent源码:

// src/core/instance/lifecycle.js
// 渲染,挂载组件
export function mountComponent (vm: Component, el: ?Element, hydrating?: boolean): Component {
  vm.$el = el
  // 如果没有render,创建一个空白的vnode,避免报错,同时警告提示
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
  }
  # 挂载之前,触发beforeMount钩子函数
  callHook(vm, 'beforeMount')
  
  // 声明一个更新组件的变量
  let updateComponent
​
  # 这里是挂载的重点:vm._render()返回生成的vnode
  // _update是真正的渲染和更新方法
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  
  # 重点:创建一个组件自身的watcher,观察组件的数据变化以及调用updateComponent重新渲染
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)  
  // isRenderWatcher标记:用于将当前创建watcher实例定义为组件自身的实例渲染watcher
  hydrating = false
​
  if (vm.$vnode == null) {
    vm._isMounted = true
    # 挂载完成,触发mounted钩子函数
    callHook(vm, 'mounted')
  }
  // 返回组件实例
  return vm
}

挂载之前,触发beforeMount钩子函数,然后定义了一个updateComponent方法,这个方法非常重要是组件真正的加载和更新函数。

和vue3源码中的componentUpdateFn函数作用一样。

然后创建了一个组件级别的renderWatcher实例,并且把updateComponent方法作为回调函数传入存储到watcher.getter属性中,它的作用就是在观察到组件的响应式数据变化后,调用updateComponent方法实现组件的重新渲染。

并且在创建watcher时,就会默认执行一次getter方法【除开computedWatcher】,这就会触发组件的首次加载过程。

// Watcher类:
class Watcher {
    constructor (vm, expOrFn, cb, options?, isRenderWatcher) {
        ...
        if (typeof expOrFn === 'function') {
            this.getter = expOrFn  // 函数名
        }
        # 除计算属性watcher外,创建watcher时都会默认执行一次get
        this.value = this.lazy ? undefined : this.get()
    },
    get() {
        try {
            // 执行getter【updateComponent】
            value = this.getter.call(vm, vm)
        } catch {
            ...
        }
    }
}

这里执行getter就是执行的updateComponent方法,我们再看看这个方法:

updateComponent = () => {
   // vm._render()返回生成的vnode, 交给_update
   vm._update(vm._render(), hydrating)
}

我们首先查看vm._render方法:

// src/core/instance/render.js
# 渲染函数:生成vnode
Vue.prototype._render = function (): VNode {
    const vm: Component = this
    # 从组件中取出render渲染函数,生产环境组件已经编译过,肯定会存在render选项
    const { render, _parentVnode } = vm.$options
​
    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots,
        vm.$scopedSlots
      )
    }
​
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      // 当前组件实例
      currentRenderingInstance = vm
      # 调用渲染函数,生成vnode
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
        try {
          vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
        } catch (e) {
          handleError(e, vm, `renderError`)
          vnode = vm._vnode
        }
      } else {
        vnode = vm._vnode
      }
    } finally {
      currentRenderingInstance = null
    }
    // if the returned array contains only a single node, allow it
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0]
    }
    # 如果render函数出错,则返回空vnode
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        )
      }
      vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    # 返回虚拟dom
    return vnode
  }

很明显vm._render方法的作用和它的名字一样,就是调用组件中的render函数生成vnode【虚拟dom】,并且返回。

然后把生成的vnode对象交给vm._update方法使用:

vm._update(vnode)

我们再继续查看_update方法源码:

// src/core/instance/lifecycle.js
// _update:组件真正的挂载和更新方法
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    # 这里的prevVnode保存的是旧的vnode
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    
    # 这里的_vnode保存的是新的vnode
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    # 根据prevVnode是否存在来区分是首次渲染还是组件更新
    if (!prevVnode) {
      # 初始化dom渲染
      // 这里的__patch__方法,实际上就是对把vm所控制的Vnode转化成DOM并替换vm.$el所指向的DOM
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      # updates组件更新渲染:利用patch算法根据传入的新旧vnode,生成最终的真实dom元素
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      # 更新dom
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }

取出旧的_vnode对象,用于区分是首次加载还是更新,然后将最新的vnode传递给patch函数,最终生成真实的dom,渲染到页面。

组件渲染完成后触发mounted钩子函数,完成组件的加载。

关于Vue应用的完整加载流程可以查看文章《Vue2.6x源码解析(五):Vue应用加载【多图预警】》。

下节我们深入理解Vue2.6x的响应式原理。