欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

MVVM之卡哇伊Vue源码分析plus

程序员文章站 2022-03-30 11:51:08
...

窗外风雪再大

也有我陪伴着你

全文字数:4731

阅读时间:30分钟

MVVM之卡哇伊Vue源码分析plus

_

前言

_

本文是对我在2019-01-01发布的名为MVVM之Vue源码分析一文的重新整理,我会首先介绍几个涉及JS方面的知识,然后将对MVVM框架的三大基本原理(即数据代理、模板解析、数据绑定)进行介绍。

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

01 Javascript基础知识介绍

1. addEventListener:

    

input监听(输入过程中发生)与change监听(失去焦点时发生),该方法将指定的监听器注册到对应元素上,当元素触发指定的事件时,指定的回调函数就会执行。

代码实例:

MVVM之卡哇伊Vue源码分析plus

本行代码是实现双向数据绑定的关键代码:

其中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()

这里我想说下前两个方法的优缺点,首先这两个方法都是接收一个伪数组作为参数,但是从执行效率上讲:

MVVM之卡哇伊Vue源码分析plus

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

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

MVVM之卡哇伊Vue源码分析plus

3. node.nodeType:

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

  • document(9)

  • Element(1)

  • Attr(2)

  • Text(3)

代码示例:

MVVM之卡哇伊Vue源码分析plus

4.Object.defineProperty

   

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

语法形式:

  • Object.defineProperty(obj,prop,descriptor

  • obj ---> 要在其上定义属性的对象

  • prop ---> 要定义或修改的属性的名称

  • descriptor ---> 将被定义或修改的属性描述符

代码示例:

MVVM之卡哇伊Vue源码分析plus

该部分代码是通过Object.defineProperty()给对应属性添加get/set方法以实现数据代理效果的实现。

5. Object.keys

该方法会返回一个由一个给定对象的自身可枚举属性组成的数组。

代码实例:

MVVM之卡哇伊Vue源码分析plus

当视图层的数据来源有一部分是通过计算属性得到的时,会调用该部分代码。

6. Object.hasOwnProperty

该方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性。

代码实例:

MVVM之卡哇伊Vue源码分析plus

该部分代码是建立watcher与dep之间的关系滴~

7. DocumentFragment

DocumentFragment接口表示一个没有父级文件的最小文档对象,举个栗子:如果现在页面有100000....个li标签,现在的需求是将这10000...个的innerHTML值改为"石璞东",那么一般的做法就是:获取所有li,通过遍历循环修改其属性即可,也挺简单,但是操作太耗性能。(过多的DOM操作会引起浏览器的重排操作,即修改一次DOM,浏览器就需要重新计算部分甚至整个页面的几何结构信息,浏览器需要重新遍历DOM树,根据CSS规则进行对受到影响的DOM元素进行计算,然后进行重新绘制,这样很耗内力的哟~)

而对于DocumentFragment来说,它不是真实DOM树的一部分(听起来像不像是在说virtual Dom),它的变化不会引起DOM树的重新渲染的操作(reflow),且不会导致性能等问题。它的对于节点的所有操作都是在内存中进行,当操作结束后,所有节点会被一次插入到文档中,也就意味着只发生一次重渲染的操作。

代码实例:

MVVM之卡哇伊Vue源码分析plus

代码解释:该部分代码是将页面的所有节点全部移入fragment中进行操作,然后操作完成之后通过appendChild方法插入页面。

8. 什么叫MVVM:

三句话:

  1. View相当于模板(即HTML中嵌套JS

  2. ViewModel相当于JS逻辑

  3. Model(数据层,可能涉及到与后台的交互)

02 MVVM框架的三大基本原理

1. 数据代理:

Vue实现:

MVVM之卡哇伊Vue源码分析plus

现在的问题就是:我明明是定义在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>

简单说下:

MVVM之卡哇伊Vue源码分析plus

这是函数的执行栈,其中涉及到两个方法get与set,当执行console.log(vm.data),即读操作时会调用该方法,当执行vm.data="newVal"会执行该操作。ok,就这么简单~

最后,来看看源码实现:

MVVM之卡哇伊Vue源码分析plus

2. 模板解析:

Vue实现:

MVVM之卡哇伊Vue源码分析plus

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""的形式不行吗,那必须行啊?来,骚一下~

MVVM之卡哇伊Vue源码分析plus

一点毛病挑不出来,来,咱言归正传~~~

问题提出:

为什么我在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值。

对于大括号语法、普通指令、事件指令的具体解析过程即函数的调用栈,我会以流程图的形式展现出来,如下所示:

MVVM之卡哇伊Vue源码分析plus

其实对于模板解析这块还涉及很多,不过道理都一样,代码展示的只是最简单的大括号解析,对于指令解析参考这张完美的图就ok了~

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

模板解析:事件指令解析

  1. 从指令名中取出事件名

  2. 根据指令的值(表达式)从methods中得到对应的事件处理函数对象

  3. 给当前节点元素绑定指定事件名和回调函数的dom事件监听

  4. 指令解析完成后,移除此指令属性

模板解析:一般指令解析

  1. 得到指令名和指令值(表达式

  2. 从data中根据表达式得到对应的值

  3. 根据指令名确定需要操作元素节点的什么属性 v-text---textContent属性   v-html---innerHTML属性  v-class---className属性

  4. 将得到的表达式的值设置到对应的属性上

  5. 指令解析完成后,移除此指令属性

模板解析:大括号解析

大概三步:

  • 匹配大括号内的值

  • 从data中取值

  • 更新值

  1. 根据正则对象得到匹配出的表达式字符串

  2. 从data中取出表达式对应的属性值

  3. 将属性值设置为文本节点的textContent

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

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

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

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

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

      1. 事件指令解析

      2. 一般指令解析

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

就是这块:

MVVM之卡哇伊Vue源码分析plus

3. 数据绑定:

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

数据劫持:

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

  2. 基本思想:

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

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

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

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

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

MVVM之卡哇伊Vue源码分析plus

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的关系,函数调用栈如下:

MVVM之卡哇伊Vue源码分析plus

在Observer.js中,有一个subs[],里面保存的是n个watcher的数组容器。

过程如下:

MVVM之卡哇伊Vue源码分析plus

当页面的数据发生改变时(即执行this.name="Cathrine"),即发生在数据更新阶段,会建立dep与watcher的关系~

MVVM之卡哇伊Vue源码分析plus

总结一下:

MVVM之卡哇伊Vue源码分析plus

所以说,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:

  1. 一个data中的属性对应(name/age)对应一个dep(dependency

  2. 一个表达式对应一个watcher

  3. 一个watcher对应多个dep(多层表达式:a.b.c

流程:

vm.name = 'abc' → data中name属性值变化 → name的set()调用 → dep ---> 相关的所有watcher  cb() → updater

项目代码参考地址:

https://github.com/DMQ/mvvm

领取资源:

公众号后台回复Linux领取尚硅谷_韩顺平_Linux教程

MVVM之卡哇伊Vue源码分析plus

_

THE

END

延伸阅读

_

_

_

REC

_

MVVM之卡哇伊Vue源码分析plus

MVVM之卡哇伊Vue源码分析plus

_

_

_