调试Vue的初始化过程

Vue

# 准备html文件

通过四个导出文件调试Vue的初始化,在Vue的examples文件夹下准备html文件

<body>
    <!-- demo root element -->
    <div id="app">
      {{ msg }}
    </div>

    <script src="../../dist/vue.js"></script>
    <script>
      // compiler
      // 需要编译器,把 template 转换成 render 函数
      const vm = new Vue({
        el: '#app',
        // template: '<h1>{{ msg }}</h1>',
        // render(h) {
        //   return h('h1', this.msg)
        // },
        data: {
          msg: 'Hello Vue'
        }
      })
    </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

npm run dev启动之后,打开F12,找到Sources -> src

image

# 设置四个断点

给core/instance/index.js中initMixin(Vue)那一行设置断点1

image

给core/index.js中initGlobalAPI(Vue)那一行设置断点2

image

给platfrom/web/runtime/index.js中Vue.config.mustUseProp = mustUseProp那一行设置断点3

image

给platfrom/web/entry-runtime-with-compile.js中const mount = Vue.prototype.$mount那一行设置断点4

image

好了现在四个断点设置完毕

image

# 开始调试

下面是调试的步骤

# core/instance/index.js

# begin

  1. 点击F5进入断点,跳转到了第一个断点,instance/index.js,initMixin上面给Vue增加了实例成员,我们为了方便监视变化,在旁边的Watch中添加监视的变量Vue

image

现在构造函数和原型上都是默认的成员

# initMixin

  1. 按下F10,执行完initMixin这个方法,可以看到Vue的原型上增加了_init方法

image

# stateMixin

  1. 按F10,执行完stateMixin方法,可以看到原型上增加了$data,$props,$delete,$set,$watch

image

# eventsMixin

  1. 按下F10,执行完eventsMixin方法之后,可以看到原型上增加了$emit,$off,$on,$once

image

# lifecycleMixin

  1. 按下F10,执行完lifecycleMixin方法之后,可以看到原型上增加了$destroy,$forceUpdate,_update方法(里面调用了__patch__,将虚拟DOM转换成真实DOM)

image

  1. 按下F10,执行完renderMixin方法之后,可以看到原型上挂载了很多下划线开头的单字母方法,当把模板转换成render函数,在render函数中要调用这些方法,上面还挂载了$nextTick和_render方法

_render的作用是调用用户传来的render函数或者模板转换成的render函数

image

  1. 这个js调试完毕,停在了keep-alive.js文件中,我们不看这个文件,按F8跳到下一个断点处core/index.js

image

# core/index.js

  1. 这个里面的initGlobalAPI中给Vue初始化了静态成员,F11进入这个函数,按F10跳转到下图的位置

image

这个Object.defineProperty(Vue, 'config', configDef)是给Vue添加config属性并初始化一些值,这些值是怎么来的?

可以看到下图中,从config文件中出来,这些配置与平台相关.这里不细看

image

  1. 按F10执行到Vue.options = Object.create(null)这里可以看到Vue上面挂载了静态方法delete,nextTick,observable,set,util

image

  1. 按F10执行Vue.options方法可以看到Watch方法里面,初始化了一个options,里面为空,也没有原型对象

image

  1. 按F10,循环遍历数组
const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]
1
2
3
4
5

将其挂载到options下面,下面可以看到

image

  1. 按F10,看到options中添加了_base属性用来存放构造函数

image

  1. 按F10,注册第一个组件keep-alive

image

  1. 按F10三下,可以看到注册了静态方法use,mixin和extend

image

  1. 按F10,可以看到添加了directive,component,filter(),这些方法是注册全局的组件,指令和过滤器

image

16.这个js文件也调试完毕,按F8进入下一个断点

# web/runtime/index.js

  1. 这些代码都是与平台相关的,下面的config都是与平台相关的配置,F10执行完

image

  1. 按F10两下,下面这两个方法都是注册全局组价和指令,执行之后看一下options中组件和指令的变化.
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
1
2

image

可以看到components中添加了Transition和TransitionGroup,在directives中添加了model和show

  1. 按两下F10,这两个方法都是给Vue的原型上添加方法,__patch__$mount,这两个方法只是定义并没有调用,是在init方法中调用的.

image

  1. 这个js的核心代码也执行完毕了,下面按F8,进入web/entry-runtime-with-compile.js

# web/entry-runtime-with-compile.js

这里先取了上一个注册的$mount,然后在这里重写,给$mount添加了编译函数的功能.

21.按F10两下到Vue.compile = compileToFunctions再按一下F10执行这个函数,可以看到在Vue上挂载了compile方法,这个方法的作用是让我们手动将模板转化成render函数

image

这里四个导出Vue的文件就调试完毕,调试的过程可以看到Vue构造函数的变化,可以看到Vue静态成员和实例成员的初始化过程.

# 调试首次渲染的过程

# 准备html基本结构

  1. 准备html基本结构,在实例化Vue的时候只传入了el和data两个选项
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Vue.js 01 component example</title>
  </head>
  <body>
    <div id="app">
      <div><h1>Hello World</h1></div>
      {{ msg }}
    </div>

    <script src="../../dist/vue.js"></script>
    <script>
      const vm = new Vue({
        el: '#app',
        data: {
          msg: 'Hello Vue'
        }
      })
    </script>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

下面进行调试,Vue是如何把模板进行渲染的.

# Vue构造函数 - core/instance/index.js

回顾之前的初始化调试过程,我们首次进入的是core/instance/index.js,我们这次要调试里面的

this._init(options)
1

image

# _init - core/instance/init.js

  1. 按F11进入_init,按F10到Vue._isVue

判断是不是Vue实例,如果是Vue实例,不进行响应式处理.

image

  1. 按F10,判断当前的Vue实例是否是组件,如果是就通过initInternalComponent来合并选项options,如果是Vue实例那么就mergeOptions将传入的options和构造函数中的options进行合并.

image

我们现在是创建Vue实例,所以这里走到else里面来,可以看到没有合并之前vm.$options是undefined

image

  1. 按F10,执行之后可以看到合并之后的$options,绿色框住的是用户传入的,其他的是构造函数自己的.

image

  1. 下面要设置 _renderProxy,即渲染时候的代理对象,如果是生产环境是 initProxy ,如果是开发环境直接将实例设置到了 _renderProxy 上面.

image

# _renderProxy

按F11进入这个函数,先判断一下当前环境是否支持Proxy对象,如果支持就new Proxy对象将实例对象代理到_renderProxy中,如果不支持还是将实例对象自己赋值给_renderProxy

image

  1. 按F10将这个函数执行完毕回到原来的地方

下面要执行一些init,这些init是给Vue实例挂载一些成员,我们关注它如何渲染,这里先跳过.将断点设置到最后 vm.$mount(vm.$options.el), 按F8跳到这个位置.

image

# vm.$mount(vm.$options.el) - entry-runtime-with-complier.js

  1. 按F11进入这个函数,此时到了entry-runtime-with-complier.js入口文件中,这里重写了$mount添加编译器.一些代码之前解释过这里不再赘述,按F10直接跳转到 const options = this.$options

判断之前合并的options中是否有render选项,如果不存在就判断template选项是否存在,如果存在要判断其是否是字符串(是否是选择器),又或者是否有nodeType属性(是否是DOM元素).如果是字符串就判断其是不是id选择器,不是就警告,如果是就获取其DOM元素返回其innerHTML作为模板,如果是nodeType就直接返回其innerHTML作为模板.

image

下面是整理的思维导图

image

可以看一下idToTemplate里面做了什么操作,利用id选择器找到其DOM元素返回innerHTML作为模板

const idToTemplate = cached(id => {
  // 根据id获取DOM元素并返回其innerHTML
  const el = query(id)
  return el && el.innerHTML
})
1
2
3
4
5

如果没有render也没有template就判断是否有el,获取el的outerHTML作为模板

image

下面看看getOuterHTML是如何实现的

function getOuterHTML (el: Element): string {
  // 如果el里面有outerHTML属性就直接返回作为模板
  if (el.outerHTML) {
    return el.outerHTML
  // 如果不是的话可能不是一个DOM元素,可能是一个文本节点或者一个注释节点
  } else {
    // 创建一个div,将el克隆一份放到div里面,最终把其innerHTML返回作为模板
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

所以调试的时候我们没有传template,只传了el,会执行outerHTML那一个步骤.

image

  1. 按F10执行完毕之后,可以看到template里面有了内容

image

  1. 下面要进行编译了,compileToFunctions函数就是帮我们把template编译成render函数,staticRenderFns是起一个优化作用.后面将编译的时候单独来说.最后将render和staticRenderFns注册到options中

image

  1. 按F10跳过,执行mount方法,我们在这个入口文件中对$mount进行了重写,而此处的mount方法是在runtime/index.js中定义的$mount,结构看下面可以看清楚:
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (){
    ...
    return mount.call(this, el, hydrating)
}
1
2
3
4
5

# Vue.prototype.$mount - runtime/index.js

  1. 按F11进入,runtime/index.js中,里面会重新获取el,为什么要重新获取el呢?

因为之前我们执行的是编译器版本的js,entry-runtime-with-compile.js中获取了el,如果是运行时就不会执行刚才获取el的代码,所以这里要再获取一次el

下面运行到mountComponent进行变化,这个方法是Vue的核心方法

image

# mountComponent - core/instance/lifecycle.js

  1. 按F11进入,到了core/instance/lifecycle.js中,里面定义了mountComponent,
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // 判断如果没有render选项,且还有template选项就发出警告,要么使用运行时版本,要么就用render函数
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  
  // 触发挂载前的生命周期钩子函数
  callHook(vm, 'beforeMount')
  
  // 这个函数是更新组件\挂载的函数
  let updateComponent
 
  // 如果是开发环境且启用了性能检测,下面是性能检测的代码,可忽略
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      ...
    }
  } else {
    // 定义updateComponent
    updateComponent = () => {
      // _render的作用是调用用户传入的render函数或者编译器生成的render,最终会返回虚拟DOM把虚拟DOM传给_update
      // _update方法会将虚拟DOM转化成真实DOM,最后更新到界面上
      // 这句话执行完毕之后就会看到模板被渲染到了界面上
      vm._update(vm._render(), hydrating)
    }
  }

  // 创建了一个Watcher对象,并把updateComponent传入,所以其执行是在Watcher中调用的
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // 最后触发了mounted生命周期钩子函数,表示已经挂载完成
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

# Watcher.get() - core/observer/watcher.js

  1. 下面我们看一下Watcher中是怎么调用的?按F11进入

进入了core/observer/watcher.js中,observer中的代码都是与响应式相关的,

watcher有三种

  • 第一种是渲染Watcher,当前的Watcher
  • 计算属性的Watcher
  • 侦听器的Watcher

可以看到其构造函数传入的值

image

下面进行一下代码解析

constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    // 是否是渲染Watcher,有三种Watcher
    /**
     * watcher有三种
     * - 第一种是渲染Watcher,当前的Watcher
     * - 计算属性的Watcher
     * - 侦听器的Watcher
     */
    isRenderWatcher?: boolean
  ) {
    ...
    // options 都不重要,主要看一下lazy
    if (options) {
      ...
      // lazy 延迟执行,watcher要更新视图,那lazy就是是否延迟更新视图,当前是首次渲染要立即更新所以值是false,如果是计算属性的话是true,当数据发生变化之后才去更新视图
      this.lazy = !!options.lazy
      ...
    } else {
      ...
    }
    ...
    // 第二个参数如果是function就直接把变量赋值给getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // 如果是字符串的话要进一步处理,如何处理先不关注,如果是侦听器的话第二个参数传入的就是字符串
      this.getter = parsePath(expOrFn)
      ...
    }
    // 给this.value赋值,先判断this.lazy,如果当前不要求延迟执行就立即执行get方法,get方法在下面定义
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  get () {
    // 调用pushTarget,将当前的Watcher对象放入栈中
    // 每个组件对应一个Watcher,Watcher会去渲染视图,如果组件有嵌套的话会先渲染内部的组件,所以要将父组件的Watcher先保存起来,这是这个pushTarget的作用
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 最关键的一句话
      // 这句话调用了getter,getter存储的是传入的第二个参数,且是函数,首次渲染是updateComponent,所以在get方法的内部调用了updateComponent,并且改变了函数内部的this指向到Vue实例vm,并且传入了vm
      value = this.getter.call(vm, vm)
    } catch (e) {
      ...
    } finally {
      ...
    }
    return value
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

this.value = this.lazy ? undefined : this.get() 中设置断点,点击F8运行到这个位置

image

  1. 按下F11就会进入get函数,按F10到value = this.getter.call(vm, vm)

image

15.按下F11进入函数,到了updateComponent里面

image

这个函数中执行了_render_update就可以将真实DOM渲染到页面中了

image

  1. 按F10跳过,可以看到页面中数据已经渲染上去了

image

  1. 然后继续按F10回到watcher中执行完会依次回到lifeCycle > runtime/index.js > rntry-runtime-with-complier.js > core/instance/init.js > core/instance/index.js
更新时间: 2022-01-11 10:46