6.实例方法

入口文件

/src/core/instance/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { initMixin, ... } from './init'

// Vue 的构造函数
function Vue (options) {
// 调用 Vue.prototype._init 方法,该方法是在 initMixin 中定义的
this._init(options)
}

// 定义 Vue.prototype._init 方法
initMixin(Vue)
/**
* 定义:
* Vue.prototype.$data
* Vue.prototype.$props
* Vue.prototype.$set
* Vue.prototype.$delete
* Vue.prototype.$watch
*/
stateMixin(Vue)
/**
* 定义 事件相关的 方法:
* Vue.prototype.$on
* Vue.prototype.$once
* Vue.prototype.$off
* Vue.prototype.$emit
*/
eventsMixin(Vue)
/**
* 定义:
* Vue.prototype._update
* Vue.prototype.$forceUpdate
* Vue.prototype.$destroy
*/
lifecycleMixin(Vue)
/**
* 执行 installRenderHelpers,在 Vue.prototype 对象上安装运行时便利程序
*
* 定义:
* Vue.prototype.$nextTick
* Vue.prototype._render
*/
renderMixin(Vue)

export default Vue

vm.$data / vm.$props

src/core/instance/state.js

这是两个实例属性,不是实例方法

1
2
3
4
5
6
7
8
9
10
// data
const dataDef = {}
dataDef.get = function () { return this._data }
// props
const propsDef = {}
propsDef.get = function () { return this._props }
// 将 data 属性和 props 属性挂载到 Vue.prototype 对象上
// 这样在程序中就可以通过 this.$data 和 this.$props 来访问 data 和 props 对象了
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)

vm.$set

/src/core/instance/state.js

1
Vue.prototype.$set = set

这个 set 函数和 5-全局API 里的 set 是一样的

1
2
3
4
// 全局 API
Vue.set = set
// 实例方法
Vue.prototype.$set = set

vm.$delete

1
Vue.prototype.$delete = del

delete 也和 5-全局API 里的 delete 一致

vm.$watch

见 4-异步更新 的 initWatch

vm.$on

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const hookRE = /^hook:/
/**
* 监听实例上的自定义事件,vm._event = { eventName: [fn1, ...], ... }
* @param {*} event 单个的事件名称或者有多个事件名组成的数组
* @param {*} fn 当 event 被触发时执行的回调函数
* @returns
*/
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) {
// event 是有多个事件名组成的数组,则遍历这些事件,依次递归调用 $on
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
// 将注册的事件和回调以键值对的形式存储到 vm._event 对象中 vm._event = { eventName: [fn1, ...] }
(vm._events[event] || (vm._events[event] = [])).push(fn)
// hookEvent,提供从外部为组件实例注入声明周期方法的机会
// 比如从组件外部为组件的 mounted 方法注入额外的逻辑
// 该能力是结合 callhook 方法实现的
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}

vm.$emit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 触发实例上的指定事件,vm._event[event] => cbs => loop cbs => cb(args)
* @param {*} event 事件名
* @returns
*/
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
if (process.env.NODE_ENV !== 'production') {
// 将事件名转换为小些
const lowerCaseEvent = event.toLowerCase()
// 意思是说,HTML 属性不区分大小写,所以你不能使用 v-on 监听小驼峰形式的事件名(eventName),而应该使用连字符形式的事件名(event-name)
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}".`
)
}
}
// 从 vm._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.$off

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 移除自定义事件监听器,即从 vm._event 对象中找到对应的事件,移除所有事件 或者 移除指定事件的回调函数
* @param {*} event
* @param {*} fn
* @returns
*/
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// 1.vm.$off() 移除实例上的所有监听器 => vm._events = {}
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// 2.移除一些事件 event = [event1, ...],遍历 event 数组,递归调用 vm.$off
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
// 3.除了 vm.$off() 之外,最终都会走到这里,移除指定事件
const cbs = vm._events[event]
if (!cbs) {
// 4.表示没有注册过该事件
return vm
}
if (!fn) {
// 5.没有提供 fn 回调函数,则移除该事件的所有回调函数,vm._event[event] = null
vm._events[event] = null
return vm
}
// 6.移除指定事件的指定回调函数,就是从事件的回调数组中找到该回调函数,然后删除
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.$once

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除
* vm.$on + vm.$off
* @param {*} event
* @param {*} fn
* @returns
*/
Vue.prototype.$once = function (event: string, fn: Function): Component {
const vm: Component = this

// 调用 $on,只是 $on 的回调函数被特殊处理了,触发时,执行回调函数,先移除事件监听,然后执行你设置的回调函数
function on() {
vm.$off(event, on)
fn.apply(vm, arguments)
}
on.fn = fn
vm.$on(event, on)
return vm
}

vm._update

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 负责更新页面,页面首次渲染和后续更新的入口位置,也是 patch 的入口位置
*/
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) {
// 首次渲染,即初始化页面时走这里
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 响应式数据更新时,即更新页面时走这里
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.
}

vm.$forceUpdate

1
2
3
4
5
6
7
8
9
10
/**
* 直接调用 watcher.update 方法,迫使组件重新渲染。
* 它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件
*/
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}

vm.$destroy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及事件监听器。
*/
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
// 表示实例已经销毁
return
}
// 调用 beforeDestroy 钩子
callHook(vm, 'beforeDestroy')
// 标识实例已经销毁
vm._isBeingDestroyed = true
// 把自己从老爹($parent)的肚子里($children)移除
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// 移除依赖监听
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
// 调用 __patch__,销毁节点
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)
if (vm.$vnode) {
vm.$vnode.parent = null
}
}

vm.$nextTick

1
2
3
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}

vm._render

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* 通过执行 render 函数生成 VNode
* 不过里面加了大量的异常处理代码
*/
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options

if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}

// 设置父 vnode。这使得渲染函数可以访问占位符节点上的数据。
vm.$vnode = _parentVnode
// render self
let vnode
try {
currentRenderingInstance = vm
// 执行 render 函数,生成 vnode
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
// 到这儿,说明执行 render 函数时出错了
// 开发环境渲染错误信息,生产环境返回之前的 vnode,以防止渲染错误导致组件空白
/* 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 是数组,并且只包含了一个元素,则直接打平
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// render 函数出错时,返回一个空的 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
}

面试题

vm.$set(obj, key, val)

vm.$set 用于向响应式对象添加一个新的 property,并确保这个新的 property 同样是响应式的,并触发视图更新。由于 Vue 无法探测对象新增属性或者通过索引为数组新增一个元素,比如:this.obj.newProperty = 'val'this.arr[3] = 'val'。所以这才有了 vm.$set,它是 Vue.set 的别名。

  • 为对象添加一个新的响应式数据:调用 defineReactive 方法为对象增加响应式数据,然后执行 dep.notify 进行依赖通知,更新视图
  • 为数组添加一个新的响应式数据:通过 splice 方法实现

vm.$delete(obj, key)

vm.$delete 用于删除对象上的属性。如果对象是响应式的,且能确保能触发视图更新。该方法主要用于避开 Vue 不能检测属性被删除的情况。它是 Vue.delete 的别名。

  • 删除数组指定下标的元素,内部通过 splice 方法来完成
  • 删除对象上的指定属性,则是先通过 delete 运算符删除该属性,然后执行 dep.notify 进行依赖通知,更新视图

vm.$watch(expOrFn, callback, [options])

vm.$watch 负责观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。当其发生变化时,回调函数就会被执行,并为回调函数传递两个参数,第一个为更新后的新值,第二个为老值。

这里需要 注意 一点的是:如果观察的是一个对象,比如:数组,当你用数组方法,比如 push 为数组新增一个元素时,回调函数被触发时传递的新值和老值相同,因为它们指向同一个引用,所以在观察一个对象并且在回调函数中有新老值是否相等的判断时需要注意。

vm.$watch 的第一个参数只接收简单的响应式数据的键路径,对于更复杂的表达式建议使用函数作为第一个参数。

至于 vm.$watch 的内部原理是:

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

vm.$on(event, callback)

监听当前实例上的自定义事件,事件可由 vm.$emit 触发,回调函数会接收所有传入事件触发函数(vm.$emit)的额外参数。

vm.$on 的原理很简单,就是处理传递的 event 和 callback 两个参数,将注册的事件和回调函数以键值对的形式存储到 vm._event 对象中,vm._events = { eventName: [cb1, cb2, …], … }。

vm.$emit(eventName, […args])

触发当前实例上的指定事件,附加参数都会传递给事件的回调函数。

其内部原理就是执行 vm._events[eventName] 中所有的回调函数。

vm.$off([event, callback])

移除自定义事件监听器,即移除 vm._events 对象上相关数据。

  • 如果没有提供参数,则移除实例的所有事件监听
  • 如果只提供了 event 参数,则移除实例上该事件的所有监听器
  • 如果两个参数都提供了,则移除实例上该事件对应的监听器

vm.$once(event, callback)

监听一个自定义事件,但是该事件只会被触发一次。一旦触发以后监听器就会被移除。

其内部的实现原理是:

  • 包装用户传递的回调函数,当包装函数执行的时候,除了会执行用户回调函数之外还会执行 vm.$off(event, 包装函数) 移除该事件
  • vm.$on(event, 包装函数) 注册事件

vm._update(vnode, hydrating)

官方文档没有说明该 API,这是一个用于源码内部的实例方法,负责更新页面,是页面渲染的入口,其内部根据是否存在 prevVnode 来决定是首次渲染,还是页面更新,从而在调用 patch 函数时传递不同的参数。该方法在业务开发中不会用到。

vm.$forceUpdate()

迫使 Vue 实例重新渲染,它仅仅影响组件实例本身和插入插槽内容的子组件,而不是所有子组件。其内部原理到也简单,就是直接调用 vm._watcher.update(),它就是 watcher.update() 方法,执行该方法触发组件更新。

vm.$destroy()

负责完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令和事件监听器。在执行过程中会调用 beforeDestroydestroy 两个钩子函数。在大多数业务开发场景下用不到该方法,一般都通过 v-if 指令来操作。其内部原理是:

  • 调用 beforeDestroy 钩子函数
  • 将自己从老爹肚子里($parent)移除,从而销毁和老爹的关系
  • 通过 watcher.teardown() 来移除依赖监听
  • 通过 vm.patch(vnode, null) 方法来销毁节点
  • 调用 destroyed 钩子函数
  • 通过 vm.$off 方法移除所有的事件监听

vm.$nextTick(cb)

vm.$nextTick 是 Vue.nextTick 的别名,其作用是延迟回调函数 cb 的执行,一般用于 this.key = newVal 更改数据后,想立即获取更改过后的 DOM 数据:

1
2
3
4
5
this.key = 'new val'

Vue.nextTick(function() {
// DOM 更新了
})

其内部的执行过程是:

  • this.key = 'new val',触发依赖通知更新,将负责更新的 watcher 放入 watcher 队列
  • 将刷新 watcher 队列的函数放到 callbacks 数组中
  • 在浏览器的异步任务队列中放入一个刷新 callbacks 数组的函数
  • vm.$nextTick(cb) 来插队,直接将 cb 函数放入 callbacks 数组
  • 待将来的某个时刻执行刷新 callbacks 数组的函数
  • 然后执行 callbacks 数组中的众多函数,触发 watcher.run 的执行,更新 DOM
  • 由于 cb 函数是在后面放到 callbacks 数组,所以这就保证了先完成的 DOM 更新,再执行 cb 函数

vm._render

官方文档没有提供该方法,它是一个用于源码内部的实例方法,负责生成 vnode。其关键代码就一行,执行 render 函数生成 vnode。不过其中加了大量的异常处理代码。

参考

https://juejin.cn/post/6953503236254859294


6.实例方法
http://example.com/2022/10/25/6-实例方法/
Author
John Doe
Posted on
October 25, 2022
Licensed under