打开Vue源码,两眼一抹黑,连入口文件都找不到,接下来,我带着大家一步步去探索一下vue源码。
获取vue源码
clone项目:git clone https://github.com/vuejs/vue.git
版本:2.6.10
目录结构
├── dist # 所有输出的运行时文件
│ ├── common # nodejs模块cjs,旧版打包器webpack1.0、browserify
│ ├── esm # es模块化,常用语webpack2.0
│ ├── runtime # 说明输出的库仅有核心运行时代码,没有编译器
│ └── vue.js # umd-universal module definition,兼容cjs和amd规范,常用于浏览器环境
├── examples # 官方准备的很多案例
├── flow # 强类型编译语言,类似于TS,flow这种语言有类型声明文件,告诉你实现了哪些方法,这些方法的基本签名是什么样的
├── packages # 里面是独立的包,比如:服务端的渲染器、weex等
├── scripts # 里面存放的是打包的脚本,比如用什么脚本打包,打包的配置是什么样的,都在这个目录下
├── src # 核心代码存放的地方
│ ├── compiler # 编译器
│ ├── core # vue核心代码
│ ├── platforms # 平台特有代码
│ │ ├── web # 浏览器
│ │ └── weex # 移动端
│ ├── server # 服务端相关
│ ├── sfc # 单文件解释器
│ └── shared # 共享代码
├── test # 测试文件
├── types # TS类型文件,TS受欢迎,所以编写了两套类型文件,vue3.0则完全是TS类型文件了
├── .eslintignore # eslint 忽略文件
├── .eslintrc.js # eslint 配置项
├── .gitignore # git 忽略文件
└── package.json # 项目基本信息,包依赖信息等
调试环境搭建
- 安装依赖
yarn/npm i
- 修改dev脚本
# package.json:添加sourcemap
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
sourceMap:
简单说,sourceMap就是一个文件,里面储存着位置信息。
仔细点说,这个文件里保存的,是转换后代码的位置,和对应的转换前的位置。
有了它,出错的时候,通过断点工具可以直接显示原始代码,而不是转换后的代码。
rollup:
打包库,纯js打包
- 运行
npm run dev
寻找入口文件
- package.json
从dev脚本中 -c scripts/config.js 指明配置文件所在,参数TARGET:web-full-dev 指明输出文件配置项
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
- scripts/config.js
在config.js中搜索web-full-dev
// 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
},
- 运行后也能看到入口文件
bundles /Users/qiaoxu/Desktop/2018-study /优秀源码/vue源码/vue/src/platforms/web/entry-runtime-with-compiler.js → dist/vue.js...
created dist/vue.js in 1.7s
初始化流程
1. 从入口文件开始分析
位置:src/platforms/web/entry-runtime-with-compiler.js
功能:入口文件主要做了一件事,扩展默认$mount方法,处理template与el选项
render优先级最高
render>template>el
$mount做了什么:将用户编写的模板——变成虚拟dom——变成真实dom
2. 寻找Vue的构造函数
位置:src/platforms/web/runtime/index.js
功能:
-
定义$mount:挂载根组件到指定宿主元素;
-
定义_patch_:补丁函数,执行patching算法进行更新
3. 继续寻找Vue的构造函数
位置:src/core/index.js
功能:实现全局API
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
initUse(Vue) // 实现Vue.use函数
initMixin(Vue) // 实现Vue.mixin函数
initExtend(Vue) // 实现Vue.extend函数
initAssetRegisters(Vue) // 注册实现Vue.component/directive/filter
4. 最终找到Vue的构造函数
位置:src/core/instance/index.js
功能:定义了Vue构造函数,文件中使用了若干混入模式,对Vue进行了扩展
function Vue (options) {
// 构造函数仅执行了_init
this._init(options)
}
initMixin(Vue) // 实现init函数
stateMixin(Vue) // 状态相关api $data,$props,$set,$delete,$watch
eventsMixin(Vue)// 事件相关api $on,$once,$off,$emit
lifecycleMixin(Vue) // 生命周期api _update,$forceUpdate,$destroy
renderMixin(Vue)// 渲染api _render,$nextTick
5. Vue初始化做了什么
位置:src/core/instance/init.js
功能:
初始化顺序:生命周期、事件监听、渲染、beforeCreate、注入、组件状态、provide、created
initLifecycle(vm) // $parent,$root,$children,$refs
initEvents(vm) // 事件监听:处理父组件传递的监听器
initRender(vm) // $slots,$scopedSlots,_c,$createElement
callHook(vm, 'beforeCreate')
initInjections(vm) // 获取注入数据
initState(vm) // 初始化props,methods,data,computed,watch
initProvide(vm) // 提供数据注入
callHook(vm, 'created')
// 如果存在el宿主,则自动执行挂载,不需要手动挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
6. $mount执行的核心方法mountComponent
位置:mountComponent src/core/instance/lifecycle.js
功能:渲染(生成虚拟dom)与更新(将虚拟dom转为真实dom)
// 定义update方法
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
// 执行patch
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.
}
// 定义updateComponent
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 = () => {
vm._update(vm._render(), hydrating) // 执行update方法
}
}
// 执行updateComponent
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
数据响应式
1. initData
位置:src\core\instance\state.js
功能:初始化数据
具体实现是在Vue初始化时,会调用initState,它会初始化data,props等,这里着重关注data初始化
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: 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
...
// observe data执行数据响应化
observe(data, true /* asRootData */)
}
2. observer
位置:src/core/observer/index.js
功能:Observer对象根据数据类型执行对应的响应化操作
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {// 数组
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {// 对象
this.walk(value)
}
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
3. 依赖收集Object.defineProperty
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
// depend()是相互添加引用的过程
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
4. dep与watcher之间的关系
- dep
位置:src/core/observer/dep.js
功能:Dep负责管理一组Watcher,包括watcher实例的增删及通知更新
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
- watcher
位置:src/core/observer/watcher.js
功能:Watcher解析一个表达式并收集依赖,当数值变化时触发回调函数,常用于$watch API和指令中。 每个组件也会有对应的Watcher,数值变化会触发其update函数导致重新渲染
/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
// watcher保存dep
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
// dep保存watcher
dep.addSub(this)
}
}
}
数组响应式
**1.修改数组原型中的7个可以改变内容的方法 **
位置:src\core\observer\array.js
功能:对数组的原型方法特殊处理
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
2. Observer中覆盖数组原型
位置:src\core\observer\index.js
功能:将修改过的数组方法覆盖给数组原型
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
}
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}