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的主要内容,如果未能解决你的问题,请参考以下文章

Vue源码之用户watcher

Vue源码之用户watcher

Vue源码实现之watcher拾遗

Vue源码实现之watcher拾遗

vue是如何避免重复渲染的?

Vue源码之计算属性watcher