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)) {
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
/*initData*/
function initData (vm: Component) {
/*得到data数据*/
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
/*对对象类型进行严格检查,只有当对象是纯javascript对象的时候返回true*/
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
/*遍历data对象*/
const keys = Object.keys(data)
const props = vm.$options.props
let i = keys.length
//遍历data中的数据
while (i--) {
/*保证data中的key不与props中的key重复,props优先,如果有冲突会产生warning*/
if (props && hasOwn(props, keys[i])) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${keys[i]}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(keys[i])) {
/*判断是否是保留字段*/
/*这里是我们前面讲过的代理,将data上面的属性代理到了vm实例上*/
proxy(vm, `_data`, keys[i])
}
}
// observe data
/*从这里开始我们要observe了,开始对数据进行绑定,下面会进行递归observe进行对深层对象的绑定。*/
observe(data, true /* asRootData */)
}
observe中new Observer(), new Observer()会将data中的所有数据调用defineReactive变成响应式。主要原理就是利用Object.defineProperty,get()时增加依赖,也就是观察者,set时通知观察者。
/*为对象defineProperty上在变化时通知的属性*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: Function
) {
/*在闭包中定义一个dep对象*/
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
/*如果之前该对象已经预设了getter以及setter函数则将其取出来,新定义的getter/setter中会将其执行,保证不会覆盖之前已经定义的getter/setter。*/
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
/*对象的子对象递归进行observe并返回子节点的Observer对象*/
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
/*如果原本对象拥有getter方法则执行*/
const value = getter ? getter.call(obj) : val
if (Dep.target) {
/*进行依赖收集*/
dep.depend()
if (childOb) {
/*子对象进行依赖收集,其实就是将同一个watcher观察者实例放进了两个depend中,一个是正在本身闭包中的depend,另一个是子元素的depend*/
childOb.dep.depend()
}
if (Array.isArray(value)) {
/*是数组则需要对每一个成员都进行依赖收集,如果数组的成员还是数组,则递归。*/
dependArray(value)
}
}
return value
},
set: function reactiveSetter (newVal) {
/*通过getter方法获取当前值,与新值进行比较,一致则不需要执行下面的操作*/
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方法则执行setter*/
setter.call(obj, newVal)
} else {
val = newVal
}
/*新的值需要重新进行observe,保证数据响应式*/
childOb = observe(newVal)
/*dep对象通知所有的观察者*/
dep.notify()
}
})
}
/*初始化computed*/
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
for (const key in computed) {
const userDef = computed[key]
/*
计算属性可能是一个function,也有可能设置了get以及set的对象。
可以参考 https://cn.vuejs.org/v2/guide/computed.html#计算-setter
*/
let getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production') {
/*getter不存在的时候抛出warning并且给getter赋空函数*/
if (getter === undefined) {
warn(
`No getter function has been defined for computed property "${key}".`,
vm
)
getter = noop
}
}
// create internal watcher for the computed property.
/*
为计算属性创建一个内部的监视器Watcher,保存在vm实例的_computedWatchers中
这里的computedWatcherOptions参数传递了一个lazy为true,会使得watch实例的dirty为true
*/
watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
/*组件正在定义的计算属性已经定义在现有组件的原型上则不会进行重复定义*/
if (!(key in vm)) {
/*定义计算属性*/
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
/*如果计算属性与已定义的data或者props中的名称冲突则发出warning*/
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
export default class RenderStream extends stream.Readable {
constructor (render: Function) {
super()
this.buffer = ''
this.render = render
this.expectedSize = 0
this.write = createWriteFunction((text, next) => {
const n = this.expectedSize
this.buffer += text
if (this.buffer.length >= n) {
this.next = next
this.pushBySize(n)
return true // we will decide when to call next
}
return false
}, err => {
this.emit('error', err)
})
this.end = () => {
// the rendering is finished; we should push out the last of the buffer.
this.done = true
this.push(this.buffer)
}
}
pushBySize (n: number) {
const bufferToPush = this.buffer.substring(0, n)
this.buffer = this.buffer.substring(n)
this.push(bufferToPush)
}
tryRender () {
try {
this.render(this.write, this.end)
} catch (e) {
this.emit('error', e)
}
}
tryNext () {
try {
this.next()
} catch (e) {
this.emit('error', e)
}
}
_read (n: number) {
this.expectedSize = n
// it's possible that the last chunk added bumped the buffer up to > 2 * n,
// which means we will need to go through multiple read calls to drain it
// down to < n.
if (isTrue(this.done)) {
this.push(null)
return
}
if (this.buffer.length >= n) {
this.pushBySize(n)
return
}
if (isUndef(this.next)) {
// start the rendering chain.
this.tryRender()
} else {
// continue with the rendering.
this.tryNext()
}
}
}
网上Vue源码解读的文章有很多,但涉及到Vuex、vue-router的就比较少了。本文主要对描述了Vue的整体代码结构,用两个例子分别描述Vue实例化及ssr的流程;接着阐述Vue插件的注册方法,Vuex、vue-router大致实现原理。
Vue
如何用例子走一遍Vue代码
目录结构
vue构造函数
我们使用vue时都会先实例化一个Vue对象,先从Vue的构造函数说起。构造函数及原型相关代码绝大部分都在
core/instance
下。至于怎么找到Vue构造函数的位置,运用从后向前的方法,从
package.json
一点点往会看就好了。首先看
core/instance/index.js
文件,该文件主要定义了Vue的构造函数,并且初始化Vue.prototype
中的一些方法。initMixin
就做了一件事情,在Vue的原型上增加_init
方法,构造Vue实例的时候会调用这个_init
方法来初始化Vue实例,下面常用使用原理中会详细说一下这一块。stateMixin
中主要声明了Vue.prototype.$data
、Vue.prototype.$props
、Vue.prototype.$set
、Vue.prototype.$watch
eventsMixin
主要定义了Vue.prototype.$on/$off/$once
,原理就是利用观察者模型,为每一个event维护一个观察队列,存放在Vue._events中。lifecycleMixin
中定义了我们Vue中经常用到的Vue.prototype._update
方法,每当我们定义的组件data发生变化或其他原因需要重新渲染时,Vue会调用该方法,对Vnode
做diff和patch操作。renderMixin
中定义了Vue.prototype._render
等方法,_render()
调用实例化时传入的render方法,生成VNode。经常与Vue.prototype.update
一起使用。常见使用原理解读
创建实例
new Vue()
其实就是调用构造函数中的this._init()
,this._init()
就是调用上述instance/init
中声明的Vue.prototype._init
initLifecycle
,主要把自己push到parent.$children中
initEvents
,主要初始化了vm._events
存放事件。$on()方法就是将事件监听存放在这里。initRender
,定义了vm.$createElement
方法,我们调用render()
方法时,传入参数就是vm.$createElement
。initState
,这里主要initProps
,initComputed
,initData
。我们首先介绍initData
,并借initData来解读一下Vue的数据响应系统
。initData
调用observer/index.js
中的observe
方法,生成observer
对象,observer
遍历data中的数据,把每一项数据都变成响应式的。initData
,主要就看最后一行调用observe()
。observe中
new Observer()
, new Observer()会将data中的所有数据调用defineReactive
变成响应式。主要原理就是利用Object.defineProperty
,get()时增加依赖,也就是观察者,set时通知观察者。我们在声明组件时,经常使用
watch
,实际调用了new watcher(a, callback)
,watcher相当于一个观察者。我们来看watcher里的代码,其实也不难理解,watcher就是一个订阅者。关键在于watcher如何与observer
联系在一起,observer中的数据set()时,如何找到对应的watcher呢?dep
出现了!注意下面的get()中的pushTarget()
,该方法就是将自己放到dep模块中的全局变量上,然后调用this.getter.call(vm, vm)
,也就是调用了obsever的get(),get()中取得dep中的全局变量,加到了自身的dep中,当set时,会遍历执行dep中存放所有watcher的run()方法,执行callback。watcher和dep代码如下。
总结一下initData及Vue的响应式数据。
我们接下来看
initComputed
,其实就是new了一个watcher,然后执行computed函数时会调用其中所有依赖数据的getter,从而将该watcher加入到其依赖数据的dep中。声明组件
这里主要说一下
Vue.component
方法与Vue.extend方法。Vue.extend(core/global-api/extend.js)
, 其实就是寄生组合继承
了Vue。Vue.component
与Vue.extend
类似,core/global-api/assets.js
组件挂载
在
Vue.prototype._init
中最后调用了vm.$mount(vm.$options.el)
流程如下:
首先什么事vdom,其实很简单,就是一个组件就是一个vdom对象,维护在Vue中,方便取新老vnode去diff,然后针对性的去渲染。
Vnode:
渲染(patch)主要逻辑大致如下
patch
patchNode
:如果两个节点都有children,
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
这块网上有很多讲解,篇幅已经够长了,就不在这啰嗦了。推荐Vue 2.0 的 virtual-dom 实现简析
ssr
流程如下
const renderer = createRenderer(rendererOptions);
返回的renderer主要是使用renderNode()
方法,根据Vnode各种拼html。const run = createBundleRunner(entry, files, basedir, runInNewContext)
返回一个Promise,在Promise中执行打包后的代码,resolve(app)=>返回实例。那么我们打包后的ssr代码,如何执行的呢?在createBundleRunner有这样一段代码。其中NativeModule.wrap()方法也是node中包裹模块时使用的方法。
下面我们来看
renderToStream
,其实是调用了renderer.renderToStream
,下面我们来看renderer.renderToStream
。其中
new RenderStream()
,RenderStream代码如下主要看
_read()
方法。 `` 所有实现可读流的实例必须实现readable._read() 方法去获得底层的数据资源。当 readable._read() 被调用,如果读取的数据是可用的,应该在最开始的实现的时候使用this.push(dataChunk)方法将该数据推入读取队列。_read() 应该一直读取资源直到推送数据方法readable.push()返回false的时候停止。想再次调用_read()方法,需要再次往可读流里面push数据。
RenderStream
继承stream.Readable
,声明_read读取底层数据,在数据流缓冲队列超过max_size
(node实现的16384),this.pushBySize
;当steam done之后,this.tryRender
tryRender:
值得借鉴的地方
1.代码组织结构。Vue的代码耦合度还是比较低的,比如核心的部分都在Core中,在Platforms的web和weex中很方便的对其进行扩展;代码组织也比较清晰,基本一个模块只做一件事情,比如compile中就是compile template的,大家看起来一目了然
2.缓存也是用的不错的,基本上可能重复用到的地方都用了缓存。
Vue插件
Vue中如何自定义插件
Vue中绝大本分插件都是通过Vue.use()方法,该方法传入一个对象作为参数,执行对象的Install方法。
那么我们一般使用插件时,以Vuex为例,直接在实例化Vue时加入,在组件中直接使用this.$store,这又是如何做到的呢?
一般会在install中注册beforeCreate的钩子,在钩子函数中将options或父组件中的方法或属性赋给自组件。利用了父组件create先于子组件的关系,从上到下的进行注册。下面以Vuex为例。
Vuex
从store的构造函数说起
this._modules = new ModuleCollection(options)
,初始化modules,返回一个Module树,数据结构如下:installModule(this, state, [], this._modules.root)
,根据上述module树,递归注册mutation,action。。。最终形成Store的数据结构如下:
mutation和action实现上又什么区别呢?可以从下面看出action执行handle,然后判断是否是Promise来决定返回。
state中的数据是怎样加入到Vue的响应体系中的呢?使用Vue.暴露出得$set。
Vue-router
挂载方法与上述类似,只不过多做了router._init及注册组件router-view和router-link。
我们接下来看一下router的构造函数
this.matcher = createMatcher(options.routes || [], this)
根据pathList, pathMap, nameMap来找出跟路由匹配的route对象。PathMap结构如下:
this.matcher.match
用来查找匹配的路由,返回route对象,主要步骤有标准化路由(normalizeLocation)、从pathMap/pathList/nameMap中取响应记录、返回route对象。router.history
,分history/hash/abstract三种,histoy、hash即咱们理解的history和hash,abstract是Vue router自己利用堆栈实现的一套记录路由的方式。大致操作方法如下。H5:
Hash
Abstract
router-view
和router-link
为vue-router默认的组件首先看router-view,router-view组件在render中首先向上遍历到根结点,找到当前router-view的深度,也就是定义router是children的深度;找到对应组件;render()。
那么,routerView是如何得知路由变化,触发其render()的呢?这又回到了View的响应式中,Vue中Vm或数据发生变化时,会调用q前文提到的
vue.update(vm.render())
方法更新操作。vue-router在初始化时
Vue.util.defineReactive(this, '_route', this._router.history.current)
将_route变成了响应式,在路由发生变化时,执行updateRoute()将新的route赋给_route。router-link
比较简单。默认a标签,监听click事件,确定是router.push还是router.replace。路由和组件时怎么对应的呢,路由变化后,组件如何变化呢?