今天听了一个大牛讲课 除了膜拜 还是膜拜
深入理解Vue的底层原理 通过一个手写的过程可以深入的理解一下vue的底层设计原理,首先vue工作机制是怎么样的 其次的vue的响应机制是怎么样的,在vue的响应中依赖收集与追踪是怎么实现的呢,最后是怎么编译compile呢?我们带着这些问题去深入探究一下
vue工作机制
初始化,在new vue()之后,首先在内部执行了一个初始化方法,它做的就是一些最基础的东西的初始化,比如说初始化生命周期,我们知道有很多生命周期的钩子,还有一些props,还有我们一些数据data的响应化等,其中最重要的是通过object.defineProperty设置getter和setter函数,用来实现响应式以及依赖收集。
在初始化之后调用$mount来执行挂载函数,我们知道Vue的初始化就是通过$mount来实现的,$mount其实就是要指定一个挂载节点,可能会是一个目标节点,也有可能会是一个dom节点,最终就是告诉我们vue将把那些写好的模板通过编译以后达到更新以后这个最新的东西 我到底要显示在什么地方,就是$mount最终指定的那个目标,然后$mount会启动这个编译器compile,这个编译器最重要的事情就是对我们的Template里的东西进行一遍扫描,做parse optimize generate这三件事,compile在这个阶段会生成一些渲染函数或者也可以叫更新函数,会生成一颗树,我们叫虚拟节点树,将来在做数据更新的时候,其实我们改变的数据并不是真正的dom操作,而是这个虚拟dom上的数值,当我们准备更新之前我们会做一个diff算法的比较,通过最新的值和之前的老值进行比较,从而计算出我们应该做的最小的dom更新,然后我们才开始执行到这个patch步骤来打补丁做界面更新,这样儿做的目的是用js里面的计算时间来换dom操作时间,我们知道浏览器的瓶颈在对页面操作这一块儿比较耗时间,Vue的核心在于减少页面渲染的次数和数量,compile除了编译渲染函数之外,还会做一个依赖收集的工作,通过这个依赖收集我们可以知道当页面数据发生变化的时候我应该去更新页面中的那一个dom节点,这也就是将来这个数据发生变化的时候,我们可以通过这个watcher观察者来知道数据发生变化,这时候调用更新渲染函数来打补丁。
上述提到的编译器在扫描dom的时候做的三件事儿parse optimize generate
1 parse是使用正则解析template中的vue指令变量等,形成语法树AST
2 optimize 标记一些静态节点,用作后面的性能优化,在diff的时候直接略过
3 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数据更新的类来实现数据响应:
我们在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 来直接使用了。
我们把新传进来的值赋值给$data, this.$data[key]的重新赋值又会触发上面我们定义在dfeinReactive中的data的setter方法,然后又开始通知,这样儿就串起来了。
当别人问你vue的编译过程是怎么样的时候,你怎样回答?
我们先说什么是编译,为什么要编译,首先因为vue写的html模板,是浏览器识别不了的,我们通过编译的过程,可以进行依赖收集,进行依赖收集之后我们就把Data中的数据模型和视图之间产生依赖关系,当模型发生变化的时候,我们就可以通知这些依赖的地方让他们进行更新,这就是我们执行编译的目的,我们把这些界面全部编译以后,更新操作,我们就可以做到模型驱动视图的变化这就是编译的过程。
双向绑定的原理是什么?
我们在做双向绑定的时候 通常会使用一个v-model这样的指令放在input这样的一个输入元素上,我们在编译的时候可以解析这个v-model ,我在做操作的时候有两件事情,第一件事情我把当前vmodel所属的这个元素上加了一个事件监听,这样如果input会生变化的时候,我就可以把最新的值设置到vue的实例上,因为vue的实例已经实现了数据的响应化,它的响应化的setter函数,会触发页面中所有依赖的更新,所以跟这个数据相关的所有部分都更新了。