-
前言
-
什么是虚拟dom
- virtual DOM 虚拟DOM,用普通js对象来描述DOM结构,因为不是真实DOM,所以称之为虚拟DOM。
虚拟 dom 是相对于浏览器所渲染出来的真实 dom而言的,在react,vue等技术出现之前,我们要改变页面展示的内容只能通过遍历查询 dom 树的方式找到需要修改的 dom 然后修改样式行为或者结构,来达到更新 ui 的目的。
这种方式相当消耗计算资源,因为每次查询 dom 几乎都需要遍历整颗 dom 树,如果建立一个与 dom 树对应的虚拟 dom 对象( js 对象),以对象嵌套的方式来表示 dom 树及其层级结构,那么每次 dom 的更改就变成了对 js 对象的属性的增删改查,这样一来查找 js 对象的属性变化要比查询 dom 树的性能开销小。
- virtual DOM 虚拟DOM,用普通js对象来描述DOM结构,因为不是真实DOM,所以称之为虚拟DOM。
-
为什么需要虚拟dom
- 在vue中,渲染视图会调用render函数,这种渲染不仅发生在组件创建时,同时发生在视图依赖的数据更新时。如果在渲染时,直接使用真实DOM,由于真实DOM的创建、更新、插入等操作会带来大量的性能损耗,从而就会极大的降低渲染效率。因此,vue在渲染时,使用虚拟dom来替代真实dom,主要为解决渲染效率的问题。
-
虚拟dom是如何转化为真实dom
- 在一个组件实例首次被渲染时,它先生成虚拟dom树,然后根据虚拟dom树创建真实dom,并把真实dom挂载到页面中合适的位置,此时,每个虚拟dom便会对应一个真实的dom。
如果一个组件受响应式数据变化的影响,需要重新渲染时,它仍然会重新调用render函数,创建出一个新的虚拟dom树,用新树和旧树对比,通过对比,vue会找到最小更新量,然后更新必要的虚拟dom节点,最后,这些更新过的虚拟节点,会去修改它们对应的真实dom这样一来,就保证了对真实dom达到最小的改动。
- 在一个组件实例首次被渲染时,它先生成虚拟dom树,然后根据虚拟dom树创建真实dom,并把真实dom挂载到页面中合适的位置,此时,每个虚拟dom便会对应一个真实的dom。
-
-
双向绑定原理
-
简介
- 响应式数据的最终目标,是当对象本身或者对象属性发生变化时,将会运行一些函数,最常见的就是render函数,在具体实现上,vue使用了几个核心部件
-
Observer
- Observer要实现的目标非常简单,就是把一个普通的对象转换为响应式的对象。为了实现这一点,Observer把对象的每个属性通过`0bject.defineProperty转换为带有gtter和setter 的属性,这样一来,当访问或设置属性时,vue就有机会做一些别的事情。Observer是vue内部的构造器,我们可以通过Vue提供的静态方法vue.observable(object)间接的使用该功能在组件生命周期中,这件事发生在beforecreate之后,created之前。具体实现上,它会递归遍历对象的所有属性,以完成深度的属性转换。由于遍历时只能遍历到对象的当前属性,因此无法监测到将来动态增加或删除的属性,因此vue 提供了$set 和$delete两个实例方法,让开发者通过这两个实例方法对已有响应式对象添加或删除属性。对于数组,vue 会更改它的隐式原型,之所以这样做,是因为vue需要监听那些可能改变数组内容的方法。总之,Observer的目标,就是要让一个对象,它属性的读取、赋值,内部数组的变化都要能够被vue感知到。
-
Dep
- 这里有两个问题没解决,就是读取属性时要做什么事,而属性变化时要做什么事,这个问题需要依靠Dep来解决。
Dep的含义是Dependency,表示依赖的意思。vue 会为响应式对象中的每个属性、对象本身、数组本身创建一个Dep 实例,每个Dep 实例都有能力做以下两件事:
记录依赖:是谁在用我
派发更新:我变了,我要通知那些用到我的人
当读取响应式对象的某个属性时,它会进行依赖收集:有人用到了我当改变某个属性时,它会派发更新:那些用我的人听好了,我变了
- 这里有两个问题没解决,就是读取属性时要做什么事,而属性变化时要做什么事,这个问题需要依靠Dep来解决。
-
Watcher
- 这里又出现一个问题,就是Dep如何知道是谁在用我?要解决这个问题,需要依靠另一个东西,就是Watcher。当某个函数执行的过程中,用到了响应式数据,响应式数据是无法知道是哪个函数在用自己的因此,vue通过一种巧妙的办法来解决这个问题我们不要直接执行函数,而是把函数交给一个叫做watcher的东西去执行,watcher是一个对象,每个这样的函数执行时都应该创建一个watcher,通过watcher去执行。
watcher会设置一个全局变量,让全局变量记录当前负责执行的watcher等于自己,然后再去执行函数,在函数的执行过程中,如果发生了依赖记录dep.depend(),那么Dep就会把这个全局变量记录下来,表示:有一个watche用到了我这个属性。当Dep进行派发更新时,它会通知之前记录的所有watcher:我变了。
- 这里又出现一个问题,就是Dep如何知道是谁在用我?要解决这个问题,需要依靠另一个东西,就是Watcher。当某个函数执行的过程中,用到了响应式数据,响应式数据是无法知道是哪个函数在用自己的因此,vue通过一种巧妙的办法来解决这个问题我们不要直接执行函数,而是把函数交给一个叫做watcher的东西去执行,watcher是一个对象,每个这样的函数执行时都应该创建一个watcher,通过watcher去执行。
-
Scheduler
- 现在还剩下最后一个问题,就是Dep通知watcher之后,如果watcher执行重运行对应的函数,就有可能导致函数频繁运行,从而导致效率低下试想,如果一个交给watcher的函数,它里面用到了属性a、b、c、d,那么a、b、c、d属性都会记录依赖,于是下面的代码将触发4次更新:
-
这样显然是不合适的,因此,watcher收到派发更新的通知后,实际上不是立即执行对应函数,而是把自己交给个叫调度器的东西。state.a = "new data"; state.b = "new data"; state.c = "new data”; state.d = "new data";
调度器维护一个执行队列,该队列同一个watcher仅会存在一次,队列中的watcher不是立即执行,它会通过一个叫做nextTick的工具方法,把这些需要执行的watcher放入到事件循环的微队列中,nextTick的具体做法是通过Promise 完成的。nextTick 通过 this.$nextTick 暴露给开发者
nextTick 的具体处理方式见:https://cn,yuejs.org2/guide/reactivity,html#%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0%E9%98%9F%E5%88%97
也就是说,当响应式数据变化时,render函数的执行是异步的,并且在微队列中。
-
- 现在还剩下最后一个问题,就是Dep通知watcher之后,如果watcher执行重运行对应的函数,就有可能导致函数频繁运行,从而导致效率低下试想,如果一个交给watcher的函数,它里面用到了属性a、b、c、d,那么a、b、c、d属性都会记录依赖,于是下面的代码将触发4次更新:
-
总体流程
- 首先原始对象交给Obsever,它会把它变成一个响应式对象,它具有getter和setter,假如有一个函数render要执行,他不是立即执行,而是把自己交给watcher,watcher会设置一个全局变量,然后运行这个函数,执行的过程中会用到响应式变量,然后属性就被收集过来了,这个过程叫做依赖收集,记录一个属性用到了这个watcher另外一个属性也用到了watcher,还有一些对象、数组用到watcher,这些都会记录,然后页面会渲染出来,当我们通过点击按钮等操作setter会做派发更新,之前有watcher用到了这个属性,然后通知watcher数据发生改变,之后watcher不是立即执行这个函数,不然的话会执行好多次,因为有可能很多数据都要不停的变化,它把自己交给一个调度器,这个调度器已经存在很多watcher,调度器的作用就是把这些watcher添加到队列中去,然后队列把执行自己的函数交给nextTick,nextTick中也会存在其它函数都是在微队列中,通过promise.then。因此等待同步代码完成以后,会异步的执行nextTick中的函数,当执行到watcher时又运行render函数,又用到响应式数据然后重新收集依赖,之前的依赖来会取消掉,循环往复。
-
- 响应式数据的最终目标,是当对象本身或者对象属性发生变化时,将会运行一些函数,最常见的就是render函数,在具体实现上,vue使用了几个核心部件
-
-
diff算法
-
参考回复
- 当组件创建和更新时,vue均会执行内部的update函数,该函数在内部调用render函数生成虚拟dom树,组件会指向新树,然后vue将新旧两树进行对比,找到差异点,最终更新到真实dom。
- 对比差异的过程叫diff,vue在内部通过一个叫patch的函数完成该过程。
- 在对比时,vue采用深度优先、逐层比较的方式进行比对。
- 在判断两个节点是否相同时,vue是通过虚拟节点的key和tag来进行判断。
- 具体来说,首先对根节点进行对比,如果相同则将旧节点关联的真实dom的引用挂到新节点上,然后根据需要更新属性到真实dom,然后再对比其子节点数组;如果不相同,则按照新节点的信息递归创建所有真实dom,同时挂到对应虚拟节点上,然后移除掉旧的dom。
- 在对比其子节点数组时,vue对每个子节点数组使用了两个指针,分别指向头尾,然后不断向中间靠拢来进对比,这样做的目的是尽量复用真实dom,尽量少的销毁和创建真实dom。如果发现相同,则进入和根节点一样的对比流程,如果发现不同,则移动真实dom到合适的位置。
- 这样一直递归的遍历下去,直到整棵树完成对比。
-
diff的时机
- 当组件创建时,以及依赖的属性或数据变化时,会运行一个函数,该函数会做两件事:运行_render生成一棵新的虚拟dom树(vnode tree)。运行_update,传入虚拟dom树的根节点,对新旧两棵树进行对比,最终完成对真实dom的更新。
核心代码如下:
diff就发生在_update 函数的运行过程中// vue构造函数 function Vue() //..,其他代码 var updateComponent=()=>{ this._update(this._render() } new Watcher(updatecomponent); //..其他代码
-
_update 函数在干什么
- _update 函数接收到一个 vnode 参数,这就是新生成的虚拟dom树。
- 同时,_update函数通过当前组件的_vnode属性,拿到旧的虚拟dom树。
- _update函数首先会给组件的_vnode属性重新赋值,让它指向新树。
- 然后会判断旧树是否存在:
- 不存在:说明这是第一次加载组件,于是通过内部的patch函数,直接遍历新树,为每个节点生成真实DOM,挂载到每个节点的elm属性上。
- 存在:说明之前已经渲染过该组件,于是通过内部的patch函数,对新旧两棵树进行对比,以达到下面2个目标:
- 完成对所有真实dom的最小化处理
- 让新树的节点对应合适的真实dom
-
patch函数的对比流程
- 「相同」:是指两个虚拟节点的标签类型、key值均相同,但input 元素还要看 type 属性。
- 「新建元素」:是指根据一个虚拟节点提供的信息,创建一个真实dom元素,同时挂载到虚拟节点的elm属性上。
- 「销毁元素」:是指:vnode.elm.remove()
- 「更新」:是指对两个虚拟节点进行对比更新,它仅发生在两个虚拟节点「相同」的情况下。具体过程稍后描述
- 「对比子节点」:是指对两个虚拟节点的子节点进行对比,具体过程稍后描述详细流程:
-
-
nextTick
-
是什么
- nextTick是等待下一次 DOM 更新刷新的工具方法。
-
原理
- 在Vue内部,nextTick之所以能够让我们看到DOM更新后的结果,是因为我们传入的callback会被添加到队列刷新函数(flushSchedulerQueue)的后面,这样等队列内部的更新函数都执行完毕,所有dom操作也就结束了,callback自然能够获取到最新的DOM值。
-