Vue2.6源码的部分解析(构造函数Vue)

144 阅读8分钟

如何找到对应的代码行开始看源码

看到很多资料都是通过new Vue的时候debug直接定位进入源码的,我以工程化的角度去说一下流程,为什么是从src\core\instance\index.js开始的

下载

git clone -b v2.6.14 https://github.com/vuejs/vue.git

package.json

工程化项目绕不开的的各种元数据和依赖关系记录文件,包括脚本定义;

  • 里边有一个看着非常眼熟的脚本,平时没少用的npm run dev执行
  scripts:{"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",}

我们可以看到一个路径scripts/config.js,并且输入了TARGET:web-full-dev作为入参

scripts/config.js

if (process.env.TARGET) {
 module.exports = genConfig(process.env.TARGET)
} else {
 exports.getBuild = genConfig
 exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}

genConfig内部通过入参TARGET读取常量buildsweb-full-dev

 'web-full-dev': {
   entry: resolve('web/entry-runtime-with-compiler.js'),
   dest: resolve('dist/vue.js'),
   format: 'umd',
   env: 'development',
   alias: { he: './entity-decoder' },
   banner
 },

此处的resolve是封装过的

const aliases = require('./alias')
const resolve = p => {
  const base = p.split('/')[0]
  if (aliases[base]) {
    return path.resolve(aliases[base], p.slice(base.length + 1))
  } else {
    return path.resolve(__dirname, '../', p)
  }
}

最后发现入口文件是src/platforms/web/entry-runtime-with-compiler.js

找到构造函数Vue

  • 进入src/platforms/web/entry-runtime-with-compiler.js
  • 发现里Vue是通过import Vue from './runtime/index'引入的
  • 进入后发现Vue又是通过import Vue from 'core/index'引入的
  • 进入core/index后发现还有import Vue from './instance/index'
  • 进入后,我们总算找到构造函数Vue了
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)
}

实例化Vue

此小节会描述函数执行的大致逻辑

Vue从src\core\instance开始

文件定义和暴露构造函数Vue,且实例化的时候,调用了this._init 此外还给Vue的原型对象加了不少属性方法


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)
}

initMixin(Vue)// 定义了_init方法,初始化Vue实例initLifecycle,initEvents,initRender,initState,initProvide,initInjections等
stateMixin(Vue)//在Vue的原型对象挂载$data,$props,$set,$delete,$watch
eventsMixin(Vue)//在Vue的原型对象挂载$on,$once,$off,$emit
lifecycleMixin(Vue)//在Vue的原型对象挂载_update,$forceUpdate,$destroy
renderMixin(Vue)//给Vue的原型对象挂载一些辅助方法,以及$nextTick,_render

export default Vue

initMixin(Vue)

在Vue的原型对象上定义了_init函数,初始化Vue实例的时候会调用,_init函数内部调用了initLifecycle,initEvents,initRender,initState,initProvide,initInjections

initInternalComponent
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

1. 设置内部组件的特定属性和配置

  • 初始化一些内部组件特有的标识或状态,以便在后续的渲染和更新过程中进行特殊处理。

2. 处理父子组件关系

  • 建立内部组件与父组件之间的联系,包括设置父组件的引用、确定父子组件之间的通信方式等。
  • 可能会处理组件的继承关系,确保内部组件能够正确地继承父组件的属性和方法。

3. 处理组件选项合并

  • 将内部组件的选项与父组件的选项进行合并,以确定最终的组件配置。这可能涉及合并数据、计算属性、方法、生命周期钩子等。

4. 准备组件的数据和响应式系统

  • 如果内部组件有自己的数据,可能会对其进行响应式处理,确保数据的变化能够触发视图的更新。
  • 可能会设置一些内部组件特有的数据观察机制,以满足特定的业务需求。

5. 初始化组件的生命周期钩子

  • 确保内部组件的生命周期钩子能够正确地被调用,以便在不同的阶段执行特定的逻辑。

  • 可能会对内部组件的生命周期钩子进行特殊处理,以适应内部组件的特殊行为。

mergeOptions

直接创建一个普通的 Vue 实例,而不是一个组件实例时。普通 Vue 实例可能没有设置 _isComponent 这个标识,就不会进入initInternalComponent,而是进入vm.$options = mergeOptions(...)

  1. resolveConstructorOptions(vm.constructor)
-   这个函数用于解析 Vue 实例的构造函数的选项。它可能会遍历构造函数的原型链,收集所有通过 `Vue.extend` 或 `Vue.component` 定义的选项以及全局混入(`Vue.mixin`)的选项等,以获取该实例的完整构造函数选项。
  1. options || {}

    • 这里传入的 options 通常是在创建 Vue 实例时用户自定义的选项对象。如果 options 不存在(为 null 或 undefined),则使用一个空对象。
  2. mergeOptions(...)

    • 调用 mergeOptions 函数将前面两个来源的选项对象进行合并。具体来说:

      • 它会整合构造函数中的默认选项和用户自定义的选项,包括数据(data)、方法(methods)、计算属性(computed)、生命周期钩子(如 createdmounted 等)、监听器(watch)等各个方面的选项。
      • 对于父子组件的关系,这个过程也会处理父组件传递给子组件的选项,确保子组件能够正确继承和扩展父组件的选项。
      • 对于一些特殊的选项,如 propsemitsslots 等,会按照特定的规则进行合并,以保证组件之间的属性传递和交互正常进行。
  3. vm.$options

    • 最后,将合并后的选项对象赋值给 vm.$options,使得 Vue 实例可以通过 this.$options 访问到完整的选项配置。在后续的实例生命周期中,Vue 内部的各个部分可以根据这个选项对象来进行相应的操作,例如在生命周期钩子中执行特定的逻辑、根据 data 选项初始化响应式数据等。

initLifecycle(vm)

初始化一些属性,给父节点收集children

initEvents(vm)

初始化事件,拿到父组件的事件,并以自身为媒介(类似于$BUS),将事件绑定到自身上

initRender(vm)
  1. 创建用于渲染的元素和属性相关的变量:

    • 例如,创建了 vm._vnode 用于存储组件的虚拟节点。
  2. 初始化与插槽相关的内容:

    • 处理插槽的配置和数据结构,为组件的插槽功能做好准备。
  3. 定义一些用于操作 DOM 的辅助方法:

    • 如 vm._c 用于创建虚拟节点,vm.$createElement 用于更方便地创建元素节点等。
  4. 建立组件与父组件、子组件之间的关联,方便在渲染和更新过程中进行通信和协调。

    • 初始化一些与事件监听和派发相关的变量和方法:为组件处理事件提供基础支持。
  5. 包括把attrsattrs和listeners变成响应式的,方便后续有变化的时候更新

callHook(vm, 'beforeCreate')

调用beforeCreate钩子

initInjections(vm)

先注入,再提供

  • 如果先提供数据,然后再处理注入,可能会导致注入的数据覆盖或干扰已经提供的数据。因此,在初始化数据之前,先处理注入。

initState(vm)

基本就是初始化prop data methods computed watch这些属性 Vue实例化的数据响应式处理也是在initState内部执行的,initData(vm)/observe(vm._data = {}, true /* asRootData */)

  • 相关的initXXX函数都在src\core\instance\state.js
export function initState (vm: Component) {
 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)
 } else {
   observe(vm._data = {}, true /* asRootData */)
 }
 if (opts.computed) initComputed(vm, opts.computed)
 if (opts.watch && opts.watch !== nativeWatch) {
   initWatch(vm, opts.watch)
 }
}

initComputed(vm, opts.computed)

在 Vue 中,initComputed 函数主要用于初始化计算属性。

  • 它会遍历定义在组件选项中的计算属性对象,为每个计算属性创建一个 Watcher 对象,并通过 Object.defineProperty 对计算属性进行劫持。

具体来说,它做了以下关键步骤:

  • 遍历计算属性对象:获取每个计算属性的名称和对应的计算函数。

  • 创建 Watcher :为每个计算属性创建一个 Watcher ,用于跟踪其依赖的数据变化。

  • 定义属性的 get 和 set 方法:

    • get 方法用于获取计算属性的值,在获取值时会执行计算函数,并收集依赖。
    • set 方法通常是一个空操作,因为计算属性默认是只读的。
  • 缓存计算结果:第一次获取计算属性的值时,会将结果缓存起来,后续再次获取时,如果依赖的数据没有变化,直接返回缓存的结果,提高性能。

  • 通过 initComputed ,使得计算属性能够根据其依赖的数据自动更新,并提供高效的访问和缓存机制。

initWatch(vm, opts.watch)

可以看到initWatch通过调用createWatcher创建了watcher


function initWatch (vm: Component, watch: Object) {
 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)
   }
 }
}

function createWatcher (
 vm: Component,
 expOrFn: string | Function,
 handler: any,
 options?: Object
) {
 if (isPlainObject(handler)) {
   options = handler
   handler = handler.handler
 }
 if (typeof handler === 'string') {
   handler = vm[handler]
 }
 return vm.$watch(expOrFn, handler, options)
}
initProvide(vm)

提供数据

callHook(vm, 'created')

调用钩子,没啥说的

可能会调用$mount
if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

stateMixin(Vue)

在Vue的原型对象挂载$data,$props,$set,$delete,$watch

$data,$props
  • 把给$data,$props设置getter,但是不设置setter,确保其只可读,不可修改
$set,$delete
  • 把响应式相关的set和delete方法挂载到Vue的原型对象上,所以可以在实例直接使用this.$set()给响应式数据新增响应式属性
    • set和delete来源于vue-2.6\src\core\observer\index.js
      • ObserverdefineReactive等响应式核心处理逻辑也在observer\index
$watch

在原型上挂载$watch,方便js动态创建监听

返回了一个unwatchFn函数,该函数调用的是watcher实例的teardown,移除监听,并且在当前实例的deps移除相关dep

Vue.prototype.$watch = function (
   expOrFn: string | Function,
   cb: any,
   options?: Object
 ): 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) {
     const info = `callback for immediate watcher "${watcher.expression}"`
     pushTarget()
     invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
     popTarget()
   }
   return function unwatchFn () {
     watcher.teardown()
   }
 }

eventsMixin(Vue)

在Vue的原型对象挂载$on,$once,$off,$emit,实现事件的发布 - 订阅模式; Vue 实例提供了强大的事件处理机制,使得开发者可以方便地实现组件之间的通信和交互。

lifecycleMixin(Vue)

在Vue的原型对象挂载_update,$forceUpdate,$destroy

_update

主要用于将虚拟 DOM 转换为实际的 DOM 操作,以更新视图

  • 里面调用了__patch__(内部执行diff算法)
  • _update会在调用mounted->mountComponent=>hook(beforeMount)的内部被封装,提供给wather当做函数更新视图时调用
$forceUpdate

当组件的数据变化没有被 Vue 的响应式系统自动检测到,或者在某些特殊情况下需要立即更新组件的视图时,可以调用 $forceUpdate 方法。

  Vue.prototype.$forceUpdate = function () {
    const vm: Component = this
    if (vm._watcher) {
      vm._watcher.update()
    }
  }
$destroy

销毁实例

  • 内部调用相关钩子,callHook-beforeDestroy+destroyed
  • 拆卸相关watcher
  • 取消事件监听
  • 释放相关相关变量

renderMixin(Vue)

给Vue的原型对象挂载$nextTick,_render以及很多辅助方法

$nextTick
 Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }
  • 用于在下次 DOM 更新循环结束之后执行延迟回调。这在需要操作更新后的 DOM 时非常有用

  • 在浏览器环境中:

    • 如果支持 Promise ,则使用 Promise.then 来实现异步执行。
    • 如果不支持 Promise 但支持 MutationObserver (用于监听 DOM 变化),则使用它来实现异步。
      • 在 Vue 的 nextTick 中使用 MutationObserver 时,通常不会监听特定的某个元素。而是创建一个新的文本节点,并将对这个文本节点的变化监听作为触发 nextTick 回调的机制。
    • 如果上述都不支持,就会采用 setTimeout 来实现异步。
  • nextTick 所使用的 Promise.then 和 MutationObserver 属于微任务,而 setTimeout 属于宏任务。

  • 微任务通常在当前脚本执行完毕后,在执行下一个宏任务之前进行处理,并且微任务的执行优先级高于宏任务。

_render

用于将组件的渲染函数转换为虚拟 DOM。 mounted->mountComponent内部封装updateComponent函数的时候,函数内部调用_update的时候会把vm._render()当做参数

  • 关于updateComponent后面会说
vm._update(vm._render(), hydrating)
辅助方法

installRenderHelpers(Vue.prototype)

export function installRenderHelpers (target: any) {
 target._o = markOnce
 target._n = toNumber
 target._s = toString
 target._l = renderList
 target._t = renderSlot
 target._q = looseEqual
 target._i = looseIndexOf
 target._m = renderStatic
 target._f = resolveFilter
 target._k = checkKeyCodes
 target._b = bindObjectProps
 target._v = createTextVNode
 target._e = createEmptyVNode
 target._u = resolveScopedSlots
 target._g = bindObjectListeners
 target._d = bindDynamicKeys
 target._p = prependModifier
}

callHook

说一下这个函数,位置在vue-2.6\src\core\instance\lifecycle.js

export function callHook (vm: Component, hook: string) {
 // #7573 disable dep collection when invoking lifecycle hooks
 pushTarget()
 const handlers = vm.$options[hook]
 const info = `${hook} hook`
 if (handlers) {
   for (let i = 0, j = handlers.length; i < j; i++) {
     invokeWithErrorHandling(handlers[i], vm, null, vm, info)
   }
 }
 if (vm._hasHookEvent) {
   vm.$emit('hook:' + hook)
 }
 popTarget()
}

一个非官方推荐,但可行的用法

可以看到callHook调用钩子,都是使用vm.$emit('hook:' + hook)这种方式派发的,结合平时实际使用,$on订阅后,可以在合适的时机调用vm.$emit来派发事件;

  • @其实是v-on的语法糖,相关事件最终会被解析编译为对应的vm.$on,监听了hook:created这个事件,在Vue实例化的时候,调用了callHook('created')就会派发时间,触发订阅(hook可以是其他钩子)
  • 下面示例helloW的vm就是helloW该实例
<helloW @hook:created='doSame'  @hook:beforeDestroy='resetSame'/>

类似场景,引入了第三方组件,但希望created的时候监听其创建了,或者计数器+1等功能的时候,不期望重写第三方组件,入侵性较小

挂载($mount)

从梦开始的地方看Vue,对Vue.prototype.$mount有个重写

  • 路径vue-2.6\src\platforms\web\entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function(el){
    if (!options.render){
        if (template){...}
        else if(el){...}
    }
}

render,template,el的优先级

一般实例化写入配置的时候有3种方式,render,template,el

  • 从源码看,render优先级最高,其次到template,最后到el
const _vue=new Vue({
        tempalte:'<div />',
        el:'#app',
        render(){

        },
        data(){
            return {
                msg:'hello world'
            }
        }
    }).$mount('#app')

mount

我们从入口文件entry-runtime-with-compiler.js可以看到有引入Vue:import Vue from './runtime/index';点进去会发现最初始对Vue的原型对象挂载的$mounted


Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

mountComponent 主要做了以下几件重要的事情:

  • 创建一个 Watcher 实例:用于监听数据的变化并触发更新操作。

  • 调用 updateComponent 方法来进行初始渲染:

    • 这个方法会获取最新的渲染结果(虚拟 DOM)。
    • 然后将虚拟 DOM 转换为真实 DOM 并进行挂载。
  • 设置更新回调:当数据变化时,会触发 Watcher 的回调,再次调用 updateComponent 进行组件的重新渲染。

  • 处理错误:在渲染过程中捕获可能出现的错误,并进行适当的处理和报告。

  • 例如,当组件首次被挂载时,mountComponent 会启动整个渲染流程,确保组件的视图正确地显示在页面上。之后,当组件中的数据发生变化,通过之前创建的 Watcher 机制,再次调用相关方法进行视图的更新。

  • 里面的new Watch的时候会控制Dep.target的指针是否存自身,如果存在,则表示有watcher在监听

mountComponent
  • 目录'core/instance/lifecycle'
  • 挂载的时候实际就是调用这个函数
export function mountComponent (
  vm,
  el,
  hydrating
){
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
  }
  callHook(vm, 'beforeMount')

  let updateComponent = () => {
      //_render创建vnode
      //_update转换为实际DOM,内部调用__patch__执行diff算法打补丁
      vm._update(vm._render(), hydrating)
    }

  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
updateComponent是内部封装生成的
  • Vue.prototype._update 方法主要用于将_render生成的虚拟 DOM 转换为实际的 DOM 操作,以更新视图。
    • 具体来说,它会执行以下主要操作:
      • 对比新的虚拟 DOM 和旧的虚拟 DOM 之间的差异。
      • 根据差异来决定是创建新的 DOM 节点、更新现有 DOM 节点的属性、删除不再需要的 DOM 节点等操作。
      • 最终将这些操作应用到实际的 DOM 中,以实现视图的更新。
      • 例如,如果新的虚拟 DOM 中添加了一个节点,_update 方法会在实际的 DOM 中创建相应的节点。如果某个节点的属性发生了变化,它会更新该节点的属性值。
    • 这是 Vue 实现高效视图更新的关键步骤之一。
  • updateComponent提供给生成的wather实例,在更新视图的时候调用

如有错误欢迎指正,响应式相关和wather后面会另出一篇文章