网络创业培训平台/sem优化师是什么意思
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
- web:生成web平台的vue版本
- full:生成完整版的vue(含编译器+运行时)
- dev:开发环境下的vue版本(不压缩)
- 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.jsexternal: 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 varsconst vars = {__WEEX__: !!opts.weex,__WEEX_VERSION__: weexVersion,__VERSION__: version}// feature flagsObject.keys(featureFlags).forEach(key => {vars[`process.env.${key}`] = featureFlags[key]})// build-specific envif (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-devmodule.exports = genConfig(process.env.TARGET)
} else {exports.getBuild = genConfigexports.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 = () => configif (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/nextTickVue.set = setVue.delete = delVue.nextTick = nextTick// observe:让一个对象可响应// 2.6 explicit observable APIVue.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 = thisconst 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] = definitionreturn 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原型上添加data/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 = thisif (isPlainObject(cb)) {return createWatcher(vm, expOrFn, cb, options)}options = options || {}options.user = trueconst 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 lookupif (hookRE.test(event)) {vm._hasHookEvent = true}}return vm}
lifecycleMixin(Vue) 混入了生命周期相关的方法
- 在原型上挂载 _update/forceUpdate/forceUpdate/forceUpdate/destory方法
- update方法中最重要的是_patch 方法;将虚拟dom转换成真实dom挂载到vm.$el上
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {const vm: Component = thisconst prevEl = vm.$elconst prevVnode = vm._vnodeconst 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__ referenceif (prevEl) {prevEl.__vue__ = null}if (vm.$el) {vm.$el.__vue__ = vm}// if parent is an HOC, update its $el as wellif (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.mount(vm.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 = createEmptyVNodeif (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._nameconst id = vm._uidconst 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 hookif (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