Vue 是利用的 Object.defineProperty() 方法进行的数据劫持,利用 set、get 来检测数据的读写。
成都创新互联是一家专业提供原阳企业网站建设,专注与成都网站制作、成都做网站、H5场景定制、小程序制作等业务。10年已为原阳众多企业、政府机构等服务。创新互联专业网站建设公司优惠进行中。
https://jsrun.net/RMIKp/embed...
MVVM 框架主要包含两个方面,数据变化更新视图,视图变化更新数据。
视图变化更新数据,如果是像 input 这种标签,可以使用 oninput 事件..
数据变化更新视图可以使用 Object.definProperty() 的 set 方法可以检测数据变化,当数据改变就会触发这个函数,然后更新视图。
我们知道了如何实现双向绑定了,首先要对数据进行劫持监听,所以我们需要设置一个 Observer 函数,用来监听所有属性的变化。
如果属性发生了变化,那就要告诉订阅者 watcher 看是否需要更新数据,如果订阅者有多个,则需要一个 Dep 来收集这些订阅者,然后在监听器 observer 和 watcher 之间进行统一管理。
还需要一个指令解析器 compile,对需要监听的节点和属性进行扫描和解析。
因此,流程大概是这样的:
Observer 是一个数据监听器,核心方法是利用 Object.defineProperty() 通过递归的方式对所有属性都添加 setter、getter 方法进行监听。
- var library = {
- book1: {
- name: "",
- },
- book2: "",
- };
- observe(library);
- library.book1.name = "vue权威指南"; // 属性name已经被监听了,现在值为:“vue权威指南”
- library.book2 = "没有此书籍"; // 属性book2已经被监听了,现在值为:“没有此书籍”
- // 为数据添加检测
- function defineReactive(data, key, val) {
- observe(val); // 递归遍历所有子属性
- let dep = new Dep(); // 新建一个dep
- Object.defineProperty(data, key, {
- enumerable: true,
- configurable: true,
- get: function() {
- if (Dep.target) {
- // 判断是否需要添加订阅者,仅第一次需要添加,之后就不用了,详细看Watcher函数
- dep.addSub(Dep.target); // 添加一个订阅者
- }
- return val;
- },
- set: function(newVal) {
- if (val == newVal) return; // 如果值未发生改变就return
- val = newVal;
- console.log(
- "属性" + key + "已经被监听了,现在值为:“" + newVal.toString() + "”"
- );
- dep.notify(); // 如果数据发生变化,就通知所有的订阅者。
- },
- });
- }
- // 监听对象的所有属性
- function observe(data) {
- if (!data || typeof data !== "object") {
- return; // 如果不是对象就return
- }
- Object.keys(data).forEach(function(key) {
- defineReactive(data, key, data[key]);
- });
- }
- // Dep 负责收集订阅者,当属性发生变化时,触发更新函数。
- function Dep() {
- this.subs = {};
- }
- Dep.prototype = {
- addSub: function(sub) {
- this.subs.push(sub);
- },
- notify: function() {
- this.subs.forEach((sub) => sub.update());
- },
- };
思路分析中,需要有一个可以容纳订阅者消息订阅器 Dep,用于收集订阅者,在属性发生变化时执行对应的更新函数。
从代码上看,将订阅器 Dep 添加在 getter 里,是为了让 Watcher 初始化时触发,,因此,需要判断是否需要订阅者。
在 setter 中,如果有数据发生变化,则通知所有的订阅者,然后订阅者就会更新对应的函数。
到此为止,一个比较完整的 Observer 就完成了,接下来开始设计 Watcher.
订阅者 Watcher 需要在初始化的时候将自己添加到订阅器 Dep 中,我们已经知道监听器 Observer 是在 get 时执行的 Watcher 操作,所以只需要在 Watcher 初始化的时候触发对应的 get 函数去添加对应的订阅者操作即可。
那给如何触发 get 呢?因为我们已经设置了 Object.defineProperty(),所以只需要获取对应的属性值就可以触发了。
我们只需要在订阅者 Watcher 初始化的时候,在 Dep.target 上缓存下订阅者,添加成功之后在将其去掉就可以了。
- function Watcher(vm, exp, cb) {
- this.cb = cb;
- this.vm = vm;
- this.exp = exp;
- thisthis.value = this.get(); // 将自己添加到订阅器的操作
- }
- Watcher.prototype = {
- update: function() {
- this.run();
- },
- run: function() {
- var value = this.vm.data[this.exp];
- var oldVal = this.value;
- if (value !== oldVal) {
- this.value = value;
- this.cb.call(this.vm, value, oldVal);
- }
- },
- get: function() {
- Dep.target = this; // 缓存自己,用于判断是否添加watcher。
- var value = this.vm.data[this.exp]; // 强制执行监听器里的get函数
- Dep.target = null; // 释放自己
- return value;
- },
- };
到此为止, 简单的额 Watcher 设计完毕,然后将 Observer 和 Watcher 关联起来,就可以实现一个简单的的双向绑定了。
因为还没有设计解析器 Compile,所以可以先将模板数据写死。
将代码转化为 ES6 构造函数的写法,预览试试。
https://jsrun.net/8SIKp/embed...
这段代码因为没有实现编译器而是直接传入了所绑定的变量,我们只在一个节点上设置一个数据(name)进行绑定,然后在页面上进行 new MyVue,就可以实现双向绑定了。
并两秒后进行值得改变,可以看到,页面也发生了变化。
- // MyVue
- proxyKeys(key) {
- var self = this;
- Object.defineProperty(this, key, {
- enumerable: false,
- configurable: true,
- get: function proxyGetter() {
- return self.data[key];
- },
- set: function proxySetter(newVal) {
- self.data[key] = newVal;
- }
- });
- }
上面这段代码的作用是将 this.data 的 key 代理到 this 上,使得我可以方便的使用 this.xx 就可以取到 this.data.xx。
虽然上面实现了双向数据绑定,但是整个过程都没有解析 DOM 节店,而是固定替换的,所以接下来要实现一个解析器来做数据的解析和绑定工作。
解析器 compile 的实现步骤:
为了解析模板,首先需要解析 DOM 数据,然后对含有 DOM 元素上的对应指令进行处理,因此整个 DOM 操作较为频繁,可以新建一个 fragment 片段,将需要的解析的 DOM 存入 fragment 片段中在进行处理。
- function nodeToFragment(el) {
- var fragment = document.createDocumentFragment();
- var child = el.firstChild;
- while (child) {
- // 将Dom元素移入fragment中
- fragment.appendChild(child);
- child = el.firstChild;
- }
- return fragment;
- }
接下来需要遍历各个节点,对含有相关指令和模板语法的节点进行特殊处理,先进行最简单模板语法处理,使用正则解析“{{变量}}”这种形式的语法。
- function compileElement (el) {
- var childNodes = el.childNodes;
- var self = this;
- [].slice.call(childNodes).forEach(function(node) {
- var reg = /\{\{(.*)\}\}/; // 匹配{{xx}}
- var text = node.textContent;
- if (self.isTextNode(node) && reg.test(text)) { // 判断是否是符合这种形式{{}}的指令
- self.compileText(node, reg.exec(text)[1]);
- }
- if (node.childNodes && node.childNodes.length) {
- self.compileElement(node); // 继续递归遍历子节点
- }
- });
- },
- function compileText (node, exp) {
- var self = this;
- var initText = this.vm[exp];
- updateText(node, initText); // 将初始化的数据初始化到视图中
- new Watcher(this.vm, exp, function (value) { // 生成订阅器并绑定更新函数
- self.updateText(node, value);
- });
- },
- function updateText (node, value) {
- node.textContent = typeof value == 'undefined' ? '' : value;
- }
获取到最外层的节点后,调用 compileElement 函数,对所有的子节点进行判断,如果节点是文本节点切匹配{{}}这种形式的指令,则进行编译处理,初始化对应的参数。
然后需要对当前参数生成一个对应的更新函数订阅器,在数据发生变化时更新对应的 DOM。
这样就完成了解析、初始化、编译三个过程了。
接下来改造一个 myVue 就可以使用模板变量进行双向数据绑定了。
https://jsrun.net/K4IKp/embed...
添加完 compile 之后,一个数据双向绑定就基本完成了,接下来就是在 Compile 中添加更多指令的解析编译,比如 v-model、v-on、v-bind 等。
添加一个 v-model 和 v-on 解析:
- function compile(node) {
- var nodenodeAttrs = node.attributes;
- var self = this;
- Array.prototype.forEach.call(nodeAttrs, function(attr) {
- var attrattrName = attr.name;
- if (isDirective(attrName)) {
- var exp = attr.value;
- var dir = attrName.substring(2);
- if (isEventDirective(dir)) {
- // 事件指令
- self.compileEvent(node, self.vm, exp, dir);
- } else {
- // v-model 指令
- self.compileModel(node, self.vm, exp, dir);
- }
- node.removeAttribute(attrName); // 解析完毕,移除属性
- }
- });
- }
- // v-指令解析
- function isDirective(attr) {
- return attr.indexOf("v-") == 0;
- }
- // on: 指令解析
- function isEventDirective(dir) {
- return dir.indexOf("on:") === 0;
- }
上面的 compile 函数是用于遍历当前 dom 的所有节点属性,然后判断属性是否是指令属性,如果是在做对应的处理(事件就去监听事件、数据就去监听数据..)
在 MyVue 中添加 mounted 方法,在所有操作都做完时执行。
- class MyVue {
- constructor(options) {
- var self = this;
- this.data = options.data;
- this.methods = options.methods;
- Object.keys(this.data).forEach(function(key) {
- self.proxyKeys(key);
- });
- observe(this.data);
- new Compile(options.el, this);
- options.mounted.call(this); // 所有事情处理好后执行mounted函数
- }
- proxyKeys(key) {
- // 将this.data属性代理到this上
- var self = this;
- Object.defineProperty(this, key, {
- enumerable: false,
- configurable: true,
- get: function getter() {
- return self.data[key];
- },
- set: function setter(newVal) {
- self.data[key] = newVal;
- },
- });
- }
- }
然后就可以测试使用了。
https://jsrun.net/Y4IKp/embed...
总结一下流程,回头在哪看一遍这个图,是不是清楚很多了。
可以查看的代码地址:Vue2.x 的双向绑定原理及实现
本文题目:Vue2.x的双向绑定原理及实现
本文URL:http://www.csdahua.cn/qtweb/news2/139402.html
网站建设、网络推广公司-快上网,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 快上网