搞明白这个,能帮助我们理解开发中出现的很多问题。
一、我们先来回顾一下vue模板的渲染过程:
(1)执行render函数,生成虚拟DOM。
render函数是根据render、templete、el这三个选项得来的,优先级依次降低。如果有render函数,则直接使用;否则如果有templete选项,则把templete作为模板编译成render函数;否则把el的outerHTML作为模板编译成render函数。单页面组件中,使用<templete>标签作为模板编译成render函数。
render函数首次执行时,会触发模板依赖数据的getter,getter会记录哪个模板用到了自己,同时模板也会记录自己用到了哪些属性,这个过程叫依赖收集。
每次模板重新渲染时,模板中的表达式、methods函数、全局函数都会重新执行。因为render函数就是个函数,当它执行时,里面的表达式和函数都会跟着执行。但这里面有一个例外,就是计算属性,因为计算属性会缓存结果,多用计算属性是个好习惯。
(2)虚拟DOM和数据结合,生成真实的DOM。
(3)如果不是首次渲染,生成的虚拟DOM会和原来的进行比较,通过diff算法,计算出来最少的需要更新的DOM。
(4)如果是首次渲染,把DOM挂载到页面中。如果是重新渲染,则更新DOM。这一步是vue能影响的最后一步。
(5)浏览器渲染页面。这一步是浏览器渲染进程操作的,和vue无关。但vue可以在上一步里,计算出DOM的最小更新,来尽量避免回流和重绘。
二、首次渲染和重新渲染的不同
(1)执行时机和原理不同
vue从创建实例,到mounted执行结束,这一整个生命周期是一个同步的过程。意味着,如果你试图在created前加async,来让异步数据获取完成后再进入beforeMount钩子,是没用的。
模板的首次渲染,也是一个同步的过程。它是从beforeMount的同步代码结束开始,同步的生成虚拟DOM、真实DOM、挂载到页面这一系列操作之后,最后进入mounted钩子。
但是,从模板首次渲染完成,也就是从进入mounted钩子开始,之后的渲染逻辑就完全不一样了,被称为异步更新队列。如果说首次渲染是生命周期驱动的,那么重新渲染,就是数据的变化驱动的。
当模板所依赖的属性被重新赋值时,该属性的setter被触发。因为这个属性记录了哪些模板用到了自己,它就通知这些模板的Watcher进入微任务队列。在同一个事件循环中,如果一个模板所依赖的多个属性发生改变,它不会被重复放入到微任务队列。这就是我们所熟悉的,多次修改数据,vue只会更新DOM一次。
哪些操作会导致模板的重新渲染呢?从mounted钩子开始,所有对模板依赖数据的更改,都会导致模板的重新渲染。
比如,在mounted中同步修改数据,在浏览器事件中修改数据,在created中异步获取数据后修改数据等。
(2)依赖数据不同
要注意的是,我们通常习惯在created里异步获取数据,异步数据是不会参与到模板的首次渲染中的。当异步数据获取成功后,数据的改变会触发模板的重新渲染。
模板首次渲染采用的数据,是prop、data、计算属性的初始值。更准确的说,是render函数开始执行之前的数据。
那么,在beforeCreate、created、beforeMount里对数据进行的同步操作,是会作用于模板的首次渲染的。而所有的异步数据,以及mounted中对数据的修改,首次渲染都是用不到的。
无论是首次渲染还是重新渲染,都是基于当前的数据。首次渲染时使用的是实例创建时的数据状态,而重新渲染则是基于数据变化后的最新状态。
三、所以,知道这些有什么用?
(1)在给data中的属性设初始值时,要考虑健壮性。不要忘了,这些初始值会用于模板的首次渲染。
在开发中我们经常犯这样的错误,就是没有考虑data初始值对首次渲染的兼容。当然,有个好处是代码会报错,帮助我们发现错误。
给我们的启发是什么呢?对data赋初值,能多赋,不要少赋。我以前喜欢把所有属性初始值都设为null,这个习惯很不好,容易报错。我们应该设置有意义的初始值,至少应该用[]、''、0这些代替null。
(2)开发中经常出现,进入页面后某个元素闪一下没了。
是因为我们在这个元素使用了v-if,首次使用初始值渲染时是有的,后面异步获取数据后重新计算就没了,所以会闪一下。
为了避免这个问题,我们应该养成一个习惯,就是元素默认是不显示的,拿到异步数据后再决定是否显示,这样就避免了闪一下的情况出现。如果在元素上再加一个进入动画,就更好了。
还有一个v-cloak指令也是解决闪烁问题的,好像是解决{{}}闪烁问题的,我还没有用过。