mountComponent代码:src/core/instance/lifecycle.js
第一部分的工作
// 核心就是先实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法,🚀
// 在此方法中调用 vm._render 方法先生成虚拟 Node,最终调用 vm._update 更新 DOM。
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
// 判断实例上是否存在渲染函数,如果不存在,则设置一个默认的渲染函数createEmptyVNode,该渲染函数会创建一个注释类型的VNode节点。
vm.$options.render = createEmptyVNode
/* istanbul ignore if */
}
// 生命周期函数🚀该钩子函数触发后标志着正式开始执行挂载操作
callHook(vm, 'beforeMount')
let updateComponent // 先_render生成虚拟DOM,再_update生成真实DOM
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
// 如果调用了updateComponent函数,就会将最新的模板内容渲染到视图页面中,这样就完成了挂载操作的一半工作
updateComponent = () => {
console.log('这部分删掉了');
}
} else {
updateComponent = () => {
//核心🚀
/**
* 首先执行渲染函数vm._render()得到一份最新的VNode节点树,返回一个VNode树
* 然后执行vm._update()方法对最新的VNode节点树与上一次渲染的旧VNode节点树进行对比并更新DOM节点(即patch操作),完成一次渲染
* 目的: 将最新的模板内容渲染到视图页面中,这样就完成了挂载操作的一半工作,即图中的上半部分:
*/
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
// 另外一半工作🚀🌟🌟🌟🌟🌟🌟🌟:还要开启对模板中数据(状态)的监控
new Watcher(
vm, // 第一个参数
updateComponent, // 第二个参数 渲染更新函数
noop, // 第三个参数
{// 第四个参数
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
},
true /* isRenderWatcher */
)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true// 设置 vm._isMounted 为 true🚀, 表示这个实例已经挂载了,同时执行 mounted 钩子函数
callHook(vm, 'mounted')
}
return vm
}
调用了updateComponent函数,就会将最新的模板内容渲染到视图页面中,这样就完成了挂载操作的一半工作,即图中的上半部分:
第二部分的工作
这是因为在挂载阶段不但要将模板渲染到视图中,同时还要开启对模板中数据(状态)的监控,当数据(状态)发生变化时通知其依赖进行视图更新。
接下来创建了一个Watcher实例,并将定义好的updateComponent函数传入。要想开启对模板中数据(状态)的监控,这一段代码是关键
new Watcher(
vm, // 第一个参数
updateComponent, // 第二个参数
noop, // 第三个参数
{ // 第四个参数
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
},
true // 第五个参数
)
可以看到,在创建Watcher实例的时候,传入的第二个参数是updateComponent函数。
回顾一下我们在数据侦测篇文章中介绍Watcher类的时候,Watcher类构造函数的第二个参数支持两种类型:函数和数据路径(如a.b.c)。如果是数据路径,会根据路径去读取这个数据;如果是函数,会执行这个函数。一旦读取了数据或者执行了函数,就会触发数据或者函数内数据的getter方法,而在getter方法中会将watcher实例添加到该数据的依赖列表中,当该数据发生变化时就会通知依赖列表中所有的依赖,依赖接收到通知后就会调用第四个参数回调函数去更新视图。
换句话说,上面代码中把updateComponent函数作为第二个参数传给Watcher类从而创建了watcher实例,那么updateComponent函数中读取的所有数据都将被watcher所监控,这些数据中只要有任何一个发生了变化,那么watcher都将会得到通知,从而会去调用第四个参数回调函数cb去更新视图,如此反复,直到实例被销毁。
这样就完成了挂载阶段的另一半工作。
如此之后,挂载阶段才算是全部完成了,接下来调用挂载完成的生命周期钩子函数mounted。
Object数据的变化侦测篇的联系
Watcher类的具体实现如下:
export default class Watcher {
constructor (vm,expOrFn,cb) { // expOrFn可以传入updateComponent函数
this.vm = vm; //实例
this.cb = cb;// 更新的时候执行的回调函数
this.getter = parsePath(expOrFn)
this.value = this.get()
}
get () { //该函数创建的时候就会执行
window.target = this;//将当前实例置为全局的一个target的点
const vm = this.vm
let value = this.getter.call(vm, vm) //执行getter函数,触发updateComponent函数
window.target = undefined; //将target置为undefined
return value
}
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue) // 更新的时候执行传入的cb回调函数
}
}
下面我们分析Watcher类的代码实现逻辑:
- 当实例化
Watcher类时,会先执行其构造函数; - 在构造函数中调用了
this.get()实例方法; - 在
get()方法中,首先通过window.target = this把实例vm(也就是this)自身赋给了全局的一个唯一对象window.target上,然后通过let value = this.getter.call(vm, vm)获取一下被依赖的数据(updateComponent函数) - 🌟获取被依赖数据的目的是触发该数据上面的
getter,上文我们说过,在getter里会调用dep.depend()收集依赖,而在dep.depend()中取到挂载window.target上的值并将其存入依赖数组中,在get()方法最后将window.target释放掉。 - 🌟而当数据变化时,会触发数据的
setter,在setter中调用了dep.notify()方法,在dep.notify()方法中,遍历所有依赖(即watcher实例),执行依赖(即watcher实例)的update()方法,也就是Watcher类中的update()实例方法,在update()方法中调用数据变化的更新回调函数(cb参数),从而更新视图。
简单总结一下就是:Watcher先把自己设置到全局唯一的指定位置(window.target),然后读取数据。因为读取了数据,所以会触发这个数据的getter。接着,在getter中就会从全局唯一的那个位置读取当前正在读取数据的Watcher,并把这个watcher收集到Dep中去。收集好之后,当数据发生变化时,会向Dep中的每个Watcher发送通知。通过这样的方式,Watcher可以主动去订阅任意一个数据的变化。为了便于理解,我们画出了其关系流程图,如下图: