待业在家程序员,复习下vue源码。

120 阅读4分钟

待业在家,只有在学习的时候才不感觉焦虑。那么下面带着问题开始复习vue源码。

1、vue生命周期在源码里是怎么体现的?

2、谈谈虚拟Dom,并描述下Diff算法的源码?

3、谈谈Vue响应式在源码里怎么实现的?


1、vue生命周期在源码里是怎么体现的?

首先项目内安装vue2.7版本npm包

可以看到2.7版本新增了几个HOOKS,后面慢慢研究 node_modules/vue/src/shared/instance/constants.ts

export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch',
  'renderTracked',
  'renderTriggered'
] as const

找到启动文件 node_modules/vue/src/core/instance/index.ts

new Vue触发的就是 Vue函数,Vue 函数里环境判断和类型判断通过后,触发到 this._init this 通过 new Vue构造函数指向 Vue.prototype,下面我们从 initMixin 方法开始看,其他的先不管。

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'

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

//@ts-expect-error Vue has function type
initMixin(Vue)
// 初始化$set、$delete、$watch方法
stateMixin(Vue)
// 在Vue.prototype上定义$on、$once、$off、$emit方法
eventsMixin(Vue)
// 在Vue.prototype上定义$forceUpdate、$destroy方法
// Vue.prototype._update 生成/更新真实的dom,然后将dom对象赋值给vm.$el
// 定义了callHook(调用生命周期钩子函数)
lifecycleMixin(Vue)
// 定义了$nextTick方法和_render函数
renderMixin(Vue)

export default Vue as unknown as GlobalAPI

下面是initMixin方法源码,node_modules/vue/src/core/instance/init.ts

export function initMixin(Vue: typeof Component) {
  Vue.prototype._init = function (options?: Record<string, any>) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    // vue性能监控相关代码,与生命周期无关
    if (__DEV__ && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // 标记为vue实例,之后创建observe函数用来判断
    vm._isVue = true
    // 标记为无需响应对象
    vm.__v_skip = true
    // EffectScope处理响应式副作用 (即计算属性和侦听器)
    vm._scope = new EffectScope(true /* detached */)
    vm._scope._vm = true
    // merge options
    if (options && options._isComponent) {
      // 如果是组件,就进行内部组件的初始化
      initInternalComponent(vm, options as any)
    } else {
      // 把构造函数 options 和当前实例 options 的合并
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor as any),
        options || {},
        vm
      )
    }
    if (__DEV__) {
      // 开发环境,调用Proxy办法,给vue实例增加代理
      initProxy(vm)
    } else {
      // 其他情况,vue实例的_renderProxy属性指向vue实例自身
      vm._renderProxy = vm
    }
    vm._self = vm
    // 初始化实例属性:$parent,$root,$children,$refs,_provided,_watcher等
    initLifecycle(vm)
    // 初始化事件函数并将父组件向子组件注册的事件注册到子组件实例中的`_events`对象里
    initEvents(vm)
    // 初始化渲染: $slots,$createElement,$attrs,$listeners等
    initRender(vm)
    // 第一个生命周期函数触发beforeCreate
    callHook(vm, 'beforeCreate', undefined, false /* setContext */)
    initInjections(vm) // 初始化 inject
    // 初始化组件内功能:props,methods,data,computed,watch,setup(兼容vue3)
    initState(vm)
    initProvide(vm) // 初始化 provide
    // 第一个生命周期函数触发created
    callHook(vm, 'created')

    // vue性能监控相关代码,与生命周期无关
    if (__DEV__ && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      // 如果有el就调用$mount进入模板编译和挂载阶段
      // 不传el需要手动调用$mount
      vm.$mount(vm.$options.el)
    }
  }
}

生命周期beforeCreatecreated在源码initMixin函数里触发。

从源码能看出来,beforeCreate触发的时候,组件还没有初始化,组件内的功能还不能使用,所以一般在这里处理一些全局的功能,如全局加载Loading等。

created触发时,props、data、provide、inject、computed、watch等组件内功能已经初始化好了,这个时候可以对组件内data等数据进行设置。插个题外话,created内调用异步事件处理数据,需要配合watch监听、$nextTick或者async created+v-if。


下面从入口文件开始往下找$mount编译函数的实现, node_modules/vue/src/platforms/web/entry-runtime-with-compiler.ts node_modules/vue/src/platforms/web/runtime-with-compiler.ts

runtime-with-compiler里设置了Vue.prototype.$mount函数的编译规则,这段源码就不贴了,简单概括一下:

优先判断 render 是否存在,如果存在,就直接使用 render 函数,

再判断有没有template,没有template就直接渲染el

编译顺序:render > template > el,template和el最终都会编译成render函数。

node_modules/vue/src/platforms/web/runtime/index.ts

runtime/index文件里将Vue.prototype.$mount指向mountComponent

下面看一下mountComponent的源码,node_modules/vue/src/core/instance/lifecycle.ts

删除了一些用于DEV的代码,只保留生命周期部分

export function mountComponent(
  vm: Component,
  el: Element | null | undefined,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    // 如果没有render节点,就默认创建一个注释节点 <--注释节点-->
    vm.$options.render = createEmptyVNode
    ......
  }
  // 这里触发了beforeMount生命周期钩子函数
  callHook(vm, 'beforeMount')

  let updateComponent
  // 性能监控相关代码
  if (__DEV__ && config.performance && mark) {
    ......
  } else {
    // 首次渲染,只传入_render函数,_update内无需做新旧节点比对
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
  // 设置new Watcher观察者的回调函数
  const watcherOptions: WatcherOptions = {
    // new Watcher监听的函数返回数据更新之前会调用
    before() {
      // 判断 DOM 是否已挂载状态(首次渲染和卸载的时候不会执行)
      if (vm._isMounted && !vm._isDestroyed) {
        // 这里触发了beforeUpdate生命周期钩子函数
        callHook(vm, 'beforeUpdate')
      }
    }
  }

  if (__DEV__) {
    // 判断了环境,只在开发环境生效
    // 这里挂载了renderTracked,renderTriggered两个生命周期钩子,一般用来调试
    // 构建虚拟dom使用到响应式数据,会收集依赖,收集到依赖会触发renderTracked生命周期钩子函数
    watcherOptions.onTrack = e => callHook(vm, 'renderTracked', [e])
    // 当某一个依赖更改后导致页面重新渲染的时候会触发这个函数
    watcherOptions.onTrigger = e => callHook(vm, 'renderTriggered', [e])
  }

  // 设置Watcher观察者,监控updateComponent函数返回的数据
  new Watcher(
    vm,
    updateComponent,
    noop,
    watcherOptions,
    true /* isRenderWatcher */
  )
  hydrating = false

  // 这个是配合 2.7 添加的 setup 处理
  // setup中定义的预执行的,watcher函数分别调用watcher.run执行一次
  const preWatchers = vm._preWatchers
  if (preWatchers) {
    for (let i = 0; i < preWatchers.length; i++) {
      preWatchers[i].run()
    }
  }

  // $vnode不存在,说明是首次调用
  if (vm.$vnode == null) {
    // 设置该组件为已挂载
    vm._isMounted = true
    // 这里触发了mounted生命周期钩子
    callHook(vm, 'mounted')
  }
  return vm
}

生命周期beforeMountmounted在源码mountComponent函数里触发。

beforeUpdaterenderTrackedrenderTriggered是在mountComponent里挂载,在Watcher观察者里触发。

有了beforeUpdate,再找一下updated,Vue.prototype._update里有两句注释

// updated hook is called by the scheduler to ensure that children are updated in a parent's updated hook.

updated生命周期钩子在scheduler里调用以确保子组件在父组件的updated中

下面找到scheduler文件里的callUpdatedHooks方法,node_modules/vue/src/core/observer/scheduler.ts

function callUpdatedHooks(queue: Watcher[]) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm && vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      // 遍历watcher数组触发updated生命周期钩子
      callHook(vm, 'updated')
    }
  }
}

从callUpdatedHooks往上找,callUpdatedHooks由flushSchedulerQueue函数触发,flushSchedulerQueue由queueWatcher函数通过nextTick异步触发,queueWatcher在core/observer/watcher.ts里由update函数触发

也就是说由队列执行完 Watcher 数组的 update 方法后调用了 updated 钩子函数


再来看看销毁阶段

Vue.prototype.$destroy = function () {
    const vm: Component = this
    // 如果该实例正在被销毁中,则return不处理
    if (vm._isBeingDestroyed) {
      return
    }
    // 这里触发了beforeDestroy生命周期钩子
    callHook(vm, 'beforeDestroy')
    // 开始进行销毁流程
    vm._isBeingDestroyed = true
    // remove self from parent
    const parent = vm.$parent
    // 如果该组件有父级&&父级没有在销毁状态&&不是抽象组件<slot>、<keep-alive>等
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      // 从父级中删除该组件
      remove(parent.$children, vm)
    }
    // 取消EffectScope处理响应式副作用
    vm._scope.stop()
    // 删除数据对象的引用
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    // 组件状态更新为已经销毁
    vm._isDestroyed = true
    // 删除虚拟dom
    vm.__patch__(vm._vnode, null)
    // 这里触发了destroyed生命周期钩子
    callHook(vm, 'destroyed')
    // 关闭事件监听
    vm.$off()
    // 删除根组件的引用
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // 删除父级的引用
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }

destroy除了手动调用,就是在core/vdom/patch.ts的时候会调用,新老节点进行比对之后,进行一些destroy操作


最后看activateddeactivatederrorCapturedserverPrefetch 这几个生命周期。

export function activateChildComponent(vm: Component, direct?: boolean) {
  if (direct) {
    vm._directInactive = false
    if (isInInactiveTree(vm)) {
      return
    }
  } else if (vm._directInactive) {
    return
  }
  if (vm._inactive || vm._inactive === null) {
    vm._inactive = false
    for (let i = 0; i < vm.$children.length; i++) {
      activateChildComponent(vm.$children[i])
    }
    callHook(vm, 'activated')
  }
}

activateChildComponent内部进行递归调用,递归执行$children里所有子组件的activated钩子函数。

上面提到过flushSchedulerQueue函数,activated钩子也是在flushSchedulerQueue里,不过是通过callActivatedHooks触发循环执行

export function deactivateChildComponent(vm: Component, direct?: boolean) {
  if (direct) {
    vm._directInactive = true
    if (isInInactiveTree(vm)) {
      return
    }
  }
  if (!vm._inactive) {
    vm._inactive = true
    for (let i = 0; i < vm.$children.length; i++) {
      deactivateChildComponent(vm.$children[i])
    }
    callHook(vm, 'deactivated')
  }
}

deactivated钩子则是在组件的destroy事件里判断,如果是keepAlive,直接执行

errorCaptured是捕获组件内报错的钩子函数,全局捕获是

config.errorHandler

// main.js 
Vue.config.errorHandler = function (err, vm, info) { 
    console.log('全局捕获 err >>>', err) 
    console.log('全局捕获 vm >>>', vm) 
    console.log('全局捕获 info >>>', info) 
}
export default{ 
    data(){}, 
    /** * 收到三个参数: 
    * 错误对象、发生错误的组件实例 
    * 以及一个包含错误来源信息的字符串。 
    * 此钩子可以返回 false 以阻止该错误继续向上传播。 
    */ 
    errorCaptured(err, vm, info){ 
        console.log(err) // -> 错误返回 console.log(vm) // -> vue实例 
        console.log(info) // -> 在哪个钩子发生错误 return false 
    } 
}

源码是在core/util/error.ts里,通过handleError遍历触发错误回调。 handleError函数在render、state、watcher、next-tick、directives会被触发,具体源码不在这里过细讨论。

serverPrefetch

服务端渲染时,组件实例在服务器上被渲染前调用


总结

beforeCreate

这个时候只是初始化了一些全局实例,组件内功能还没加载,只能做一些不依赖组件的动作。 比如:对路由的拦截、处理全局的loading

created

这个时候组件已经初始化完成,组件内部的props,methods,data,computed,watch等功能已经处理好了,不过dom还没渲染,所以这个时候,可以访问组件的vm挂载一些方法、请求数据

beforeMount

这个是dom渲染前会调用,和created很接近,所以一般不用。如果真的要说的话,就是可以在dom渲染之前,再修改一下初始化好的数据

mounted

dom已经渲染好了,可以做一些访问dom的操作,比如this.$ref

beforeUpdate

从源码看出,beforeUpdate是在Watcher里触发,也就是说被添加到视图观察者的数据变更了,会触发beforeUpdate。这个生命周期可以修改数据、可以访问dom,不过在这里操作要谨慎,因为视图马上要更新了

updated

和beforeUpdate一样,只会在一些特殊业务需求里会用到,比如这个组件视图更新了要做些操作。能访问到的更新后的dom结构,做更新后的处理

beforeDestory

组件销毁前触发,data、methods等功能还能访问,一般做组件内事件的移除和功能的重置

destroyed

组件此时已销毁,可以销毁一些不依赖组件的功能,定时器、事件监听、事件订阅等

activated

包裹的组件,第一次加载created和activated都会触发。切换组件再回来,因为组件已经被缓存,所以只有activated会触发

deactivated

被 keep-alive 缓存的组件停用时调用。在deactived里面,在里面进行一些善后操作