Vue源码之渲染watcher
Posted 天地会珠海分舵
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue源码之渲染watcher相关的知识,希望对你有一定的参考价值。
1. 前文回顾
在上一篇文章《Vue源码之计算属性watcher》中,我们学习了计算属性watcher是如何与计算属性的computedGetter协作,在计算属性所依赖的数据发生变化时,通知渲染watcher去设置一个dirty标识位,然后在页面更新过程中,通过render方法计算最新的虚拟DOM时,会读取页面引用的该计算属性,从而触发computedGetter,发现该计算属性变dirty了,继而会调用对应的计算属性watcher的求值方法evaluate,然后调用get方法,即我们自己写的计算属性函数,来计算出最新的计算属性值,最终交给patch来渲染最新的真实DOM呈现给用户。
这里提到了渲染watcher,我们此时还只是知其然而不知其所以然,今天我们就来扒一扒。不过开始之前,我们还是先列下vue使用watcher 的几个场景
- 计算属性watcher: vue会为我们在computed选项写的每个我们自定义的计算属性创建一个计算属性watcher,该watcher会在依赖的响应式数据变化时将计算属性标志位设置成dirty,使得页面在下次更新时调用计算属性函数进行求值。
- 渲染watcher: vue会为每个组件创建一个渲染watcher来在依赖的响应式数据状态发生变化时重新渲染页面
- 用户watcher: vue会为我们在watch选项写的每个要监控的属性创建一个watcher来在其变化时执行提供的回调函数
2. vue实例和每个vue组件实例拥有各自的渲染watcher
其实vue不仅会为每个组件创建一个渲染watcher,还会为我们在main.js中创建的vue实例对象创建一个渲染watcher。但是因为Vue类和VueComponent类都是指向相同的prototype的,所以Vue实例对象也可以看成是组件实例对象,只是有些轻微的区别,比如组件实例对象的options配置项不能提供$el选项之类的。
Vue和VueComponent的关系今后会另外开一篇文章来分析,在本文中,我们只需要知道他们的prototype是一样的,即Vue类和VueComponent类的Vue.prototype._init是同一个方法。
而从下面的分析中我们将会看到,凡是经过_init发起的一系列调用,无论是vue实例还是组件实例,都会创建一个与之对应的渲染watcher。
现在我们先从new Vue开始分析,看下这个渲染watcher是怎么建立起来的。
3. 渲染watcher创建流程分析
3.1. vue实例初始化和vue组件实例初始化用的是同一个_init流程
首先,假设我们有代码如下
<!DOCTYPE html>
<html lang="en">
...
<body>
<div id="app">
<div>msg</div>
</div>
<script>
const vm = new Vue(
el: "#app",
data:
msg: "Hello world!",
,
methods: ,
);
</script>
</body>
</html>
文件的目的就是通过vue实现一个页面显示data里面的Hello world!这个msg,期间会通过new Vue调用Vue构造函数来实例化Vue实例对象。
下面我们看下Vue的构造函数
function Vue(options)
...
this._init(options);
直接调用_init, 但是要注意这里的_init是在Vue.prototype下面的,即Vue类和VueComponent类用的是同一个。
这里我们顺便看下创建组件的代码,其实里面也是调用了_init方法的
Vue.extend = function (extendOptions: Object): Function
extendOptions = extendOptions || ;
...
const Sub = function VueComponent(options)
this._init(options);
;
...
return Sub;
;
这里的extend方法其实是vue提供的创建一个组件的的方法,参数就是我们平时写的data,methods这些options。 该方法返回的是组件的构造函数VuecComponent。
我们平时写代码都是import一个组件,然后components上声明使用下该组件,然后就直接在模板中进行使用,并没有调用过 extend方法,那是因为vue框架在背后为我们做了这个事情。
这个’背后‘,指的就是在patch(关于patch更多的分析,请参考之前的文章《Vue源码虚拟DOM将去往何处?》)根据虚拟DOM更新真实DOM的过程中,会调用组件构造函数来为该组件节点创建相应的组件实例。
好,我们继续分析_init方法
3.2. 初始化过程会进行组件的挂载并创建对应渲染watcher
Vue.prototype._init = function (options?: Object)
const vm: Component = this;
// a uid
vm._uid = uid++;
...
if (vm.$options.el)
// debugger;
vm.$mount(vm.$options.el);
;
_init中会调用一系列的初始化函数来处理我们选项中提供的data,methods,props等选项,但是因为和这里我们的分析关系不是很大,为了更好的理解,这里统统省略掉。
因为我们在上面的示例代码中提供了$el选项,这里会进入到vm.$mount的代用逻辑。如果没有提供$el选项的话,那么示例代码中new 完Vue之后必须手动调用下$mount方法。
我们继续往下看$mount的调用。
这里$mount有两个版本,如果最终所用的vue是带有compiler的版本,那么会先进入到platforms/web/entry-runtime-with-compiler页面中的$mount进行模板编译等处理,然后再调用platforms/web/runtime中的$mount,否则直接调用后者。
这里我们并不关心模板编译之类的,所以我们直接看后面的$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating);
;
这里$mount的函数体我连省略号都没有写,因为它真的就只有mountComponent这一行。注意这里的this,因为$mount函数是Vue的原型函数,且这里一系列的调用都是在new Vue创建Vue实例对象或者new VueComponent的语境下发生的,所以这里的this指向的就是Vue实例对象或者Vue组件实例对象。
export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component
vm.$el = el;
...
callHook(vm, "beforeMount");
let updateComponent;
...
updateComponent = () =>
const vnode = vm._render();
vm._update(vnode, hydrating);
;
new Watcher(
vm,
updateComponent,
noop,
before()
if (vm._isMounted && !vm._isDestroyed)
callHook(vm, "beforeUpdate");
,
,
true /* isRenderWatcher */
);
...
return vm;
这里创建的watcher就是所谓的渲染watcher,因为同一个组件和vue实例都只会创建一次,即_init下来的这一系列方法只会调用一次,所以我们通常说一个组件对应个一个渲染watcher。
这里updateComponent方法我们在前面的虚拟DOM相关的文章也都分析过,里面的render和update方法是vue组件的灵魂,render负责重新生成虚拟DOM,而update负责新老虚拟DOM做diff然后上树更新页面。
3.3. 渲染watcher内部构建细节
这根据渲染watcher创建时提供的参数,我们可以整理下watcher的构造函数:
export default class Watcher
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
)
this.vm = vm;
if (isRenderWatcher)
vm._watcher = this;
vm._watchers.push(this);
// options
if (options)
...
this.lazy = !!options.lazy;
...
if (typeof expOrFn === "function")
this.getter = expOrFn;
else
...
this.value = this.lazy ? undefined : this.get();
首先,第二个参数updateComponent作为expOrFn形参对应的实参,是个function,所以会赋值给getter, 而getter会在watcher的get方法中触发,这我们往下会看到。
然后第四个参数作为options形参对应的实参,没有提供lazy设置,所以这了this.lazy为false。也就是最后那一句代码会判断为去执行this.get()方法,即watcher的get方法。
最后一个参数实参为true,那么将该渲染watcher放到组件实例对象的_vm中保存起来,以便今后使用。比如我们也许会用过Vue的forceUpdate方法,里面的实现就是直接调用_vm的update方法来强制更新真实DOM的。
Vue.prototype.$forceUpdate = function ()
const vm: Component = this;
if (vm._watcher)
vm._watcher.update();
;
3.4. 渲染watcher如何触发渲染
我们这里继续看下watcher的get方法
get()
pushTarget(this);
let value;
const vm = this.vm;
try
value = this.getter.call(vm, vm);
catch (e)
...
finally
...
popTarget();
...
return value;
根据上面的分析,这里的getter就是updateComponent方法。所以这里的get方法就是去调用updateComponent方法来去收集依赖,因为updateComponent调用render方法去生成虚拟DOM的时候会去读页面模板引用到的data等上面的数据,这时就会触发这些属性在defineReactive时创建的对应的getter,从而将渲染watcher收集为该数据的依赖,也就是将渲染watcher放到对应数据的dep.subs数组下。
既然渲染watcher被作为data下的数据收集为依赖,一旦该数据发生变化,必然就会被notify通知到对应的渲染watcher来更新,从而触发watcher的get方法,继而调用updateComponent来重新生成虚拟DOM,并渲染最新的更新到页面。
4. 一些前置知识
如果你对上面两段话比较迷糊的话,那很有可能你还没有学习watcher的相关实现知识,这时我会建议你先看下我之前的几篇文章:
以上两篇文章的知识点必须要有。下面两篇说虚拟DOM的最好也能看下
以上是关于Vue源码之渲染watcher的主要内容,如果未能解决你的问题,请参考以下文章