热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Vue工作原理小结

本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模块2、缓解好奇心的同时了解如何实现双向绑定为了便于说明原理与实现,本文相关代码主要摘自v

本文能帮你做什么?
1、了解vue的双向数据绑定原理以及核心代码模块
2、缓解好奇心的同时了解如何实现双向绑定
为了便于说明原理与实现&#xff0c;本文相关代码主要摘自vue源码, 并进行了简化改造&#xff0c;相对较简陋&#xff0c;并未考虑到数组的处理、数据的循环依赖等&#xff0c;也难免存在一些问题&#xff0c;欢迎大家指正。不过这些并不会影响大家的阅读和理解&#xff0c;相信看完本文后对大家在阅读vue源码的时候会更有帮助<
本文所有相关代码均在github上面可找到 https://github.com/DMQ/mvvm

相信大家对mvvm双向绑定应该都不陌生了&#xff0c;一言不合上代码&#xff0c;下面先看一个本文最终实现的效果吧&#xff0c;和vue一样的语法&#xff0c;如果还不了解双向绑定&#xff0c;猛戳Google

<div id&#61;"mvvm-app"> <input type&#61;"text" v-model&#61;"word"> <p>{{word}}p> <button v-on:click&#61;"sayHi">change modelbutton> div> <script src&#61;"./js/observer.js">script> <script src&#61;"./js/watcher.js">script> <script src&#61;"./js/compile.js">script> <script src&#61;"./js/mvvm.js">script> <script> var vm &#61; new MVVM({ el: &#39;#mvvm-app&#39;, data: { word: &#39;Hello World!&#39; }, methods: { sayHi: function() { this.word &#61; &#39;Hi, everybody!&#39;; } } }); script>

效果&#xff1a;

几种实现双向绑定的做法

目前几种主流的mvc(vm)框架都实现了单向数据绑定&#xff0c;而我所理解的双向数据绑定无非就是在单向绑定的基础上给可输入元素&#xff08;input、textare等&#xff09;添加了change(input)事件&#xff0c;来动态修改model和 view&#xff0c;并没有多高深。所以无需太过介怀是实现的单向或双向绑定。

实现数据绑定的做法有大致如下几种&#xff1a;

发布者-订阅者模式&#xff08;backbone.js&#xff09;

脏值检查&#xff08;angular.js&#xff09;

数据劫持&#xff08;vue.js&#xff09;

发布者-订阅者模式: 一般通过sub, pub的方式实现数据和视图的绑定监听&#xff0c;更新数据方式通常做法是 vm.set(&#39;property&#39;, value)&#xff0c;这里有篇文章讲的比较详细&#xff0c;有兴趣可点这里

这种方式现在毕竟太low了&#xff0c;我们更希望通过 vm.property &#61; value 这种方式更新数据&#xff0c;同时自动更新视图&#xff0c;于是有了下面两种方式

脏值检查: angular.js 是通过脏值检测的方式比对数据是否有变更&#xff0c;来决定是否更新视图&#xff0c;最简单的方式就是通过 setInterval() 定时轮询检测数据变动&#xff0c;当然Google不会这么low&#xff0c;angular只有在指定的事件触发时进入脏值检测&#xff0c;大致如下&#xff1a;

  • DOM事件&#xff0c;譬如用户输入文本&#xff0c;点击按钮等。( ng-click )

  • XHR响应事件 ( $http )

  • 浏览器Location变更事件 ( $location )

  • Timer事件( $timeout , $interval )

  • 执行 $digest() 或 $apply()

数据劫持: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式&#xff0c;通过Object.defineProperty()来劫持各个属性的setter&#xff0c;getter&#xff0c;在数据变动时发布消息给订阅者&#xff0c;触发相应的监听回调。

思路整理

已经了解到vue是通过数据劫持的方式来做数据绑定的&#xff0c;其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持&#xff0c;达到监听数据变动的目的&#xff0c;无疑这个方法是本文中最重要、最基础的内容之一&#xff0c;如果不熟悉defineProperty&#xff0c;猛戳这里
整理了一下&#xff0c;要实现mvvm的双向绑定&#xff0c;就必须要实现以下几点&#xff1a;
1、实现一个数据监听器Observer&#xff0c;能够对数据对象的所有属性进行监听&#xff0c;如有变动可拿到最新值并通知订阅者
2、实现一个指令解析器Compile&#xff0c;对每个元素节点的指令进行扫描和解析&#xff0c;根据指令模板替换数据&#xff0c;以及绑定相应的更新函数
3、实现一个Watcher&#xff0c;作为连接Observer和Compile的桥梁&#xff0c;能够订阅并收到每个属性变动的通知&#xff0c;执行指令绑定的相应回调函数&#xff0c;从而更新视图
4、mvvm入口函数&#xff0c;整合以上三者

上述流程如图所示&#xff1a;

1、实现Observer

ok, 思路已经整理完毕&#xff0c;也已经比较明确相关逻辑和模块功能了&#xff0c;let&#39;s do it
我们知道可以利用Obeject.defineProperty()来监听属性变动
那么将需要observe的数据对象进行递归遍历&#xff0c;包括子属性对象的属性&#xff0c;都加上 settergetter
这样的话&#xff0c;给这个对象的某个值赋值&#xff0c;就会触发setter&#xff0c;那么就能监听到了数据变化。。相关代码可以是这样&#xff1a;

var data &#61; {name: &#39;kindeng&#39;};
observe(data);
data.name &#61; &#39;dmq&#39;; // 哈哈哈&#xff0c;监听到值变化了 kindeng --> dmq function observe(data) { if (!data || typeof data !&#61;&#61; &#39;object&#39;) { return; } // 取出所有属性遍历 Object.keys(data).forEach(function(key) { defineReactive(data, key, data[key]); }); }; function defineReactive(data, key, val) { observe(val); // 监听子属性 Object.defineProperty(data, key, { enumerable: true, // 可枚举 configurable: false, // 不能再define get: function() { return val; }, set: function(newVal) { console.log(&#39;哈哈哈&#xff0c;监听到值变化了 &#39;, val, &#39; --> &#39;, newVal); val &#61; newVal; } }); }

这样我们已经可以监听每个数据的变化了&#xff0c;那么监听到变化之后就是怎么通知订阅者了&#xff0c;所以接下来我们需要实现一个消息订阅器&#xff0c;很简单&#xff0c;维护一个数组&#xff0c;用来收集订阅者&#xff0c;数据变动触发notify&#xff0c;再调用订阅者的update方法&#xff0c;代码改善之后是这样&#xff1a;

// ... 省略
function defineReactive(data, key, val) { var dep &#61; new Dep(); observe(val); // 监听子属性 Object.defineProperty(data, key, { // ... 省略 set: function(newVal) { if (val &#61;&#61;&#61; newVal) return; console.log(&#39;哈哈哈&#xff0c;监听到值变化了 &#39;, val, &#39; --> &#39;, newVal); val &#61; newVal; dep.notify(); // 通知所有订阅者 } }); } function Dep() { this.subs &#61; []; } Dep.prototype &#61; { addSub: function(sub) { this.subs.push(sub); }, notify: function() { this.subs.forEach(function(sub) { sub.update(); }); } };

那么问题来了&#xff0c;谁是订阅者&#xff1f;怎么往订阅器添加订阅者&#xff1f;
没错&#xff0c;上面的思路整理中我们已经明确订阅者应该是Watcher, 而且var dep &#61; new Dep();是在 defineReactive方法内部定义的&#xff0c;所以想通过dep添加订阅者&#xff0c;就必须要在闭包内操作&#xff0c;所以我们可以在 getter里面动手脚&#xff1a;

// Observer.js
// ...省略
Object.defineProperty(data, key, {get: function() { // 由于需要在闭包内添加watcher&#xff0c;所以通过Dep定义一个全局target属性&#xff0c;暂存watcher, 添加完移除 Dep.target && dep.addDep(Dep.target); return val; } // ... 省略 }); // Watcher.js Watcher.prototype &#61; { get: function(key) { Dep.target &#61; this; this.value &#61; data[key]; // 这里会触发属性的getter&#xff0c;从而添加订阅者 Dep.target &#61; null; } }

这里已经实现了一个Observer了&#xff0c;已经具备了监听数据和数据变化通知订阅者的功能&#xff0c;完整代码。那么接下来就是实现Compile了

2、实现Compile

compile主要做的事情是解析模板指令&#xff0c;将模板中的变量替换成数据&#xff0c;然后初始化渲染页面视图&#xff0c;并将每个指令对应的节点绑定更新函数&#xff0c;添加监听数据的订阅者&#xff0c;一旦数据有变动&#xff0c;收到通知&#xff0c;更新视图&#xff0c;如图所示&#xff1a;

因为遍历解析的过程有多次操作dom节点&#xff0c;为提高性能和效率&#xff0c;会先将跟节点el转换成文档碎片fragment进行解析编译操作&#xff0c;解析完成&#xff0c;再将fragment添加回原来的真实dom节点中

function Compile(el) {this.$el &#61; this.isElementNode(el) ? el : document.querySelector(el);if (this.$el) {this.$fragment &#61; this.node2Fragment(this.$el); this.init(); this.$el.appendChild(this.$fragment); } } Compile.prototype &#61; { init: function() { this.compileElement(this.$fragment); }, node2Fragment: function(el) { var fragment &#61; document.createDocumentFragment(), child; // 将原生节点拷贝到fragment while (child &#61; el.firstChild) { fragment.appendChild(child); } return fragment; } };

compileElement方法将遍历所有节点及其子节点&#xff0c;进行扫描解析编译&#xff0c;调用对应的指令渲染函数进行数据渲染&#xff0c;并调用对应的指令更新函数进行绑定&#xff0c;详看代码及注释说明&#xff1a;

Compile.prototype &#61; {// ... 省略compileElement: function(el) {var childNodes &#61; el.childNodes, me &#61; this;[].slice.call(childNodes).forEach(function(node) {var text &#61; node.textContent;var reg &#61; /\{\{(.*)\}\}/; // 表达式文本// 按元素节点方式编译if (me.isElementNode(node)) { me.compile(node); } else if (me.isTextNode(node) && reg.test(text)) { me.compileText(node, RegExp.$1); } // 遍历编译子节点 if (node.childNodes && node.childNodes.length) { me.compileElement(node); } }); }, compile: function(node) { var nodeAttrs &#61; node.attributes, me &#61; this; [].slice.call(nodeAttrs).forEach(function(attr) { // 规定&#xff1a;指令以 v-xxx 命名 // 如 中指令为 v-text var attrName &#61; attr.name; // v-text if (me.isDirective(attrName)) { var exp &#61; attr.value; // content var dir &#61; attrName.substring(2); // text if (me.isEventDirective(dir)) { // 事件指令, 如 v-on:click compileUtil.eventHandler(node, me.$vm, exp, dir); } else { // 普通指令 compileUtil[dir] && compileUtil[dir](node, me.$vm, exp); } } }); } }; // 指令处理集合 var compileUtil &#61; { text: function(node, vm, exp) { this.bind(node, vm, exp, &#39;text&#39;); }, // ...省略 bind: function(node, vm, exp, dir) { var updaterFn &#61; updater[dir &#43; &#39;Updater&#39;]; // 第一次初始化视图 updaterFn && updaterFn(node, vm[exp]); // 实例化订阅者&#xff0c;此操作会在对应的属性消息订阅器中添加了该订阅者watcher new Watcher(vm, exp, function(value, oldValue) { // 一旦属性值有变化&#xff0c;会收到通知执行此更新函数&#xff0c;更新视图 updaterFn && updaterFn(node, value, oldValue); }); } }; // 更新函数 var updater &#61; { textUpdater: function(node, value) { node.textContent &#61; typeof value &#61;&#61; &#39;undefined&#39; ? &#39;&#39; : value; } // ...省略 };

这里通过递归遍历保证了每个节点及子节点都会解析编译到&#xff0c;包括了{{}}表达式声明的文本节点。指令的声明规定是通过特定前缀的节点属性来标记&#xff0c;如v-text便是指令&#xff0c;而other-attr不是指令&#xff0c;只是普通的属性。
监听数据、绑定更新函数的处理是在compileUtil.bind()这个方法中&#xff0c;通过new Watcher()添加回调来接收数据变化的通知

至此&#xff0c;一个简单的Compile就完成了&#xff0c;完整代码。接下来要看看Watcher这个订阅者的具体实现了

3、实现Watcher

Watcher订阅者作为Observer和Compile之间通信的桥梁&#xff0c;主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个update()方法
3、待属性变动dep.notice()通知时&#xff0c;能调用自身的update()方法&#xff0c;并触发Compile中绑定的回调&#xff0c;则功成身退。
如果有点乱&#xff0c;可以回顾下前面的思路整理

function Watcher(vm, exp, cb) {this.cb &#61; cb; this.vm &#61; vm; this.exp &#61; exp; // 此处为了触发属性的getter&#xff0c;从而在dep添加自己&#xff0c;结合Observer更易理解 this.value &#61; this.get(); } Watcher.prototype &#61; { update: function() { this.run(); // 属性值变化收到通知 }, run: function() { var value &#61; this.get(); // 取到最新值 var oldVal &#61; this.value; if (value !&#61;&#61; oldVal) { this.value &#61; value; this.cb.call(this.vm, value, oldVal); // 执行Compile中绑定的回调&#xff0c;更新视图 } }, get: function() { Dep.target &#61; this; // 将当前订阅者指向自己 var value &#61; this.vm[exp]; // 触发getter&#xff0c;添加自己到属性订阅器中 Dep.target &#61; null; // 添加完毕&#xff0c;重置 return value; } }; // 这里再次列出Observer和Dep&#xff0c;方便理解 Object.defineProperty(data, key, { get: function() { // 由于需要在闭包内添加watcher&#xff0c;所以可以在Dep定义一个全局target属性&#xff0c;暂存watcher, 添加完移除 Dep.target && dep.addDep(Dep.target); return val; } // ... 省略 }); Dep.prototype &#61; { notify: function() { this.subs.forEach(function(sub) { sub.update(); // 调用订阅者的update方法&#xff0c;通知变化 }); } };

实例化Watcher的时候&#xff0c;调用get()方法&#xff0c;通过Dep.target &#61; watcherInstance标记订阅者是当前watcher实例&#xff0c;强行触发属性定义的getter方法&#xff0c;getter方法执行的时候&#xff0c;就会在属性的订阅器dep添加当前watcher实例&#xff0c;从而在属性值有变化的时候&#xff0c;watcherInstance就能收到更新通知。

ok, Watcher也已经实现了&#xff0c;完整代码。
基本上vue中数据绑定相关比较核心的几个模块也是这几个&#xff0c;猛戳这里 , 在src 目录可找到vue源码。

最后来讲讲MVVM入口文件的相关逻辑和实现吧&#xff0c;相对就比较简单了~

4、实现MVVM

MVVM作为数据绑定的入口&#xff0c;整合Observer、Compile和Watcher三者&#xff0c;通过Observer来监听自己的model数据变化&#xff0c;通过Compile来解析编译模板指令&#xff0c;最终利用Watcher搭起Observer和Compile之间的通信桥梁&#xff0c;达到数据变化 -> 视图更新&#xff1b;视图交互变化(input) -> 数据model变更的双向绑定效果。

一个简单的MVVM构造器是这样子&#xff1a;

function MVVM(options) {this.$options &#61; options;var data &#61; this._data &#61; this.$options.data; observe(data, this); this.$compile &#61; new Compile(options.el || document.body, this) }

但是这里有个问题&#xff0c;从代码中可看出监听的数据对象是options.data&#xff0c;每次需要更新视图&#xff0c;则必须通过var vm &#61; new MVVM({data:{name: &#39;kindeng&#39;}}); vm._data.name &#61; &#39;dmq&#39;; 这样的方式来改变数据。

显然不符合我们一开始的期望&#xff0c;我们所期望的调用方式应该是这样的&#xff1a;
var vm &#61; new MVVM({data: {name: &#39;kindeng&#39;}}); vm.name &#61; &#39;dmq&#39;;

所以这里需要给MVVM实例添加一个属性代理的方法&#xff0c;使访问vm的属性代理为访问vm._data的属性&#xff0c;改造后的代码如下&#xff1a;

function MVVM(options) { this.$options &#61; options; var data &#61; this._data &#61; this.$options.data, me &#61; this; // 属性代理&#xff0c;实现 vm.xxx -> vm._data.xxx Object.keys(data).forEach(function(key) { me._proxy(key); }); observe(data, this); this.$compile &#61; new Compile(options.el || document.body, this) } MVVM.prototype &#61; { _proxy: function(key) { var me &#61; this; Object.defineProperty(me, key, { configurable: false, enumerable: true, get: function proxyGetter() { return me._data[key]; }, set: function proxySetter(newVal) { me._data[key] &#61; newVal; } }); } };

这里主要还是利用了Object.defineProperty()这个方法来劫持了vm实例对象的属性的读写权&#xff0c;使读写vm实例的属性转成读写了vm._data的属性值&#xff0c;达到鱼目混珠的效果&#xff0c;哈哈

至此&#xff0c;全部模块和功能已经完成了&#xff0c;如本文开头所承诺的两点。一个简单的MVVM模块已经实现&#xff0c;其思想和原理大部分来自经过简化改造的vue源码&#xff0c;猛戳这里可以看到本文的所有相关代码。
由于本文内容偏实践&#xff0c;所以代码量较多&#xff0c;且不宜列出大篇幅代码&#xff0c;所以建议想深入了解的童鞋可以再次结合本文源代码来进行阅读&#xff0c;这样会更加容易理解和掌握。

总结

本文主要围绕“几种实现双向绑定的做法”、“实现Observer”、“实现Compile”、“实现Watcher”、“实现MVVM”这几个模块来阐述了双向绑定的原理和实现。并根据思路流程渐进梳理讲解了一些细节思路和比较关键的内容点&#xff0c;以及通过展示部分关键代码讲述了怎样一步步实现一个双向绑定MVVM。文中肯定会有一些不够严谨的思考和错误&#xff0c;欢迎大家指正&#xff0c;有兴趣欢迎一起探讨和改进~

最后&#xff0c;感谢您的阅读&#xff01;

转自&#xff1a;http://www.cnblogs.com/bilibilicai/p/6826432.html



推荐阅读
  • Go GUIlxn/walk 学习3.菜单栏和工具栏的具体实现
    本文介绍了使用Go语言的GUI库lxn/walk实现菜单栏和工具栏的具体方法,包括消息窗口的产生、文件放置动作响应和提示框的应用。部分代码来自上一篇博客和lxn/walk官方示例。文章提供了学习GUI开发的实际案例和代码示例。 ... [详细]
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
  • Google Play推出全新的应用内评价API,帮助开发者获取更多优质用户反馈。用户每天在Google Play上发表数百万条评论,这有助于开发者了解用户喜好和改进需求。开发者可以选择在适当的时间请求用户撰写评论,以获得全面而有用的反馈。全新应用内评价功能让用户无需返回应用详情页面即可发表评论,提升用户体验。 ... [详细]
  • FeatureRequestIsyourfeaturerequestrelatedtoaproblem?Please ... [详细]
  • 【shell】网络处理:判断IP是否在网段、两个ip是否同网段、IP地址范围、网段包含关系
    本文介绍了使用shell脚本判断IP是否在同一网段、判断IP地址是否在某个范围内、计算IP地址范围、判断网段之间的包含关系的方法和原理。通过对IP和掩码进行与计算,可以判断两个IP是否在同一网段。同时,还提供了一段用于验证IP地址的正则表达式和判断特殊IP地址的方法。 ... [详细]
  • JavaScript和HTML之间的交互是经由过程事宜完成的。事宜:文档或浏览器窗口中发作的一些特定的交互霎时。能够运用侦听器(或处置惩罚递次来预订事宜),以便事宜发作时实行相应的 ... [详细]
  • 在Docker中,将主机目录挂载到容器中作为volume使用时,常常会遇到文件权限问题。这是因为容器内外的UID不同所导致的。本文介绍了解决这个问题的方法,包括使用gosu和suexec工具以及在Dockerfile中配置volume的权限。通过这些方法,可以避免在使用Docker时出现无写权限的情况。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • Android开发实现的计时器功能示例
    本文分享了Android开发实现的计时器功能示例,包括效果图、布局和按钮的使用。通过使用Chronometer控件,可以实现计时器功能。该示例适用于Android平台,供开发者参考。 ... [详细]
  • iOS Swift中如何实现自动登录?
    本文介绍了在iOS Swift中如何实现自动登录的方法,包括使用故事板、SWRevealViewController等技术,以及解决用户注销后重新登录自动跳转到主页的问题。 ... [详细]
  • 在JavaScript中,函数没有重载的概念,如果声明了多个重名的函数,不管函数的形参个数是否一样,只有最后一个有效。如果调用函数时传入的参数个数与函数定义时的参数个数不符,会出现不同的情况。函数调用时,传入的参数个数少于函数定义时的参数个数,未传入的参数会被当做undefined处理,可能会导致错误。而传入的参数个数多于函数定义时的参数个数,多余的参数不会被使用,但不会报错。 ... [详细]
  • Android实战——jsoup实现网络爬虫,糗事百科项目的起步
    本文介绍了Android实战中使用jsoup实现网络爬虫的方法,以糗事百科项目为例。对于初学者来说,数据源的缺乏是做项目的最大烦恼之一。本文讲述了如何使用网络爬虫获取数据,并以糗事百科作为练手项目。同时,提到了使用jsoup需要结合前端基础知识,以及如果学过JS的话可以更轻松地使用该框架。 ... [详细]
  • 本文介绍了Python字典视图对象的示例和用法。通过对示例代码的解释,展示了字典视图对象的基本操作和特点。字典视图对象可以通过迭代或转换为列表来获取字典的键或值。同时,字典视图对象也是动态的,可以反映字典的变化。通过学习字典视图对象的用法,可以更好地理解和处理字典数据。 ... [详细]
author-avatar
相思和怀恋_811_372
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有