「这是我参与2022首次更文挑战的第41天,活动详情查看:2022首次更文挑战」
一、前情回顾 & 背景
本篇小作文讲述了 Vue 处理自定义组件产生 VNode 的方法 createComponent;这里最需要理解的就是子组件不是由 Vue 直接创建的,而是由 Vue 的子类创建的;
我们日常的开发,以写 .vue 文件为例,在 script 部分,我们导出的其实就是一个选项对象,但是即便写多个组件,他们之间互不影响,这就是因为多个组件是多个实例,实例之间是天然隔离的。既然说是实例,那么就是由构造函数创建,这个构造函数就是通过咱们写的这个选项对象扩展得来的子类;
还有一个重点就是每个组件都会有自己的 hook 对象,这个对象后四个方法 init、prepatch、insert、destroy 四个钩子方法,其中 init 负责子组件实例的初始化,这个过程相当于 new Vue 的全流程,包含子组件的模板编译、渲染函数的创建过程;
今天我们聊下一个主题——渲染 watcher,本篇的重点将放在渲染 watcher 和 Vue 中对模板中的数据进行依赖收集。
二、Vue.prototype.$mount
我们讨论的这个版本是包含编译器的版本,所以 Vue.prototype.$mount 是被重写过,在开启挂载前加入了编译获取 render 函数的逻辑,这部分逻辑在不包含编译的版本中是没有的,这一点需要注意一下;
// 缓存原有的 $mount,这个 $mount 不包含编译模板为渲染函数的逻辑
const mount = Vue.prototype.$mount
// 重写 $mount 方法
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
if (!options.render) {
if (template) {
const { render, staticRenderFns } = compileToFunctions(template, {..}, this)
// 将两个渲染函数放到 vm.$options 上,
// 即 this.$options,给 Vue.prototype._render 方法用啊
options.render = render
options.staticRenderFns = staticRenderFns
}
}
// 执行挂载,这里将作为今天的重点
// mount 是被重载之前的 Vue.prototype.$mount
return mount.call(this, el, hydrating)
}
三、mount 方法
从上面的代码可以看出,mount 方法是被重写之前的 Vue.prototype.$mount;
方法位置:src/platforms/web/runtime/index.js -> Vue.prototype.$mount
方法参数:
el,页面中的挂载点元素div#apphydrating:忽略他吧
方法作用:调用 mountComponent 创建渲染 watcher,期间会执行前面得到的 render 函数,最终实现元素渲染到页面的能力;
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
三、mountComponent
方法位置:src/core/instance/lifecycle.js -> function mountComponent
方法参数:
vm,Component,el, 页面中挂载元素div#apphydrating, 忽略它
方法作用:
- 触发
beforeMount生命周期钩子; - 声明
updateComponent方法,updateComponent是用于传递给渲染 watcher的方法,在该方法内调用vm._render方法得到虚拟 DOM,并传递给vm._update方法进入patch阶段;当渲染 watcher依赖的数据发生变化时,这个updateComponent同样会被调用,进而更新视图; - 用上一步得到的
updateComponent方法创建渲染 watcher实例,渲染 watcher也是前面数据响应式时用到的Watcher一样,都是Watcher的实例,只不过他的expOrFn是负责渲染的updateComponent方法; - 触发手动挂载是的
mounted生命周期钩子;
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') { // 没有 render 函数抛出警告 }
}
callHook(vm, 'beforeMount')
let updateComponent
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
// ...
} else {
// updateComponent 是核心
updateComponent = () => {
// 执行 vm._render() 函数,得到虚拟 VNode,
// 并将 VNode 传递给 vm._update 方法,接下来就该到 patch 阶段了
vm._update(vm._render(), hydrating)
}
}
// 这个玩意儿就是渲染 watcher
// 在初始化渲染 watcher 的时候,Watcher 类的构造函数会判断,
// 如果是渲染 watcher 就把 watcher 挂载到 vm 实例上:vm._watcher
// 之所以要这么处理,是因为渲染 watcher 的初始化可能
// 会调用时可能会调用 $forceUpdate 方法,
// 比如在某个子组件的 mounted 生命周期钩子中,
// 此时就依赖 vm._watcher 已经被定义过
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* true 就是标识这个 watcher 是渲染 watcher */)
hydrating = false
// 手动挂载实例调用 vm 上的 mounted 钩子
// 由渲染函数创建的子组件的 mounted 钩子将会在组件的 inserted hook 中调用
// inserted hook 就是前面 createComponent 的 installComponentHooks
// 时给组件的 data 增加的 hook 对象,其中有 init、prepatch、insert、destroy 钩子
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
3.1 updateCompnent
updateComponent 是个中间方法,后面作为创建渲染 watcher 的 expOrFn,这个参数是 watcher 求值的时候要执行的方法;对于渲染 watcher 来说,求值就是将虚拟 DOM 挂载到页面上的操作了。
let updateComponent
// ....
updateComponent = () => {
// 执行 vm._render() 函数,得到虚拟 VNode,并将
// VNode 传递给 vm._update 方法,接下来就该到 patch 阶段了
vm._update(vm._render(), hydrating)
}
3.2 创建渲染 watcher
new Watcher(
vm,
updateComponent,
noop,
{
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
},
true /* 渲染 watcher 标识符 */
)
3.3 再看 Watcher
export default class Watcher {
// ...
constructor (
vm: Component, // 对应上面传入的 vm
expOrFn: string | Function, // 上面的 updateComponent
cb: Function, // noop 忽略,是个空函数
options?: ?Object, // 对应上面的 { before () {...} } 对象
isRenderWatcher?: boolean // 是否渲染watcher,对应上面的 true
) {
this.vm = vm
if (isRenderWatcher) {
// 如果是渲染 watcher 就在该渲染 watcher 挂载到 vm 上
// 前面的 mountComponent 说过是给子组件中调用 vm.$forceUpdate 时准备的
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
// 把传进来的 before 方法挂载到当前 watcher 实例上
this.before = options.before
} else {
// ...
}
if (typeof expOrFn === 'function') {
// 注意这个 expOrFn 就是 updateComponent 方法了
this.getter = expOrFn
} else {
// ....
}
// 对 watcher 进行求值,就会触发上面的 expOrFn,
// 对于渲染 watcher 来说就是 updateComponent
// this.lazy 我们只在计算属性中见过 true,一般为false
this.value = this.lazy
? undefined
: this.get() // get 方法在下面,就是调用 this.getter 方法
}
// 执行 this.getter,也就是 updateComponent 方法
// this.getter 是实例化 watcher 时传的 updateComponent
// 收集模板中的依赖
get () {
// 打开 Dep.target,Dep.target = this,this 就是 watcher 实例啊
// 这个地方再下面的收集依赖的时候用
pushTarget(this)
let value
const vm = this.vm
try {
// 执行回调函数,比如 updateComponent,进入 patch 阶段
value = this.getter.call(vm, vm)
} catch (e) {
} finally {
if (this.deep) {
traverse(value)
}
// 关闭 Dep.target, Dep.target = null
popTarget()
this.cleanupDeps()
}
return value
}
//....
}
3.3.1 watcher.get & 依赖收集
前面我们在说数据响应式的时候也讲过 Watcher 了,但是这里为啥还要重新讲?这是因为两者侧重不同,这里要说的是渲染 watcher。
它的求值过程是要收集模板中的依赖,讲的是模板中的依赖的数据,比如用 :xx/v-bind:xx/{{ xx }} 是如何被收集到的一个过程;这一步骤,对于讲清楚数据更新后为啥页面更新的问题至关重要。
首先大致梳理一下调用流程:
new Watcher
-> 执行 Watcher 构造函数
-> Watcher 构造函数中 this.getter = updateComponent
-> this.get
-> this.getter 执行,等价于 updateComponent
updateComponent就做了一件简单的事情,调用vm._render()得到VNode,然后传递给vm._update()方法,这个操作就进入到patch阶段了,所谓patch就是更新(这里是新建)v-dom树,并且挂载到页面上
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
那么 vm._render() 中就会调用我们前面九牛二虎得到的 render 函数。。。。。
当然距离 render 函数调用还需要时间,我们先说说依赖收集,这个工作算是前置了;
我们找一段渲染函数代码看看:
// 处理 <span
// v-for="item in someArr"
// :key="index">{{item}}</span>
// 这是个示例
function () {
return with (this) {
_l(
(someArr), // someArr 是 data 上的数据
function (item) {
return _c(
'span',
{ key:index },
[
_v(_s(item))
]
)
}
)
}
}
这个 render 函数调用时绑定的 this 就是 Vue 的实例,所以这个 with this 里面的 someArr 是访问 Vue 实例 this.someArr,而 this.someArr 来自数据响应式的时候将 data 中的 someArr 数组处理成可观测对象后代理到 this 上的。
还记得之前我们把 data 中的数据都变成了可观测对象,即 getter 和 setter,此时访问数据就会触发 someArr 的 getter 了,大致代码如下;
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 创建 Dep 实例,这个下面的 Dep 上的 depend 会用到
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// get 拦截对 obj[key] 的读取操作
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
// 依赖收集,在 dep 中添加 watcher,也在 watcher 中添加 dep
dep.depend()
}
return value
},
// set 拦截对 obj[key] 的设置操作
set: function reactiveSetter (newVal) {
}
})
}
3.3.2 dep.depend & 依赖收集
有了上面的 Wather.prototype.get 中的 pushTarget(this) 结合 Object.defineProperty 中的 dep.depend,再看 Dep.prototype.depend 就清晰多了;
export default class Dep {
// Dep.target 是 pushTarget(watcher) 是赋值的,值就是 watcher
// 向 watcher 中增加 dep
// 把观察者放到被观察者里面去
depend () {
if (Dep.target) {
// Dep.target.addDep 就是
// Watcher.prototype.addDep 只不过 this 是 Dep 的实例
// 哪里来的呢?是 defineProperty 里 const dep = new Dep() 创建的
Dep.target.addDep(this)
}
}
// 在 dep 中增加 watcher
addSub (sub: Watcher) {
this.subs.push(sub)
}
}
3.3.3 wathcer.addDep & 依赖收集
通过上面的 defineReactive方法可以看出,每个定义为响应式的数据都有一个 dep实例,然后通过 ep.depend执行,ep.denpend又调用 dep.target.addDep也就是 Wather.prototype.addDep
而 addDep 方法做的就是:
- 把这个数据的
dep添加到watcher.newDeps数组中; - 通过
dep.push把这个watcher添加到dep.subs中;
export default class Watcher {
// ...
// 两件事:
// 1. 添加 dep 给自己 watcher
// 2. 添加自己 (watcher) 到 dep
addDep (dep: Dep) {
// 判重,如果 dep 已经存在则不重复添加
const id = dep.id
if (!this.newDepIds.has(id)) {
// 缓存 dep.id 用于判重
this.newDepIds.add(id)
this.newDeps.push(dep)
// 避免在 dep 中重复添加 watcher,
// this.depIds 的设置在 cleanupDeps 方法中
if (!this.depIds.has(id)) {
// 添加 watcher 自己到 dep 中
dep.addSub(this)
}
}
}
}
四、总结
本篇小作文的核心是 mount 方法,而 mount 方法的核心是 mountComponent 方法,该方法创建了渲染 watcher,关于渲染 watcher 我们复习了 Watcher、Dep 类,并分析了模板中的依赖收集过程:
- 首先数据响应式初始化的时候把
data.someArr通过defineReactive变成了响应式数据结构,即变成了getter/setter,最后把someArr代理到vm上 ; - 我们上面举了一个例子,即渲染函数引用了一个
someArr的数据进行列表渲染。执行render 函数的渲染 watcher执行了pushTarget(渲染watcher),Dep.target就是这个渲染 watcher了。 - 然后要列表渲染就需要访问
someArr的值,进而触发了someArr的getter; getter判断有Dep.target的值,然后将dep放到watcher.newDeps数组中,而dep则将渲染 watcher放到dep.subs数组中。- 如果数据发生变化,
dep就负责告诉渲染 watcher,数据变了,调用watcher.update方法重新求值,当然这是后话;