vue2源码系列-执行过程

目录

一、前期工作
二、初始化
step 1
initLifecycle 中的属性
resolveSlots
step 2
initProps
initMethods
initData
initComputed
initWatch
step 3
三、挂载
四、更新
五、卸载
六、Proxy

Vue3 正如火如荼,Vue3 与 Vue2 还是有些许共通之处的,我们通过源码来简单回顾一下 Vue2 的相关知识,这里以 Vue 的 2.6.14 版本为例讲解。文中响应式相关的内容将在后续文章中补充

先扔张官网生命周期的图镇楼

vue2源码系列-执行过程
生命周期

官方的图已经将主流程描述的很清楚了,这里就不赘述流程了,我们直接开始

一、前期工作

...
function Vue (options) {
  ...
  this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

一切都从 new Vue(options) 开始,但在开始前,还有准备工作要做

  • initMixin:初始化,下一小节详解
  • stateMixin:使用 Object.defineProperty 给 Vue.protorype 加上 $data$props 属性,然鹅只给这两个属性配置了 getter,因此无法修改、删除这两个属性,也不能枚举;继续增加 $set$delete$watch 方法,这几个方法都与 数据双向绑定 有关,将在后续的文章详细介绍。
  • eventsMixin:Vue 原型增加 $on$once$off$emit 方法,通过 _events 对象维护所有事件处理函数
    • $on:监听当前实例上的自定义事件
    • $once:监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器
    • $off:移除自定义事件监听器
    • $emit:触发、执行事件
  • lifecycleMixin:继续往 Vue 原型上加,_update$forceUpdate$destroy 方法
    • _update:更新视图
    • $forceUpdate:通过 _watcher 更新视图
    • $destroy: 卸载组件
  • renderMixin:还是往 Vue 原型上加东西,这次是 $nextTick_render 方法
    • $nextTick:收集事件,在下一次事件循环一次性执行
    • _render:根据配置返回对应的 VNode(Vue 中的虚拟节点)

小知识:无法枚举的属性在浏览器控制台展示的颜色会比正常属性浅(谷歌为浅紫色)

二、初始化

准备工作完成,现在可以开始执行 new Vue() 了。new Vue() 会调用 initMixin 挂在 Vue 原型上的 _init() 方法。

export function initMixin (Vue: Class<Component>{
  Vue.prototype._init = function (options?: Object{
    const vm: Component = this
    ...
    vm._self = vm
    // step 1
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    // step 2
    initInjections(vm)
    initState(vm)
    initProvide(vm)
    callHook(vm, 'created')
    ...
    // step 3
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

step 1

  • initLifecycle:这个比较简单,给属性赋上默认值,包括 $parent$children$refs、上一小节提到的 _watcher 属性等。
  • initEvents:若存在父组件绑定在当前组件上的事件,则调用 updateComponentListeners 更新这些方法
  • initRender:调用 resolveSlots 进行组件插槽(slot)的初始化;新增 $attrs$listeners 属性,同时使用 defineReactive 将这两个属性设为响应式属性
  • 触发 beforeCreate

initLifecycle 中的属性

名称 描述
$parent 指定已创建的实例之父实例,在两者之间建立父子关系。子实例可以用 this.children 数组中。
$root 当前组件树的根 Vue 实例。如果当前实例没有父实例,此实例将会是其自己。
$children 当前实例的直接子组件。需要注意 $children 并不保证顺序,也不是响应式的。
$refs 一个对象,持有已注册过 ref 的所有子组件。
_watcher 组件实例相应的 watcher 实例对象。
_inactive 表示 keep-alive 中组件状态,值为 false 就是激活状态,反之为 true。
_directInactive 也是表示 keep-alive 中组件状态的属性。
_isMounted 当前实例是否完成挂载(对应生命周期图示中的 mounted)。
_isDestroyed 当前实例是否已经被销毁(对应生命周期图示中的 destroyed)。
_isBeingDestroyed 当前实例是否正在被销毁,还没有销毁完成(介于生命周期图示中 deforeDestroy 和 destroyed 之间)。

resolveSlots

对插槽的处理

代码就不贴了,简单讲一下是如何处理的,详细代码在 resolve-slots.js

  • 若组件 children 子节点为空,则组件中 this.$slots 对象为{}空对象
  • 若组件子节点存在,则先移除 slot 属性,具名 slot 按照定义的 name 进行存储
  • 当定义 slot 的标签为 template 时,则获取它的子节点
  • 最后删除只包含空白字符(即不包含具体内容的 slot)

step 2

这一步将需要处理的属性都挂在当前组件实例的 $options 属性上,如:injectpropsdata 等。

  • initInjections:
    • inject 使用 Symbol 作为 key,则使用 Reflect.ownKeys 获取 inject 的所有 key 值,否则使用 Object.keys
    • 之后对 inject 中的各个 key 进行遍历,沿着父组件链一直向上查找 provide 中和inject 对应的属性,直到查找到根组件或者找到为止,然后返回结果
    • 最后使用 defineReactive 将其属性设为响应式,在非生产环境下在当前组件内直接修改 inject 的属性发出告警
  • initState:这一步囊括了很多初始化操作(以下排列顺序对应源码中的执行顺序)
    • initProps
    • initMethods
    • initData
    • initComputed
    • initWatch
  • initProvide:将 provide 映射到当前组件实例的 _provided 属性上
  • 到这里,一个 Vue 组件实例算是创建完成了,触发 created

initProps

遍历 props

  • 检查 props 的属性是否为保留字符,是的话则告警
  • 通过 defineReactive 将属性设为响应式,在非生产环境下在当前组件内直接修改 props 的属性发出告警
  • props 的属性映射到当前组件实例的 _props

initMethods

遍历 methods

  • 非生产环境下,检查类型是否为 function 类型,不是则告警
  • 非生产环境下,检查是否与 props 的属性重名了,是则告警
  • 非生产环境下,检查是否为 Vue 保留字符,是则告警
  • 将方法映射到当前组件实例上,并通过 bind 将方法内的 this 指向当前组件实例

initData

首先判断 data 属性是否为函数且返回一个对象,不满足则将当前执行环境下的 data 置空,在非生产环境下告警,若满足条件,则开始遍历 data

  • 非生产环境下检查是否与 methods 的属性重名,是则告警
  • 检查是否与 props 的属性重名
    • 是,则告警
    • 否,则将属性映射到当前组件实例的 _data
  • 调用 observe,数据双向绑定的关键,之后的文章详解

initComputed

这里要提前讲一下,根据功能划分,Vue 中的 watcher 可以分为三种

|类型| 作用| |render-watcher| 每个组件都有自己的 watcher| |user-watcher |用户自定义 watcher,比如通过 watcher API 定义| |computed-watcher |使用 computed 属性定义的 watcher,具有惰性特性|

创建顺序:computed-render -> user-watcher -> render-watcher

initComputed 内维护了一个 computed-watcher 的队列 _computedWatchers

开始遍历 computed

  • 如果不是服务端渲染(SSR),则新建 watcher,并且传入 lazy=true,回调函数为空,以 computed 的属性为 key 加入 _computedWatchers
  • 通过 Object.defineProperty 将属性映射到当前组件实例上,并将 setter 设为 noop(不含函数体的函数),getter 分两种情况:
    • 非服务端渲染为 createGetterInvoker,直接返回用户定义的 getter
    • 服务端渲染为 createComputedGetter
function createComputedGetter(key{
  return function computedGetter({
    // getter step 1
    const watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      if (watcher.dirty) {
        // getter step 2
        watcher.evaluate();
      }
      if (Dep.target) {
        // getter step 3
        watcher.depend();
      }
      return watcher.value;
    }
  };
}

页面上引用了 computed 的值时,触发 watcher.update,因为这里的 watcher 的 lazytrue,所以 watcher.dirty 被设为 true;走到 getter step 2 调用 evaluate 主动触发一遍 watcher.get,刷新当前的 value 值,同时将 dirty 重新设为 false,等待下次页面引用时再重新计算。

这里 Dep.target 的值为组件的 render-watcher,走到 getter step 3 computed-watcher 收集依赖。

export default class Watcher {
  dirty: boolean;
  lazy: boolean;
  ...
  update() {
    if (this.lazy) {
      this.dirty = true;
    }
    ...
  }
  ...
  evaluate() {
    this.value = this.get();
    this.dirty = false;
  }
}

initWatch

遍历 watch 调用 createWatcher

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

因为 watch 可以以 对象方法名字符串数组方法 四种形式传入,所以在 createWatcher 中进一步判断类型,获取相应的处理函数(handler),最后调用在前期工作中 stateMixin 定义在 Vue 原型上的 $watch 方法

export function stateMixin (Vue: Class<Component>{
  ...
  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  
): Function 
{
    const vm: Component = this
    // watch step 1
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    // watch step 2
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      // watch step 3
      const info = `callback for immediate watcher "${watcher.expression}"`
      pushTarget()
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
      popTarget()
    }
    // watch step 4
    return function unwatchFn ({
      watcher.teardown()
    }
  }
}
  • watch step 1:判断回调函数(cb)是不是一个纯对象,是的话意味着其中可能还有配置信息,再调用一遍 createWatcher
  • watch step 2:新建一个 watcher(user-watcher)
  • watch step 3:如果配置了 immediate=true,先暂停依赖收集,立即执行回调函数,之后再继续收集依赖
  • watch step 4: 返回一个取消观察函数,用来停止触发回调,watcher.teardown() 用来将自身从所有依赖收集订阅列表删除

step 3

// initMixin
if (vm.$options.el) {
  vm.$mount(vm.$options.el);
}

再回到 initMixin 中,接下来是 step 3,若存在 el 则调用 $mount,$mount 方法最终调用的是 mountComponent 方法,接下来就进入到挂载阶段

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component 
{
  // 判断是不是浏览器渲染
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating);
};

三、挂载

// lifecycle.js
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component 
{
  // step 1
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      ...
    }
  }
  callHook(vm, 'beforeMount')
  ...
  // step 2
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  // step 3
  new Watcher(vm, updateComponent, noop, {
    // step 4
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // step 5
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
  • step 1:将 el 替换为 $el,若 render 方法不存在,则给一个空节点的创建方法,触发 beforeMount
  • step 2:updateComponent 作为下一步创建的 Watcher 对象的 getter 函数,用来依赖收集。_update 最终调用的是 __patch__ 方法,用于将 Virtual DOM 渲染成真实 Dom 节点,不同平台(web/weex)有略微区别
  • step 3:新建一个 watcher 实例,这里就是 render-watcher,会挂到当前组件实例的 _watcher 属性上;注意与 _watchers 属性区分开
    • _watcher:当前实例的 render-watcher
    • _watchers:存放当前实例的所有 watcher,包括 render-watcher(由于是挂载阶段才创建的,一般放在数组末尾,即在 computed-watcher 与 user-watcher 之后)
  • step 4:初始 patch 操作可能会调用 $forceUpdate,如: 在子组件的挂载钩子内部,因此在更新前增加 beforeUpdate 的 hook
  • step 5:$vnodenull 时即 挂载完成,修改 _isMounted 为 true,触发 mounted

四、更新

  • beforeUpdate:在上一步挂载阶段的 render-watcher 在异步更新前触发,即:在调用 watcher.run() 之前执行 watcher.before()
  • updated:同样是 render-watcher 的异步更新,在调用 watcher.run() 之后触发

五、卸载

keep-alive 组件,在 patch 阶段检测到旧节点存在,但新节点不存在时触发卸载,卸载过程的具体实现在 $destroy 方法中。

export function lifecycleMixin (Vue: Class<Component>{
  ...
  Vue.prototype.$destroy = function ({
    const vm: Component = this
    // step 1
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy')
    // step 2
    vm._isBeingDestroyed = true
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    // step 3
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    // step 4
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    // step 5
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    // step 6
    vm._isDestroyed = true
    vm.__patch__(vm._vnode, null)
    callHook(vm, 'destroyed')
    // step 7
    vm.$off()
    // step 8
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
}
  • step 1:根据 _isBeingDestroyed 判断当前组件是否已经在执行卸载操作了,若不是则触发 beforeDestroy
  • step 2:标记开始进行卸载组件,移除父组件对当前组件的引用
  • step 3:将当前组件实例从其他数据的依赖列表(deps)中删除
  • step 4:实例内的数据对其他数据的依赖都会存放在实例的_watchers 属性中,所以我们只需遍历 _watchers,将其中的每一个 watcher 都调用 teardown 方法,从而实现移除实例内数据对其他数据的依赖
  • step 5:从 _data.__ob__ 中删除引用,__ob__ 指向 Observer
  • step 6:当前实例上添加 _isDestroyed 属性来表示当前实例已经被销毁,同时将实例的 VNode 树设置为 null 并更新视图,之后触发 destroyed
  • step 7:移除实例上的所有事件监听器
  • step 8:移除 __vue__ 的引用,移除对父组件的引用

六、Proxy

在初始化时,非生产环境下调用了个 initProxy,之后才开始执行 initLifecycleinitEvents 等初始化操作

export function initMixin (Vue: Class<Component>{
  Vue.prototype._init = function (options?: Object{
    const vm: Component = this
    ...
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    ...

这里用的 Proxy 就是 Vue3 中使用的 Proxy API,不过在 Vue2.x 中的 Proxy 主要作用是在非生产环境下拦截对象属性的操作,对非法操作做出告警

// proxy.js
if (process.env.NODE_ENV !== 'production') {
  ...
  // step 1
  const hasProxy =
    typeof Proxy !== 'undefined' && isNative(Proxy)
  if (hasProxy) {
    ...
    // step 2
    config.keyCodes = new Proxy(config.keyCodes, {
      set (target, key, value) {
        if (isBuiltInModifier(key)) {
          warn(`Avoid overwriting built-in modifier in config.keyCodes: .${key}`)
          return false
        } else {
          target[key] = value
          return true
        }
      }
    })
  }
  const hasHandler = {
    // step 3
    has (target, key) {
      const has = key in target
      const isAllowed = allowedGlobals(key) ||
        (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
      if (!has && !isAllowed) {
        if (key in target.$data) warnReservedPrefix(target, key)
        else warnNonPresent(target, key)
      }
      return has || !isAllowed
    }
  }

  const getHandler = {
    // step 4
    get (target, key) {
      if (typeof key === 'string' && !(key in target)) {
        if (key in target.$data) warnReservedPrefix(target, key)
        else warnNonPresent(target, key)
      }
      return target[key]
    }
  }
  // step 5
  initProxy = function initProxy (vm{
    if (hasProxy) {
      const options = vm.$options
      const handlers = options.render && options.render._withStripped
        ? getHandler
        : hasHandler
      vm._renderProxy = new Proxy(vm, handlers)
    } else {
      vm._renderProxy = vm
    }
  }
}
export { initProxy }
...
  • step 1:判断当前环境是否支持 Proxy API
  • step 2:若支持 Proxy 则走到这一步,添加 set 捕捉器,在给 KeyCodes 设置属性时触发,检测自定义的 key 是否与内置修饰符重名
  • step 3:设置 has 捕捉器,查看或遍历组件实例的属性时触发,检测 $options 的属性名是否合法,是否存在
  • step 4:设置 get 捕捉器,获取组件实例的属性时触发,同样判断属性名称的前缀是否为保留字符,以及属性是否存在
  • step 5:若支持 Proxy,则使用 Proxy 捕捉 $options 的行为,并将 Proxy 实例映射到当前组件实例的 _renderProxy 属性;不支持则将当前组件实例本身映射到_renderProxy 属性

由于个人订阅号不具备评论功能,如有其它意见,欢迎前往以下任一站点评论留言:

  1. https://melonfield.club/column/detail/cvwmo7Ae5VH(个人独立开发运营)
  2. https://juejin.cn/post/6999264503443488805

8参考

  • https://github.com/vuejs/vue
  • https://www.imooc.com/article/29256
  • https://blog.csdn.net/a895865025/article/details/118852781


原文始发于微信公众号(MelonField):vue2源码系列-执行过程

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/93882.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!