目标
- vue.use set,el. d a t a , data, data,el.mount等
- 创建vue实例,初始化好数据后,vue如何渲染到页面的
- 响应式
目录
根据不同功能把代码拆分不同文件夹(提高可读性可维护性)
compiler转换render函数
core核心
globalApi Vue的静态方法
instance 创建vue实例
observer 响应式实现(重点讲)
vueDom 重写了snabdom
server 服务端渲染
sfc 单文件组件 把单文件组件转成js对象
了解flow即可 vue3已经全面TS开发
flow可以使js有java搬的开发体验,运行前检查。
准备工作-调试
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web- full-dev"
调试
examples 的示例中引入的 vue.min.js 改为 vue.js
打开 Chrome 的调试工具中的 source
一路f11调试。
(开启sourmap可以根据src不同模块不同功能调试,不开启会直接用dist/vue.js 里面一万多行代码,易读性差)
Vue的不同构建版本
full-完整版, runtime-only-运行时
前俩行完整版代码
后俩行压缩版本
编译器的作用是把template转换成render渲染函数(转Vnode)。
编译器代码三千多行。
- 尝试完整版
// 注意因为有template这里的#app没了
- 尝试运行时版本
报错,无法解析模板
把template修改render函数
- 尝试基于vue-cli创建的项目(运行时版本)
查看webpack的配置(vue对webpack做了深度封装一般看不了)
vue inspect 是把webpack配置打印出来,看起来不方便,使用这个输出到文件
vue inspect > output.js
// Compiler
// 需要编译器,把 template 转换成 render 函数
// const vm = new Vue({ // el: '#app', // template: '<h1>{{ msg }}</h1>', // data: { // msg: 'Hello Vue' // } // }) // Runtime // 不需要编译器 const vm = new Vue({ el: '#app', render (h) { return h('h1', this.msg) },data: { msg: 'Hello Vue' } }) 123
注意: *.vue 文件中的模板是在构建时预编译的,最终打包后的结果不需要编译器,只需要运行时版本即可
寻找入口文件
查看 dist/vue.js 的构建过程
npm run dev
script/config.js 的执行过程
- 作用:生成 rollup 构建的配置文件
- 使用环境变量 TARGET = web-full-dev
结果 - 把 src/platforms/web/entry-runtime-with-compiler.js 构建成 dist/vue.js,如果设置 –
sourcemap 会生成 vue.js.map - src/platform 文件夹下是 Vue 可以构建成不同平台下使用的库,目前有 weex 和 web,还有服务
器端渲染的库
从入口开始
src/platform/web/entry-runtime-with-compiler.js
如何找到$mount方法何时调用?
方法:如下
先打包出带sourcemap的vue文件
向下依次点击会顺序执行
render和template先执行那个?
el 不能是 body 或者 html 标签
如果没有 render,把 template 转换成 render 函数
如果有 render 方法,直接调用 mount 挂载 DOM
调试的方法
Vue初始化的过程
运行时版
直接导出vue
完整版比较复杂
这里的核心就是把template转为render
从runtime开始
开始探索vue构造函数
core中的代码和平台无关
观察导入的vue
终于找到vue构造函数
这个文件主要做了:
1创建vue构造函数
2设置vue实例的成员
注意:关于vue构造函数
此处不用 class 的原因是因为方便后续给 Vue 实例混入实例成员(通过原型挂载,用类的话就在语法上很不搭配)
总结
- 四个导出 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初始化-两个问题
解决报红问题
问题2
解决前
下载
解决
虽然安装了上面的插件 但没有点击跳转的功能(无大碍跳过即可)
Vue初始化-静态成员
globalApi中
runtime/index中
继续看 globalApi
其实extend就是一个浅拷贝
回到globalApi中
extend(Vue.options.components, builtInComponents)//在注册全局组件 这个builtInComponents是全局组件
builtInComponents ↓
回到globalApi中,下面的函数都在global文件夹中
initUse的实现
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
// 这里的this 谁调用用指向谁
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters
// 把arguments转换数组,去掉数组中的第一个元素(plugin)去除
const args = toArray(arguments, 1)
// 把this(Vue)插入第一个元素的位置
args.unshift(this)
if (typeof plugin.install === 'function') {
// 这里的apply为了展开参数
// 调用插件方法 传递参数
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
}
initMixin
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
//拷贝
this.options = mergeOptions(this.options, mixin)// this 是vue
return this
}
}
initExtend
核心就是返回组件的构造函数
太长了,后面放源码自己看
initAssetRegisters(Vue)
// 注册 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 数组,为 Vue 定义相应方法
// ASSET_TYPES 包括了directive、 component、filter
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)
}
// Vue.component('comp', { template: '' })
// 判断是否组件 isPlainObject 是否原始对象
// export function isPlainObject (obj: any): boolean {
// return _toString.call(obj) === '[object Object]'
// }
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
// 把组件配置转换为组件的构造函数
definition = this.options._base.extend(definition) //this.options._base = vue extend把对象转换成组件构造函数
}
// 如果是指令,俩种情况,第一种如果是对象直接执行最后一句,第二种函数进入if分支
if (type === 'directive' && typeof definition === 'function') {
// 这里的bind和update 是方法
definition = { bind: definition, update: definition }
}
// 全局注册,存储资源并赋值
// this.options['components']['comp'] = definition
this.options[type + 's'][id] = definition //如果直接传组件构造函数会直接执行这句话
return definition
}
}
})
}
总结 定义vue.config,set,delete,observable等,初始化options,继续注册keepalive,use,mixin,extent等,以上都是静态成员
Vue初始化-实例成员
这段代表都是给vue实例混入相关成员
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'
// 此处不用 class 的原因是因为方便后续给 Vue 实例混入实例成员
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue) //判断是否生成环境和是否new出来的
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
// 调用 _init() 方法 这里的是下面的initMixin注册的
this._init(options)
}
// 注册 vm 的 _init() 方法,初始化 vm
initMixin(Vue)
// 注册 vm 的 $data/$props/$set/$delete/$watch
stateMixin(Vue)
// 初始化事件相关方法
// $on/$once/$off/$emit
eventsMixin(Vue)
// 初始化生命周期相关的混入方法
// _update/$forceUpdate/$destroy
lifecycleMixin(Vue)
// 混入 render
// $nextTick/_render
renderMixin(Vue)
export default Vue
这里开始就不一一贴代码了,自己一个一个点进去看会笔记快,源码我都写了注释
-
先看initMixin
这里的init方法相当于整个vue的入口所有事情都从这里开始
看源码快捷键 按ctrl和鼠标进入后 alt+← 可以快速回去 -
stateMixin(Vue)
// 注册 vm 的 d a t a / data/ data/props/ s e t / set/ set/delete/$watch -
eventsMixin(Vue)
// 初始化事件相关方法
// o n / on/ on/once/ o f f / off/ off/emit
注意这里的初始化的方法 很不错
- lifecycleMixin(Vue)
// 初始化生命周期相关的混入方法
// _update/
f
o
r
c
e
U
p
d
a
t
e
/
forceUpdate/
forceUpdate/destroy
- renderMixin(Vue)
// 混入 render
// $nextTick/_render
render方法中
Vue初始化-实例成员-init
静态成员和实例成员初始化完后会调用vue的构造函数,然后调用init,init是在initMixin中初始的
下面的都直接看源码 都写注释了
以下方法都一个个点进去看源码都写注释了
依赖注入原理
Vue初始化-实例成员-initState
点进去一个一个看
调试Vue初始化过程
主要调试vue导出的四个文件
继续找到俩个跟平台相关的
patch把vdom转成dom,然后mount挂载到页面
继续找第四个断点,打包文件后的入口,此文件中重写了mount方法,增加了把模板变成render的功能。
开始调试 f5
进入instance/index
最开始只有默认成员和默认构造函数
观察initMixin执行完后 prototype的变化。
按f10跳过此函数,发现prototype中增加了 init方法
继续f10跳过stateMinx,发先多了
d
a
t
a
/
data/
data/props/
s
e
t
/
set/
set/delete/$watch但都是undefined,后面通过选项赋值
继续f10跳过eventsMixin,发现多了事件相关的
o
n
/
on/
on/once/
o
f
f
/
off/
off/emit
继续f10跳过lifecycleMixin,发现多了_update/
f
o
r
c
e
U
p
d
a
t
e
/
forceUpdate/
forceUpdate/destroy,其中update调用了patch,把vdom转成dom
继续f10跳过renderMixin,发现多了_开头的方法,等把模板转换成render的时候调用这些方法
还多了nexttick和render(调用用户的render或者把模板转成render)。
这里不看keepalive的执行过程 直接f8到下一个断点
core/index中
f11进入globalApi
按f10执行到
接下来又初始化了util然后是set和delte等
继续一步一步到
初始化option 此时的空对象(啥都没有连proto都没有),此option存储全局指令和组件
foreach结束后
到base这里,base主要存储vue的构造函数
初始化第一个组件keepalive组件
开始初始静态方法
开始注册全局组件和过滤器
初始完成静态成员的过程结束。f8跳到下一个断点
web/runtime/index 此时的代码都是和平台相关的
开始注册平台相关方法
点击f10 执行到
继续下一步,初始化patch和mount,这里在vue的proto上挂载了,但未执行,会在vue的init中调用
f10 到
发现已经添加到vue的proto中
观察结束,f8进入下一个
到了打包文件的入口
执行完后 又给vue挂载了compile方法(作用是手工转换成render函数)
调式完毕四个文件,可以发现vue构造函数的变化,以及初始化静态和实例成员的过程。
下面开始调试init方法,也就是首次渲染的过程。
首次渲染过程
重点调试init,观察首次渲染的过程
f8进入下一个文件
core/index
f8进入下一个文件
runtime/index
f8进入下一个文件
f8再f11进入init
f10一步一步往下走
此时不是组件,开始合并option
合并前
合并后
f11进入initProxy,大概先判断当前环境是否支持proxy,然后通过proxy代理
跳出initproxy,调试结束,下面的init等方法先不看,现在重点看如何渲染的。
新增断点,然后f8,然后f11进入mount
f10执行到
一直f10如果有方法就f11跳进去看实现
最后到mount方法,返回runtime/index中重写的mount
按f11进入mount
这里重新获取el是因为,如果是运行时版本的话需要获取el,但现在用的是完整版的,已经在之前的方法获取了
进入mountComponent(vue核心代码)
一直下一步
这里定义了但还没执行_update,是在
继续调试到 创建watch,在这里才执行updateCompenent(准确的说是在watcher中的get方法调用了)
f8 f11进入watch
observe文件中的代码都是和响应式相关的
补充 vue中一般有三种watvh 1渲染2计算属性的3侦听器的
lazy的作用是延迟执行,因为当前是首次渲染需要立即更新。
如果是计算属性会true,数据更新后才更新视图。
继续往下走
继续走,这里会判断是否lazy 如果不是就立即执行 get方法
观察get
存入当前watch入栈(每个组件对应每个watch》watch去渲染视图》如果组件嵌套则先渲染内部的》所以需要保存父组件的watch)
get中最关键的一句话,调用刚刚存的getter(这里的getter就是刚刚穿过来的updateComponent)
继续设置断点,f8,f11进入get
一直执行到
f11进入后,到了updateComponent,updateComponent中的update执行完后就会渲染到页面上。f10跳过回到get方法,发现页面已经渲染完完毕
执行结束会继续执行watchr,watchr执行完后 回到lifecircle里
继续f10,把lifecircle执行完毕回到
继续f10 又会会到入口文件》又回到init 》 构造函数,首次渲染结束。
首次渲染过程-总结
最后触发mounted页面挂载完毕。
init相当vue的入口
第一个mount(entry-runtime-with-compiler)是把模板编译成render函数
第二个mount(runtime/index)重新获取el,然后调用mountComponent。
mountComponent
get
首次渲染结束。