通过上篇重读Vue源码系列四—— “一无所有”的Vue如何实现全局API以及实例属性,我们已经知道了Vue是通过prototype实现原型属性和全局API等功能的。而Vue的初始化就是从最开始的原型上只有construct函数开始,逐步在Vue.prototype和Vue上增加功能属性的。
这篇文章将逐步揭开Vue实现$nextTick、$emit、$on、$off、$forceUpdate、$watch、$delete、$set等属性的面纱。
一、initMixin(Vue)
- 先看下源码
/vue/src/core/instance/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)
}
console.info(`=====Vue最原始构造函数,此时一无所有`);
console.info(`=====VueinitMixin Before Vue.prototype._init=`,Vue.prototype._init);
debugger;
initMixin(Vue) //prototype_init 传递Vue的目的是为了共用一个Vue类
console.info(`=====VueinitMixin Finish Vue.prototype._init=`,Vue.prototype._init);
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue) //原型添加$nextSick,_render
console.log(`core instance index.js`);
export default Vue
-
断点控制台打印会看到此时原型上只有构造函数
-
接着看下initMixin方法,路径在
/src/core/instance/init.js
src\core\instance\init.js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this; // vm其实就是this
// a uid
vm._uid = uid++; //Vue实例的唯一编号uid,从0开始,后面每次实例之后+1
//此处省略做性能分析的代码......
// a flag to avoid this being observed
vm._isVue = true
// merge options 合并
if (options && options._isComponent) { //暂时先不看,组件的时候用到的
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) // 初始化一些和生命周期相关的内容
initEvents(vm) // 初始化事件相关属性,当有父组件的方法绑定在子组件时候,供子组件调用
initRender(vm) // 添加slot属性
callHook(vm, 'beforeCreate') //调用beforeCreate钩子
initInjections(vm) // resolve injections before data/props
initState(vm) //对props methods data computed watcher等属性进行初始化操作 初始化数据,进行双向绑定 state/props
initProvide(vm) // resolve provide after data/props 注入provider的值到子组件中
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)
}
;
if (vm.$options.el) {
console.log(`vm.$mount(vm.$options.el)`,vm.$mount(vm.$options.el).$el)
}
}
}
通过源码看到Vue构造函数里面的this._init方法就是在这里实现的,关于_init细节会在Vue的实例化中讲。
此外,initMixin还实现了
initLifecycle(vm) // 初始化一些和生命周期相关的内容
initEvents(vm) // 初始化事件相关属性,当有父组件的方法绑定在子组件时候,供子组件调用
initRender(vm) // 添加slot属性
callHook(vm, 'beforeCreate') //调用beforeCreate钩子
initInjections(vm) // resolve injections before data/props
initState(vm) //对props methods data computed watcher等属性进行初始化操作 初始化数据,进行双向绑定 state/props
initProvide(vm) // resolve provide after data/props 注入provider的值到子组件中
callHook(vm, 'created') //调用created生命周期钩子
这些都会在Vue的实例化中讲~
initMixin执行完成之后,我们看下Vue原型上的变化
二、stateMixin(Vue)
export function stateMixin (Vue: Class<Component>) {
// flow somehow has problems with directly declared definition object
// when using Object.defineProperty, so we have to procedurally build up
// the object here.
const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
//禁止对$data和$props重新赋值操作
if (process.env.NODE_ENV !== 'production') {
dataDef.set = function () {
warn(
'Avoid replacing instance root $data. ' +
'Use nested data properties instead.',
this
)
}
propsDef.set = function () {
warn(`$props is readonly.`, this)
}
}
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
Vue.prototype.$set = set //Vue.set(vm.someObject, 'key', val)
Vue.prototype.$delete = del//
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()
}
}
}
从源码中我们看到stateMixin会在原型上增加以下原型属性
- 实例property两个
Object.defineProperty(Vue.prototype, '$data', dataDef)(Vue 实例观察的数据对象。Vue 实例代理了对其 data 对象 property 的访问)
Object.defineProperty(Vue.prototype, '$props', propsDef)(当前组件接收到的 props 对象。Vue 实例代理了对其 props 对象 property 的访问)
- 实例方法三个,这三个实例方法是我们开发者经常用到的~
Vue.prototype.$set
Vue.prototype.$delete
Vue.prototype.$watch
stateMixin执行完成之后,我们看下Vue原型上的变化
三、eventsMixin(Vue)
/src/core/instance/events.js
export function eventsMixin (Vue: Class<Component>) {
const hookRE = /^hook:/
Vue.prototype.$on = function (event: string | Array<string>, 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
}
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
}
Vue.prototype.$off = function (event?: string | Array<string>, 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
}
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
}
}
通过源码我们发现eventsMixin实现的功能就是我们平常使用的组件通信方法事件总线 参考官方文档,其实它的实现也只有几十行代码,看到这里你一定会感到惊讶,原来事件总线的实现也复杂呀,怪不得面试总会被问到。
Vue的事件总线与Node中的EventEmitter原理一样的,大致是先定义一个对象(Vue的事件总线是挂载在Vue实例上的,所以不需要重新定义对象),对象里面定义4个方法emit(定义事件)、off(移除事件)
执行完eventsMixin之后,我们会发现Vue的原型上又有了新的变化:
四、lifecycleMixin
export function lifecycleMixin (Vue: Class<Component>) {
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
// 执行 vm.__patch__ 去把 VNode 转换成真正的 DOM 节点
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) {
console.warn(`====insert`, vm.$parent.$el, vm.$el)
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// teardown watchers
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// call the last hook...
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null)
// fire destroyed hook
callHook(vm, 'destroyed')
// turn off all instance listeners.
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
}
lifecycleMixin的功能是增加原型属性
- Vue.prototype._update 这个是渲染的时候用的,后面讲会详细讲解,现在知道就行了~
- Vue.prototype.$forceUpdate vue文档的位置 迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。
- Vue.prototype.$destroy
执行完lifecycleMixin之后,我们会发现Vue的原型上又有了新的变化:
五、renderMixin
src/core/instance/render.js
export function renderMixin (Vue: Class<Component>) {
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
Vue.prototype._render = function (): VNode {
//此处代码省略
return vnode
}
}
从源码中我们知道renderMixin方法是和渲染相关的,实现了两个原型属性
- Vue.prototype.$nextTick 这个我们再熟悉不过了,功能是将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新,现在先知道是在这里初始化的,后面会详细讲~
- Vue.prototype._render _render是渲染使用的,后面讲渲染部分会着重讲解
执行完lifecycleMixin之后,我们会发现Vue的原型上又有了新的变化:
六、总结
通过这篇文章我们逐步揭开了Vue实现$nextTick、$emit、$on、$off、$forceUpdate、$watch、$delete、$set的面纱,接下来会继续讲解Vue全局API的实现。
后续有其它的原型属性的源码讲解也会在这里持续补充~