什么是MVVM
model-view-viewModel。
model:数据模型
view:视图层
viewModel:可以理解为沟通view和model的桥梁
他的设计思想就是关注Model
的变化,通过viewModel
自动去更新DOM
的状态,也就是Vue
的一大特点:数据驱动。
我们知道Vue使用简洁的模板语法来将数据渲染进DOM的系统。
例如: {{...}}
来绑定数据,或者,使用v-html
指令输出html
代码等等。
首先我们要做到把这些渲染成目标的内容,这里的实现思路在后面的mounted
板块有详细讲解。
当我们发现数据变化的时候,我们可以监听到数据的变化,然后再执行第一步,操作DOM节点更新内容。这就涉及到我们常说的发布订阅模式。这一部分内容,在文章后面beforeUpdate
板块有讲解。
结合Vue生命周期谈MVVM思想在Vue的实现
Vue官网对Vue生命周期的解释:
Vue创建Vue实例对象,用这个对象来处理DOM元素,这时候这个Vue对象就可以访问了。
使用beforeCreate
这个钩子,我们能在对象初始化之前执行一些方法。
el:undefined // 虚拟DOM的形式存在,还没有被挂载
data:undefined
在这个阶段,所有的data和内置方法都初始化完成,但el
依旧没有挂载,这时,data
可以访问。
当然内置方法的初始化顺序是props => methods =>data => computed => watch
。
如果有需要请求的动态数据,可以在这个阶段发起请求。
el:undefined // 虚拟DOM的形式存在,还没有被挂载
data:[object Object] // 已被初始化
这一步做了很多事情:
首先判断是否有el
对象,如果没有,停止编译,Vue
实例的生命周期走到create
就结束了。
如果有挂载的DOM
节点,再查找是否有任何模板(template
)被用在了DOM层。
如果有,则把template
放到render
函数中(如果是单文件组件,这个模板的编译将提前进行)。
如果没有template
,则将外部HTML作为模板编译(于是,我们发现template
优先级大于外部HTML
)。
当然这个过程中,如果我们使用了模板语法,例如{{...}}
v-html
等,他们还是以虚拟DOM形式存在,并没有被编译
el:[object HTMLDivElement] // 已经挂载,但是模板语言还没有被编译
// 例如:<div id="app">{{name}}</div>
data:[object Object] // 已被初始化
这一步就是用Compile模块编译模板语言,当然这一步因为内容的替换,会引起大量的回流和重绘,所以这一步,在内存中进行(document.createDocumentFragment())
。
对于内容的替换我们大致有这样一个思路:
递归遍历所有的节点,分为文本节点和元素节点。
/* param 所有的元素节点(this.el)把节点放到内存中 */
node2fragment(node){
// 内存中创建一个文档碎片
let fragment = document.createDocumentFragment();
let firstChild;
while(firstChild = node.firstChild){
// 每拿到一个元素碎片,都放到内存里
// 因为节点被放到内存里,可以想成一个类似于出栈的操作
// 每一次拿到的都是新的节点,直到节点取完
fragment.appendChild(firstChild);
}
return fragment
}
对于文本节点:找到是否含有{{}}
,如果有的话,获取{{}}
内的表达式,获取表达式相应的内容,渲染内容
对于元素节点:我们要寻找是否有v-相关属性,如果有的话,获取v-后面的指令,同时获得指令的表达式相应的值
/* param 内存节点 编译内存中的DOM节点,用data替换{{}}内容等 */
compile(fragment){
// 获得第一层的子节点
let childNodes = fragment.childNodes;
[...childNodes].forEach( item =>{
if( this.isElementNode(item) ){
// 如果是元素,找有没有v-model类似的指令
this.CompileElement(item);
// 如果是元素,要递归遍历元素的子节点
this.compile(item);
}else{
// 如果是文本,看有没有{{}}
this.CompileText(item);
}
})
}
举个例子:
<div>
{{name}}
<input v-model="name"/>
</div>
<script>
let vm = new Vue({
el:"#app",
data(){
return{
name:"追梦猪"
}
}
})
</script>
我们遍历节点,一个div元素,一个input,四个文本元素:一个{{name}}和三个空的换行
然后我们对他们进行判断,有用的节点是{{name}}和一个input元素,于是我们分别对他们进行处理
对于文本节点{{}},我们希望的是把它替换成文本“Amy”,对于节点input我们希望属性name和value绑定。
编译结束,我们的DOM树就完成渲染到页面。
el:[object HTMLDivElement] // 已经挂载,模板语言编译完成
// <div id="app">Amy</div>
data:[object Object] // 已被初始化
补充浏览器的渲染机制
- 解析HTML代码生成DOM树
- 解析CSS生成CSSOM
- 结合DOM树和CSSOM树生成渲染树(render tree)
- 采用深度优先遍历(diff算法)遍历渲染节点4. 采用深度优先遍历(diff算法)遍历渲染节点
当然,这里css的渲染顺序完全就是编写顺序,如果css编写顺序不规范,这样一步也可能引起大量回流和重绘
beforeUpdate
在监听到数据改变之前执行,虚拟DOM重新渲染,并应用更新,完成改变之后执行updated
。
这里有个问题就是Vue是怎么监听到数据发生改变的呢?又是如何通知视图层进行更新的呢?
这就是我们MVVM最核心的思想,通过view-Model连接视图层和数据模型,Vue里用到了我们常说的发布订阅模式,这里涉及到几个类。
进行数据劫持,实现数据的双向绑定。在这里,他就是一个发布者,发布数据变更的消息。
我们使用Object.defineProperty方法给data的所有属性都绑定get和set方法,这样就可以监听到每次读取数据和修改数据。
收集Watcher依赖,一个属性有一个Dep,同来通知watcher数据变更。
构造器里有一个sub数组存放多个watcher,因为一个属性可能在多个节点使用,每个使用这个属性的节点都有一个watcher。
一般两个方法,一个订阅addSub(添加Watcher),一个发布notify(通知Watcher进行更新)
订阅者。编译器Compiler为每一个编译过的元素节点和文本节点添加watcher,一旦数据更新,触发watcher回调,通知视图层进行变更。
Vue被破坏并从内存释放之前,这个时候所有的方法和实例都可以访问。
比如,我们一般在这个阶段清空计时器。
Vue实例内存被释放,这时所有的子组件、实践监听器、watcher都被清除。
Vue2.0用Object.definePeoperty来劫持数据;而Vue3.0采用Proxy代理数据。
其中最大的区别是Object.definePeoperty只能代理某一个对象的某个属性,但Proxy可以直接代理对象和数组。