MVVM之卡哇伊Vue源码分析plus
窗外风雪再大
也有我陪伴着你
全文字数:4731字
阅读时间:30分钟
前言
本文是对我在2019-01-01发布的名为MVVM之Vue源码分析一文的重新整理,我会首先介绍几个涉及JS方面的知识,然后将对MVVM框架的三大基本原理(即数据代理、模板解析、数据绑定)进行介绍。
需要你了解的本文没有介绍的知识:Javascript继承(尤其是原型链继承)、数组方法(forEach等)、this指针、函数的嵌套调用与递归调用等。还有一项重要的技能就是:debug调试
01 Javascript基础知识介绍
1. addEventListener:
input监听(输入过程中发生)与change监听(失去焦点时发生),该方法将指定的监听器注册到对应元素上,当元素触发指定的事件时,指定的回调函数就会执行。
代码实例:
本行代码是实现双向数据绑定的关键代码:
其中this.bind(node,vm,exp,'model')实现的是单项的数据绑定(即model==>view),即数据层到视图层的初始化显示(以及创建对应的watcher),其余代码是实现view==>model的绑定(即当视图层数据变化时,对应数据层的相应数据也发生改变的功能)。
node.addEventListener("input",function(e){})---其中第一个参数是input是绑定的事件类型(即当表单元素检测到输入时就会触发),第二个回调函数是当事件触发时所要执行的功能.有时还可能遇到第三个参数(布尔值的形式),当该参数设置为true就在捕获过程中执行,反之就在冒泡过程中执行处理函数。
2. 伪(类)数组转换成真数组:
实现方法:
Array.prototype.slice.call()
[].slice.call()
ES6中的方法:Array.from()
这里我想说下前两个方法的优缺点,首先这两个方法都是接收一个伪数组作为参数,但是从执行效率上讲:
从图中可以看到,slice方法是定义在原型上的,所以第一种方法会直接到原型上查找,一点毛病没有,而第二种方法会首先在实例上查找,如果实例上开发者没有定义一个slice方法才会去原型上查找,所以相比之下会消耗时间。
代码实例:(模板解析部分的代码)
3. node.nodeType:
只介绍四个常用节点类型:
document(9)
Element(1)
Attr(2)
Text(3)
代码示例:
4.Object.defineProperty
会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。
语法形式:
Object.defineProperty(obj,prop,descriptor)
obj ---> 要在其上定义属性的对象
prop ---> 要定义或修改的属性的名称
descriptor ---> 将被定义或修改的属性描述符
代码示例:
该部分代码是通过Object.defineProperty()给对应属性添加get/set方法以实现数据代理效果的实现。
5. Object.keys
该方法会返回一个由一个给定对象的自身可枚举属性组成的数组。
代码实例:
当视图层的数据来源有一部分是通过计算属性得到的时,会调用该部分代码。
6. Object.hasOwnProperty
该方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性。
代码实例:
该部分代码是建立watcher与dep之间的关系滴~
7. DocumentFragment
DocumentFragment接口表示一个没有父级文件的最小文档对象,举个栗子:如果现在页面有100000....个li标签,现在的需求是将这10000...个的innerHTML值改为"石璞东",那么一般的做法就是:获取所有li,通过遍历循环修改其属性即可,也挺简单,但是操作太耗性能。(过多的DOM操作会引起浏览器的重排操作,即修改一次DOM,浏览器就需要重新计算部分甚至整个页面的几何结构信息,浏览器需要重新遍历DOM树,根据CSS规则进行对受到影响的DOM元素进行计算,然后进行重新绘制,这样很耗内力的哟~)
而对于DocumentFragment来说,它不是真实DOM树的一部分(听起来像不像是在说virtual Dom),它的变化不会引起DOM树的重新渲染的操作(reflow),且不会导致性能等问题。它的对于节点的所有操作都是在内存中进行,当操作结束后,所有节点会被一次插入到文档中,也就意味着只发生一次重渲染的操作。
代码实例:
代码解释:该部分代码是将页面的所有节点全部移入fragment中进行操作,然后操作完成之后通过appendChild方法插入页面。
8. 什么叫MVVM:
三句话:
View相当于模板(即HTML中嵌套JS)
ViewModel相当于JS逻辑
Model(数据层,可能涉及到与后台的交互)
02 MVVM框架的三大基本原理
1. 数据代理:
Vue实现:
现在的问题就是:我明明是定义在data中的name,为什么可以通过vm.name直接访问到呢?
ok,来把问题整理下,毕竟咱是个有面子的人是吧~
问题提出:
现在有a和b两个对象,且b对象是a对象的一个子集,b对象中有"name"等属性,由此可知,通过b.name可以直接实现对b中name的访问,但是如果我直接想通过a对象来访问呢?那显然有两种可以直接想到的思路:
第一种:既然我想通过a来访问b的属性,那么我就把b的所有属性直接在a上重新定义一遍不就ok了
第二种:我定义两个方法:通过a.name获取值的方法(get)和通过a.name="newVal"的设置新值的方法(set),如果当前用户只是获取元素的值,那么通过get方法去data对象里面取相关属性的值就行了,如果当前用户是修改属性的值,那么通过set方法修改值即可。
很明显,采用第二种方法~
优点:可以直接通过vue实例操作data中的数据
那既然思路都有了,实现起来也就很简单了,来看方法:
通过Object.defineProperty(vm,key,{})给vm添加与data对象的属性对应的属性描述符
所有添加的属性都包含get/set方法
在set/get方法中去操作data中对应的属性
来现在看看github上代码的实现:
<div id="app">
</div>
<script src="./MVVM/compile.js"></script>
<script src="./MVVM/mvvm.js"></script>
<script src="./MVVM/observer.js"></script>
<script src="./MVVM/watcher.js"></script>
<script>
const vm = new MVVM({
el:'#app',
data:{
name:'dong2'
}
});
console.log(vm.name,vm); //vm代理对数据的读操作 vm实例中并没有存储name属性的值 name属性的值是存在_data中
vm.name = "turbo2";//vm代理对数据的写操作
console.log(vm._data.name,vm.name)
</script>
简单说下:
这是函数的执行栈,其中涉及到两个方法get与set,当执行console.log(vm.data),即读操作时会调用该方法,当执行vm.data="newVal"会执行该操作。ok,就这么简单~
最后,来看看源码实现:
2. 模板解析:
Vue实现:
github实现:
<div id="app">
<!--<p>{{msg}}</p>-->
<p>""msg""</p>
</div>
<script src="./MVVM/compile.js"></script>
<script src="./MVVM/observer.js"></script>
<script src="./MVVM/watcher.js"></script>
<script src="./MVVM/mvvm.js"></script>
<script>
new MVVM({
el:'#app',
data:{
msg:'石璞东',
}
})
</script>
首先,什么叫模板:即HTML嵌套了JS代码
问句题外话,为什么我要写成{{name}}的形式,写成其他形式不行吗,比如说""name""的形式不行吗,那必须行啊?来,骚一下~
一点毛病挑不出来,来,咱言归正传~~~
问题提出:
为什么我在p标签内部写{{name}}就可以将data中的数据解析出来呢?是不是有点太魔性了~
其实也不难,通俗点讲,不就是要解析{{name}}的值吗,简单啊,通过正则匹配到{{}},然后调用更新函数改变节点的textContent值不就行了~
问题提出:
在写vue的过程中,大家对于 :
//第一种写法
<p v-text="msg"></p>
//第二种写法
<p>{{msg}}</p>
这两种写法想必都不陌生吧,但是为什么这样写就可以将data中的msg数据解析出来呢?
简单来说,当为<p>{{name}}</p>时,代码会执行对其进行大括号解析,然后从data中获取的相应属性值,然后修改其元素的textContent值。<p>{{msg}}</p>和<p v-text="msg"></p>效果一样,最终都会执行UpdaterFn函数来修改元素标签的textContent值。
对于大括号语法、普通指令、事件指令的具体解析过程即函数的调用栈,我会以流程图的形式展现出来,如下所示:
其实对于模板解析这块还涉及很多,不过道理都一样,代码展示的只是最简单的大括号解析,对于指令解析参考这张完美的图就ok了~
在此把指令解析的思路列出来:
模板解析:事件指令解析
从指令名中取出事件名
根据指令的值(表达式)从methods中得到对应的事件处理函数对象
给当前节点元素绑定指定事件名和回调函数的dom事件监听
指令解析完成后,移除此指令属性
模板解析:一般指令解析
得到指令名和指令值(表达式)
从data中根据表达式得到对应的值
根据指令名确定需要操作元素节点的什么属性 v-text---textContent属性 v-html---innerHTML属性 v-class---className属性
将得到的表达式的值设置到对应的属性上
指令解析完成后,移除此指令属性
模板解析:大括号解析
大概三步:
匹配大括号内的值
从data中取值
更新值
根据正则对象得到匹配出的表达式字符串
从data中取出表达式对应的属性值
将属性值设置为文本节点的textContent
总结一下,模板解析的大概流程就是:
将el的所有子节点取出,添加到一个新建的文档fragment中去
对fragment中的所有层次子节点递归进行编译解析处理
对大括号表达式文本节点进行解析
对元素节点的指令属性进行解析
事件指令解析
一般指令解析
3. 将解析后的fragment添加到el中显示
就是这块:
3. 数据绑定:
一般来讲,数据绑定包括两个方面:初始化显示和更新显示。所谓数据绑定,是指一旦更新了data中的某个属性数据,所有页面上直接使用或间接使用此属性的节点都会更新,实现这个功能的效果就是数据劫持。
数据劫持:
数据劫持是vue中用来实现数据绑定的一种技术
-
基本思想:
通过defineProperty()来监视data中所有属性(任意层次)数据的变化,一旦变化就去更新界面
可能你会疑问,数据绑定和数据代理好像啊,这俩哥们有区别么?
当然啊,数据代理是给vm添加set与get,数据绑定是给data里面的数据绑定set与get,这能一样么~~
来,咱先把思路顺下来:数据绑定无非就两个思路,初始化显示(模板解析技术)、更新显示(数据劫持技术)。
万恶的源头,开始监视的地方:
Vue代码实现:
<div id="test">
<p>{{name}}</p>
<button @click="update">更新</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const vm = new Vue({
el:'#test',
data:{
name:'shipudong'
},
methods:{
update(){
this.name="新年快乐!"
}
}
});
</script>
github代码实现:
<div id="app">
<p>{{name}}</p>
<span>{{name}}</span>
<button v-on:click="update">更新</button>
</div>
<script src="./MVVM/mvvm.js"></script>
<script src="./MVVM/compile.js"></script>
<script src="./MVVM/observer.js"></script>
<script src="./MVVM/watcher.js"></script>
<script>
new MVVM({
el:'#app',
data:{
name:'Jeffery',
wife:{
name:'marui',
age:19
}
},
methods:{
update(){
this.name = "Cathrine"
}
}
})
</script>
问题提出:
利用数据劫持的技术实现数据绑定的效果,那么,这是一种什么样的效果呢?想象一种场景:当页面初始化完成之后,如果要对页面的某个数据进行修改,从原生层面来讲,正常的思路就是:获取元素标签修改DOM值,那既然咱已经用了框架,那么就不能使用这么low的技术了吧,来看看人家的思路:
vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,调用自身的update()方法,实际上是调用compile.js中的UpdaterFn方法去更新界面。
通俗点讲,当页面初始化的时候,通过get方法会建立watcher与dep的关系,函数调用栈如下:
在Observer.js中,有一个subs[],里面保存的是n个watcher的数组容器。
过程如下:
当页面的数据发生改变时(即执行this.name="Cathrine"),即发生在数据更新阶段,会建立dep与watcher的关系~
总结一下:
所以说,dep.subs[]里面存放watcher是为了通知watcher并进行数据的更新,那么watcher里面的dep.Ids{}存放dep是为了干啥呢?
答:防止重复建立关系(假如相应属性的dep.id已经在当前watcher的depIds里,说明不是一个新的属性,仅仅是改变了其值而已则不需要将当前watcher添加到该属性的dep里)
这种情况就如图中例子所示:
data:{
name:"dong",
obj:{
name:'songsidi'
}
}
这种情况则不需要在dep的subs[]里面新增watcher对象,因为这并不是新的属性啊~
三句概括watcher与dep:
一个data中的属性对应(name/age)对应一个dep(dependency)
一个表达式对应一个watcher
一个watcher对应多个dep(多层表达式:a.b.c)
流程:
vm.name = 'abc' → data中name属性值变化 → name的set()调用 → dep ---> 相关的所有watcher → cb() → updater
项目代码参考地址:
https://github.com/DMQ/mvvm
领取资源:
公众号后台回复Linux领取尚硅谷_韩顺平_Linux教程
THE
END
延伸阅读
REC
上一篇: Vue双向绑定原理及实现
下一篇: 前端基础总结之html_html5