Vue源码之核心API的具体实现
在使用Vue时,很多时候会使用它原型上的全局方法,但不清楚它们背后运行的原理,总是会出现一些莫名的bug让我没有头绪,为此我想来从源码里看看它们的一一实现。
Vue自带的方法肯定都是在初始化时完成定义的,所以接下来我就先从Vue的构造函数看起,
全局方法
初始化Vue实例
src/core/instanse/index.js
...
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)
可以看到,在Vue进行初始化时,会把原型上options
属性和实例化Vue对象时传入的参数进行合并,得到一个新的options
,接下来一个个的来看看它们做了什么事
1.initMixin(Vue)
在这个步骤了,Vue主要是给自身添加了一些用于性能追踪的tag,调用merge合并options的方法,然后调用一系列的初始化方法
src/core/instanse/init.js
...
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
...
从以上代码可以看出,data
的初始化是在调用钩子函数beforeCreate
和created
之间的,接下来看initState
方法
src/core/instanse/state.js
...
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
...
在这个方法里,我们可以得知,先初始化了Props
、Methods
,才多数据进行双向数据绑定,这也就是为什么在data
里能够通过this
来获取到Props
、Methods
的原因,他们先于data
被初始化。
至于双向数据绑定,这个概念就不细谈了,我在另一篇博文里提及过,也具体实践过,不过源码里值得学习的地方在于它代码的组织形式、边界值的判定和对性能的要求。
2.stateMixin(Vue)
src/core/instanse/state.js
...
const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
...
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = ...
...
它主要干的事情,是在Vue的构造函数里添加静态属性$data
、$props
和方法$set
、$delete
、$watch
2.1 $data和$props
在src/core/instanse/state.js
文件中,方法initData
和initProps
的最后,都调用了代理方法proxy
...
proxy(vm, `_data`, key)
...
proxy(vm, `_props`, key)
...
将_data
和_props
挂载到了原型上,在stateMixin
方法中,只是做了取到这两个值,放到$data
和$props
的操作而已
2.2 $set
在源码中找到$set
函数的最终实现方法
src/core/observer/index.js
export function set (target: Array | Object, key: any, val: any): any {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
从代码中可以看到,如果目标是一个数组,那么就执行Array.splice()
方法;如果目标时一个对象,那就直接给属性复制;如果目标是Vue实例本身,就会发出提示,避免在Vue实例上或根$data
上添加响应式属性,最后再对已设置的属性进行数据绑定,触发依赖。
2.3 $delete
src/core/observer/index.js
export function del (target: Array | Object, key: any) {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
if (!hasOwn(target, key)) {
return
}
delete target[key]
if (!ob) {
return
}
ob.dep.notify()
}
类似于,$set
方法,对数组使用Array.splice()
,对象的话使用delete
命令,同样不能删除实例本身和根$data
上的属性,最后触发依赖。
2.4 $watch
src/core/observer/index.js
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const 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()
}
}
如果参数cb
是一个纯对象,那么就执行createWatcher
,该方法其实是一个递归,最后还是会回到$watch
方法里来
核心是new Watcher()
实例化,该对象的构造函数主要做了一件事,借助于双向数据绑定的功能,主动执行渲染函数,通过触发被监听对象的get
拦截器函数,完成依赖收集。
然后判断是否需要立即执行回调函数。
3.eventsMixin(Vue)
src/core/instance/events.js
...
Vue.prototype.$on = ...
Vue.prototype.$once = ...
Vue.prototype.$off = ...
Vue.prototype.$emit = ...
...
该方法使用观察者模式,挂载了和自定义事件相关的4个方法,接下来我们一一来看他们的具体实现
3.1 $on
Vue.prototype.$on = function (event: string | Array, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
使用一个递归,保证每个事件都能添加到事件容器数组vm._events
中去,最后做一个是否包含钩子函数事件的标识判断
3.2 $once
Vue.prototype.$once = function (event: string, fn: Function): Component {
const vm: Component = this
function on () {
vm.$off(event, on)
fn.apply(vm, arguments)
}
on.fn = fn
vm.$on(event, on)
return vm
}
添加一个on
方法作为$on
方法的回调函数,在执行后调用$off
方法删除该方法,从而保证该方法只能运行一次
3.3 $off
Vue.prototype.$off = function (event?: string | Array, fn?: Function): Component {
const vm: Component = this
// all
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of events
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
// specific event
const cbs = vm._events[event]
if (!cbs) {
return vm
}
if (!fn) {
vm._events[event] = null
return vm
}
// specific handler
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
return vm
}
通过递归的形式,遍历特定事件类型的存储容器vm._events
,使用数组的splice
方法将事件一一移除。
3.4 $emit
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
if (process.env.NODE_ENV !== 'production') {
const lowerCaseEvent = event.toLowerCase()
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
tip(
`Event "${lowerCaseEvent}" is emitted in component ` +
`${formatComponentName(vm)} but the handler is registered for "${event}". ` +
`Note that HTML attributes are case-insensitive and you cannot use ` +
`v-on to listen to camelCase events when using in-DOM templates. ` +
`You should probably use "${hyphenate(event)}" instead of "${event}".`
)
}
}
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
遍历存储容器vm._events
,执行每一个事件的回调函数
4 lifecycleMixin(Vue)
该方法主要就是定义生命周期中所调用钩子函数在回调之前之后,对Vue做一些诸如标记的工作,按下不表。
5 renderMixin(Vue)
此方法了主要定义了$nextTick
方法
找到该方法的实现源码(代码太多就不贴了),总结得出4中实现$nextTick
的方法,从上至下按它们的优先级排列:
promise
mutationObserver
:有着广泛的支持,但是在IOS版本>=9.3.3的情况下,UIWebView在触发几次后就会完全停止工作,适用于ios7.X版本setImmediate
:仅IE支持,相对于setTimeout的优势在将回调函数注册为macrotask之前不会一直做超时检测setTimeout