最近又将Javascript红宝书仔细地研究学习了3-7章,因为之前有许多理解不了的地方直接走马观花地跳过了。像什么闭包、原型链、原型对象及各种继承真得是看得脑壳疼,不过这一次看起来竟然将之前的疑问都能解决,自我感觉还不错。另外老大鼓励我可以试着学习看一下vue的源码,看别人的框架是怎么实现的。于是乎,开始了vue源码学习之旅。
最开始,我想着自己能否研究出个所以然,从github上面下载源码(地址为:https://github.com/vuejs/vue),盯了大半天无奈无从下手。果然还是高估了自己,后来看到了许多人推荐的这篇博客——Vue2.1.7源码学习,确实讲的很是全面。从宏观整体上来学习,在学习的过程中,自己也遇到了些许不同的疑问,现将部分记载。
声明:初次学习源码很困难,是参考了多方资料学习理解的,这里我只是记录下这个思路,方便后续自己回头学习。如果有和哪位朋友的内容冲突了,请联系我撤下,实在不是有意copy的,这篇博文是自己看了之后做的笔记然后再记录的。如果觉得有些地方理解有误,欢迎给我指出,相互进步,谢谢^ ^
如下图所示,打开vue项目就会看到尤其庞大,由于没有经验,不知道从何看起,内容实在是太多了。事实证明还是要站在前人的肩膀上才能看得更远。
一、首先寻找Vue的构造函数
寻找思路,先从package.json入手,寻找编译命令看到:
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"
小小补充一下:vue中打包工具用的是rollup,类似于webpack。后面的的scripts/config.js时编译的文件,进入该目录下查看,发现是打包配置文件,于是试图找到入口文件,看哪一个的输出是dist/vue.js。
...
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)
}
}
...
// 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
},
...
这时候的entry:resolve(‘web/entry-runtime-with-compiler.js’)
resolve常量是一个函数,路径参数p=’web/entry-runtime-with-compiler.js’传递进入后:
base = 'web';
//下面的reslove是另一个alias.js中的resolve = => path.resolve(__dirname, '../', p)
alias[web] = reslove('src/platforms/web');
即alias[web] = 'vue-dev/src/platforms/web';
return path.resolve(aliases[base], p.slice(base.length + 1));
即return path.resolve(alias[web], 'entry-runtime-with-compiler.js');
即return 'vue-dev/src/platforms/web/entry-runtime-with-compiler.js'
最终找到入口文件在’vue-dev/src/platforms/web/entry-runtime-with-compiler.js’路径下。会发现头部的import Vue from ‘./runtime/index’,所以这还不是我们要找的构造函数,于是进入该目录进一步向上寻找,发现import Vue from ‘core/index’……一步步向上寻找,终于找到构造函数的真正目录:vue-dev/source/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) {
//在生产环境下实例Vue还不存在时
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue) //初始化
stateMixin(Vue) //状态混合
eventsMixin(Vue) //事件混合
lifecycleMixin(Vue) //生命周期混合
renderMixin(Vue) //渲染
export default Vue
查看vue构造函数,发现创建Vue实例对象的同时就会调用this._init(options),看名字猜想是初始化系列的数据和方法。所以接下来,让我们仔细地查看一下。
二、查看_init(options)方法,进而了解Data的监听
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
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 */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
//暴露对象自身
vm._self = vm
//初始化生命周期
initLifecycle(vm)
//初始化事件:on,once,off,emit
initEvents(vm)
//初始化渲染:涉及到Virtual DOM
initRender(vm)
//触发beforeCreate生命周期钩子
callHook(vm, 'beforeCreate')
//初始化data/props前初始化Injections
initInjections(vm) // resolve injections before data/props
//初始化状态选项
initState(vm)
//初始化data/props后初始化Provide
initProvide(vm) // resolve provide after data/props
//触发created生命周期钩子
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)
}
//如果vue配置项中有el,直接挂到DOM
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
可以看到_init()是vue的一个原型方法,主要后面调用了init*系列方法,处理Vue实例对象,并做一些初始化的工作。既然是研究数据,那我们进入initState(),并找到下面的initData函数。
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
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
)
}
}
//遍历data数据,判断是否有data与props的key同名
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
)
} else if (!isReserved(key)) {
//使用_data做数据劫持,即就是通过对访问器属性中的set和get函数设置特殊的处理,实现数据监听和对象属性的代理
//即可以通过vm.name而不是vm.data.name访问
proxy(vm, `_data`, key)
}
}
// observe data,执行observe方法,监听data的变化
observe(data, true /* asRootData */)
}
这里initData函数所做的工作有:
(1)获取data数据;
(2)遍历data对象中的key,判断是否有与proxy同名,如果有发出警告,否则再调用proxy函数(相当于一个数据数据劫持,即在set和get中编写自己代码实现特定的功能),即使用_data做数据劫持,通过访问器属性中的set和get函数设置特殊的处理。
补充1:可以看到上面源码中有vm:Component,这是因为vue中用了facebook下的flow静态检查工具,可以帮我们规避检查出不必要的错误,例如这里就是标志Vue对象类型必须是Component型的,如果不是那么编译时就会报错提示,利用flow静态检查工具可以方便我们检查和规范自己的代码,特别是通过事先声明好的类型来检查参数类型。
此外,flow静态检查语法的一般形式就是: a:b,表明a的类型只能是b。不过还有很多中写法搭配了?,这个又是typeScript的语法。不得不说作者真得是很厉害,a? : b这时的问号就表示a是可选的,这种语法一般都用于函数参数中,表示a参数是可选的。找了好久,最后才发现是ts里的语法,作者大大太强了。
但是另一种情况,我真得是不知道了! a: ?b,凉凉,虽然在flow的官方网站看到多次这个写法,但文档没有给出说明。只有等后续在项目中验证一下到底是什么意思。
此外的此外,有些地方又形如java中的泛型,java中的泛型是指:指定一个表示类型的变量,用它来替代实际类型用于编程,然后再实际调用时再传入那个变量的限定类型来判断。例如:List<变量>中的变量最开始可能就是一个变量没有实际的类型,但随着实际的编程调用时,通过传入或推导的类型来更换。不过仍旧有疑问component是在哪里定义的类型呢???祝后面能成功找到!
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
//对get属性进行了改造,通过vm.name而不是vm._data.name
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
//同理对set属性也进行了改造,在设置的时候也可以直接通过vm.name就可以直接设置到了
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
(3)最后通过调用observe(data,true),来监听数据的变化。
2.1 再进一步了解observe(data,true)函数
进入该函数所在目录下,找到该部分源码:
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
//如果存在__obj__属性,说明该对象没有被observe过,不是observer类
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
//如果数据没有被observe过,且数据是array或object类型,
//那么将数据转化为observe类型,所以observe类接收的是对象和数组
ob = new Observer(value)
}
//如果是RootData,新建vue实例时,传到data里的值,只有RootData在每次observe的时候会进行计数
//vmCount是用来记录次Vue实例被使用的次数的
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
可以看到该函数的返回值是一个Observer实例对象,于是再查看Observe类的实现(这里其实是用了ES6的语法,class Observer其实也就相当于function Observer构造函数,即里面的constructor中通过this定义属性和方法都都和原来定义的实例属性和方法一样。如果没有通过this声明的则是定义在原型对象上的方法或者属性!并且值得注意的是类里定义的方法都是不可枚举的不同于原生的写法定义的方法enumerable的值是true即可枚举)
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
//如果是数组,触发observeArray方法,如果是对象触发walk方法
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through each property 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])
}
}
}
整理一下构造函数的思路:
(1)首先定义了属性value、dep(Dep依赖者的一个实例对象)、及vmCount(当前对象作为根数据对象的个数);
(2)判断数据是属于对象还是数组(PS:虽然数组本质上也是对象。。。),如果是数组,就会调用专用的监听函数observerArray();对象则执行walk函数,在walk中又会调用defineReactive(用于为对象属性添加set和get方法),也是MVVM的核心;
2.2 再进一步了解defineReactive
如下所示,是defineRactive函数实现。
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
){
const dep = new Dep() //创建依赖对象
const property = Object.getOwnPropertyDescriptor(obj, key) //获取obj对象的key属性描述符
//属性的描述符中如果configurable为false则属性的任何修改将无效
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
/*
直接将对象描述符的get和set是否存在以getter和setter来标志???什么意思命名值只能是true或者false,但是后面却有getter.call()和setter.call()??????????
当没有手动设置get和set时,getter和setter是undefined
*/
const getter = property && property.get
const setter = property && property.set
//这个有疑问,后面的条件不是一直都不存在么,必须有的都有三个参数哒!
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
//创建一个观察者对象,进行递归绑定
let childOb = !shallow && observe(val)
//每一个属性都要变为可观察的
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
//先调用默认的get方法取值
const value = getter ? getter.call(obj) : val
/*这里只有Dep.target存在的情况下才收集依赖,主要是Dep.target其实一个watcher对象,
全局的Dep.target是唯一的,只有在watcher收集依赖的时候才会执行dep.depend(),在直接使用js访问属性时直接取值
这里就劫持了get方法,也是作者一个巧妙设计,
在创建watcher实例的时候,通过调用对象的get方法往订阅器里dep上添加这个创建的watcher实例
*/
if (Dep.target) {
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()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
//继续监听新的属性值
childOb = !shallow && observe(newVal)
//这个是真正劫持的目的,data对象的属性值改变就会触发notify,对订阅者发通知了
dep.notify()
}
})
}
将实现流程也梳理一下:
(1)创建Dep依赖对象(dep是介于订阅者Watcher和观察者Observer间的一个连接桥);
(2)获取该对象属性的描述符,确定可以被修改才能继续进行后续的操作(configurable = false时不能对属性进行任何修改);
(3)创建一个观察者对象childOd(即Observer类的实例对象);
(4)(2)中可以修改前提下,利用Object.defineProperty(obj,key,{})方法对属性的set和get方法进行修改。都分别以流程图的形式展示,加强理解。
ps:不过这里不懂,后面的 const getter = property && property.get和const setter = property && property.set自我理解两个常量的值应该是boolean类型,但在后面却调用了getter.call()和setter.call()方法莫名地就晕了!是如何将set和get重新改名为setter和getter吗
(4.1)get属性:
(4.2)set属性:
关于Watcher实例中的get方法实现逻辑:
get () {
//pushTarget函数将当前watcher实例传递给订阅者对象dep.target
pushTarget(this)
let value
const vm = this.vm
//就在这一步收集依赖
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
//清理依赖
this.cleanupDeps()
}
return value
}
(1)将当前的watcher实例传递给订阅者对象dep.target;
(2)执行检测属性的getter方法;
(3)清理工作;
2.3 最后对Data监听流程做总结
1.initState方法及后续的initData函数,获取数据data后,遍历数据对象key,通过proxy()代理,将数据劫持处理后,使我们可以通过vm.key访问到属性值而不是vm.data.key。
2.执行Observer方法,创建实例对象,通过walk()方法中调用defineReactive()将对象属性设置为访问器属性,创建setter和getter方法。这其中又有一个疑问! 为什么set属性是定义在get属性之中的???
已解决:再次到github上面查看源码,只进行了一次一次Dep.target的判断,可能是自己在研究过程中不小心改到了。因此上面的疑问也不复存在,set和get两个属性都是分开定义的^ ^
3.在创建getter和setter方法是,创建了依赖者对象实例dep。接着在getter方法中收集watcher,通过执行dep.depend——>在执行Dep.target.addDep(this)到最后dep.js中的addSub(),将订阅者添加;
setter方法中执行了dep.notify(),最后比那里执行依赖中各自watcher的回调函数,即get方法——>进而调用每个属性的getter方法。
4.Dep类似于watcher和Observer间中间件,两者的联系通过Dep建立。是数据更新的发布者,在获取数据时进行依赖收集。
5.watcher作用是监听变化执行回调,当创建watcher对象时就会将自身传递给Dep.target。
6.当监测数据发生变化时,执行setter方法,触发dep.notify(),遍历Dep中的watcher订阅者数组,然后分别执行回调函数。
补充2:莫名不知道在哪里看到了slot插槽分发。
其实就相当于一个组件,我们在一个整体中将这个组件放进去就是了。下面的代码中就是在div里放置了两部分内容,第二部分先用slot占坑,后面的坑里面实际填的内容就是alert-box。
//html
<alert-box></alert-box>
//js
Vue.component('alert-box',{
template:
<div calss = "demo-alert-box">
<strong>Error!</strong>
<slot></slot>
</div>
})
嗯~很混乱!目前很多地方理解的可能有问题,不过大概的思路实现算是没问题。还是先从全局出发吧,掌握主要的思想脉络,相信后续再次看时又会有更深的理解,现在暂且这样。