重读Vue源码系列五—— 逐步揭开Vue构造函数实现原型属性的面纱

571 阅读3分钟

通过上篇重读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
  • 断点控制台打印会看到此时原型上只有构造函数 image.png

  • 接着看下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原型上的变化 image.png

二、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原型上的变化 image.png

三、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个方法on(监听事件)、on(监听事件)、emit(定义事件)、once(定义只执行一次的事件)once(定义只执行一次的事件)、off(移除事件)

执行完eventsMixin之后,我们会发现Vue的原型上又有了新的变化:

image.png

四、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的原型上又有了新的变化:

image.png

五、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的原型上又有了新的变化:

image.png

六、总结

通过这篇文章我们逐步揭开了Vue实现$nextTick、$emit、$on、$off、$forceUpdate、$watch、$delete、$set的面纱,接下来会继续讲解Vue全局API的实现。

后续有其它的原型属性的源码讲解也会在这里持续补充~