从 vue 源码看问题 —— vue 中的实例方法你学废了吗?

简介: 从 vue 源码看问题 —— vue 中的实例方法你学废了吗?

image.png


前言

上一篇了解了 vue 全局 api 的相关内容,为了有更好的对比,本篇一起来看看 vue 中的实例方法吧!

深入源码

index.js —— 入口文件

根据前面几篇文章的内容可以很容易知道,vue 实例方法的文件入口位置为:src\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) {
  // 提示信息,可以忽略
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // 初始化方法:Vue.prototype._init
  this._init(options)
}
// 定义 this._init 初始化方法
initMixin(Vue)
// 定义 $data $props $et $dlete $watch 实例方法
stateMixin(Vue)
// 定义 $on $once $off $emit 实例方法
eventsMixin(Vue)
// 定义 _update $forceUpdate $destroy 实例方法
lifecycleMixin(Vue)
// 定义 $nextTick _render 实例方法
renderMixin(Vue)
export default Vue
复制代码

initMixin(Vue)

文件位置:src\core\instance\init.js

这一部分在初始化篇章中就已经解读过了,因此在这就不在重复,可点击 initMixin(Vue) 查看详情.

stateMixin(Vue)

文件位置:src\core\instance\state.js

  • 处理 propsdata 数据,为它们定义 getset
  • get 方法返回的是对应的 this._prorpsthis._data
  • set 方法控制 propsdata 不能够被直接赋为另一个新值
  • dataprops 分别代理到 Vue.prototype 上的 $data$props,支持通过 this.$datathis.$props 的形式直接访问
  • 定义实例 this.$watch 方法
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.
  // 处理 data 数据,定义 get 方法,访问 this._data
  const dataDef = {}
  dataDef.get = function () { return this._data }
  // 处理 props 数据,定义 get 方法,访问 this._props
  const propsDef = {}
  propsDef.get = function () { return this._props }
  // 不允许直接替换 data 和 props 属性
  if (process.env.NODE_ENV !== 'production') {
    // 不可以通过 this.$data = newVal
    // 只能通过 this.$data.key = newVal
    dataDef.set = function () {
      warn(
        'Avoid replacing instance root $data. ' +
        'Use nested data properties instead.',
        this
      )
    }
    // 直接提示 props 是只读的
    propsDef.set = function () {
      warn(`$props is readonly.`, this)
    }
  }
  // 把 data 和 props 分别代理到 Vue.prototype 上的 $data 和 $props,支持通过 this.$data 和 this.$props 的形式直接访问
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)
  // 定义实例 this.$set 和 this.$delete 方法,它们是全局 Vue.set 和 Vue.delete 方法的别名
  Vue.prototype.$set = set
  Vue.prototype.$delete = del
  // 定义实例 this.$watch 方法
  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    // 处理 cb 是对象的情况,这里其实是为了保证后续接收到的 cb 一定是个函数
    if (isPlainObject(cb)) {
      // 通过 createWatcher 方法进行处理
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    // 标记是一个用户 watcher
    options.user = true
    // 实例化一个 watcher
    const watcher = new Watcher(vm, expOrFn, cb, options)
    // immediate 为 true 则立即执行 
    if (options.immediate) {
      const info = `callback for immediate watcher "${watcher.expression}"`
      pushTarget()
      // 通过 apply | call 调用 cd 
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
      popTarget()
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}
复制代码

eventsMixin(Vue)

文件位置:src\core\instance\events.js

export function eventsMixin (Vue: Class<Component>) {
  const hookRE = /^hook:/
  // 监听单个或者多个事件,将所有事件对应的回调放到 vm._events 对象上
  // 格式为:vm._events = { eventType1:[cb1, ...] , eventType1:[cb1,...]}
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    // 如果 event 是数组,遍历这个数组通过 vm.$on(event[i], fn) 依次进行监听
    // this.$on(['event1','event2',...], function(){})
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn)
      }
    } else {
      // 通过 vm._events 保存当前实例上监听的事件,每个事件类型以数组形式进行保存事件处理
      (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
      // 当使用了如 <comp @hoook:mounted="handleHoookMounted" /> 时,将 vm._hasHookEvent 标记设置为 true
      if (hookRE.test(event)) {
        vm._hasHookEvent = true
      }
    }
    return vm
  }
  // 监听一个自定义事件,但只触发一次,一旦触发之后,监听器就会被移除
  Vue.prototype.$once = function (event: string, fn: Function): Component {
    const vm: Component = this
    // 将外部传入的事件回调包装在 on 方法中
    // 调用方法前先移出指定事件的回调,然后通过 apply 调用外部传入的 fn
    function on () {
      vm.$off(event, on)
      fn.apply(vm, arguments)
    }
    on.fn = fn
    // 将包装的 on 函数作为,vm.$on 中的事件回调
    vm.$on(event, on)
    return vm
  }
  /**
   * 
   * 移除 vm._events 上的自定义事件监听器:
   *  1. 如果没有提供参数,则移除所有的事件监听器,vm._events = Object.create(null)
   *  2. 如果只提供了事件,则移除该事件所有的监听器,vm._events[event] = null
   *  3. 如果同时提供了事件与回调,则只移除这个回调的监听器,vm._events[event].splice(i,1)
  */
  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    const vm: Component = this
    // 没有传递 event 参数:代表移除当前实例上所有的监听事件 
    if (!arguments.length) {
      // 直接给 vm._events 赋值为一个纯对象
      vm._events = Object.create(null)
      return vm
    }
    // event 参数为数组:遍历数组依次从实例上移除监听事件
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$off(event[i], fn)
      }
      return vm
    }
    //  event 参数为字符串:从  vm._events 获取到对应的事件回调数组
    const cbs = vm._events[event]
    // 回调数组不存在则直接返回
    if (!cbs) {
      return vm
    }
    // 没有传递 fn 参数:则把当前传入的 event 事件类型全部清空
    if (!fn) {
      vm._events[event] = null
      return vm
    }
    // 存在 fn 参数:则通过循环找到 event 事件类型回调数组中对应的回调,在通过 splice 方法进行删除
    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
    /*
     这里是提示使用者,注意 HTML 属性不区分大小写,对于 HTML 上的属性尽量不要使用驼峰命名,因为编译之后全部都会变成小写形式,比如:
      html 模板中:<comp @customEvent="handler" /> 等价于 <comp @customevent="handler" />
      js 中:this.$emit('customEvent')
      这样就会导致在 js 中触发的事件名和在 HTML 模板上监听的事件名不一致的问题,更推荐用法是: 
       html 模板中:<comp @custom-event="handler" />
       js 中:this.$emit('custom-event')
    */ 
    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++) {
        // 调用回调函数,并将参数传递给回调函数,同时使用 try catch 进行异常捕获
        invokeWithErrorHandling(cbs[i], vm, args, vm, info)
      }
    }
    return vm
  }
}
复制代码

lifecycleMixin(Vue)

文件位置:src\core\instance\lifecycle.js

其中关于 Vue.prototype._update 方法中的 vm.__patch__ 涉及到 patch 操作和 diff 算法的部分,后面会单独进行解读,这里就暂时跳过.

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.
    // prevVnode 不存在,代表是初始化渲染
    if (!prevVnode) {
      // patch 阶段:patch、diff 算法
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
       // prevVnode 存在,代表是后续更新阶段
      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) {
      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 实例重新渲染,注意它仅影响实例本身和插入插槽内容的子组件,而不是所有子组件
  Vue.prototype.$forceUpdate = function () {
    const vm: Component = this
    if (vm._watcher) {
      // 核心就是执行组件实例上的 _watcher.update() 方法
      vm._watcher.update()
    }
  }
  /*
  完全销毁一个实例:
    1. 清理它与其它实例的连接,解绑它的全部指令及事件监听器
    2. 触发 beforeDestroy 和 destroyed 的钩子
  一般不会手动调用这个函数,官方推荐使用 v-if 和 v-for 的方式控制子组件的生命周期
  */ 
  Vue.prototype.$destroy = function () {
    const vm: Component = this
    // 如果当前组件已经开始销毁,直接返回
    if (vm._isBeingDestroyed) {
      return
    }
    // 调用 beforeDestroy 生命周期钩子
    callHook(vm, 'beforeDestroy')
    // 标记当前组件开始进行销毁
    vm._isBeingDestroyed = true
    // remove self from parent
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      // 将当前组件从父组件的 children 属性中移除掉
      remove(parent.$children, vm)
    }
    // teardown watchers
    // 移除和当前组件相关的所有 watcher
    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.
    // 删除当前组件上 _data.__ob__ 的引用数
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    // 标记当前组件已经销毁
    vm._isDestroyed = true
    // 在当前渲染树中调用 destroy 钩子
    vm.__patch__(vm._vnode, null)
    // 调用 destroyed 声明周期函数钩子
    callHook(vm, 'destroyed')
    // 移除当前组件的所有监听器
    vm.$off()
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // release circular reference (#6759)
    // 将当前组件的父组件设置为 null,断开和父组件中间的关系
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
}
复制代码

renderMixin(Vue)

文件位置:src\core\instance\render.js

export function renderMixin (Vue: Class<Component>) {
  // install runtime convenience helpers
  // 在组件实例上挂载一些运行时需要的工具方法
  installRenderHelpers(Vue.prototype)
  // 这里就是 Vue.nextTick 的一个别名
  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }
  // 执行组件的 render 函数,得到组件的 vnode
  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    /*
      获取 render 函数:
        1. 用户实例化 vue 时提供了 render 配置项
        2. 编译器编译模板时生成了 render 
    */ 
    const { render, _parentVnode } = vm.$options
    // 标准化作用域插槽
    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots,
        vm.$scopedSlots
      )
    }
    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      // There's no need to maintain a stack because all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      currentRenderingInstance = vm
      // 执行 render 函数得到组件的 vnode
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
        try {
          vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
        } catch (e) {
          handleError(e, vm, `renderError`)
          vnode = vm._vnode
        }
      } else {
        vnode = vm._vnode
      }
    } finally {
      currentRenderingInstance = null
    }
    // 如果返回的 vnode 数组只包含一个节点,则则直接 vnode 取这个节点 
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0]
    }
    // return empty vnode in case the render function errored out
    // 如果渲染函数中包含了多个根节点,则返回空的 vnode
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        )
      }
      vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }
}
复制代码

installRenderHelpers(Vue.prototype)

// 在组件实例上挂载一些运行时需要的工具方法
export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  // v-for
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  // 渲染静态节点
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
  target._d = bindDynamicKeys
  target._p = prependModifier
}
复制代码

总结

在前面的内容中主要涉及到以下的实例方法:

  • vm.$set
  • vm.$delete
  • vm.$watch
  • vm.$on
  • vm.$emit
  • vm.$off
  • vm.$once
  • vm._update
  • vm.$forceUpdate
  • vm.$destroy
  • vm.$nextTick
  • vm._render

其中大部分实例方法都在上一篇文章中有过解读和总结,下面就不在重复进行总结,点此可查看详情

vm.$watch(expOrFn, callback, [options]) 做了什么?观察 Vue 实例上的一个表达式或者一个函数计算结果的变化.

回调函数 callback 得到的参数为新值和旧值,表达式只接受简单的键路径,如:vm.$watch('obj.person.name', callback)

对于更复杂的表达式,用一个函数取代,如:vm.$watch(function(){ return this.obj.person.name }, callback)

注意:如果被观察的值的类型是对象,在观察一个对象并且在回调函数中有新老值是否相等的判断时需要注意

  • 数组 arr,当使用被重写的 7 个数组方法修改数组中的元素时,回调函数被触发时接收的新老值相同,因为它们指向同一个引用
  • 普通对象 obj,当通过类似 obj.key = newValue 时,回调函数被触发时接收的新老值相同,因为它们指向同一个引用

vm.$watch 处理的内容:

  • 通过设置 options.user = true,标志当前 watcher 是一个用户 watcher
  • 实例化一个 Watcher 实例,当检测到数据更新时,通过 watcher 去触发回调函数的执行,并传递新老值作为回调函数的参数
  • 返回一个 unwatch 函数,用于移除观察

vm.forceUpdate()做了什么?∗∗迫使‘Vue‘实例重新渲染,注意它只影响实例本身和插入插槽内容的子组件,而不是所有子组件,内部其实就是直接调用 ‘vm.watcher.update()‘,本质就是 ‘watcher.update()‘ 方法,执行该方法触发组件更新.∗∗vm.forceUpdate() 做了什么?** 迫使 `Vue` 实例重新渲染,注意它只影响实例本身和插入插槽内容的子组件,而不是所有子组件,内部其实就是直接调用 `vm._watcher.update()`,本质就是 `watcher.update()` 方法,执行该方法触发组件更新. **vm.forceUpdate()做了什么?迫使Vue实例重新渲染,注意它只影响实例本身和插入插槽内容的子组件,而不是所有子组件,内部其实就是直接调用vm.watcher.update(),本质就是watcher.update()方法,执行该方法触发组件更新.vm.destroy() 做了什么?完全销毁一个实例,清理当前组件实例与其它实例的连接,解绑它的全部指令及事件监听器,这个过程会触发 beforeDestroydestroyed 的钩子.

注意: 官方不推荐直接调用这个方法,最好使用 v-ifv-for 指令以数据驱动的方式控制子组件的生命周期.

详细过程如下:

  • 如果当前组件已经开始销毁,直接返回,比如多次调用 vm.$destroy()
  • 否则调用 beforeDestroy 生命周期钩子,接着通过 vm._isBeingDestroyed = true 标记组件开始销毁
  • 如果 vm.$parent 父组件存在,则将当前组件从父组件的 children 属性中移除掉
  • 通过 vm._watcher.teardown() 移除和当前组件相关的所有 watcher
  • 通过 vm._data.__ob__.vmCount-- 删除当前组件上 _data.__ob__ 的引用数
  • 通过 vm._isDestroyed = true 标识组件已完成销毁,接着调用 destroyed 声明周期函数钩子
  • 通过 vm.$off() 移除当前组件的所有监听器
  • 将当前组件的父组件设置为 null,断开和父组件之间的关系

vm._update(vnode, hydrating) 做了什么?该方法是一个用于源码内部的实例方法,主要负责更新页面,是页面渲染的入口,内部根据是否存在 prevVnode 来决定是 初始化渲染页面更新,从而在调用 patch 函数时传递不同的参数.

vm._render 做了什么?该方法是一个用于源码内部的实例方法,负责生成 vnode

  • 获取 render 函数:
  • 用户实例化 vue 时提供了 render 配置项
  • 编译器编译模板时生成了 render 函数
  • 调用 render 函数得到 vnode
  • 如果 render 函数中有多个根节点,通过 vnode = createEmptyVNode() 生成空的 vnode 节点


目录
相关文章
|
1天前
|
JavaScript 前端开发 网络架构
vue 路由器history和hash工作模式
vue 路由器history和hash工作模式
|
3天前
|
JSON JavaScript 前端开发
vue尚品汇商城项目-day00【项目介绍:此项目是基于vue2的前台电商项目和后台管理系统】
vue尚品汇商城项目-day00【项目介绍:此项目是基于vue2的前台电商项目和后台管理系统】
10 1
|
3天前
|
JavaScript 前端开发
vue尚品汇商城项目-day01【4.完成非路由组件Header与Footer业务】
vue尚品汇商城项目-day01【4.完成非路由组件Header与Footer业务】
13 2
|
3天前
|
JavaScript 前端开发
vue尚品汇商城项目-day01【3.项目路由的分析】
vue尚品汇商城项目-day01【3.项目路由的分析】
11 1
|
3天前
|
JavaScript 前端开发 数据安全/隐私保护
vue尚品汇商城项目-day01【5.路由组件的搭建】
vue尚品汇商城项目-day01【5.路由组件的搭建】
9 0
vue尚品汇商城项目-day01【5.路由组件的搭建】
|
3天前
|
JSON 缓存 JavaScript
vue尚品汇商城项目-day01【1.vue-cli脚手架初始化项目生成文件的介绍】
vue尚品汇商城项目-day01【1.vue-cli脚手架初始化项目生成文件的介绍】
10 0
|
3天前
|
JavaScript
vue尚品汇商城项目-day01【2.vue-cli脚手架初始化项目的其他配置】
vue尚品汇商城项目-day01【2.vue-cli脚手架初始化项目的其他配置】
9 0
|
3天前
|
JavaScript
vue尚品汇商城项目-day01【6.Footer组件的显示与隐藏】
vue尚品汇商城项目-day01【6.Footer组件的显示与隐藏】
11 0
|
3天前
|
JavaScript 前端开发
vue尚品汇商城项目-day01【7.路由传参】
vue尚品汇商城项目-day01【7.路由传参】
11 0
|
3天前
|
JavaScript API
vue尚品汇商城项目-day02【vue插件-13.nprogress进度条的使用】
vue尚品汇商城项目-day02【vue插件-13.nprogress进度条的使用】
8 0