vue2.0原理的理解

Vue2.0采用折中粒度的数据响应方式,通过`object.defineProperty`为数据对象添加getter/setter,利用观察者模式实现数据变化监听。Observer类负责收集依赖,Watcher类用于执行回调,Dep类作为依赖管理。数据变化时,setter触发通知更新,使用virtual DOM diff更新对应组件。Vue还处理了数组和对象的特殊情况,通过$set/$delete以及数组变异方法实现响应式。此外,Vue利用模板编译生成render函数,通过Watcher和Render Function实现数据到视图的映射,并利用Virtual DOM优化DOM操作。
摘要由CSDN通过智能技术生成

页面dom操作方式:
(1).直接dom节点操作,我们命令什么,她就做什么 (js和jq这种方式)
(2).输入数据状态变化,输入视图,无需观察他是否做了什么操作(vue和react)
系统数据变化分为两种:

第一种是系统不可感知数据变化,典型框架react和angular,他们不知道数据什么时候变了,但是它们师徒是什么时候去更新的呢?比如react就是通过setState发信号告诉系统可能数据变化了,然后通过virtual dom diff去渲染视图,angular则是通过脏检查流程,遍历对比
第二种是系统可感知数据变化 典型的框架是vue,通过观察者模式,使用Observable (可观察对象),Observer (观察者)(或者是watcher)去订阅(比如视图渲染这一类,其实也可以当成一个观察者去订阅数据了,后面会提到),系统是可以很准确知道哪里数据变了的,从而也就能实现视图更新渲染。
上者系统不可感知数据变化,粒度粗,有时候还得手动优化(比如pureComponet和shouldComponentUpdate)去跳过一些数据不会更新的视图从而提升性能
下者系统可感知数据变化,粒度细,但是绑定大量观察者,有大量的依赖追踪的内存开销所以这里也就终于提到本文的主角Vue2,它采用了折中粒度的方式,粒度到组件级别上,由watcher订阅数据,当数据变化我们可以得知哪个组件数据变了,然后采用virtual dom diff的方式去更新相应组件。

数据响应原理
object.defineProperty
vue数据响应的核心使用了object.defineProperty方法(IE9+)在对象中定义属性或者修改属性,其中存取描述符很关键的就是get和set,提供给属性getter和setter方法可以看下面例子,我们拦截到了数据获取以及设置

顺便提到那个小细节的问题app.message如何拿到vue data中的message?其实也是跟Object.defineProperty有关
Vue在初始化数据的时候会遍历data代理这些数据

proxy这个方法是干什么的?

其实就是用Object.defineProperty多加了一层的访问
因此我们就可以用app.message访问到app.data.message
也算个Object.defineProperty小应用吧
讲完这语法的核心层面得知了如何知道数据发生变化,但是响应,是还有回应的,接下来来谈下Vue是如何实现数据响应的?
其实就是解决下面的问题,如何实现$watch?

观察者模式(Observer, Watcher, Dep)
Vue实现响应式有三个很重要的类,Observer类,Watcher类,Dep类
我这里先笼统介绍一下(详细可见源码英文注解)

  • Observer类主要用于给Vue的数据defineProperty增加getter/setter方法,并且在getter/setter中收集依赖或者通知更新
  • Watcher类来用于观察数据(或者表达式)变化然后执行回调函数(其中也有收集依赖的过程),主要用于$watch API和指令上
  • Dep类就是一个可观察对象,可以有不同指令订阅它(它是多播的)

    观察者模式,跟发布/订阅模式有点像

但是其实略有不同,发布/订阅模式是由统一的事件分发调度中心,on则往中心中数组加事件(订阅),emit则从中心中数组取出事件(发布),发布和订阅以及发布后调度订阅者的操作都是由中心统一完成

但是观察者模式则没有这样的中心,观察者订阅了可观察对象,当可观察对象发布事件,则就直接调度观察者的行为,所以这里观察者和可观察对象其实就产生了一个依赖的关系,这个是发布/订阅模式上没有体现的。

其实Dep就是dependence依赖的缩写

如何实现观察者模式呢?

我们先看下面代码,下面代码实现了Watcher去订阅Dep的过程,Dep由于是可以被多个Watcher所订阅的,所以它拥有着订阅者数组,订阅了它,就把Watcher放入数组即可。
我们实现了订阅,那通知发布呢,也就是上面的notify在哪里实现呢?
我们到这里就可以联系到数据响应,我们需要的是数据变化去通知更新,那显然是会在defineProperty中的setter中去实现了,聪明的你应该想到了,我们可以把每一个数据当成一个Dep实例,然后setter的时候去notify就行了,所以我们可以在defineProperty中new Dep(),通过闭包setter就可以取到Dep实例了

就像下面这样

然后这里就又产生了一个问题
你都把Dep实例放里面了,我怎么让我的Watcher实例订阅到这个Dep实例呢,Vue在这里实现了精妙的一笔,从get里面做手脚,在get中是可以取到这个Dep实例的,所以可以在执行watch操作的时候,执行获取数值,触发getter去收集依赖

这里我们也要结合Watcher的实现来看

所以我们在new Watcher的时候会执行一个求值的操作,然后因为标记了这个Watcher触发的,所以收集了依赖,也就是观察者订阅了依赖(这个求值有可能不止触发了一个getter,有可能触发了很多个getter,那就收集了多个依赖),我们可以再注意一下上面的run操作,也就是dep.notify()后watcher会执行的操作,还会出现一个get操作,我们可以注意到这里重新收集了一波依赖!(当然里面有相关的去重操作)

我们再回来回顾上面我们要解决的小例子

$watcher其实就是一个new Watcher的封装
即new Watcher(vm, 'msg', () => console.log("msg变了"))

  • 首先是new Vue遍历了数据,给数据defineProperty加上了getter/setter方法
  • 我们new Watcher(vm, 'msg', () => console.log("msg变了")),首先标记了全局变量Dep.target = 该Watcher实例,然后执行msg的get操作,触发到了它的getter,然后dep成功获取到它的订阅者,放入它的订阅者数组,最后我们将Dep.target = null
  • 最后设置vm.msg = 2,触发到了setter,闭包中的dep.notify,遍历订阅者数组,执行相应的回调操作。

其实讲到这里,核心的响应式原理就讲得差不多了。
但是其实Object.defineProperty并不是万能的,

  • 数组的push/pop等操作
  • 不能监测数组length长度的变化
  • 数组的arr[xxx] = yyy无法感知
  • 同样的,对象属性的添加和删除无法感知

为了解决这些本身js限制的问题

  • Vue首先是对数组方法进行变异,用__proto__继承那些方法(如果不行则直接一个个defineProperty到数组上),具体的变异方法就是在后面加上dep.notify的操作
  • 至于属性的添加和删除,我们可以想象到,增加属性,那我们根本没有defineProperty,删除属性则连我们之前的defineProperty都给删了,所以这里Vue增加了一个$set/$delete的API去实现这些操作,同样也是在最后加上了dep.notify的操作
  • 当然以上就不是单纯靠defineProperty中每一个数据所对应的dep来实现了,在Observer类也有一个dep实例,同时会给数据挂载一个__ob__属性去获取它的Observer实例,像数组和对象的上面特殊操作,在watch收集依赖的时候都会把这个依赖收集到,然后最后使用的是这个dep去notify更新

这部分就不详细介绍了,有兴趣的读者可以阅读源码

这里我们可以稍微提一下一个ES6的新特性Proxy,很有可能是下一代响应机制的主角,因为它可以解决我们上面的缺陷,但是由于兼容问题还不能很好地使用,可以让我们期待一下~

现在我们再来看看Vue官网的这张图

至少目前我们对右半部分很清晰了,Data如何和Watcher联系已经很清楚,但是Render Function,Watcher怎么Trigger Render Function这个还需要去解答,当然还有左下角的Virtual DOM Tree
数据与视图如何联系
我这里摘出一段关键的Vue代码

这个其实就是Watcher和Render的核心关系
还记得我们上面所说的,在执行new Watcher会有一个求值的操作,这里的求值是一个函数表达式,也就是执行updateComponent,执行updateComponent后,会再执行vm._render(),传参数给vm._update(vm._render(), hydrating),收集完依赖以后才结束,这里有两个关键的点,vm._render在做什么?vm._update在做什么?

vm._render

我们看下Vue.prototype._render是何方神圣(以下为删减代码)

所以它这里我们可以看到里面是执行了render函数,render函数来自options,然后返回了vnode
所以到这里我们可以把我们的目光移到这个render函数从哪里来的
如果熟悉Vue2的朋友可能知道,Vue提供了一个选项是render就是作为这个函数的,假如没有提供这个选项呢
我们不妨看看生命周期部分图:

我们可以看到Compile template into render function(没有template会将el的outerHTML当成template),所以这里就有一个模板编译的过程模板编译

再摘一段核心代码

我们可以看到上面分成三部分

  • 将模板转化为抽象语法树
  • 优化抽象语法树
  • 根据抽象语法树生成代码
    那里面具体做了什么呢?这里我简略讲一下
  • 第一部分其实就是各种正则了,对左右开闭标签的匹配以及属性的收集,通过栈的形式,不断出栈入栈去匹配以及更换父节点,最后生成一个对象,包含children,children又包含children的对象
  • 第二部分则是以第一部分为基础,根据节点类型找出一些静态的节点并标记
  • 第三部分就是生成render函数代码了

所以最后会产生这样的效果模板
<div id="container"> <p>Message is: {{ message }}</p> </div>

数据到视图的整体流程

所以到这里我们就可以得出一个数据到视图的整体流程的结论了

  • 在组件级别,vue会执行一个new Watcher
  • new Watcher首先会有一个求值的操作,它的求值就是执行一个函数,这个函数会执行render,其中可能会有编译模板成render函数的操作,然后生成vnode(virtual dom),再将virtual dom应用到视图中
  • 其中将virtual dom应用到视图中(这里涉及到diff后文会讲),一定会对其中的表达式求值(比如{{message}},我们肯定会取到它的值再去渲染的),这里会触发到相应的getter操作完成依赖的收集
  • 当数据变化的时候,就会notify到这个组件级别的Watcher,然后它还会去求值,从而重新收集依赖,并且重新渲染视图

我们再一次来看看Vue官网的这张图

 

为什么会有Virtual DOM?

做过前端性能优化的朋友应该都知道,DOM操作都是很慢的,我们要减少对它的操作

为啥慢呢?

我们可以尝试打出一层DOM的key

我们可以看出它的属性是庞大,更何况这只是一层

同时直接对DOM的操作,就必须很注意一些有可能触发重排的操作。

那Virtual DOM是什么角色呢?它其实就是我们代码到操作DOM的一层缓冲,既然操作DOM慢,那我操作js对象快吧,我就操作js对象,然后最后把这个对象再一起转换成真正的DOM就行了

所以就变成 代码 => Virtual DOM( 一个特殊的js对象) => DOM

什么是Virtual DOM

上文其实我们就解答了什么是虚拟DOM,它就是一个特殊的js对象

我们可以看看Vue中的Vnode是怎么定义的?

用以上这些属性就能来表示一个DOM节点
Virtual DOM算法
这里我们讲的就是涉及上面vm.update的操作

  • 首先是js对象(Virtual DOM)描述树(vm._render),转换dom插入(第一次渲染)
  • 状态变化,生成新的js对象(Virtual DOM),比对新旧对象
  • 将变更应用到DOM上,并保存新的js对象(Virtual DOM),重复第二步操作

用js对象描述树(生成Virtual DOM),Vue中就是先转成AST生成code,然后通过$creatElement通过Vnode的那种形式生成Virtual DOM (vm._render的操作)

这里我们可以具体看下vm._update(其实就是Virtual DOM算法的后两步


可以看到一个关键点vm.__patch__,其实它就是Virtual DOM Diff的核心,也是它最后把真实DOM插入的
Virtual DOM Diff
完整Virtual DOM Diff算法,根据有一篇论文(我忘记在哪里了),是需要O(n^3)的,因为它涉及跨层级的复用,这种时间复杂度是不可接受的,同时考虑到DOM较少涉及跨层级的复用,所以就减少至当前层级的复用,这个算法的复杂度就降到O(n)了,Perfect~

引用一张React经典的图来帮助大家理解吧,左右同一颜色圈起来的就是比较/复用的范围


步入正题,我们看看Vue的patch函数

所以patch大概做下面几件事

  • 判断老节点存不存在
  • 不存在则为首次渲染,直接创建元素
    存在的话则sameVnode使用判断根节点是否相同
  • 相同则使用patchVnode给老节点打补丁
    • 不相同则使用新节点直接替换老节点

对于sameVnode判断,其实就是简单比较了几个属性判断

对于patchVnode
其实就是比较节点的子节点,分别对新老节点的拥有的子节点做判断,假如两者都没有或者一者有一者没有,就比较容易,直接删除或者增加即可,但是假如两者都有子节点,这里就涉及到列表对比以及一些复用操作了,实现的方法是updateChildren

我们最后再来看看这个updateChildren
这部分其实就是最小编辑距离问题,这里也并没有用复杂的动态规划算法(复杂度为O(m * n))去实现最小的移动操作,而是选择可牺牲一定的dom操作去优化部分场景,复杂度可以降低到O(max(m, n),比较分别首尾节点,如果没有匹配到,则使用第一个节点key(这里就是我们常在v-for用的)去找相同的key去patch比较,假如没有key的话,则是直接遍历找相似的节点,有则patch移动,没有则创建新节点

这里告诉我们列表假如有可能有复用的节点,可以使用唯一的key去标识,提升patch效率,但是也不能乱设置key,假如根本不一样,但是你设置一样的话,会导致框架没找到真正相似的节点去复用,反而降低效率,会增加一个创建dom的消耗

这里代码较多,有兴趣的读者可以深入阅读,这里我就不画图了,读者也可以找网上的相应updateChildren的图,有助于理解patch的过程


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值