前言

网上讲 vue 原理,mvvm 模式的实现,数据双向绑定的文章一搜一大堆,不管写的谁好谁坏,都是写的自己的理解,我也发一篇文章记录自己的理解,如果对看官有帮助,那也是我莫大的荣幸,不过看完之后,你们以后如果再被面试官问到 vue 的原理的时候,千万不要只用一句【通过 javascrit 的 Object.defineProperty 将 data 进行劫持,发生改变的时候改变对应节点的值】这么笼统的话来应付了。如果有不懂的,可以问我。话不多说,上效果图:

效果

以及代码

  1. <body>
  2. <div id="root">
  3. <h1>{{a}}</h1>
  4. <button v-on:click="changeA">changeA</button>
  5. <h2 v-html="b"></h2>
  6. <input type="text" v-model="b">
  7. </div>
  8. </body>
  9. <script src="./Watcher.js"></script>
  10. <script src="./Compile.js"></script>
  11. <script src="./Dep.js"></script>
  12. <script src="./Observe.js"></script>
  13. <script src="./MVVM.js"></script>
  14. <script>
  15. var vue = new MVVM({
  16. el: '#root',
  17. data: {
  18. a: 'hello',
  19. b: 'world'
  20. },
  21. methods: {
  22. changeA () {
  23. this.a = 'hi'
  24. }
  25. }
  26. })
  27. </script>

怎么样,是不是跟vue的写法很像,跟着我的思路,你们也可以的。

原理

talk is cheap, show you the picture

如图,实现一个mvvm,需要几个辅助工具,分别是 Observer, Compile, Dep, Watcher。每个工具各司其职,再由 MVVM 统一掉配从而实现数据的双向绑定,下面我分别介绍下接下来出场的几位菇凉

  1. Compile 能够将页面中的页面初始化,对指令进行解析,把 data 对应的值渲染上去的同时,new 一个 Watcher,并告诉它,当渲染的这个数据发生改变时告诉我,我好更新视图。
  2. Observer 能够实现将 data 中的数据通过Object.defineProperty进行劫持,当获取 data 中的值的时候,触发get里方法,把 Compile 新建的 Watcher 抓过来,关到 Dep(发布订阅者模式)的小黑屋里狂...,当值修改的时候,触发 set 里的方法,通知小黑屋(Dep)里所有 Watcher 菇凉们,你们解放啦。
  3. Dep 就是传说中的小黑屋了,其内在原理是发布订阅者模式,不了解发布订阅者模式的话可以看我 这篇文章
  4. Watcher 们从小黑屋里逃出来之后就赶紧跑到对应的 Compile 那,告诉他开始更新视图吧,看,我是爱你的。

哈哈,通过我很(lao)幽(si)默(ji)的讲解。你们是不是都想下车了?

嗯,知道大概是怎么回事之后,我分别讲他们的功能。不过话说前面,mvvm 模式之前有千丝万缕的联系,必须要全部看完,才能真正理解 mvvm 的原理。

Observe

我的 mvvm 模式中 Observe 的功能有两个。1.对将data中的数据绑定到上下文环境上,2.对数据进行劫持,当数据变化的时候通知 Dep。下面用一个 demo 来看看,如何将数据绑定到环境中,并劫持数据

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Document</title>
  6. </head>
  7. <body>
  8. </body>
  9. <script>
  10. var data = {
  11. a: 'hello',
  12. b: 'world'
  13. }
  14. class Observer {
  15. constructor(obj, vm) {
  16. this.walk(obj, vm);
  17. }
  18. walk(obj, vm) {
  19. Object.keys(obj).forEach(key => {
  20. Object.defineProperty(vm, key, {
  21. configurable: true,
  22. enumerable: true,
  23. get () {
  24. console.log('获取obj的值' + obj[key])
  25. return obj[key];
  26. },
  27. set(newVal) {
  28. var val = obj[key];
  29. if (val === newVal) return;
  30. console.log(`值更新啦`);
  31. obj[key] = newVal;
  32. }
  33. })
  34. })
  35. }
  36. }
  37. new Observer(data, window);
  38. console.log(window.a);
  39. window.a = 'hi';
  40. </script>
  41. </html>

可以看到将 data 数据绑定到 window 上,当数据变化时候,会打印 '值更新啦',那么 data 变化 是如何通知 Dep 的呢?首先我们要明白,observe 只执行一遍,将数据绑定到 mvvm 实例上,Dep也只有一个,之前说把所有的 Watcher 抓过来,全放在这个 Dep 里,还是看代码说话把。

  1. function observe (obj, vm) {
  2. if (!obj || typeof obj !== 'object') return;
  3. return new Observer(obj, vm)
  4. }
  5. class Observer {
  6. constructor(obj, vm) {
  7. // vm 代表上下文环境,也是指向 mvvm 的实例 (调用的时候会传入)
  8. this.walk(obj, vm);
  9. // 实例化一个 Dep;
  10. this.dep = new Dep();
  11. }
  12. walk (obj, vm) {
  13. var self = this;
  14. Object.keys(obj).forEach(key => {
  15. Object.defineProperty(vm, key, {
  16. configurable: true,
  17. enumerable: true,
  18. get () {
  19. // 当获取 vm 的值的时候,如果 Dep 有 target 时执行,目的是将 Watcher 抓过来,后面还会说明
  20. if (Dep.target) {
  21. self.dep.depend();
  22. }
  23. return obj[key];
  24. },
  25. set (newVal) {
  26. var val = obj.key;
  27. if (val === newVal) return;
  28. obj[key] = newVal;
  29. // 当 劫持的值发生变化时候触发,通知 Dep
  30. self.dep.notify();
  31. }
  32. })
  33. })
  34. }
  35. }

Dep

接下来讲讲 Dep 的实现,Dep 功能很简单,难点是如何将 watcher 联系起来,先看代码吧。

  1. class Dep {
  2. constructor (props) {
  3. this.subs = [];
  4. this.uid = 0;
  5. }
  6. addSub (sub) {
  7. this.subs.push(sub);
  8. this.uid++;
  9. }
  10. notify () {
  11. this.subs.forEach(sub => {
  12. sub.update();
  13. })
  14. }
  15. depend (sub) {
  16. Dep.target.addDep(this, sub);
  17. }
  18. }
  19. Dep.target = null;

subs 是一个数组,用来存储 Watcher 的,当数据更新时候(由Observer告知),会触发 Dep 的 notify 方法,调用 subs 里所有 Watcher 的 update 方法。
接下来是不是迫不及待的想知道 Dep 是如何将 Watcher 抓过来的吧(污污污),别着急我们先看看 Watcher 是如何诞生的。

Compile

我觉得 Compile 是 mvvm 中最劳苦功高的一个了,它的任务是页面过来时候,初始化视图,将页面中的{{.*}}解析成对应的值,还有指令解析,如绑定值的 v-text、v-html 还有绑定的事件 v-on,还有创造 Watcher 去监听值的变化,当值变化的时候又要更新节点的视图。
我们先看看 Compile 是如何初始化视图的

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Document</title>
  6. </head>
  7. <body>
  8. <div id="root">
  9. <h1>{{a}}</h1>
  10. <div v-html="b"></div>
  11. </div>
  12. </body>
  13. <script>
  14. var data = {
  15. a: 'hello',
  16. b: 'world'
  17. }
  18. class Compile {
  19. constructor(el, vm) {
  20. this.$el = this.isElementNode(el) ? el : document.querySelector(el);
  21. this.$vm = vm;
  22. if (this.$el) {
  23. this.compileElement(this.$el);
  24. }
  25. }
  26. compileElement(el) {
  27. // 将所有的最小节点拿过来,循环判断是文本节点就看看是不是 {{}} 包裹的字符串,是元素节点就看看是不是v-html喝v-text
  28. var childNodes = Array.from(el.childNodes);
  29. if (childNodes.length > 0) {
  30. childNodes.forEach(child => {
  31. var childArr = Array.from(child.childNodes);
  32. // 匹配{{}}里面的内容
  33. var reg = /\{\{((?:.)+?)\}\}/;
  34. if (childArr.length > 0) {
  35. this.compileElement(child)
  36. }
  37. if (this.isTextNode(child)) {
  38. var text = child.textContent.trim();
  39. var matchTextArr = reg.exec(text);
  40. var matchText;
  41. if (matchTextArr && matchTextArr.length > 1) {
  42. matchText = matchTextArr[1];
  43. this.compileText(child, matchText);
  44. }
  45. } else if (this.isElementNode(child)) {
  46. this.compileNode(child);
  47. }
  48. })
  49. }
  50. }
  51. compileText(node, exp) {
  52. this.bind(node, this.$vm, exp, 'text');
  53. }
  54. compileNode(node) {
  55. var attrs = Array.from(node.attributes);
  56. attrs.forEach(attr => {
  57. if (this.isDirective(attr.name)) {
  58. var directiveName = attr.name.substr(2);
  59. if (directiveName.includes('on')) {
  60. // 绑定事件
  61. node.removeAttribute(attr.name);
  62. var eventName = directiveName.split(':')[1];
  63. this.addEvent(node, eventName, attr.value);
  64. } else {
  65. // v-text v-html 绑定值
  66. node.removeAttribute(attr.name);
  67. this.bind(node, this.$vm, attr.value, directiveName);
  68. }
  69. }
  70. })
  71. }
  72. addEvent(node, eventName, exp) {
  73. node.addEventListener(eventName, this.$vm.$options.methods[exp].bind(this.$vm));
  74. }
  75. bind(node, vm, exp, dir) {
  76. if (dir === 'text') {
  77. node.textContent = vm[exp];
  78. } else if (dir === 'html') {
  79. node.innerHTML = vm[exp];
  80. } else if (dir === 'value') {
  81. node.value = vm[exp];
  82. }
  83. }
  84. // 是否是指令
  85. isDirective(attr) {
  86. if (typeof attr !== 'string') return;
  87. return attr.includes('v-');
  88. }
  89. // 元素节点
  90. isElementNode(node) {
  91. return node.nodeType === 1;
  92. }
  93. // 文本节点
  94. isTextNode(node) {
  95. return node.nodeType === 3;
  96. }
  97. }
  98. new Compile('#root', data);
  99. </script>
  100. </html>

额,感觉还好理解吧,这里只是讲了 Compile 是如何将data中的值渲染到视图上,买了个关子,没有说如何创建 Watcher 的,思考一下,如果要创建 Watcher ,应该在哪个位置创建比较好呢?
答案是渲染值的同时,同时创造一个 Watcher 来监听,上代码:

  1. class Compile {
  2. constructor (el, vm) {
  3. this.$el = this.isElementNode(el) ? el : document.querySelector(el);
  4. this.$vm = vm;
  5. if (this.$el) {
  6. this.$fragment = this.nodeFragment(this.$el);
  7. this.compileElement(this.$fragment);
  8. this.$el.appendChild(this.$fragment);
  9. }
  10. }
  11. nodeFragment (el) {
  12. let fragment = document.createDocumentFragment();
  13. let child;
  14. while (child = el.firstChild) {
  15. fragment.appendChild(child);
  16. }
  17. return fragment;
  18. }
  19. compileElement (el) {
  20. var childNodes = Array.from(el.childNodes);
  21. if (childNodes.length > 0) {
  22. childNodes.forEach(child => {
  23. var childArr = Array.from(child.childNodes);
  24. // 匹配{{}}里面的内容
  25. var reg = /\{\{((?:.)+?)\}\}/;
  26. if (childArr.length > 0) {
  27. this.compileElement(child)
  28. }
  29. if (this.isTextNode(child)) {
  30. var text = child.textContent.trim();
  31. var matchTextArr = reg.exec(text);
  32. var matchText;
  33. if (matchTextArr && matchTextArr.length > 1) {
  34. matchText = matchTextArr[1];
  35. this.compileText(child, matchText);
  36. }
  37. } else if (this.isElementNode(child)) {
  38. this.compileNode(child);
  39. }
  40. })
  41. }
  42. }
  43. compileText(node, exp) {
  44. this.bind(node, this.$vm, exp, 'text');
  45. }
  46. compileNode (node) {
  47. var attrs = Array.from(node.attributes);
  48. attrs.forEach(attr => {
  49. if (this.isDirective(attr.name)) {
  50. var directiveName = attr.name.substr(2);
  51. if (directiveName.includes('on')) {
  52. node.removeAttribute(attr.name);
  53. var eventName = directiveName.split(':')[1];
  54. this.addEvent(node, eventName, attr.value);
  55. } else if (directiveName.includes('model')) {
  56. // v-model
  57. this.bind(node, this.$vm, attr.value, 'value');
  58. node.addEventListener('input', (e) => {
  59. this.$vm[attr.value] = e.target.value;
  60. })
  61. }else{
  62. // v-text v-html
  63. node.removeAttribute(attr.name);
  64. this.bind(node, this.$vm, attr.value, directiveName);
  65. }
  66. }
  67. })
  68. }
  69. addEvent(node, eventName, exp) {
  70. node.addEventListener(eventName, this.$vm.$options.methods[exp].bind(this.$vm));
  71. }
  72. bind (node, vm, exp, dir) {
  73. if (dir === 'text') {
  74. node.textContent = vm[exp];
  75. } else if (dir === 'html') {
  76. node.innerHTML = vm[exp];
  77. } else if (dir === 'value') {
  78. node.value = vm[exp];
  79. }
  80. new Watcher(exp, vm, function () {
  81. if (dir === 'text') {
  82. node.textContent = vm[exp];
  83. } else if (dir === 'html') {
  84. node.innerHTML = vm[exp];
  85. }
  86. })
  87. }
  88. hasChildNode (node) {
  89. return node.children && node.children.length > 0;
  90. }
  91. // 是否是指令
  92. isDirective (attr) {
  93. if (typeof attr !== 'string') return;
  94. return attr.includes('v-');
  95. }
  96. // 元素节点
  97. isElementNode (node) {
  98. return node.nodeType === 1;
  99. }
  100. // 文本节点
  101. isTextNode (node) {
  102. return node.nodeType === 3;
  103. }
  104. }

这里比上面演示的demo多创建一个文档碎片,可以加快解析速度,另外在 80 行创建了 Watcher,当数据变化时,执行回调函数,从而更新视图。

Watcher

期待已久的 Watcher 终于出来了,我们先看看它长什么样:

  1. class Watcher {
  2. constructor (exp, vm, cb) {
  3. this.$vm = vm;
  4. this.$exp = exp;
  5. this.depIds = {};
  6. this.getter = this.parseGetter(exp);
  7. this.value = this.get();
  8. this.cb = cb;
  9. }
  10. update () {
  11. let newVal = this.get();
  12. let oldVal = this.value;
  13. if (oldVal === newVal) return;
  14. this.cb.call(this.vm, newVal);
  15. this.value = newVal;
  16. }
  17. get () {
  18. Dep.target = this;
  19. var value = this.getter.call(this.$vm, this.$vm);
  20. Dep.target = null;
  21. return value;
  22. }
  23. parseGetter (exp) {
  24. if (/[^\w.$]/.test(exp)) return;
  25. return function (obj) {
  26. if (!obj) return;
  27. obj = obj[exp];
  28. return obj;
  29. }
  30. }
  31. addDep (dep) {
  32. if (!this.depIds.hasOwnProperty(dep.id)) {
  33. this.depIds[dep.id] = dep;
  34. dep.subs.push(this);
  35. }
  36. }
  37. }

也不怎么样嘛,只有30多行代码,接下来睁大眼睛啦,看看它是怎么被 Dep 抓过来的。

  1. 当 Compile 创建 Watcher 出来的时候,也将 Dep.target 指向了 Watcher。同时获取了该节点要渲染的值,触发了 Observer 中的 get 方法,Dep.target 有值了,就执行 self.dep.depend();
  2. depend 方法里执行 Dep.target.addDep(this); 而现在 Dep.target 指向 Watcher,所以执行的是 Watcher 里的 addDep 方法 同时把 Dep 实例传过去。
  3. Watcher 里的 addDep 方法是将 Watcher 放在的 Dep实例的 subs 数组里。
  4. 当vm里的值放生变化时,触发 Observer 的 set 方法,触发所有 subs 里的 Watcher 执行 Watcher 里的 update 方法。
  5. update 方法里有 Compile 的回调,从而更新视图。

好吧,真想大白了,原来 Watcher 是引诱 Dep 把自己装进小黑屋的。哈哈~
源码已放在我自己的git库里,点击这里获取源码
讲了半天,正主该出来了,mvvm 是如何将上面四个小伙伴给自己打工的呢,其实很简单,上代码

  1. class MVVM {
  2. constructor (options) {
  3. this.$options = options;
  4. var data = this._data = this.$options.data;
  5. observe(data, this);
  6. new Compile(options.el || document.body, this);
  7. }
  8. }

就是实例 MVVM 的时候,调用数据劫持,和 Compile 初始化视图。到此就全部完成了mvvm模式。

参考

  1. 合格前端系列第三弹-实现一个属于我们自己的简易MVVM库
  2. vue.js 权威指南

190行代码实现mvvm模式的更多相关文章

  1. 圣思源Java视频36节练习源码分享(自己的190+行代码对比老师的39行代码)

    题目: * 随机生成50个数字(整数),每个数字范围是[10,50],统计每个数字出现的次数 * 以及出现次数最多的数字与它的个数,最后将每个数字及其出现次数打印出来, * 如果某个数字出现次数为0, ...

  2. 1000行代码实现MVVM (类似Angular1.x.x , Vue)

    最近花了近半个多月的时间, 自己纯手工写了一个很小型的类angularjs/vue的mvvm 库. 目前已经用于公司一个项目. 项目托管在github https://github.com/leonw ...

  3. 为 ItemsControl 类型的控件提供行号,mvvm模式 绑定集合

    从网络上看到的两种方式,一种是,在 codebehind 里为 控件写事件,下面是将集合绑定到 DataGrid 控件: private void DataGridSoftware_LoadingRo ...

  4. Vue中MVVM模式的双向绑定原理 和 代码的实现

      今天带大家简单的实现MVVM模式,Object.defineProperty代理(proxy)数据   MVVM的实现方式: 模板编译(Compile) 数据劫持(Observer) Object ...

  5. dynamic-css 动态 CSS 库,使得你可以借助 MVVM 模式动态生成和更新 css,从 js 事件和 css 选择器的苦海中脱离出来

    dynamic-css 使得你可以借助 MVVM 模式动态生成和更新 css,从而将本插件到来之前,打散.嵌套在 js 中的修改样式的代码剥离出来.比如你要做元素跟随鼠标移动,或者根据滚动条位置的变化 ...

  6. 转:界面之下:还原真实的 MVC、MVP、MVVM 模式

    前言 做客户端开发.前端开发对MVC.MVP.MVVM这些名词不了解也应该大致听过,都是为了解决图形界面应用程序复杂性管理问题而产生的应用架构模式.网上很多文章关于这方面的讨论比较杂乱,各种MV*模式 ...

  7. angular中的MVVM模式

    在开始介绍angular原理之前,我们有必要先了解下mvvm模式在angular中运用.虽然在angular社区一直将angular统称为前端MVC框架,同时angular团队也称它为MVW(What ...

  8. MVVM 模式下iOS项目目录结构详细说明

    ➠更多技术干货请戳:听云博客 我们在做项目的时候,会经常用到各种设计模式,最常见的要数 MVC (模型,视图,控制器)了.但是,今天我们要说的是另一种设计模式——MVVM. 所以 MVVM 到底是什么 ...

  9. 《第一行代码——Android》

    <第一行代码——Android> 基本信息 作者: 郭霖 丛书名: 图灵原创 出版社:人民邮电出版社 ISBN:9787115362865 上架时间:2014-7-14 出版日期:2014 ...

随机推荐

  1. Oracle DB 查看预警日志

    “Database(数据库)”主页>“Related Links相关链接)”区域> “Alert Log Content (预警日志内容)” 查看预警日志每个数据库都有一个alert_&l ...

  2. Arithmetic Sequence

    Arithmetic Sequence Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Othe ...

  3. (转)k8s集群部署二:flannel网络

    转:https://blog.csdn.net/sinat_35930259/article/details/79946146 Overlay Network模式 覆盖网络,在基础网络上叠加的一种虚拟 ...

  4. 015-Spring Boot 定制和优化内嵌的Tomcat

    一.内嵌web容器 参看http://www.cnblogs.com/bjlhx/p/8372584.html 查看源码可知提供以下三种: 二.定制优化tomcat 2.1.配置文件配置 通过appl ...

  5. Invalid bound statement (not found)--spring boot集成mybatis

    问题: {"timestamp":"2019-07-02T10:21:32.379+0000","status":500,"err ...

  6. Golang通过反射获取结构体的标签

    Golang通过反射获取结构体的标签 例子: package main import ( "fmt" "reflect" ) type resume struc ...

  7. Drone - 安装,搭配 GitLab 下的配置和使用

    参考资料: Drone 官网地址:https://drone.io Drone 的 GitHub 地址:https://github.com/drone/drone 简介:https://imnerd ...

  8. WCF权限认证多种方式

    WCF身份验证一般常见的方式有:自定义用户名及密码验证.X509证书验证.ASP.NET成员资格(membership)验证.SOAP Header验证.Windows集成验证.WCF身份验证服务(A ...

  9. Reinforcement Learning for Self Organization and Power Control of Two-Tier Heterogeneous Networks

    R. Amiri, M. A. Almasi, J. G. Andrews and H. Mehrpouyan, "Reinforcement Learning for Self Organ ...

  10. 任务调度(02)Spring Schedule

    任务调度(02)Spring Schedule [toc] Spring 3.0 提供两种任务调度方式:一是定时任务调度:二是异步任务调度.这两种任务调度方式都是基于 JUC 实现的,是一种非常轻量级 ...