Vue数据绑定原理之依赖收集触发

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传入的是updateComponentcb是一个空函数noopoptions中定义了一个钩子before,最后传入了isRenderWatchertrue,表明这是一个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就是通过传入expcb来实现观测具体某个属性的。

比如:

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。

再来看这张图,至此,WatcherRender的路径我们也清晰了。

data.png

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更新时,就会调用该Watcherupdate方法。

所以上面那张图中的renderdata这条线也清晰了吧,这也就是官方文档上说的接触(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) { //... } }

介于这里内容比较复杂,暂时就不讲了,我们留着下一篇再见。

上一篇 Vue数据绑定原理之数据劫持
下一篇 vue3的数据劫持是如何做的