前言
上一篇了解了 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
- 处理
props
和data
数据,为它们定义get
和set
get
方法返回的是对应的this._prorps
和this._data
set
方法控制props
和data
不能够被直接赋为另一个新值
- 将
data
和props
分别代理到Vue.prototype
上的$data
和$props
,支持通过this.$data
和this.$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() 做了什么?完全销毁一个实例,清理当前组件实例与其它实例的连接,解绑它的全部指令及事件监听器,这个过程会触发
beforeDestroy
和destroyed
的钩子.
注意: 官方不推荐直接调用这个方法,最好使用 v-if
和 v-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
节点