摘要:原理如图,实现一个,需要几个辅助工具,分别是。我的模式中的功能有两个。对将中的数据绑定到上下文环境上,对数据进行劫持,当数据变化的时候通知。到此就全部完成了模式。
前言
网上讲 vue 原理,mvvm 模式的实现,数据双向绑定的文章一搜一大堆,不管写的谁好谁坏,都是写的自己的理解,我也发一篇文章记录自己的理解,如果对看官有帮助,那也是我莫大的荣幸,不过看完之后,你们以后如果再被面试官问到 vue 的原理的时候,千万不要只用一句【通过 javascrit 的 Object.defineProperty 将 data 进行劫持,发生改变的时候改变对应节点的值】这么笼统的话来应付了。如果有不懂的,可以问我。话不多说,上效果图:
效果以及代码
{{a}}
怎么样,是不是跟vue的写法很像,跟着我的思路,你们也可以的。
原理talk is cheap, show you the picture
如图,实现一个mvvm,需要几个辅助工具,分别是 Observer, Compile, Dep, Watcher。每个工具各司其职,再由 MVVM 统一掉配从而实现数据的双向绑定,下面我分别介绍下接下来出场的几位菇凉
Compile 能够将页面中的页面初始化,对指令进行解析,把 data 对应的值渲染上去的同时,new 一个 Watcher,并告诉它,当渲染的这个数据发生改变时告诉我,我好更新视图。
Observer 能够实现将 data 中的数据通过Object.defineProperty进行劫持,当获取 data 中的值的时候,触发get里方法,把 Compile 新建的 Watcher 抓过来,关到 Dep(发布订阅者模式)的小黑屋里狂...,当值修改的时候,触发 set 里的方法,通知小黑屋(Dep)里所有 Watcher 菇凉们,你们解放啦。
Dep 就是传说中的小黑屋了,其内在原理是发布订阅者模式,不了解发布订阅者模式的话可以看我 这篇文章
Watcher 们从小黑屋里逃出来之后就赶紧跑到对应的 Compile 那,告诉他开始更新视图吧,看,我是爱你的。
哈哈,通过我很(lao)幽(si)默(ji)的讲解。你们是不是都想下车了?
嗯,知道大概是怎么回事之后,我分别讲他们的功能。不过话说前面,mvvm 模式之前有千丝万缕的联系,必须要全部看完,才能真正理解 mvvm 的原理。
Observe我的 mvvm 模式中 Observe 的功能有两个。1.对将data中的数据绑定到上下文环境上,2.对数据进行劫持,当数据变化的时候通知 Dep。下面用一个 demo 来看看,如何将数据绑定到环境中,并劫持数据
Document
可以看到将 data 数据绑定到 window 上,当数据变化时候,会打印 "值更新啦",那么 data 变化 是如何通知 Dep 的呢?首先我们要明白,observe 只执行一遍,将数据绑定到 mvvm 实例上,Dep也只有一个,之前说把所有的 Watcher 抓过来,全放在这个 Dep 里,还是看代码说话把。
function observe (obj, vm) { if (!obj || typeof obj !== "object") return; return new Observer(obj, vm) } class Observer { constructor(obj, vm) { // vm 代表上下文环境,也是指向 mvvm 的实例 (调用的时候会传入) this.walk(obj, vm); // 实例化一个 Dep; this.dep = new Dep(); } walk (obj, vm) { var self = this; Object.keys(obj).forEach(key => { Object.defineProperty(vm, key, { configurable: true, enumerable: true, get () { // 当获取 vm 的值的时候,如果 Dep 有 target 时执行,目的是将 Watcher 抓过来,后面还会说明 if (Dep.target) { self.dep.depend(); } return obj[key]; }, set (newVal) { var val = obj.key; if (val === newVal) return; obj[key] = newVal; // 当 劫持的值发生变化时候触发,通知 Dep self.dep.notify(); } }) }) } }Dep
接下来讲讲 Dep 的实现,Dep 功能很简单,难点是如何将 watcher 联系起来,先看代码吧。
class Dep { constructor (props) { this.subs = []; this.uid = 0; } addSub (sub) { this.subs.push(sub); this.uid++; } notify () { this.subs.forEach(sub => { sub.update(); }) } depend (sub) { Dep.target.addDep(this, sub); } } Dep.target = null;
subs 是一个数组,用来存储 Watcher 的,当数据更新时候(由Observer告知),会触发 Dep 的 notify 方法,调用 subs 里所有 Watcher 的 update 方法。
接下来是不是迫不及待的想知道 Dep 是如何将 Watcher 抓过来的吧(污污污),别着急我们先看看 Watcher 是如何诞生的。
我觉得 Compile 是 mvvm 中最劳苦功高的一个了,它的任务是页面过来时候,初始化视图,将页面中的{{.*}}解析成对应的值,还有指令解析,如绑定值的 v-text、v-html 还有绑定的事件 v-on,还有创造 Watcher 去监听值的变化,当值变化的时候又要更新节点的视图。
我们先看看 Compile 是如何初始化视图的
Document {{a}}
额,感觉还好理解吧,这里只是讲了 Compile 是如何将data中的值渲染到视图上,买了个关子,没有说如何创建 Watcher 的,思考一下,如果要创建 Watcher ,应该在哪个位置创建比较好呢?
答案是渲染值的同时,同时创造一个 Watcher 来监听,上代码:
class Compile { constructor (el, vm) { this.$el = this.isElementNode(el) ? el : document.querySelector(el); this.$vm = vm; if (this.$el) { this.$fragment = this.nodeFragment(this.$el); this.compileElement(this.$fragment); this.$el.appendChild(this.$fragment); } } nodeFragment (el) { let fragment = document.createDocumentFragment(); let child; while (child = el.firstChild) { fragment.appendChild(child); } return fragment; } compileElement (el) { var childNodes = Array.from(el.childNodes); if (childNodes.length > 0) { childNodes.forEach(child => { var childArr = Array.from(child.childNodes); // 匹配{{}}里面的内容 var reg = /{{((?:.)+?)}}/; if (childArr.length > 0) { this.compileElement(child) } if (this.isTextNode(child)) { var text = child.textContent.trim(); var matchTextArr = reg.exec(text); var matchText; if (matchTextArr && matchTextArr.length > 1) { matchText = matchTextArr[1]; this.compileText(child, matchText); } } else if (this.isElementNode(child)) { this.compileNode(child); } }) } } compileText(node, exp) { this.bind(node, this.$vm, exp, "text"); } compileNode (node) { var attrs = Array.from(node.attributes); attrs.forEach(attr => { if (this.isDirective(attr.name)) { var directiveName = attr.name.substr(2); if (directiveName.includes("on")) { node.removeAttribute(attr.name); var eventName = directiveName.split(":")[1]; this.addEvent(node, eventName, attr.value); } else if (directiveName.includes("model")) { // v-model this.bind(node, this.$vm, attr.value, "value"); node.addEventListener("input", (e) => { this.$vm[attr.value] = e.target.value; }) }else{ // v-text v-html node.removeAttribute(attr.name); this.bind(node, this.$vm, attr.value, directiveName); } } }) } addEvent(node, eventName, exp) { node.addEventListener(eventName, this.$vm.$options.methods[exp].bind(this.$vm)); } bind (node, vm, exp, dir) { if (dir === "text") { node.textContent = vm[exp]; } else if (dir === "html") { node.innerHTML = vm[exp]; } else if (dir === "value") { node.value = vm[exp]; } new Watcher(exp, vm, function () { if (dir === "text") { node.textContent = vm[exp]; } else if (dir === "html") { node.innerHTML = vm[exp]; } }) } hasChildNode (node) { return node.children && node.children.length > 0; } // 是否是指令 isDirective (attr) { if (typeof attr !== "string") return; return attr.includes("v-"); } // 元素节点 isElementNode (node) { return node.nodeType === 1; } // 文本节点 isTextNode (node) { return node.nodeType === 3; } }
这里比上面演示的demo多创建一个文档碎片,可以加快解析速度,另外在 80 行创建了 Watcher,当数据变化时,执行回调函数,从而更新视图。
Watcher期待已久的 Watcher 终于出来了,我们先看看它长什么样:
class Watcher { constructor (exp, vm, cb) { this.$vm = vm; this.$exp = exp; this.depIds = {}; this.getter = this.parseGetter(exp); this.value = this.get(); this.cb = cb; } update () { let newVal = this.get(); let oldVal = this.value; if (oldVal === newVal) return; this.cb.call(this.vm, newVal); this.value = newVal; } get () { Dep.target = this; var value = this.getter.call(this.$vm, this.$vm); Dep.target = null; return value; } parseGetter (exp) { if (/[^w.$]/.test(exp)) return; return function (obj) { if (!obj) return; obj = obj[exp]; return obj; } } addDep (dep) { if (!this.depIds.hasOwnProperty(dep.id)) { this.depIds[dep.id] = dep; dep.subs.push(this); } } }
也不怎么样嘛,只有30多行代码,接下来睁大眼睛啦,看看它是怎么被 Dep 抓过来的。
当 Compile 创建 Watcher 出来的时候,也将 Dep.target 指向了 Watcher。同时获取了该节点要渲染的值,触发了 Observer 中的 get 方法,Dep.target 有值了,就执行 self.dep.depend();
depend 方法里执行 Dep.target.addDep(this); 而现在 Dep.target 指向 Watcher,所以执行的是 Watcher 里的 addDep 方法 同时把 Dep 实例传过去。
Watcher 里的 addDep 方法是将 Watcher 放在的 Dep实例的 subs 数组里。
当vm里的值放生变化时,触发 Observer 的 set 方法,触发所有 subs 里的 Watcher 执行 Watcher 里的 update 方法。
update 方法里有 Compile 的回调,从而更新视图。
好吧,真想大白了,原来 Watcher 是引诱 Dep 把自己装进小黑屋的。哈哈~
源码已放在我自己的git库里,点击这里获取源码
讲了半天,正主该出来了,mvvm 是如何将上面四个小伙伴给自己打工的呢,其实很简单,上代码
class MVVM { constructor (options) { this.$options = options; var data = this._data = this.$options.data; observe(data, this); new Compile(options.el || document.body, this); } }
就是实例 MVVM 的时候,调用数据劫持,和 Compile 初始化视图。到此就全部完成了mvvm模式。
参考合格前端系列第三弹-实现一个属于我们自己的简易MVVM库
vue.js 权威指南
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/54553.html
摘要:原理如图,实现一个,需要几个辅助工具,分别是。我的模式中的功能有两个。对将中的数据绑定到上下文环境上,对数据进行劫持,当数据变化的时候通知。到此就全部完成了模式。 前言 网上讲 vue 原理,mvvm 模式的实现,数据双向绑定的文章一搜一大堆,不管写的谁好谁坏,都是写的自己的理解,我也发一篇文章记录自己的理解,如果对看官有帮助,那也是我莫大的荣幸,不过看完之后,你们以后如果再被面试官问...
摘要:在模式中一般把层算在层中,只有在理想的双向绑定模式下,才会完全的消失。层将通过特定的展示出来,并在控件上绑定视图交互事件,一般由框架自动生成在浏览器中。三大框架的异同三大框架都是数据驱动型的框架及是双向数据绑定是单向数据绑定。 MVVM相关概念 1) MVVM典型特点是有四个概念:Model、View、ViewModel、绑定器。MVVM可以是单向绑定也可以是双向绑定甚至是不绑...
摘要:具体代码执行方式进入到的目录下,命令行运行即可。确保为一个对象如果对象下有则不需要再次生成函数返回该对象的实例,这里判断了如果该对象下已经有实例,则直接返回,不再去生产实例。这就确保了一个对象下的实例仅被实例化一次。 看这篇之前,如果没有看过之前的文章,可拉到文章末尾查看之前的文章。 回顾 在 step4 中,我们大致实现了一个 MVVM 的框架,由3个部分组成: defineRe...
阅读 1960·2021-09-09 09:33
阅读 1107·2019-08-30 15:43
阅读 2644·2019-08-30 13:45
阅读 3296·2019-08-29 11:00
阅读 845·2019-08-26 14:01
阅读 3558·2019-08-26 13:24
阅读 470·2019-08-26 11:56
阅读 2681·2019-08-26 10:27