You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
var obj = {};
Object.defineProperty(obj, 'msg', {
get () {
console.log('get')
},
set (newValue) {
console.log('set', newValue)
}
});
obj.msg // get
obj.msg = 'hello world' // set hello world
function initData (vm) {
let data = vm.$options.data
vm._data = data
const keys = Object.keys(data)
let i = keys.length
while (i--) {
const key = keys[i]
proxy(vm, `_data`, key)
}
observe(data)
}
proxy做了哪些操作呢?
function proxy (target, sourceKey, key) {
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get () {
return this[sourceKey][key]
}
set () {
this[sourceKey][key] = val
}
})
}
前言
都说框架好,框架令人老,几番费思量,还是VUE好。
现代主流框架均使用一种数据=>视图的方式,隐藏了繁琐的dom操作,采用了声明式编程(Declarative Programming)替代了过去的类jquery的命令式编程(Imperative Programming)
前者我们详细地写了如何去操作dom节点的过程,我们命令什么,它就操作什么;
后者则是我们输入了数据状态,输出视图(我们不关心中间的过程,它们均由框架帮助我们实现);
前者固然直接,但是当应用变得复杂则代码将难以维护,而后者框架帮我们实现了一系列的操作,无需管理过程,优势显然可见。
为了实现这一点,就是实现如何输入数据,输出视图,我们就会注意到上面的render函数,render函数的实现,主要在对dom性能的优化上,当然实现方式也多种多样,直接的innerHTML、使用documentFragment、还有virtual dom,在不同场景下性能上有所不同,但是框架追求的是在大部分场景中框架已经满足你的优化需求,这里我们也不加以赘述,后文会提到。
当然还有数据变化侦测,从而re-render视图,数据变化侦测中,值得一提的是数据生产者(Producer)和数据消费者(Consumer)之间的联系,这里,我们可以暂且将系统(视图)作为一个数据的消费者,我们的代码设置数据的变化,作为数据的生产者
我们这里可以分为系统不可感知数据变化和系统可感知数据变化
像React/Angular这类框架并不知道数据什么时候变了,但是它视图什么时候更新呢,比如React就是通过setState发信号告诉系统有可能数据变了,然后通过virtual dom diff去渲染视图,angular则是有一个脏值检查流程,遍历比对
Rx.js / vue这一类响应式的,通过观察者模式,使用Observable (可观察对象),Observer (观察者)(或者是watcher)去订阅(比如视图渲染这一类,其实也可以当成一个观察者去订阅数据了,后面会提到),系统是可以很准确知道哪里数据变了的,从而也就能实现视图更新渲染。
上者系统不可感知数据变化,粒度粗,有时候还得手动优化(比如pureComponet和shouldComponentUpdate)去跳过一些数据不会更新的视图从而提升性能
下者系统可感知数据变化,粒度细,但是绑定大量观察者,有大量的依赖追踪的内存开销
所以
这里也就终于提到本文的主角Vue2,它采用了折中粒度的方式,粒度到组件级别上,由watcher订阅数据,当数据变化我们可以得知哪个组件数据变了,然后采用virtual dom diff的方式去更新相应组件。
后文我们也将展开它是如何实现这些过程的,我们可以先从一个简单的应用开始。
从一个简单的应用看起
从这里我们也可以提出几个问题,让后面原理的解析更有针对性。
当然同时我们也会讲解一些收集依赖等相关的概念。
数据响应原理
Object.defineProperty
Vue数据响应核心是使用了Object.defineProperty方法(IE9+)在对象中定义属性或者修改属性,其中存取描述符很关键的就是get和set,提供给属性getter和setter方法
可以看下面例子,我们拦截到了数据获取以及设置
顺便提到那个小细节的问题
其实也是跟Object.defineProperty有关
Vue在初始化数据的时候会遍历data代理这些数据
proxy做了哪些操作呢?
其实就是用Object.defineProperty多加了一层的访问
因此我们就可以用app.message访问到app.data.message
也算个Object.defineProperty小应用吧
讲完这语法的核心层面得知了如何知道数据发生变化,但是响应,是还有回应的,接下来来谈下Vue是如何实现数据响应的?
其实就是解决下面的问题,如何实现$watch?
观察者模式(Observer, Watcher, Dep)
Vue实现响应式有三个很重要的类,Observer类,Watcher类,Dep类
我这里先笼统介绍一下(详细可见源码英文注解)
观察者模式,跟发布/订阅模式有点像
但是其实略有不同,发布/订阅模式是由统一的事件分发调度中心,on则往中心中数组加事件(订阅),emit则从中心中数组取出事件(发布),发布和订阅以及发布后调度订阅者的操作都是由中心统一完成
但是观察者模式则没有这样的中心,观察者订阅了可观察对象,当可观察对象发布事件,则就直接调度观察者的行为,所以这里观察者和可观察对象其实就产生了一个依赖的关系,这个是发布/订阅模式上没有体现的。
如何实现观察者模式呢?
我们先看下面代码,下面代码实现了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操作,我们可以注意到这里重新收集了一波依赖!(当然里面有相关的去重操作)
我们再回来回顾上面我们要解决的小例子
其实讲到这里,核心的响应式原理就讲得差不多了。
但是其实Object.defineProperty并不是万能的,
为了解决这些本身js限制的问题
现在我们再来看看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),所以这里就有一个模板编译的过程
模板编译
再摘一段核心代码
我们可以看到上面分成三部分
那里面具体做了什么呢?这里我简略讲一下
所以最后会产生这样的效果
模板
生成render函数
这里我们又可以结合上面的代码了
其中_c就是vm.$createElement
vm.$createElement其实就是一个创建vnode的一个API
知道了vm._render()创建了vnode返回,接下来就是vm._update了
vm._update
vm._update部分也是跟virtual dom有关,下一节具体介绍,我们可以先透露下函数的功能,顾名思义,就是更新视图,根据传入的vnode更新到视图中。
数据到视图的整体流程
所以到这里我们就可以得出一个数据到视图的整体流程的结论了
我们再一次来看看Vue官网的这张图
一切顺理成章!
Virtual DOM
我们上一节隐藏了很多Virtual DOM的细节,是因为Virtual DOM大篇幅有可能让我们忘记我们所要探究的问题,这里我们来揭开Virtual DOM的谜团,它其实并没有那么神秘。
为什么会有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),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
这部分其实就是leetcode.com/problems/ed… 最小编辑距离问题,这里也并没有用复杂的动态规划算法(复杂度为O(m * n))去实现最小的移动操作,而是选择可牺牲一定的dom操作去优化部分场景,复杂度可以降低到O(max(m, n),比较分别首尾节点,如果没有匹配到,则使用第一个节点key(这里就是我们常在v-for用的)去找相同的key去patch比较,假如没有key的话,则是直接遍历找相似的节点,有则patch移动,没有则创建新节点
总结
到这里整体Vue2原理也就讲解结束了,还有很多细节没有深入,读者可以阅读源码去深入研究。
我们可以再回顾下开头的问题(其实文中也是不断的在提出问题解决问题),作为看到这里的你,希望你能有所收获~
参考链接/推荐阅读
The text was updated successfully, but these errors were encountered: