Vue数据绑定原理之依赖收集触发
在上一篇我们讲到了数据劫持,和数据观测。那么怎么将数据和相关的DOM关联起来呢?本篇我们将解开这个过程。
从实例化Watcher开始
上一篇讲解中我们知道Watcher
是实际执行数据变更之后操作的主要对象,我们先找到它的实例化路径,发现它是在mount
的时候进行的操作。
Vue -> this._init -> initLifecycle -> mountComponent
我们在这个方法中找到了关于Watcher
的实例化代码
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
结合之前的Watcher
构造函数:
class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
}
}
我们先解释下参数:首先传入了组件实例vm
,然后是expOrFn
传入的是updateComponent
,cb
是一个空函数noop
,options
中定义了一个钩子before
,最后传入了isRenderWatcher
为true
,表明这是一个RenderWatcher
,就会将该Wathcer
挂载到组件上。
而这里关键的地方就是updateComponent
。我们在上一篇分析中提到,当数据变更,依赖会通知所有订阅者Watcher
做出相应更新,也就是watcher.update
,而watcher.update
不管是同步还是异步,其核心是调用wathcer.run
去执行相关操作。
run () {
if (this.active) {
const value = this.get()
// ...
this.cb.call(this.vm, value, oldValue)
}
}
这个函数有两个关键的地方,一个是获取值,调用了get
方法,而另一个就是执行回调函数cb
,在上一篇我们同样提到,我们自定义的watch
就是通过传入exp
和cb
来实现观测具体某个属性的。
比如:
new Vue({
data: {
msg: ''
},
watch: {
msg: function() {}
}
})
这里的watch
就是通过new Watcher(vm, 'msg', fn)
类似这样的方式定义的,这和我们现在看见的完全不一样。
这也是困惑的一点,我们现在看到的RenderWatcher
传入了一个空函数作为cb
,也就是说执行cb
是没有任何作用的,那么在数据更新时是怎么通知到视图层的呢?我们发现在run
方法中,除了执行cb
外,还执行了get
方法。这就是关键!
在上一篇中我们提到get
方法其实调用的就是getter
,在传入的第二个参数expOrFn
类型为function
时,getter = expOrFn
。那还记得传了什么进去吗?updateComponent
!
我们理一下思路,并暂时移除掉无关代码:
// function mountComponent
new Watcher(vm, updateComponent, noop)
// class Watcher
class Watcher {
constructor(vm, fn, cb) {
this.vm = vm
this.getter = fn
this.value = this.get()
}
get() {
this.getter.call(this.vm, this.vm)
}
update() {
this.run()
}
run() {
const value = this.get()
// ...
this.cb.call(this.vm, value, this.value)
}
}
这里有几个要点。第一,在初始化Watcher
的时候就调用过一次get
;第二,在数据更改触发更新时又会调用get
。再根据实际执行的函数名updateComponent
,我想你也猜到了,这个函数就是用来渲染DOM的,并且在每次观测到数据变更时都会重新渲染DOM。
再来看这张图,至此,Watcher
到Render
的路径我们也清晰了。
updateComponent
我们猜测该函数是用来更新DOM的,但我们还是得实际看一下它是如何实现的,因为这里面其实涉及到了更多技术,十分值得学习。
那我们还是一步一步的来,看完相关代码,可以总结出:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
它最主要就是调用了两个函数,_render
和_update
。
_render
我们先来看看_render
,它是通过renderMixin
加在原型上的,所以相关定义会在不同的地方。
Vue.prototype._render = function (): VNode {}
我们先看下这个函数声明,其返回值是一个VNode
类型,如果你有仔细读过官方文档,你就会对这个词有点印象。
在创建一个Vue组件的时候我们可以不使用template
选项来写DOM模板,而使用render
选项。而render
函数返回值的类型就是VNode
。很显然,从函数名上来看,内部的_render
是对传入render
的二次包装。
看一看源码概括:
Vue.prototype._render = function (): VNode {
const { render, _parentVnode } = vm.$options
// ...
vnode = render.call(vm._renderProxy, vm.$createElement)
// ...
return vnode
}
该函数调用了render
并返回了VNode
。
这里发什么什么?仅仅是调用render
函数这么简单吗?
我们来看看比如下面这个render
函数:
render: function (createElement) {
return createElement('h1', this.blogTitle)
}
他用到了this.blogTitle
,很明显这里是访问属性,也就是会调用到该属性的get
方法,上一篇我们再讲Observer
时讲过,属性的get
里面会进行依赖收集。此时,blogTitle
有了新的订阅者subs.push(Watcher)
,而该Watcher
的依赖deps
也增加了blogTitle
,在blogTitle
更新时,就会调用该Watcher
的update
方法。
所以上面那张图中的render
到data
这条线也清晰了吧,这也就是官方文档上说的接触
(touched
)!
_update
OK,其实到这里,整个数据劫持,依赖收集过程都已经很明了了。接下来的是Vue怎么优化DOM渲染,提升性能的操作。
我们现在知道_render
是创建虚拟DOM的,那么创建完虚拟DOM之后干嘛?当然是渲染成真实DOM啊!这也就是_update
的作用,那为什么它叫做update
而不是create
或者transform
呢?这也是有知识点在里面的。
// Vue.prototype._update
const prevVnode = vm._vnode
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
能看见清晰的注释,initial render
/updates
,也就是该方法处理了新建和更新两种操作。新建的时候会将VNode
挂载在vm
上表示已经创建过了,之后只需要更新就行了,减少消耗。
而这里又用到了另一个方法__patch__
:
// runtime/index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop
// runtime/patch.js
export const patch: Function = createPatchFunction({ nodeOps, modules })
// vdom/patch.js
export function createPatchFunction (backend) {
return function patch (oldVnode, vnode, hydrating, removeOnly) {
//...
}
}
介于这里内容比较复杂,暂时就不讲了,我们留着下一篇再见。