vue源码初调试(一)vue查找入口文件以及调试初始化

959 阅读15分钟

前言:

记录调试vue源码的方法,理解vue的核心:系统性的理解一个好的框架的产生。我自己的学习方法是,最先概括性的全局了解一下,然后针对各个知识点搜集知识逐个攻破,得到正反馈。

看完这篇笔记可以让你清楚的知道怎么调试vue 然后收藏它 当你有时间 打开vue源码开始调试它。

准备:

1.vue源码:2.6.14 github拉取源码

2.使用Rollup 打包工具 比webpack更轻便 rollup只处理js文件 在这些库中更方便 vue就是用的rollup

3.给package.json 的dev命令设置 sourcemap 方便调试源码 查看报错信息

目标:

了解怎么看源码;学会查找入口文件;熟悉vue的入口文件;vue的实例成员;vue的静态成员;

这篇文章会涉及到许多知识:我自己的建议是结合着实例去掌握自己不会不懂的知识。再针对不懂的知识进行逐个击破:

阅读源码的时候,或者是做项目的时候,会遇到很多自己不懂的知识点 或者不熟悉的知识点,或者出现问题。不过不要怕,很多问题都是已经遇到过得,有了答案,很多知识点都是前人已经总结好的,我们只需要迈出去那一小步,去看去学习去总结。一个一个的攻克会给你带来成就感的。

1阅读源码前准备

这个小标题下涉及到的知识 以及可以延伸的知识点,感兴趣的可以在主任务外自由开发分支:

1.打包工具的区别:Rollup、Webpack、Gulp、Grunt

2.打包后不同文件夹后缀的含义:xxx.js xxx.min.js xxx.runtime.js xxx.js.map

3.sourcemap的含义;runtime的含义 对应第二个知识点

4.npm script 脚本传参的含义:--sourcemap

下载好源码后:

1.安装依赖
npm i 
2.设置 sourcemap
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
3.启动项目:
npm run dev 

01.png 可以看到这个打包的js platforms\web\entry-runtime-with-compiler.js

平台相关目录下的web平台下的entry-runtime-with-compiler版本打包的jis runtime 是运行时版本(稍后解释)

4.调试案例:

随便找个案例:

examples/grid/index.html 将第八行的vue.min.js 改为vue.js 方便开发环境查看控制台的警告

02.png

5.1运行 npm run build

把vue的不同版本js 都打包出来

03.png

这里的vue.js.map 就是之前我们 npm dev 的时候 在 npm script里 添加了--sourcemap参数 是源文件的信息文件记录

dist文件里的readme.md 对vue的不同版本解释

04.png

Explanation of Build Files 编译文件说明
UMDCommonJSES Module
Fullvue.jsvue.common.jsvue.esm.js
Runtime-onlyvue.runtime.jsvue.runtime.common.jsvue.runtime.esm.js
Full (production)vue.min.js
Runtime-only (production)vue.runtime.min.js

Terms

  • Full: builds that contain both the compiler and the runtime. (包含编译器和运行时的构建。)
  • 完整版 同时包含编译器和运行时的版本
  • Compiler: code that is responsible for compiling template strings into JavaScript render functions.(负责将模板字符串编译成JavaScript渲染函数的代码。)
  • 编译器 用来将模板字符串编译成JavaScript渲染函数的代码 体积大 体积大 效率低(就是我们new Vue时 传递的template 把这个转换成render函数 我们可以传递template参数 和 render函数 )
  • Runtime: code that is responsible for creating Vue instances, rendering and patching virtual DOM, etc. Basically everything minus the compiler.(负责创建Vue实例、渲染和修补虚拟DOM等的代码。基本上除了编译器。)
  • 运行时 体积小 效率高 去除了编译器的代码
  • UMD: UMD builds can be used directly in the browser via a <script> tag. The default file from Unpkg CDN at unpkg.com/vue is the Runtime + Compiler UMD build (vue.js).(UMD构建可以通过上的默认文件是Runtime + Compiler UMD构建(vue.js)。)
  • UMD版本的通用模块版本 支持多种模块方式 vue.js 默认文件就是运行时+编译器的UMD版本
  • CommonJS: CommonJS builds are intended for use with older bundlers like browserify or webpack 1. The default file for these bundlers (pkg.main) is the Runtime only CommonJS build (vue.runtime.common.js).(CommonJS构建旨在与较老的打包工具(如browserify或webpack 1)一起使用。这些绑定器的默认文件(pkg.main)是仅运行时的CommonJS构建(vue.runtime.common.js)。)
  • CommonJS版本用来配合老的打包工具
  • ES Module: ES module builds are intended for use with modern bundlers like webpack 2 or rollup. The default file for these bundlers (pkg.module) is the Runtime only ES Module build (vue.runtime.esm.js).(ES模块构建旨在与webpack 2或rollup等现代打包工具一起使用。这些绑定器(pkg.module)的默认文件是仅运行时ES模块构建(vue.runtime.esm.js)。)
  • 从2.6开始 Vue提供两个ESM构建文件 为现代打包工具提供的版本 ESM 格式被设计为可以被静态分析 所以打包工具可以利用这一点进行 tree-shaking 并将用不到的代码排除出最终的包

Runtime + Compiler vs. Runtime-only

If you need to compile templates on the fly (e.g. passing a string to the template option, or mounting to an element using its in-DOM HTML as the template), you will need the compiler and thus the full build.

When using vue-loader or vueify, templates inside *.vue files are compiled into JavaScript at build time. You don't really need the compiler in the final bundle, and can therefore, use the runtime-only build.

Since the runtime-only builds are roughly 30% lighter-weight than their full-build counterparts, you should use it whenever you can. If you wish to use the full build instead, you need to configure an alias in your bundler.

如果你需要动态编译模板(例如传递一个字符串到模板选项,或者挂载一个元素使用它的dom内HTML作为模板),你将需要编译器,因此需要完整的构建。

当使用vue-loader或vueify时,*. .vue文件在构建时编译成JavaScript。最终包中并不需要编译器,因此可以使用仅运行时构建。

由于仅运行时构建的重量大约比完整构建的重量轻30%,所以您应该尽可能地使用它。如果希望使用完整构建,则需要在绑定器中配置别名。

5.2 完整版和运行时的区别 vue.js 和vue.runtime.js

在example文件夹里面新建一个调试文件

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Runtime+Compiler</title>
</head>
<body>
  <div id="app">
    Hello World
  </div>
  <script src="../../dist/vue.js"></script>
  <!-- <script src="../../dist/vue.runtime.js"></script> -->
  <script>
    // compiler
    // 需要编译器,把 template 转换成 render 函数
    const vm = new Vue({
      el: '#app',
      template: '<h1>{{ msg }}</h1>',
      data: {
        msg: 'Hello Vue'
      }
    })
​
    // const vm1 = new Vue({
    //   el: '#app',
    //   // template: '<h1>{{ msg }}</h1>',
    //   render(h) {
    //     return h('h1', this.msg)
    //   },
    //   data: {
    //     msg: 'Hello Vue'
    //   }
    // })
​
​
​
    // // 如果同时设置template和render此时会渲染什么?
    // const vm2 = new Vue({
    //   el: '#app',
    //   template: '<h1>Hello Template</h1>',
    //   render(h) {
    //     return h('h1', 'Hello Render')
    //   }
    // })
    
  </script>
</body>
</html>

其中 vue.js 可以使用template 因为包含编译器 可以将template 转换成render函数

<script src="../../dist/vue.js"></script>
  <script>
    // compiler
    // 需要编译器,把 template 转换成 render 函数
    const vm = new Vue({
      el: '#app',
      template: '<h1>{{ msg }}</h1>',
      data: {
        msg: 'Hello Vue'
      }
    })
</script>
​
​

vue.runtime.js 不可以使用template,可以直接使用render函数

 <script src="../../dist/vue.runtime.js"></script>
  <script>
​
     const vm = new Vue({
       el: '#app',
       // template: '<h1>{{ msg }}</h1>',
       render(h) {
         return h('h1', this.msg)
       },
       data: {
         msg: 'Hello Vue'
       }
     })
</script>
​
​

这是对有没有编译器版本的vuejs文件的测试


知识点:
1.打包工具

可以查看我之前的 webpack源码笔记 和gulp的学习笔记 以及这次出现的rollup打包工具,熟悉掌握他们的不同,最新新出现的vite打包工具 满满的都是知识 这里稍稍总结一下:

rollup:像vue 或者其它开源库使用,只处理js文件,比webpack方便快捷 即框架、组件库、生成单一umd文件的场景

webpack:前端工程化绕不开的更全面、强大的打包工具,我们做项目 工程化的时候频繁使用 即应用程序开发

gulp:基于流的操作,输入、输出的管道思想对静态资源处理,提供方法,开发人员自己对文件进行操作处理。对 静态资源密集型场景,如css、img等静态资源整合常使用

传送门:

1.webpack、rollup、gulp对比

2.webpack、gulp、rollup、tsc/babel 使用对比

2.打包后不同文件夹后缀的含义:xxx.js xxx.min.js xxx.runtime.js xxx.js.map

打包后的文件:xxx.js xxx.min.js xxx.runtime.js xxx.js.map

.js是JavaScript 打包后的源码文件

.min.js是压缩版的js文件 目的就是为了减小体积,防止窥视和窃取源代码。

.runtime.js 是vue的运行时文件

xxx.js.map 是源码打包后的存储信息位置的独立文件 runtime已经知道了 现在看一下sourcemap

sourcemap:Source map就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置

在vue里的体现就是 多出来了vue.js.map 是对应vue.js的位置信息

在 npm script的命令行里添加的 --sourcemap 在vue.js的最后一行就出现了这个

06.png 延伸一下: 在webpack这个打包工具上,用的到sourcemap的时候是 webpack.config.js 中添加 devtools参数,

传送门:devtools参数

4.npm script 脚本传参的含义:--sourcemap

所以这个 --sourcemap 也就知道怎么回事了 我们这个是学习script的用法

稍稍总结一下:

npm 允许在package.json文件里面,使用scripts字段定义脚本命令

执行npm run,就会自动新建一个 Shell,在这个 Shell 里面执行指定的脚本命令

就会先去当前目录的node_modules/.bin子目录里面去寻找, 在webpack的时候就有介绍 执行webpack cli 的时候 就是去.bin目录下最后寻找到文件夹执行了compiler.run函数

npm scripts 使用指南

npm scripts介绍

延伸一下:

npm的命令和npx的命令区别 npm npx yarn cnpm pnpm 满满的知识 可以看我之前的gulp的学习笔记有介绍

2.查找vue的入口文件

1.npm script 命令行查找script/config.js

我们在npm run dev 的时候就已经出现了一个文件夹

可以看到这个打包的js platforms\web\entry-runtime-with-compiler.js

还可以在npm script的命令行查看 "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",

scripts/config.js

07.png

08.png

2.web/entry-runtime-with-compiler.js

都可以找到这个文件夹 web/entry-runtime-with-compiler.js

09.png

这个文件夹看做了什么:

1.是web平台相关的入口

2.重写了平台相关的$mount()方法

  1. 注册了Vue.compile()方法,传递一个HTML字符串,返回render函数。

这里我们看到Vue是什么 还没有找到 继续去./runtime/index 这个文件夹寻找 就是src/platforms/web/runtime/index##### 3.src/platforms/web/runtime/index

10.png

11.png

看这个文件夹做了什么

1.web平台相关

2.注册和平台相关的全局指令 v-model v-show

3.注册和平台相关的全局组件 v-transition v-transition-group

4.全局方法: patch 把虚拟dom转换成真实dom

5.$mount 挂载方法

继续看到Vue引用在core/index

4.core/index

12.png

1.与平台无关

2.设置vue的静态方法 initGlobalAPI(VUE)

继续看到vue不在这里 在/instance/index

5.src/core/instance/index

13.png

终于找到了 Vue 在这里定义了vue构造函数 导出了vue

1.定义构造函数 调用this._init方法

2.给vue中混入了常用的实例成员 #### 3.vue的静态成员

知识点:

我们回到 刚刚在core/index里发现的initGlobalAPI 这个方法

12.png

1.跳转到 global-api/index

从这个文件夹名称可以看出 这是一个存放全局api的地方

14.png

15.png

最上面是import的 一些引入 然后是

1.初始化Vue.config对象 并禁止对config赋值 可以在里面挂载方法

2.初始化Vue.until 工具方法

3.初始化Vue的静态方法 set delete nextTick

4.初始化 Vue.observable 让对象可响应式

5.初始化Vue.options对象 并赋值 'components', 'directives', 'filters'三个成员

6.设置keep-alive组件 extend把一个对象的属性拷贝到另一个对象中来

7.注册Vue.use 用来注册插件

1.1extend
//   设置keep-alive组件   extend把一个对象的属性拷贝到另一个对象中来
// export function extend (to: Object, _from: ?Object): Object {
 //  for (const key in _from) {
 //    to[key] = _from[key]
 //  }
 //  return to
// }
1.2 initUse(Vue)

给Vue.ues赋值 参数是plugin 是一个函数或者对象

处理this

如果插件是一个对象,必须提供 install 方法

/* @flow */import { toArray } from '../util/index'export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    // 谁调用 this就是谁 这是vue这个构造函数
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    // // 判断插件有没有被注册
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }
​
    // additional parameters 把 plugin第一个参数去掉,  把this插入到第一个元素
    const args = toArray(arguments, 1) 
    args.unshift(this)//this 指向 Vue 对象,所以数组参数第一个始终是vue对象
    // 调用插件install方法和传递参数
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args) // plugin是函数的话 ,直接调用,传入参数。注意:其内this为null
    }
    installedPlugins.push(plugin)
    return this
  }
}

插件:

插件通常用来为 Vue 添加全局功能。

添加全局方法或者 property。如:vue-custom-element

添加全局资源:指令/过滤器/过渡等。如 vue-touch

通过全局混入来添加一些组件选项。如 vue-router

添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现

一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router

1.3initMixin(Vue):

注册Vue.mixin 实现混入

混入:

混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

minix:混入的含义: 公共逻辑: 比如页码

将组件的公共逻辑或者配置提取出来,哪个组件需要用到时,直接将提取的这部分混入到组件内部即可。这样既可以减少代码冗余度,也可以让后期维护起来更加容易。

mixin中的生命周期函数会和组件的生命周期函数一起合并执行。

mixin中的data数据在组件中也可以使用

mixin中的方法在组件内部可以直接调用

生命周期函数合并后执行顺序:先执行mixin中的,后执行组件的

同组件中的mixin是相互独立的!

源码:

// vue/src/core/global-api/mixin.js
export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}
// vue/src/core/util/options.js
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
​
if (child.mixins) { // 判断有没有mixin 也就是mixin里面挂mixin的情况 有的话递归进行合并
    for (let i = 0, l = child.mixins.length; i < l; i++) {
    parent = mergeOptions(parent, child.mixins[i], vm)
    }
}
  const options = {} 
  let key
  for (key in parent) {
    mergeField(key) // 先遍历parent的key 调对应的strats[XXX]方法进行合并
  }
  for (key in child) {
    if (!hasOwn(parent, key)) { // 如果parent已经处理过某个key 就不处理了
      mergeField(key) // 处理child中的key 也就parent中没有处理过的key
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key) // 根据不同类型的options调用strats中不同的方法进行合并
  }
  return options
}

上述代码的作用主要是这3点

  • 优先递归处理 mixins
  • 先遍历合并 parent 中的key,调用mergeField方法进行合并,然后保存在变量options
  • 再遍历 child,合并补上 parent 中没有的key,调用mergeField方法进行合并,保存在变量options

其实核心在于strats中对应的不同类型的处理方法,我们接下来分为几种类型来看下对应的合并策略

在我们调用Vue.mixin的时候会通过mergeOptions方法将全局基础options(component', 'directive', 'filter)进行合并在mergeOptions内部优先进行mixins的递归合并,然后先父再子调用mergeField进行合并,不同的类型走不同的合并策略 它不是简单的把属性从一个对象里复制到另外一个对象里,而是根据被合并的不同的选项有着不同的合并策略。这就是设计模式中非常典型的策略模式

替换型策略有props、methods、inject、computed, 就是将新的同名参数替代旧的参数

合并型策略是data, 通过set方法进行合并和重新赋值

队列型策略有生命周期函数和watch,原理是将函数存入一个数组,然后正序遍历依次执行

叠加型有component、directives、filters,将回调通过原理链联系在一起

mixin的优缺点

优点

  • 提高代码复用性
  • 无需传递状态
  • 维护方便,只需要修改一个地方即可

缺点

  • 命名冲突
  • 滥用的话后期很难维护
  • 不好追溯源,排查问题稍显麻烦
  • 不能轻易的重复代码
var mixin = {
  created: function () {
    console.log('混入对象的钩子被调用')
  }
}
​
new Vue({
  mixins: [mixin],
  created: function () {
    console.log('组件钩子被调用')
  }
})
​
// => "混入对象的钩子被调用"
// => "组件钩子被调用"
1.4.initExtend(Vue)

基于传入的option 返回一个组件的构造函数

Vue.extend是 Vue 构造函数的一个静态方法,它提供了一种灵活的挂载组件的方式,它在日常开发中使用不多,但是在一些特殊场景会派上用场。在 ElementUI 里,我们使用this.$message('hello')的时候,其实就是通过这种方式创建一个组件实例,然后再将这个组件挂载到了 body 上。

  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid //SuperId就保存Vue中的唯一标识(每个实例都有自己唯一的cid)
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }
​
    const name = extendOptions.name || Super.options.name //name变量来保存组件的名字
    if (process.env.NODE_ENV !== 'production' && name) {
      validateComponentName(name)
    }
    
    const Sub = function VueComponent (options) {
        // 调用init初始化
      this._init(options)
    }
    // 原型继承Vue
    //创建一个子类Sub,这里我们通过继承,使Sub拥有了Vue的能力,并且添加了唯一id(每个组件的唯一标识符)
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    //这里调用了mergeOptions函数实现了父类选项与子类选项的合并,并且子类的super属性指向了父类。
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    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. 初始化了props和computed.
    if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }
​
    // allow further extension/mixin/plugin usage 将父类的方法复制到子类 
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use
​
    // create asset registers, so extended classes
    // can have their private assets too.
    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 将父类的id保存在子类的属性上,属性值为子类,在之前会进行判断如果构造过子类,就直接将父类保存过的id值给返回了,就是子类本身不需要重新初始化,,作为一个缓存策略。
    cachedCtors[SuperId] = Sub
    return Sub
  }

建一个类来继承了父级,顶级一定是Vue.这个类就表示一个组件,我们可以通过new的方式来创建。学习了extend我们就很容易的实现一个编程式组件。

案例:

其实这个初始化和平时的new Vue()是一样的,毕竟两个执行的同一个方法。但是在实际的使用中,我们可能还需要给组件传 props,slots 以及绑定事件.

<template>
  <div class="message-box">
    {{ message }}
  </div>
</template><script>
  export default {
    props: {
      message: {
        type: String,
        default: ''
      }
    }
  }
</script>
const MessageBoxCtor = Vue.extend(MessageBox)
new MessageBoxCtor({
  propsData: {
    message: 'hello'
  }
}).$mount('#target')

为什么使用propsData

if (opts.props) initProps(vm, opts.props)
function initProps(vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = (vm._props = {})
  // ...省略其他逻辑
}

这里的 propsData 就是数据源,他会从vm.$options.propsData上取,上文说过在执行_init的时候new MessageBoxCtor(options)options会被合并和vm.$options上,所以我们就可以在options中传入propsData属性,使得initProps()能取到这个值,从而进行props的初始化。绑定事件:

const MessageBoxCtor = Vue.extend(MessageBox)
const messageBoxInstance = new MessageBoxCtor({
  propsData: {
    message: 'hello'
  }
}).$mount('#target')
messageBoxInstance.$on('some-event', () => {
  console.log('success')
})

使用插槽;

<template>
  <div class="message-box">
    {{ message }}
    <slot name="footer"/>
  </div>
</template><script>
  export default {
    props: {
      message: {
        type: String,
        default: ''
      }
    }
  }
</script>
const MessageBoxCtor = Vue.extend(MessageBox)
const messageBoxInstance = new MessageBoxCtor({
  propsData: {
    message: 'hello'
  }
})
const h = this.$createElement
messageBoxInstance.$scopedSlots = {
  footer: function() {
    return [h('div', 'slot-content')]
  }
}
messageBoxInstance.$mount('#target')

这里需要注意的是$mount一定要在设置完$scopedSlots之后,因为$mount中会执行渲染函数,我们要保证在执行渲染函数时能获取到$scopedSlots

如果你想使用作用域插槽,也很简单,和普通插槽是一样的,只需要在函数中接收参数就可以了:

<slot name="head" :message="message"></slot>
复制代码
messageBoxInstance.$scopedSlots = {
  footer: function(slotData) {
    return [h('div', slotData.message)]
  }
}
复制代码

这样就可以成功渲染出message了。

1.5 initAssetRegisters(Vue)

注册Vue.component(), Vue.directive(), Vue.filter()

/* @flow */import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'export function initAssetRegisters (Vue: GlobalAPI) {
  /**
   * Create asset registration methods.
   */
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          definition = this.options._base.extend(definition)
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

'shared/constants

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

这里的 vue的静态成员就介绍完了#### 4.vue的实例成员

4.1src/core/instance/index

13.png

注册vm的_init方法 初始化vm

initMixin(Vue)

注册vm的data/data/props/set/set/delete/$watch/

stateMixin(Vue)

初始化事件相关方法 on/on/off/$emit

eventsMixin(Vue)

初始化生命周期相关的混入方法 _update forceUpdateforceUpdate destroy

lifecycleMixin(Vue)

混入render $nextTick _render

renderMixin(Vue)

4.2_initMixin(Vue) _init()

16.png

    // 给Vue类的原型上绑定_init方法
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++
​
    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }
​
    // a flag to avoid this being observed
    vm._isVue = true
    // merge options 合并options  用户传入的和vue构造函数的options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else 设置渲染时代理对象  _renderProxy */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
        // 渲染过程中 会使用
      vm._renderProxy = vm
    }
    // expose real self 调用一些初始化函数来为Vue实例初始化一些属性,事件,响应式数据等
    vm._self = vm
    initLifecycle(vm) // 初始化生命周期
    initEvents(vm) // 初始化事件 vm的事件监听初始化 父组件绑定在当前组件上的事件 
    initRender(vm)   // 初始化渲染
    callHook(vm, 'beforeCreate') // 调用生命周期钩子函数
    initInjections(vm) // resolve injections before data/props  //初始化injections
    initState(vm)  // 初始化props,methods,data,computed,watch
    initProvide(vm) // resolve provide after data/props // 初始化 provide
    callHook(vm, 'created') // 调用生命周期钩子函数
​
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }
   // 如果传入了则调用$mount函数进入模板编译与挂载阶段, 没有传入 需要用户手动执行vm.$mount方法才进入下一个生命周期阶段。
​
   // 创建构造器
  /* var Profile = Vue.extend({
    template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
    data: function() {
      return {
        firstName: 'Walter',
        lastName: 'White',
        alias: 'Heisenberg'
      }
    }
  })
  // 创建 Profile 实例,并挂载到一个元素上。   new Profile()就会调用_init方法
  new Profile().$mount('#mount-point')
  也可以这样子 new Profile({ el: '#mount-point' })
  
  */
​
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

1.vm赋值 const vm = this

2.合并用户传入的和vue构造函数的options

2.1 mergeOptions ( parent: Object,child: Object, vm?: Component): Object {} src/core/util/options.js

3.调用一些初始化函数来为Vue实例初始化一些属性,事件,响应式数据等

4.2.1 initLifecycle(vm)

// 初始化生命周期

Vue实例上挂载了一些属性并设置了默认值 找到当前组件的根组件$root 抽象组件实例中一定有个属性abstract:true 这就是一个自上到下将根实例的$root属性依次传递给每一个子实例的过程。

4.2.2 initEvents(vm)

// 初始化事件 初始化实例的事件系统。

父组件给子组件的注册事件中,把自定义事件传给子组件,在子组件实例化的时候进行初始化;而浏览器原生事件是在父组件中处理。

实例初始化阶段调用的初始化事件函数initEvents实际上初始化的是父组件在模板中使用v-on或@注册的监听子组件内触发的事件

4.2.3 initRender(vm)

// 初始化渲染

初始化插槽信息$slots以及初始化$createElement方法,

使用defineReactive方法让$attrs$listeners响应式

整个过程其实就是解析了组件的 options 配置项与父组件的绑定参数,并对插槽和数据域插槽进行不同处理,最后给组件添加 _createElement 的事件指向绑定,并响应式处理两个组件内部没有直接定义的参数/事件。

4.2.4 callHook(vm, 'beforeCreate')

// 调用生命周期钩子函数

4.2.5 initInjections(vm)

// resolve injections before data/props //初始化injections

inject选项,那必然离不开provide选项,这两个选项都是成对出现的,它们的作用是:允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。并且

provide 选项应该是一个对象或返回一个对象的函数

4.2.6initState(vm)

// 初始化props,methods,data,computed,watch这个顺序也就知道了为什么data中可以使用props,在watch中可以观察dataprops,之所以可以这样做,就是因为在初始化的时候遵循了这种顺序,先初始化props,接着初始化data,最后初始化watch

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

initProps

function initProps (vm: Component, propsOptions: Object) {
    //父组件传入的真实props数据。
  const propsData = vm.$options.propsData || {}
  //指向vm._props的指针,所有设置到props变量中的属性都会保存到vm._props中
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  //指向vm.$options._propKeys的指针,缓存props对象中的key,将来更新props时只需遍历vm.$options._propKeys数组即可得到所有props的key
  const keys = vm.$options._propKeys = []
  //   当前组件是否为根组件。
  const isRoot = !vm.$parent
  // root instance props should be converted  判断当前组件是否为根组件,如果不是,那么不需要将props数组转换为响应式的,toggleObserving(false)用来控制是否将数据转换成响应式
  if (!isRoot) {
    toggleObserving(false)
  }
  // 遍历props选项拿到每一对键值,先将键名添加到keys中,然后调用validateProp函数(关于该函数下面会介绍)
//校验父组件传入的props数据类型是否匹配并获取到传入的值value,然后将键和值通过defineReactive函数添加到props(即vm._props)中
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () => {
        if (vm.$parent && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      defineReactive(props, key, value)
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    //断这个key在当前实例vm中是否存在,如果不存在,则调用proxy函数在vm上设置一个以key为属性的代码,当使用vm[key]访问数据时,其实访问的是vm._props[key]
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)

validateProp (key,propOptions,propsData,vm)

validateProp函数的定义位于源码的src/core/util/props.js

key:遍历propOptions时拿到的每个属性名。

propOptions:当前实例规范化后的props选项。

propsData:父组件传入的真实props数据。

vm:当前实例

const prop = propOptions[key]
const absent = !hasOwn(propsData, key)
let value = propsData[key]
​

prop:当前keypropOptions中对应的值。

absent:当前key是否在propsData中存在,即父组件是否传入了该属性。

value:当前keypropsData中对应的值,即父组件对于该属性传入的真实值

判断proptype属性是否是布尔类型(Boolean),getTypeIndex函数用于判断proptype属性中是否存在某种类型,如果存在,则返回该类型在type属性中的索引(因为type属性可以是数组),如果不存在则返回-1。

assertProp

assertProp (prop,name,value,vm,absent)

是校验父组件传来的真实值是否与proptype类型相匹配,如果不匹配则在非生产环境下抛出警告。

initmethods

判断method有没有?method的命名符不符合命名规范?如果method既有又符合规范那就把它挂载到vm实例上。

function initMethods (vm, methods) {
  const props = vm.$options.props
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      if (methods[key] == null) {
        warn(
          `Method "${key}" has an undefined value 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
        )
      }
        //判断如果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[key] = methods[key] == null ? noop : bind(methods[key], vm)
  }
}

initData

通过一系列条件判断用户传入的data选项是否合法,最后将data转换成响应式并绑定到实例vm

// 获取到用户传入的data选项,赋给变量data,同时将变量data作为指针指向vm._data
function initData (vm: Component) {
    // 获取到用户传入的data选项,赋给变量data,同时将变量data作为指针指向vm._data
  let data = vm.$options.data
//   判断data是不是一个函数,如果是就调用getData函数获取其返回值,将其保存到vm._data中
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
    // 无论传入的data选项是不是一个函数,它最终的值都应该是一个对象,如果不是对象的话,就抛出警告:提示用户data应该是一个对象
  if (!isPlainObject(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对象中的每一项,在非生产环境下判断data对象中是否存在某一项的key与methods中某个属性名重复,
// 如果存在重复,就抛出警告:提示用户属性名重复。
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  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
        )
      }
    }
    // 再判断是否存在某一项的key与prop中某个属性名重复,如果存在重复,就抛出警告:提示用户属性名重复
    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
      )
    //   调用proxy函数将data对象中key不以_或$开头的属性代理到实例vm上,这样,我们就可以通过this.xxx来访问data选项中的xxx数据了
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data 调用observe函数将data中的数据转化成响应式
  observe(data, true /* asRootData */)
}

initComputed

initWatch

以上两个参考: Vue源码系列-Vue中文社区###### 4.2.7 initProvide(vm)

// resolve provide after data/props // 初始化 provide

在调用initInjections函数对inject初始化完之后需要先调用initState函数对数据进行初始化,最后再调用initProvide函数对provide进行初始化。

callHook(vm, 'created') // 调用生命周期钩子函数

provideinject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

4.判断是否有el属性,如果传入了则调用mount函数进入模板编译与挂载阶段,没有传入需要用户手动执行vm.mount函数进入模板编译与挂载阶段, 没有传入 需要用户手动执行vm.mount方法才进入下一个生命周期阶段。

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
    
    
       // 如果传入了则调用$mount函数进入模板编译与挂载阶段, 没有传入 需要用户手动执行vm.$mount方法才进入下一个生命周期阶段。
​
   // 创建构造器
  /* var Profile = Vue.extend({
    template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
    data: function() {
      return {
        firstName: 'Walter',
        lastName: 'White',
        alias: 'Heisenberg'
      }
    }
  })
  // 创建 Profile 实例,并挂载到一个元素上。   new Profile()就会调用_init方法
  new Profile().$mount('#mount-point')
 new Profile({ el: '#mount-point' })  //也可以这样子 
  
  */

剩下这几个就不细说了 和initMixin(Vue)一样的查看方法 不懂的就要谷歌。

// 注册vm的data/data/props/set/set/delete/$watch/

stateMixin(Vue)

// 初始化事件相关方法 on/on/off/$emit

eventsMixin(Vue)

// 初始化生命周期相关的混入方法

// _update forceUpdateforceUpdate destroy

lifecycleMixin(Vue)

// 混入render $nextTick _render

renderMixin(Vue)

5.调试vue初始化

主要调试四个导出Vue的文件

1.准备文件:

在example下新建一个文件夹 example/initVue/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>initVue</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>
2.打断点

四个断点分别是:

1.src/core/index : initGlobalAPI(Vue)

17.png

  1. src/core/instance/index.js initMixin(Vue)

18.png

3.src/platforms/web/entry-runtime-with-compiler.js const mount = Vue.prototype.$mount

19.png

4.src/platforms/web/runtime/index.js Vue.config.mustUseProp = mustUseProp

20.png

  1. 监视Vue构造函数变化 在控制台如上图监视那里添加Vue对象
3.调试

21.png

断点打好之后,刷新进行调试 首先是 // 注册vm的_init方法 初始化vm initMixin(Vue)

点击F10 进行下一个函数调用后 可以看到 Vue的原型上出现了_init函数

然后继续点击 可以看到vue原型继续注册一些方法和属性**
$delete**

$destroy

$emit

$forceUpdate

$off

$on

$once

$set

$watch

_init

_update

$data 属性

$props 属性

22.png

继续点击F10 出现字母开头的函数

23.png

  1. 继续点击F10 到下一个断点 initGlobalAPI(Vue) 点过去 可以看到 vue实例增加了许多方法

24.png

**util**

**use**

**set**

**options**

**observable**

**nextTick**

**mixin**

**filter**

**extend**

**directive**

**delete**

**component**

2. 然后是和平台相关的代码

注册了 组件 和一些指令

25.png

以及原型中的 $mount

patch

最后一个断点 可以看到是重写了$mount方法 最后又挂载了compile方法

之后断点执行完毕

这一次的调试到这里 之后的会继续学习完成之后 更新