Vue源码解析-响应式原理
- 课程目标
- 源码目录结构
- 准备工作-调试
- 准备工作-Vue的不同构建版本
- 完整版Vue举例
- 小结
- 寻找入口文件
- 执行构建
- script/config.js文件执行过程
- 从入口开始
- 阅读源码记录
- Vue初始化过程
- 小结-四个导出Vue的模块
- Vue初始化-静态成员
- src\core\index.js中,initGlobalAPI(Vue)详解
- initUse(Vue) 注册Vue.use() 用来注册插件
- initMixin(Vue) 注册Vue.mixin 实现混入
- initExtend 注册Vue.extend 基于传入的options返回一个组件的构造函数
- initAssetRegisters 注册Vue.directive(),Vue.component(),Vue.filter()
- Vue初始化-实例成员
- initMixin(Vue) 注册Vue.prototype._init()方法
- 实例成员--initState
- stateMixin(Vue)
- eventsMixin(Vue)
- lifecycleMixin(Vue) 混入了生命周期相关的方法
- renderMixin(Vue)
- 调试Vue初始化过程
- 首次渲染过程
课程目标
- Vue.js的静态成员和实例成员的初始化过程
- 初次渲染过程
- 数据响应式原理
源码目录结构
准备工作-调试
调试设置
-
打包
- 打包工具 Rollup
- Vue.js 源码的打包工具使用的是 Rollup,比 webpack 轻量
- webpack 会把所有文件当做模块,Rollup 只处理 js 文件,更适合在 Vue.js 这样的库中使用(开发项目使用 webpack,开发库使用 Rollup)
- Rollup打包不会生成冗余的代码
- 打包工具 Rollup
-
安装依赖
npm i
- 设置sourcemap
- package.json 文件的 dev 脚本中添加参数 --sourcemap
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
- 执行dev
- npm run dev 执行打包,用的是rollup, -w 参数是监听文件的变化,文件变化自动重新打包; -c是执行的配置文件
- 结果
- 以vue源码中examples中的grids为例进行调试
- 在进行代码调试的时候,如果没有开启sourcemap,则不会生成src目录(实际上是dist中的map文件指向src),断点不会进入src(源码),而直接进入打包后(dist)目录中压缩的vue.js文件,因为是压缩,编译后的代码,不方便调试,而我们希望断点直接进入src目录下的源码中调试,所以需要设置sourcemap
准备工作-Vue的不同构建版本
- npm run build 重新打包所有文件
- 官方文档-对不同构建版本的解释
- 完整版:同时包含编译器和运行时的版本。
- 编译器:用来将模板字符串编译成为 JavaScript 渲染函数的代码。=>将template装换成render函数
- 运行时:用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码。基本上就是除去编译器的其它一切。
完整版Vue举例
<div id="app"></div>
<!-- 完整版 -->
<script src="../../dist/vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
template: "<h1>{{msg}}</h1>",
data: {
msg: "Hello Vue",
},
});
</script>
- 结果:可以正常显示
<div id="app"></div>
<!-- 运行时版本 -->
<script src="../../dist/vue.runtime.js"></script>
<script>
const vm = new Vue({
el: "#app",
template: "<h1>{{msg}}</h1>",
data: {
msg: "Hello Vue",
},
});
</script>
- 结果:报错
- 使用的是Vue的仅运行时版本,其中模板编译器不可用。方法一:将模板手动预编译为render函数,方法二:使用带编译器的vue版本
- 方法二就是上面的引用完整版本的vue,下面介绍方法一
- 方法一,将template模板字符串转换成render函数
<div id="app"></div>
<!-- 运行时版本 -->
<script src="../../dist/vue.runtime.js"></script>
<script>
const vm = new Vue({
el: "#app",
render(h) {
return h("h1", this.msg);
},
data: {
msg: "Hello Vue",
},
});
- 结果:能正常显示
小结
vue-cli默认引用的是运行时版本(不带编译器),并且是ESModule=>vue.runtime.esm.js
- 因为vue-cli对webpack做了一个深度的封装,我们在vue-cli创建的项目中,看不到引入vue的版本,但vue-cli提供的一个命令行工具,通过这个工具可以查看webpack的配置
- 命令行输入vue inspect
- webpack配置项在命令行中显示查看起来不太友好,直接输出到文件中(>代表把前面命令生成的结果输入到指定文件中)
vue inspect > output.js
- 我们在开发项目的时候,会有很多单文件组件(.vue文件),这些单文件组件浏览器是不支持的,所以在打包的时候我们会将这些单文件组件转换成js对象,在转换js对象的过程中,还会将template模板转换成render函数(vue-loader来实现),所以单文件组件的运行是不需要编译器的
寻找入口文件
- 查看dist/vue.js的构建过程
执行构建
npm run build
"build": "node scripts/build.js",
通过node 生成所有版本的vue
npm run dev
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
rollup:通过rollup构建工具生成单一的vue版本
- environment:设置环境变量TARGET:web-full-dev
- script/config.js 的执行过程
- 作用:生成rollup构建的配置文件
- 使用环境变量TARGET:web-full-dev
script/config.js文件执行过程
- 以npm run dev 为 例
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
const path = require('path')
const buble = require('rollup-plugin-buble')
const alias = require('rollup-plugin-alias')
const cjs = require('rollup-plugin-commonjs')
const replace = require('rollup-plugin-replace')
const node = require('rollup-plugin-node-resolve')
const flow = require('rollup-plugin-flow-no-whitespace')
const version = process.env.VERSION || require('../package.json').version
const weexVersion = process.env.WEEX_VERSION || require('../packages/weex-vue-framework/package.json').version
const featureFlags = require('./feature-flags')
const banner =
'/*!\n' +
` * Vue.js v${version}\n` +
` * (c) 2014-${new Date().getFullYear()} Evan You\n` +
' * Released under the MIT License.\n' +
' */'
const weexFactoryPlugin = {
intro () {
return 'module.exports = function weexFactory (exports, document) {'
},
outro () {
return '}'
}
}
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)
}
}
const builds = {
// Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
'web-runtime-cjs-dev': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.common.dev.js'),
format: 'cjs',
env: 'development',
banner
},
'web-runtime-cjs-prod': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.common.prod.js'),
format: 'cjs',
env: 'production',
banner
},
// Runtime+compiler CommonJS build (CommonJS)
'web-full-cjs-dev': {
// resolve函数将相对路径转换成绝对路径(web是别名)
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.common.dev.js'),
format: 'cjs',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
'web-full-cjs-prod': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.common.prod.js'),
format: 'cjs',
env: 'production',
alias: { he: './entity-decoder' },
banner
},
// Runtime only ES modules build (for bundlers)
'web-runtime-esm': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.esm.js'),
format: 'es',
banner
},
// Runtime+compiler ES modules build (for bundlers)
'web-full-esm': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.esm.js'),
format: 'es',
alias: { he: './entity-decoder' },
banner
},
// Runtime+compiler ES modules build (for direct import in browser)
'web-full-esm-browser-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.esm.browser.js'),
format: 'es',
transpile: false,
env: 'development',
alias: { he: './entity-decoder' },
banner
},
// Runtime+compiler ES modules build (for direct import in browser)
'web-full-esm-browser-prod': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.esm.browser.min.js'),
format: 'es',
transpile: false,
env: 'production',
alias: { he: './entity-decoder' },
banner
},
// runtime-only build (Browser)
'web-runtime-dev': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.js'),
format: 'umd',
env: 'development',
banner
},
// runtime-only production build (Browser)
'web-runtime-prod': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.min.js'),
format: 'umd',
env: 'production',
banner
},
// Runtime+compiler development build (Browser)
'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 // 文件头
},
// Runtime+compiler production build (Browser)
'web-full-prod': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.min.js'),
format: 'umd',
env: 'production',
alias: { he: './entity-decoder' },
banner
},
// Web compiler (CommonJS).
'web-compiler': {
entry: resolve('web/entry-compiler.js'),
dest: resolve('packages/vue-template-compiler/build.js'),
format: 'cjs',
external: Object.keys(require('../packages/vue-template-compiler/package.json').dependencies)
},
// Web compiler (UMD for in-browser use).
'web-compiler-browser': {
entry: resolve('web/entry-compiler.js'),
dest: resolve('packages/vue-template-compiler/browser.js'),
format: 'umd',
env: 'development',
moduleName: 'VueTemplateCompiler',
plugins: [node(), cjs()]
},
// Web server renderer (CommonJS).
'web-server-renderer-dev': {
entry: resolve('web/entry-server-renderer.js'),
dest: resolve('packages/vue-server-renderer/build.dev.js'),
format: 'cjs',
env: 'development',
external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
},
'web-server-renderer-prod': {
entry: resolve('web/entry-server-renderer.js'),
dest: resolve('packages/vue-server-renderer/build.prod.js'),
format: 'cjs',
env: 'production',
external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
},
'web-server-renderer-basic': {
entry: resolve('web/entry-server-basic-renderer.js'),
dest: resolve('packages/vue-server-renderer/basic.js'),
format: 'umd',
env: 'development',
moduleName: 'renderVueComponentToString',
plugins: [node(), cjs()]
},
'web-server-renderer-webpack-server-plugin': {
entry: resolve('server/webpack-plugin/server.js'),
dest: resolve('packages/vue-server-renderer/server-plugin.js'),
format: 'cjs',
external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
},
'web-server-renderer-webpack-client-plugin': {
entry: resolve('server/webpack-plugin/client.js'),
dest: resolve('packages/vue-server-renderer/client-plugin.js'),
format: 'cjs',
external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
},
// Weex runtime factory
'weex-factory': {
weex: true,
entry: resolve('weex/entry-runtime-factory.js'),
dest: resolve('packages/weex-vue-framework/factory.js'),
format: 'cjs',
plugins: [weexFactoryPlugin]
},
// Weex runtime framework (CommonJS).
'weex-framework': {
weex: true,
entry: resolve('weex/entry-framework.js'),
dest: resolve('packages/weex-vue-framework/index.js'),
format: 'cjs'
},
// Weex compiler (CommonJS). Used by Weex's Webpack loader.
'weex-compiler': {
weex: true,
entry: resolve('weex/entry-compiler.js'),
dest: resolve('packages/weex-template-compiler/build.js'),
format: 'cjs',
external: Object.keys(require('../packages/weex-template-compiler/package.json').dependencies)
}
}
function genConfig (name) {
// builds是一个对象,包含各种环境变量对应的配置项,name是环境变量的值
/*
builds:{
// Runtime+compiler development build (Browser)
'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 // 文件头
}
}
*/
// options是基础信息
const opts = builds[name]
// config是rollup的完整配置信息
const config = {
input: opts.entry, // 入口文件 src\platforms\web\entry-runtime-with-compiler.js
external: opts.external,
plugins: [
flow(),
alias(Object.assign({}, aliases, opts.alias))
].concat(opts.plugins || []),
output: {
file: opts.dest,
format: opts.format,
banner: opts.banner,
name: opts.moduleName || 'Vue'
},
onwarn: (msg, warn) => {
if (!/Circular/.test(msg)) {
warn(msg)
}
}
}
// built-in vars
const vars = {
__WEEX__: !!opts.weex,
__WEEX_VERSION__: weexVersion,
__VERSION__: version
}
// feature flags
Object.keys(featureFlags).forEach(key => {
vars[`process.env.${key}`] = featureFlags[key]
})
// build-specific env
if (opts.env) {
vars['process.env.NODE_ENV'] = JSON.stringify(opts.env)
}
config.plugins.push(replace(vars))
if (opts.transpile !== false) {
config.plugins.push(buble())
}
Object.defineProperty(config, '_name', {
enumerable: false,
value: name
})
// 返回配置项
return config
}
// 判断是否有TARGET环境变量
if (process.env.TARGET) {
// 有环境变量TARGET:web-full-dev
module.exports = genConfig(process.env.TARGET)
} else {
exports.getBuild = genConfig
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}
- 入口文件是:src\platforms\web\entry-runtime-with-compiler.js
从入口开始
- src\platforms\web\entry-runtime-with-compiler.js
- 通过查看源码解决下面问题=>我们知道编译器会将template模板编译成render函数,当同时存在render函数和template模板的时候,源码中怎么运行?
- 此处的$mount是谁调用的呢?什么位置调用的呢?
阅读源码记录
- el不能是body或者html标签
- 如果没有render,把template转换成render函数
- 如果有render函数,直接调用mount挂载dom
// 1:el不能是body或者html标签
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
if(!options.render){
// 2:把template/el转换成render函数
...
}
// 3:调用mount方法,挂载DOM
- 调试代码
<div id="app"></div>
<!-- 完整版本 -->
<script src="../../dist/vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
template: "<h3>Hello template</h3>",
render(h) {
return h("h3", "Hello render");
},
data: {
msg: "Hello Vue",
},
});
</script>
- $mount <= Vue._init(Vue初始化) <= Vue (Vue构造函数) <= Vue实例化
Vue初始化过程
-
完整版vue在入口文件src\platforms\web\entry-runtime-with-compiler.js中实际上相对于运行时vue版本针对Vue构造函数做了两件事
- 增强Vue构造函数的原型对象$mount方法的功能,如果没有render函数,将模板(template)转换成render函数
- Vue构造函数绑定了compile静态方法=>在$mount方法中将模板template字符串转换成render函数时调用
-
在src\platforms\web\runtime\index.js中的Vu我们做了三件事件
- 给Vue构造函数注册了平台相关的通用方法=>Vue.config.isReservedTag = isReservedTag
- 注册了与平台相关的全局指令和组件(Vue全局的执行和组件分别保存在Vue.options.directives和Vue.optiosns.componnets中)
- Vue构造函数的与原型对象上挂载_patch和$mount方法
- _patch方法:将Vnode转换成对应的真实DOM
- $mount 将差异渲染到页面上
-
src\core\index.js中给Vue构造函数添加静态属性和方法
-
静态属性 Vue.config/Vue.options
-
讲台方法 Vue.delete/Vue.set/Vue.nextTick/Vue.observable
小结-四个导出Vue的模块
-
src\platforms\web\entry-runtime-with-compiler.js
- web平台相关入口
- 重写了平台相关的$mount方法
- 注册了Vue.compile()方法,传递一个HTML字符串,返回render函数
-
src\platforms\web\runtime\index.js
- web平台相关
- 注册和平台相关的全局指令:v-model,v-show
- 注册和平台相关的全局组件:v-transition,v-transition-group
- 全局方法
- patch:把虚拟DOM转换成真实DOM
- $mount:挂载方法
-
src\core\index.js
- 与平台无关
- 定义了Vue的静态属性和方法,initGlobalAPI(Vue)
-
src\core\instance\index.js
- 与平台无关
- 定义了构造函数,调用了this._init(options)方法
- 定义了Vue实例的属性和方法
Vue初始化-静态成员
src\core\index.js中,initGlobalAPI(Vue)详解
/* @flow */
import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'
import { observe } from 'core/observer/index'
import {
warn,
extend,
nextTick,
mergeOptions,
defineReactive
} from '../util/index'
export function initGlobalAPI (Vue: GlobalAPI) {
// config
// Vue的Config静态属性
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
Object.defineProperty(Vue, 'config', configDef)
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
// 这些工具方法不视作全局API的一部分,除非你已经意识到某些风险,否则不要去依赖他们(只在Vue内部使用它们,我们可以忽略该代码)
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
// 静态方法set/delete/nextTick
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// observe:让一个对象可响应
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
// 初始化Vue.options对象,并扩展该对象(添加components,directives,filters等属性)
Vue.options = Object.create(null)
// ASSET_TYPES=['component','directive','filter'],
// 分别存储全局的组件/指令/过滤器,即通过Vue.component(),Vue.directive(),Vue.filter()注册的全局组件,全局指令和全局过滤器,都会存储到对应的Vue.options属性上来
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue
/*
export function extend (to: Object, _from: ?Object): Object {
for (const key in _from) {
to[key] = _from[key]
}
return to
}
*/
//将一个对象的成员拷贝给另一个对象(浅拷贝)
// 注册全局的keep-alive组件
extend(Vue.options.components, builtInComponents)
// 注册Vue.use() 用来注册插件
initUse(Vue)
// 注册Vue.mixin 实现混入
initMixin(Vue)
// 注册Vue.extend 基于传入的options返回一个组件的构造函数
initExtend(Vue)
// 注册Vue.directive(),Vue.component(),Vue.filter()
// 因为三个方法的参数是一样的,所以一起注册的
initAssetRegisters(Vue)
}
- 注册全局的组件和指令,就是将组件和指令模块放到对应的Vue.options选项中
// 注册全局的keep-alive组件
extend(Vue.options.components, builtInComponents)
initUse(Vue) 注册Vue.use() 用来注册插件
/* @flow */
import { toArray } from '../util/index'
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
// installedPlugins:我们所安装的插件,this指向Vue构造函数
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
// 如果传入的插件已经存在,直接返回
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters
// 注册插件
// 当Vue.use()传递多个参数的时候(第一个参数为插件)
const args = toArray(arguments, 1)
// 将Vue构造函数塞到args的第一个元素中,所以插件调用install方法时第一个参数就是Vue构造函数
args.unshift(this)
if (typeof plugin.install === 'function') {
//如果plugin是对象,调用对象的install方法
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
// 将注册的组件插入到installedPlugins数组中,防止重复注册
installedPlugins.push(plugin)
return this
}
}
initMixin(Vue) 注册Vue.mixin 实现混入
/* @flow */
import { mergeOptions } from '../util/index'
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
// 将用户传入的options和Vue构造函数自带的options合并(合并策略比较复杂,之后再看)
this.options = mergeOptions(this.options, mixin)
return this
}
}
initExtend 注册Vue.extend 基于传入的options返回一个组件的构造函数
/* @flow */
export function initExtend (Vue: GlobalAPI) {
Vue.extend = function (extendOptions: Object): Function {
const Super = this
const Sub = function VueComponent (options) {
// 子构造器有VueComponent静态方法
this._init(options)
}
// 子构造器集成自Vue构造函数
Sub.prototype = Object.create(Super.prototype)
return Sub
}
}
initAssetRegisters 注册Vue.directive(),Vue.component(),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 => {
// ASSET_TYPES=['component','directive','filter'],
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
// 如果没有传递第二个参数,代表获取响应的组件,指令和过滤器
return this.options[type + 's'][id]
} else {
if (type === 'component') {
definition = this.options._base.extend(definition)
}
if (type === 'directive') {
// 如果传入的是函数
definition = { bind: definition, update: definition }
}
// 将全局注册的组件/指令/过滤器存储到Vue.options对应的属性中
this.options[type + 's'][id] = definition
return definition
}
}
})
}
Vue初始化-实例成员
- src\core\instance\index.js
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'
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
// Vue必须是构造函数,不能作为普通函数调用
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
// 下面的方法是给Vue实例上混入成员
// 设置Vue实例的成员(定于Vue原型的对象的属性和方法)
// 注册vm的_init()方法,初始化vm
initMixin(Vue)
// 注册vm的$data/$props/$set/$delete/$watch
stateMixin(Vue)
// 初始化事件相关方法 $on/$emit/$once/$off
eventsMixin(Vue)
// _update/$forceUpdate/$destory
lifecycleMixin(Vue)
// _render/$nextTick
renderMixin(Vue)
export default Vue
- 定义了Vue的构造函数,在构造函数下面定义了几个以Minix结尾的方法,参数都是Vue的构造函数,目的是给Vue原型上混入相应的成员,也就是在Vue实例上增加了响应的成员
initMixin(Vue) 注册Vue.prototype._init()方法
- 给Vue原型上添加了_init()方法,该方法在Vue构造函数中调用,相当于整个Vue的入口,所有的事情都是从它内部开始的
定义了几个私有成员
- vm = this;
- vm._uid = uid++ vm的唯一标识
- vm.isVue = true 如果当前的实例是Vue实例,不需要被observe处理(响应式)
- vm._renderProxy = vm 设置渲染时的代理对象,当我们在渲染的时候会看到该对象的使用
- vm._self = vm
定义了实例的$options
=>将Vue构造函数的options和用户new Vue时传入的options及vm自身的options进行合并
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
vm实例属性和方法的初始化 (模块化)
其中initInjections(vm)和initProvide(vm)两个函数共同实现依赖注入
实例成员–initState
nitState帮我们初始化了vm.$options中的props,methods,data,computed和watch,并且将它们的成员都注入到vm实例中
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
// 判断vm.$options中=是否有data,props,computed,watch,methods选项,如果有响应的选项,init(初始化)该选项
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
// 如果有data选项
initData(vm)
} else {
// 没有data选项,observe把某个对象转换成响应式的对象
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
- initProps(vm, opts.props) 将vm._props中的成员转换成响应式,并且注入到vm实例中
- initMethods(vm, opts.methods) 把选项中的methods成员注入到vm实例,在注入之前,先判断命名是否在props中有重名的属性,并且检测了下函数命名的规范,不能以_或者$开头
- initData(vm) 把data中的成员转换成响应式,并且注入到vm实例中
stateMixin(Vue)
- Vue原型上添加 d a t a / data/ data/props属性
const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
if (process.env.NODE_ENV !== 'production') {
// 在开发环境下,不允许给$data/$props赋值
dataDef.set = function () {
warn(
'Avoid replacing instance root $data. ' +
'Use nested data properties instead.',
this
)
}
propsDef.set = function () {
warn(`$props is readonly.`, this)
}
}
// 访问$data和$props就像访问_data与_props一样,可访问,不能重新赋值
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
- Vue原型上添加$set/$delete/$watch方法
// 给原型上挂载$watcher方法,监控数据变化
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) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
eventsMixin(Vue)
- 在原型上挂载$on/$emit/$off/$once方法
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
// 当第一个参数是数组的时候,遍历调用$on,可以给多个事件注册同一个事件处理函数
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
// 判断该事件是否在_events数组中存在,没有的话就赋值空数组,然后将事件处理函数添加到该数组中
(vm._events[event] || (vm._events[event] = [])).push(fn)
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
lifecycleMixin(Vue) 混入了生命周期相关的方法
- 在原型上挂载 _update/
f
o
r
c
e
U
p
d
a
t
e
/
forceUpdate/
forceUpdate/destory方法
- update方法中最重要的是_patch 方法;将虚拟dom转换成真实dom挂载到vm.$el上
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
// 首次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
// 数据变化
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
renderMixin(Vue)
- _render/$nextTick
调试Vue初始化过程
- 在引入Vue的四个文件中依次打上断点
- 与平台相关(src\platforms\web\entry-runtime-with-compiler.js)
- 与平台相关(src\platforms\web\runtime\index.js)
- 与平台无关(src\core\index.js)
- 与平台无关(src\core\instance\index.js)
- 因为我们始终想观察Vue的变化,监听Vue
- F5刷新,定位到initMixin
- F10执行initMixin(Vue)函数,发现在Vue.pototype上混入了_init方法
- F10执行stateMixin(Vue),发现在Vue.pototype上混入了$data,$props,$set,$delete,$watch成员
- F10执行evenetMixin(Vue),发现在Vue.pototype上混入了$on,$emit,$off,$once成员
- F10执行lifecycleMixin(Vue),发现在Vue.pototype上混入了_update,$forceUpdate,$destory成员
- F10执行renderMixin(Vue),发现在Vue.pototype上混入了_render,$nextTick成员
- F8跳到下一断点initGlobalAPI(Vue)=>给Vue构造函数挂载静态成员(属性和方法)
- 属性
-
Vue.config属性
-
Vue.options属性[components",“directives”,“filters”,"_base"],其中components中含有keep-alive全局组件,_base指向Vue构造函数本身
-
Vue.set
-
Vue.delete
-
Vue.nextTick
-
Vue.observable
-
方法
- Vue.use()
- Vue.mixin()
- Vue.extend()
- Vue.component()
- Vue.directive()
- Vue.filter()
-
- 属性
- 当我们调用Vue.component(),Vue.directive(),Vue.fileter()注册全局的组件,指令和过滤器的时候,会将该模块注入带Vue.options相应的属性中
首次渲染过程
- 在引入Vue四个文件夹定义了Vue属性和方法,以及Vue原型对象的属性和方法,在项目中const Vue from "vue"完成了上述功能
- new Vue创建Vue实例(vm)的时候调用了this._init()方法
- 打断点
- 重点关注_init()函数中的 vm.
m
o
u
n
t
(
v
m
.
mount(vm.
mount(vm.options.el),当是编译版本的话
- 在src\platforms\web\runtime\index.js中
- 关注mountComponent做了什么
import { mountComponent } from 'core/instance/lifecycle'
参数一:vm实例 参数二:el
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
//判断当前选项是否有render函数
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
// 如果是运行时版本,且传入的tempalte选项(没有render选项),则抛出一个警告
// 当前使用的是运行时版本,编译器是无效的,你应该传入render函数或者使用带编译器的vue版本
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
)
}
}
}
// 触发beforeMount生命周期钩子(挂载之前)
callHook(vm, 'beforeMount')
// 定义了更新组件的函数(实际上就是挂载)
let updateComponent
// 定义函数的内容
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
// 开启了性能监测
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
// 未开启性能监测
updateComponent = () => {
// 实际上调用的lifecycleMixin(Vue)生成的Vue.prototype._update()方法及renderMixin(Vue)生成的Vue.prototype._render方法
// vm._render()内部调用的options选项中的render函数生成虚拟dom=>vnode = render.call(vm._renderProxy, vm.$createElement)
// vm._update会将vnode转换成真实dom挂载到vm.$el属性上 vm.$el = vm.__patch__(prevVnode, vnode),视图此时还没更新哦
vm._update(vm._render(), hydrating)
}
// 此时的updateComponent只是定义了,还没执行(执行的结果是真实dom对象)
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
// 创建Watcher对象,传入updateComponent函数,所以updateComponent是在Watcher中调用的
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
// 页面挂载完毕
callHook(vm, 'mounted')
}
return vm
}
- 再来重点关注Watcher中调用的updateComponent方法=>observer中的代码都是和响应式相关的
src\core\observer\watcher.js
在Vue中Watcer有三种
第一种:渲染watcher,也就是当前我们创建的Watcher,
第二种:计算属性的watcher
第三种:侦听器的watcher,也就是说计算属性和侦听器都是通过watcher来实现的
Watcher
参数一:vm实例
参数二:expOrFn(expression或者function))可以是函数也可以是字符串
参数三:回调函数cb
参数四:选项options
参数五:isRenderWatcher是否是渲染watcher