Vue渲染原理讲解


本文的主要内容是详细地介绍Vue的内部渲染原理,从而帮助大家深入掌握关于Vue Options、生命周期等概念。为了帮助Vue使用经验较少的同学快速理解Vue,我们先从Vue的简介开始,第二部分再详细介绍Vue渲染原理。

 Vue官方网站。

为了简单,我们先以一个cdn版本的Vue为例:

<script type="text/javascript" src="https://unpkg.com/vue"></script>

="app"></div> <script> let app = new Vue({ el: '#app', data: { title: '标题' }, template: '

{{title}}

', methods: { changeTitle (title) { this.title = title; } } }); setTimeout(function () { app.changeTitle('新标题'); }, 1000); </script>

执行完script脚本对应的框架代码后,window上会新增一个构造函数Vue,用于构建Vue实例。我们向new Vue传入了一个配置对象,这个对象包含如el、data、template、methods等属性,用于为Vue实例添加属性和方法。Vue会根据这些配置,生成一个可以自动生成视图的响应式的Vue组件,它不仅负责管理视图层和业务层,还负责两者的同步。

我们来简单看一下一些常用配置的作用:

  1. el
    根元素,该参数只能由根节点声明,表示当前Vue应用需要被挂载到页面的哪个DOM节点上。如上面的例子指定了根元素为#app,那么该Vue实例生成的DOM就会直接替换id为app的元素。
  2. name
    组件的名字,主要用于全局注册组件,如:
import MyComponent from 'MyComponent';
Vue.component(MyComponent.name, MyComponent);
  1. components
    声明当前组件的外部依赖,相当于局部注册组件,在编写单组件时,如果需要用到其他的项目内组件通常会提供该参数。
  2. props
    来自父级组件的数据依赖,这个依赖是响应式的。
  3. data
    业务数据,这个参数是model层的核心,相关的业务逻辑都是围绕data展开的。
  4. computed
    计算属性,定义一组变量,这组变量的值是基于一个或多个props、data计算而来,computed内变量的值会根据这些依赖的值变化而自动更新,并且会自动缓存上次的计算结果。
  5. watch
    手动监控props、data或者computed的变化,定义变化时的回调函数。
  6. 生命周期方法
    定义Vue组件在各个生命周期需要执行的回调函数,Vue在执行到对应的阶段时会调用它们。生命周期与Vue组件创建的细节是第二部分渲染原理的重点。
  7. methods
    组件的工具方法集。methods定义了一组工具方法,可以在computed、watch、生命周期方法或者其他工具方法中调用。

有了这些基本知识的铺垫,下面我们就开始详细介绍Vue的渲染过程。

Vue源码笔记之响应式系统。

Vue源码笔记之虚拟DOM,里面有详细的patch过程和图解。

也就是说,调用updateComponent时,如果组件尚未渲染,则依据vnode渲染组件(该过程主要就是用document.createElement创建真实DOM标签,然后用appendChild添加到页面上);如果组件已经存在,则比对vnode,产生高效更新算法,用原生的DOM方法去操作真实DOM,完整视图更新。

显然,定义这个函数是为了在数据变化时自动调用以更新视图,也就是说它必须接入到响应式系统才有意义。接下来的代码就是将其接入响应式系统:

function mountCompnent () {
  ...
  // 上述函数
  const updateComponent = () => {
    vm._update(vm._render());
  }
  // 将updateComponent接入响应式系统
  new Watcher(vm, updateComponent, noop, {
    before () {
      callhook('beforeUpdate');
    }
  })
}

还记得watcher的作用吗,它是数据对象的订阅者,负责在数据变化时执行某些操作。上面的代码为当前组件实例构造了一个watcher,初始化watcher的过程中会触发data属性的get方法,因此这个watcher就会被Dep收集起来,传入的回调函数正是它的updateComponent方法。当数据变化时,Observer会通知Dep,Dep依次调用订阅者watcher的run方法,run里面会执行上述回调函数(即updateComponent),于是视图得到更新。这样就实现了修改数据之后自动更新视图。再次看一下此时data的结构:

this._data = {
    message: ‘’,
    __ob__: {
        dep: {
             subs: []
        }
    },
    get message (): {}  // 调用get时,依赖会被收集
    set message (): {    // 内部包含对该属性的观察者对象
        // 这里包含组件内对message的订阅者(watcher)
    }
}

注意,_data内的__ob__.dep.subs保存的并不是当前组件内的watcher,而是外部watcher。如果当前模板中这样绑定了data内的message:

{{message}}
,那么组件对应的watcher就会以闭包的形式保存在set message () { ... }内。当message值变化时,set方法就会调用,它会继而触发依赖收集者dep的notify方法通知订阅者,该方法会依次调用subs中每个watcher的run方法,依次调用它们提供的回调函数。对于用于更新组件的watcher来说,这个回调函数就是它的updateComponent方法。

而每当updateComponent被调用前,Vue都会调用callHook('beforeUpdate'),执行该生命周期钩子函数,因为视图即将被更新。当然,当updateComponent执行完毕后,Vue又会调用callHook('updated'),执行更新完毕的生命周期钩子函数。

最后是组件的销毁过程。当手动调用this.$destroy(),或由于v-if属性等原因导致组件必须被销毁时,Vue主要执行了以下过程:
在这里插入图片描述
当触发$destroy方法时,首先是调用beforeDestroy生命周期钩子函数。接着主要是清除组件的依赖关系,以及销毁watcher等。此时组件已经失去了响应能力,相当于它的状态被销毁了,因此Vue会调用destroyed生命周期钩子函数。最后注销组件的事件监听,清除一些附属参数,组件彻底被销毁(对于Vue组件来说,一旦状态被销毁,它就被认为是销毁了,所以destroyed是在事件被销毁前调用的)。

最后附赠本文的示例代码和完整的console输出供大家学习:

<!DOCTYPE html>
  >
  >
       <script src="vue-2.6.10-learning.js"></script>
  </head>
  >
    
="app"></div> <script> var app = new Vue({ el: '#app', template: "

标题

{{ message }}

", data () { return { message: 1 } }, mounted () { console.log(this.$data); setTimeout(() => { this.message = 2; }, 1000); setTimeout(() => { this.$destroy(); }, 5000); } }) </script> </body> </html>

在这里插入图片描述

总结

本文主要讲解了Vue组件的完整渲染过程,如果能结合源码看本文,效果会更好。通过本文,我希望能够帮助读者对Vue的渲染过程有一个全局的了解,从而能够更深入地思考实际项目中出现的一些问题。

vue