MVVM之卡哇伊Vue源码分析plus

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析DNS,个人版 1个月
简介: 本文是对我在2019-01-01发布的名为MVVM之Vue源码分析一文的重新整理,我会首先介绍几个涉及JS方面的知识,然后将对MVVM框架的三大基本原理(即数据代理、模板解析、数据绑定)进行介绍。

前言


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


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


01 Javascript基础知识介绍


1. addEventListener:

   

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


代码实例:


微信图片_20220610214041.jpg


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


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


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


微信图片_20220610214052.jpg


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


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


微信图片_20220610214058.jpg


3. node.nodeType:


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

  • document(9)
  • Element(1)
  • Attr(2)
  • Text(3)


代码示例:


微信图片_20220610214103.png


4.Object.defineProperty

 

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


语法形式:

  • Object.defineProperty(obj,prop,descriptor
  • obj ---> 要在其上定义属性的对象
  • prop ---> 要定义或修改的属性的名称
  • descriptor ---> 将被定义或修改的属性描述符


代码示例:


微信图片_20220610214132.jpg


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


5. Object.keys


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


代码实例:


微信图片_20220610214152.jpg


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


6. Object.hasOwnProperty


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


代码实例:


微信图片_20220610214201.jpg


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


7. DocumentFragment


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


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


代码实例:


微信图片_20220610214206.jpg


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


8. 什么叫MVVM:


三句话:


  1. View相当于模板(即HTML中嵌套JS
  2. ViewModel相当于JS逻辑
  3. Model(数据层,可能涉及到与后台的交互)


02 MVVM框架的三大基本原理


1. 数据代理:


Vue实现:


微信图片_20220610214210.jpg


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

简单说下:


微信图片_20220610214216.png


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


最后,来看看源码实现:


微信图片_20220610214221.jpg


2. 模板解析:


Vue实现:


微信图片_20220610214224.png


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


微信图片_20220610214228.jpg


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


问题提出:


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


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


微信图片_20220610214232.jpg


其实对于模板解析这块还涉及很多,不过道理都一样,代码展示的只是最简单的大括号解析,对于指令解析参考这张完美的图就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中显示


就是这块:


微信图片_20220610214236.jpg


3. 数据绑定:


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


数据劫持:

  1. 数据劫持是vue中用来实现数据绑定的一种技术
  2. 基本思想:
    通过defineProperty()来监视data中所有属性(意层次)数据的变化,一旦变化就去更新界面


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


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


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


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


微信图片_20220610214240.png


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


微信图片_20220610214246.jpg


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


过程如下:


微信图片_20220610214251.jpg


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


微信图片_20220610214256.jpg


总结一下:


微信图片_20220610214300.png


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



相关文章
|
3天前
|
JavaScript
Vue中如何设置在执行删除等危险操作时给用户提示(二次确认后执行对应的操作)
这篇文章介绍了在Vue项目中如何实现执行删除等危险操作时的二次确认机制,使用Element UI的`el-popconfirm`组件来弹出确认框,确保用户在二次确认后才会执行删除操作。
Vue中如何设置在执行删除等危险操作时给用户提示(二次确认后执行对应的操作)
|
3天前
|
JavaScript
如何创建一个Vue项目(手把手教你)
这篇文章是一篇手把手教读者如何创建Vue项目的教程,包括使用管理员身份打开命令行窗口、找到存放项目的位置、通过vue-cli初始化项目、填写项目信息、进入项目目录、启动项目等步骤,并提供了一些常见第三方库的引入方法。
如何创建一个Vue项目(手把手教你)
|
3天前
|
前端开发
StringBoot+Vue实现游客或用户未登录系统前、可以浏览商品等信息、但是不能购买商品或者加入购物车等操作。登录系统显示用户的登录名(源码+讲解)
这篇文章介绍了使用StringBoot+Vue实现用户登录状态判断的方法,包括前端加载用户信息和后端设置session的源码示例。
|
3天前
|
JavaScript
如何在Vue页面中引入img下的图片作为背景图。../的使用
这篇文章介绍了在Vue页面中如何引入`img`目录下的图片作为背景图,提供了两种使用相对路径的方法。第一种是使用`../assets/img/`作为路径引入图片,第二种是使用`../../assets/img/`作为路径。文章还展示了使用这些方法的代码实现和效果展示,并鼓励读者学无止境。
如何在Vue页面中引入img下的图片作为背景图。../的使用
|
3天前
|
JavaScript
如何通过点击商品的信息(图片或者文字)跳转到更加详细的商品信息介绍(前后端分离之Vue实现)
该博客文章介绍了如何在Vue 2框架下实现前后端分离的商品信息展示和详情页跳转,包括排序筛选、详情展示、加入购物车和分享功能。
如何通过点击商品的信息(图片或者文字)跳转到更加详细的商品信息介绍(前后端分离之Vue实现)
|
3天前
|
JavaScript
如何查看Vue使用的版本
这篇文章介绍了如何在项目中查看Vue及其相关库的版本信息,比如element-ui和element-china-area-data。要查看Vue的版本,需要查看项目中的`package.json`文件,在`dependencies`部分可以找到Vue的版本号。如果需要查询不同版本的兼容性,可以访问相应的官方文档或资源网站。
|
3天前
|
存储 JavaScript 前端开发
Vue中如何通过三元运算符来展示不同的操作
这篇文章讲述了在Vue中如何使用三元运算符结合v-if指令来根据订单的不同状态展示不同的操作按钮,例如在待发货状态显示退款按钮,在待付款或完成状态显示删除按钮。
|
JavaScript 测试技术 容器
Vue2+VueRouter2+webpack 构建项目
1). 安装Node环境和npm包管理工具 检测版本 node -v npm -v 图1.png 2). 安装vue-cli(vue脚手架) npm install -g vue-cli --registry=https://registry.
1029 0
|
4天前
|
JavaScript 编译器
成功解决:Module build failed: Error: Vue packages version mismatch
这篇文章记录了解决Vue项目中遇到的"Module build failed: Error: Vue packages version mismatch"错误的步骤,原因是项目中Vue依赖的版本不一致,解决方法是删除`node_modules`后重新安装指定版本的Vue和`vue-template-compiler`,确保版本匹配,最终成功运行项目。
成功解决:Module build failed: Error: Vue packages version mismatch
|
4天前
|
JavaScript
在Vue中使用Avue、配置过程以及实际应用
这篇文章介绍了作者在Vue项目中集成Avue组件库的完整过程,包括安装、配置和实际应用,展示了如何利用Avue实现动态表单和数据展示的功能。
在Vue中使用Avue、配置过程以及实际应用