vue编译过程是怎样的?
1.先说什么是编译?为什么要编译?
首先编译,vue这个模板这些语句html根本就不识别,我们通过编译的过程可以进行依赖收集,进行依赖收集以后,就可以把我们data中的数据模型和视图之间产生了绑定关系、依赖关系。那么以后如果模型发生变化的时候,我们就可以通知这些依赖的地方让他们进行更新,这就是我们执行编译的目的,我们把这些界面全部编译以后,更新操作,然后我们就可以做到模型驱动视图的变化,这就是编译过程。
双向数据绑定的原理?
我们在做双向数据绑定的时候,通常会使用v-model这样的指令放在input上,我们为什么要放在v-model,因为我们在编译的时候可以解析这个v-model,然后我在做操作的时候,我们v-model所属的元素上面加了一个事件监听,把指定的事件回调函数做为input他的事件监听的回调函数,如果input发生变化的时候,我就可以把最新的值设置到vue的实例上,因为Vue的实例已经实现了数据的响应化,他的响应化的set函数会触发界面中所有模型依赖的更新,会通知所有依赖去做更新,所以视图中所有跟这个更新相关的部分都会得到更新了。
生命周期:
1.创建vue实例对象
2.beforeCreate钩子函数执行
3.Observe Data 监控Data中所有数据变化:
4.Vue内部初始化事件
5.created钩子函数执行 (此时还没有el)
6.判断对象是否有el选项。如果有的话就继续向下编译,如果没有el选项,则停止编译,也就意味着停止了生命周期,直到在该vue实例上调用vm.$mount(el)。
7.如果vue实例对象中有template,则将其作为模板编译成render函数。
如果没有template选项,则将外部HTML作为模板编译。
详述编译过程:
在template
编译(渲染成UI)有一个过程。模板通过编译生成AST,再由AST生成Vue的渲染函数,渲染函数结合数据生成Virtual DOM树,对Virtual DOM进行diff
和patch(Vue中diff算法过程就是调用名为patch函数)
后生成新的UI。
8.编译模板时候,把data里面的数据和模板生成html
9.beforeMount : 开始执行这个钩子,注意此时还没有生成html到页面上去
10.用上面编译好的内容替换el属性指向的dom对象或者选择权对应的html标签里面的内容
11.mounted :挂载完成,也就是模板中的html渲染到了html页面中,mounted钩子只会执行一次。
12. beforeUpdate :更新之前的事件钩子
13. beforeDestroy : Vue实例销毁前执行的钩子
14.destroyed:Vue实例销毁
自我总结思路:
1.通过Object.definedPrototype进行数据劫持,劫持的目的是什么?
就是想修改数据的时候能更新视图,因为我们通过视图去修改数据很容易实现:比如input 上加一个oninput事件: 在视图上去改变input的value,字符串str(数据)能实时同步
但是我们通过更改数据,做不到视图的更新(只能刷新页面),正常情况下是我们修改完数据,直接ctrl+s保存,webpack中的dev-server打一个包(放内存里的),这个过程其实我们并没有刷新页面(其实这里是用了热更新)
下面来全面串一下VUE到底是怎么实现双向数据绑定的?(也只有v-model是双向的,因为他用input上的事件实现了视图更新数据),主要是剖析如何做到数据更新视图?
首先:通过Object.definedPrototype进行set和get做了数据劫持,然后给每个数据装上了watcher(观察者)也叫订阅者,一个watcher对应一个属性(data中的数据),其实这也是收集依赖的过程,怎么收集依赖呢?
需要扫描视图收集依赖(Dep,订阅者集合),知道视图中(template)到底哪些地方对数据(data)有依赖(template中哪些地方绑定了data中的值),这样当数据变化的时候就能有的放矢了。在vue中的数据劫持中的set函数(发布者),set函数一旦发生变化,就会发出通知,Dep中的订阅者们一个个的watcher就要调用update方法去重新构建虚拟DOM通过diff算法比对以最小成本找到不同去替换更新,然后更新真实DOM。
Dep中有个notify方法,作用在于通知所有的watcher去做更新。(循环遍历Dep这个数组,让里面的每个订阅者watcher去调用watcher的update方法去做更新)
watcher.js这个函数中的Watcher类,会把自己的this(实例)给到Dep.target,在数据劫持中的get函数中dep实例:dep.addWatcher方法去push了每一个Dep.target(也就相当于push进去了每一个watcher实例),这样每一个dep实例中都会有一个或者多个watcher实例(为什么是多个watcher?),为什么要放在get函数中?
因为get函数就是我们获取属性如 obj.name,调用的obj.name就会执行get函数,这样就把watcher添加进了dep实例中,又因为他们是在Object.definedPrototype中去设置obj, key, {} 那么在get函数中去做dep收集watcher的操作其实就是关联了每一个obj的属性key,因为get函数在执行的时候,里面的dep收集watcher会执行,就会成为obj这个key的一份子(你在别人身上,那就跟这个人做了关联,不知道怎么更好的表述)
因为可能一个属性在视图中被多次绑定,比如视图中有两个 {{name}} {{name}},这样一个dep实例中就有个两个同样的watcher实例
其实本身就是data中的每一个属性都会关联上一个watcher,视图上多次使用的话,就会有多个watcher装在一个dep实例中,每一个dep实例都是互相独立的,但他们又都属于Dep类,每一个dep实例都有一个deps数组存储这些watcher,遍历dep其实就是遍历这些dep实例存放watcher实例的数组。
当data中数据改变时,就激活了发布者set函数,set函数除了会设置新的val,还会去触发dep实例的notify方法,notify方法做的就是遍历当前dep实例中存储的数组watcher,让这些watcher调用其update方法,update方法就会通知compile函数类,去进行生成新的虚拟DOM节点,通过diff算法,进行真实的DOM更新。
其实set设置的那个新值,被get函数return出去了,return出去的是最新的值,值是有了,怎么在页面上去更新出来呢,这就是上面说了这么一大串,最终通过set发布者通知订阅者管家dep,dep去告知里面的每个watcher调用其自身的update方法再去找compile函数去执行虚拟DOM,经过diff算法以最小成本更新DOM,这样DOM更新了,最新值就显示出来了。
这就是所谓的数据驱动视图。
下面说一说编译,为什么要编译?
我们在template中写了很多{{}}或者v-xxx @xxx,这些浏览器是不认识的,我们需要把其解析成浏览器认识的。
具体步骤:
1.拿到el对应的#app这个标签(容器)中的所有的内容转换为代码片段
2.遍历这些片段,把其中的有特殊表示的如{{}} v-xxx @xxx 这些给替换成我们的值,然后把编译完的结果追加到$el(也就是#app),通过appendChild
搞一个方法,方法传入$el。创建一个文档碎片,让el中的所有元素搬家至文档碎片中:
3.编译过程