周末时间比较空闲,想想用vue有一段时间了,我们学习框架只是学习它是怎么用的,但是对于vue底层原理理解不是很多的,利用空闲时间,就在哔哩哔哩上找了大佬的视频对着vue源码了解了vue的底层原理,并且做了总结!!
首先vue工作机制是怎么样的 其次的vue的响应机制是怎么样的,在vue的响应中依赖收集与追踪是怎么实现的呢,最后是怎么编译compile呢?
工作机制
初始化,new vue()之后,先在内部执行一个初始化方法,如初始化生命周期,有一些props,还有data的响应化等,最重要是通过object.defineProperty设置getter()和setter()来实现响应式以及依赖收集。
初始化后调用$mount指定挂载节点执行挂载函数,最终就是告诉vue将把那些写好的模板通过编译以后达到更新以后最新的东西 要显示在什么地方为$mount最终指定的那个目标,然后$mount会启动compile编译器对Template里的东西进行一遍扫描,做parse optimize generate三件事,compile在这阶段会更新函数,生成一颗树,叫虚拟节点树,将来做数据更新时,改变的数据并不是真正的dom操作,而是虚拟dom上的数值,准备更新之前会做一个diff。算法的比较,通过最新的值和之前的老值进行比较,从而计算出应该做的最小dom更新,然后才开始执行到这个patch步骤来打补丁做界面更新,其目的是用js里面的计算时间来换dom操作时间,我们知道浏览器的瓶颈在对页面操作这一块儿比较耗时间,Vue的核心在于减少页面渲染的次数和数量,compile除了编译渲染函数之外,还会做一个依赖收集的工作,通过这个依赖收集可以知道当页面数据发生变化时该去更新页面中的那个dom节点,这也就是将来这个数据发生变化的时候,我们可以通过这个watcher观察者来知道数据发生变化,这时候调用更新渲染函数来打补丁。
new vue
上述提到的编译器在扫描dom的时候做的三件事儿parse optimize generate
parse是使用正则解析template中的vue指令变量等,形成语法树AST
optimize 标记一些静态节点,用作后面的性能优化,在diff的时候直接略过
generate 把第一部生成的AST转化为渲染函数
下面实现一个mvvm,实现一个observer数据劫持监听,当数据发生变化的时,通知watcher变化,让他去调视图更新,从而去做界面的更新,开始会做一些编译的过程,会初始化视图,在初始化视图的同时还做了另外一件事情,就是初始了我们的观察者watcher
更新视图
数据修改触发setter,然后监听器会通知进行修改,通过对比两个dom树,得到改变的地方,就是patch然后需要把这些差异修改即可
vue响应式的原理:defineProperty
首先定义一个对象obj 并期望obj.name='xx' 可以直接显示在标签内,这操作是不是就是所谓的数据驱动,数据的响应式,平常在写的Vue时是不是就是这样,当一个属性发生变化时,界面中的值就会动态的发生变化。我们使用object.defineProperty来添加属性
通过defineProperty我们就可以知道vue数据响应式原理,来给我们的Data添加属性 当这个属性发生改变时,我们就可以指定的规则来作更新。
那么我们在面试的时候怎么回答 vue的原理是怎样的呢?
vue是利用了Object.defineProperty,它把data中的每一个属性都定义成一个属性,赋予getter和setter,这样的话就有机会去监听这些属性的变化,当这些属性发生变化时,可通知那些需要更新的地方去更新。
先简单模拟一个vue数据更新的类来实现数据响应:
KVue.js
index.html
在defineProperty中的set中监听到了数据更新。通过运行index.html可以看到test 和 bar的更新可打印出来
当发现在数据发生的变化时,我们需要做界面的更新,上述中只是打印出的变化,因此引出来数据的依赖收集的概念
比如在界面中引用了name1 name2 name1 这时created name1和name3会发生什么事情吗?
name1会发现在页面中有两个依赖,在name1变化时,通知两个部分变化就可以了,这时name3发生变化,其实不会做任何通知,因为根本没有任何依赖,这就是为什么在程序开始之前一定会对模板进行一次遍历,然后会从中找出一些和我数据有依赖的部分,收集保存下来,在数据需要更新时调用。
依赖收集需要引入两个概念,一个是依赖对象depnice 一个是监听对象watcher,这两个对象首先遵从一个发布订阅模式,dep是订阅者,它非常关心数据发生变化,观察者其实是例子中的setter函数,当数据发生变化时,dep发出通知,去调用所有watcher。
我们定义了一个dep类的对象,用来收集watcher对象,读数据时,会触发getter函数把当前的watcher对象(存放在Dep.target中)收集到Dep类中,写数据时,则会触发stter方法,通知Dep类调用notyfy来触发所有的watcher对象的update方法更新对应视图。
要注意,每一个依赖针对于一个单个的属性,每个依赖当中还有可能会有多个watcher,key出现几次就会有几个watcher。
编译compile
核心逻辑获取dom,遍历dom,获取{{}}格式的变量,以及每个dom的属性,截获k- @等开头设置响应式
在compile.js中,首先需要将内容转换为代码片断,以减少对dom的操作,然后进行编译,将编译结果再追加到宿主对象el上。
上述步骤中 转换代码片断的方法node2Fragment,我们先创建一个代码片断,通过查找宿主对象el上的firstChild,逐次的添加到新创建的代码片断中,最后返回这个代码片断的集合。
在编译的方法compile中,遍历代码片断,判断是元素对象还是插值对象,对应做的相应的操作,这个时候由于遍历对象中也可能会包含子节点,所以要通过判断el.childNodes节点是否存在来做递归判断。
这时在kvue.js中去new Compile(options.el, this)一个compile实例时,就可以完成字符串转换,但是在created中如果有一个setTimeout来this.name时,就不会发生任何变化了,这是因为还没有做任何依赖收集的工作,当属性更新的时候setter函数没有被触发,所以我们需要一个更新函数update 添加依赖收集。
这时我们就需要改一下compileText方法,添加一个update方法的通用方法,执行第一次修改,然后添加依赖。
首先watcher的构造函数需要接收三个参数vm,key,cb,这时回到kvue.js 我们需要在Watcher函数中,需要对三个参数做引用,接下来添加依赖属性的地方,可以再读一次添加的属性,由次来添加依赖,触发setter,然后再置空。避免重复添加,下一次再创建的时候还是这个过程。这时候update函数里,就可以直接执行cb
到此这个流程基本上就串起来了,这时会发现在watcher中,触发属性时使用的是vm,而不是$data,所以要变成大家熟悉的那种写法,因此需要写一个代理。
接下来的代理工作,要回到observe中,在定义defineReactive时,还可以再定义一个代理proxyData,代理data中的属性到vue的实例上,这时可以用vue.xx 来直接使用了。
proxyData
把新传进来的值赋值给$data, this.$data[key]的重新赋值又会触发上面定义在dfeinReactive中的data的setter方法,然后又开始通知,这样就串起来了。
当别人问你vue的编译过程是怎么样的时候,你怎样回答?
我们先说什么是编译,为什么要编译,首先因为vue写的html模板,是浏览器识别不了的,通过编译的过程,可以进行依赖收集,进行依赖收集之后就把Data中的数据模型和视图之间产生依赖关系,当模型发生变化时,就可以通知这些依赖的地方让他们进行更新,这就是执行编译的目的,把这些界面全部编译以后,更新操作,就可以做到模型驱动视图的变化这就是编译的过程。
双向绑定的原理是什么?
在做双向绑定的时候 通常会使用一个v-model这样的指令放在input这样的一个输入元素上,在编译的时候可以解析这个v-model ,在做操作的时候有两件事情,第一件事情把当前vmodel所属的这个元素上加了一个事件监听,这样如果input会生变化时,就可以把最新的值设置到vue的实例上,因为vue的实例已经实现了数据的响应化,它的响应化的setter函数,会触发页面中所有依赖的更新,所以跟这个数据相关的所有部分都更新了。
扫码关注个人小程序