前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MVVM之Vue源码分析

MVVM之Vue源码分析

作者头像
石璞东
修改2020-04-22 22:24:42
8150
修改2020-04-22 22:24:42
举报
文章被收录于专栏:石璞东 | haha石璞东 | haha

本系列文章将会围绕Vue框架中涉及到的技术进行一系列的分析,在本文中:

我会首先介绍几个涉及JS方面的知识,然后着重对于实现一个MVVM框架的三大基本原理(即数据代理、模板解析、数据绑定)进行介绍,在接下来的几篇文章中会对涉及到的其他技术(发布订阅、热重载、Virtual DOM等)逐一介绍.

需要你了解的本文没有介绍的知识:Javascript继承(尤其是原型链继承)、数组方法(forEach等)、this指针、函数的嵌套调用与递归调用等.

还有一项重要的技能就是:debug调试

debug调试是最重要的技能,其实我之前对于控制台的理解也就是看看结果、爬虫的时候看看请求的文件、看看localStorage等,并没有意识到这些,也是在一位美团大牛的带领下(手动@漂流瓶)才了解到,不过我真正学会还是在看Vue源码的时候.再说三遍:debug很重要*3~~~

对于MVVM之Vue源码的分析会参考github:https://github.com/DMQ/mvvm

Javascript基础知识介绍

1. addEventListener:

input监听(输入过程中发生)与change监听(失去焦点时发生)

该方法将指定的监听器注册到对应元素上,当元素触发指定的事件时,指定的回调函数就会执行.

代码实例:

本行代码是实现双向数据绑定的关键代码:其中this.bind(node,vm,exp,'model')实现的是单项的数据绑定(即model==>view),即数据层到视图层的初始化显示(以及创建对应的watcher),其余代码是实现view==>model的绑定(即当视图层数据变化时,对应数据层的相应数据也发生改变的功能).

node.addEventListener("input",function(e){})---其中第一个参数是input是绑定的事件类型(即当表单元素检测到输入时就会触发),第二个回调函数是当事件触发时所要执行的功能.有时还可能遇到第三个参数(布尔值的形式),当该参数设置为true就在捕获过程中执行,反之就在冒泡过程中执行处理函数。

2. 伪(类)数组转换成真数组:

实现方法:

代码语言:javascript
复制
Array.prototype.slice.call()
[].slice.call()

ES6中的方法:Array.from()

这里我想说下前两个方法的优缺点:

首先这两个方法都是接收一个伪数组作为参数,但是从执行效率上讲:

从图中可以看到,slice方法是定义在原型上的,所以第一种方法会直接到原型上查找,一点毛病没有,而第二种方法会首先在实例上查找,如果实例上开发者没有定义一个slice方法才会去原型上查找,所以相比之下会消耗时间.

代码实例:(模板解析部分的代码)

3. node.nodeType:

只介绍四个常用节点类型:

代码语言:javascript
复制
document(9)
Element(1)
Attr(2)
Text(3)

代码示例:

4. Object.defineProperty:

会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象.

语法形式:

代码语言:javascript
复制
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(数据层,可能涉及到与后台的交互)

关于MVVM、MVP、MVC的区别我会在系列文章的最后一篇结合Vue的源码分析做以总结,读者大可放心.

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中的数据

那既然思路都有了,实现起来也就很简单了,来看方法:

  1. 通过Object.defineProperty(vm,key,{})给vm添加与data对象的属性对应的属性描述符
  2. 所有添加的属性都包含get/set方法
  3. 在set/get方法中去操作data中对应的属性

来现在看看github上代码的实现:

代码语言:javascript
复制
<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实现:

代码语言:javascript
复制
<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了~~~

在此把指令解析的思路列出来:

模板解析:事件指令解析

  1. 从指令名中取出事件名
  2. 根据指令的值(表达式)从methods中得到对应的事件处理函数对象
  3. 给当前节点元素绑定指定事件名和回调函数的dom事件监听
  4. 指令解析完成后,移除此指令属性

模板解析:一般指令解析

  1. 得到指令名和指令值(表达式)
  2. 从data中根据表达式得到对应的值
  3. 根据指令名确定需要操作元素节点的什么属性
代码语言:javascript
复制
v-text  ---  textContent属性
v-html  ---  innerHTML属性
v-class  --- className属性   
  1. 将得到的表达式的值设置到对应的属性上
  2. 指令解析完成后,移除此指令属性

哦,还有大括号解析:

模板解析:大括号解析 大概三步:匹配大括号内的值 从data中取值 更新值

  1. 根据正则对象得到匹配出的表达式字符串
  2. 从data中取出表达式对应的属性值
  3. 将属性值设置为文本节点的textContent

总结一下,模板解析的大概流程就是:

1)将el的所有子节点取出,添加到一个新建的文档fragment中去

2)对fragment中的所有层次子节点递归进行编译解析处理

对大括号表达式文本节点进行解析

对元素节点的指令属性进行解析

事件指令解析

一般指令解析

3)将解析后的fragment添加到el中显示

就是这块:

3. 数据绑定:

一般来讲,数据绑定包括两个方面:初始化显示和更新显示.所谓数据绑定,是指一旦更新了data中的某个属性数据,所有页面上直接使用或间接使用此属性的节点都会更新,实现这个功能的效果就是数据劫持.

数据劫持:

1)数据劫持是vue中用来实现数据绑定的一种技术

2)基本思想:

通过defineProperty()来监视data中所有属性(任意层次)数据的变化,一旦变化就去更新界面

可能你会疑问,数据绑定和数据代理好像啊,这俩哥们有区别么?

当然啊,数据代理是给vm添加set与get,数据绑定是给data里面的数据绑定set与get,这能一样么~~

来,咱先把思路顺下来:数据绑定无非就两个思路,初始化显示(模板解析技术)、更新显示(数据劫持技术).

万恶的源头,开始监视的地方:

Vue代码实现:

代码语言:javascript
复制
<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的关系~

总结一下:

<Observer.js>dep里面的subs[]数组存放的是Watcher对象,当对data中的相关属性进行改变时(即执行如下代码:this.name="Cathrine")会自动调用set方法,

该方法会通过dep.notify()通知订阅者(即通过遍历dep.subs[]数组),对其中的每一个watcher对象通过updaterFn && updaterFn(node, value, oldValue)<watcher.js>来实现数据的更新.

所以说,dep.subs[]里面存放watcher是为了通知watcher并进行数据的更新,那么watcher里面的dep.Ids{}存放dep是为了干啥呢?

答:防止重复建立关系(假如相应属性的dep.id已经在当前watcher的depIds里,说明不是一个新的属性,仅仅是改变了其值而已则不需要将当前watcher添加到该属性的dep里)

这种情况就如例子所示,

代码语言:javascript
复制
data:{
name:"dong",
obj:{
name:'marui'
}

这种情况则不需要再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

声明

更多内容请移步我的公众号平台hahaCoder或者个人网站http://www.shipudong.com进行查看。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-01-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 hahaCoder 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Javascript基础知识介绍
  • MVVM框架的三大基本原理
  • 声明
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档