资讯专栏INFORMATION COLUMN

190行代码实现mvvm模式

liangzai_cool / 620人阅读

摘要:原理如图,实现一个,需要几个辅助工具,分别是。我的模式中的功能有两个。对将中的数据绑定到上下文环境上,对数据进行劫持,当数据变化的时候通知。到此就全部完成了模式。

前言

网上讲 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

我觉得 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

相关文章

  • 190代码实现mvvm模式

    摘要:原理如图,实现一个,需要几个辅助工具,分别是。我的模式中的功能有两个。对将中的数据绑定到上下文环境上,对数据进行劫持,当数据变化的时候通知。到此就全部完成了模式。 前言 网上讲 vue 原理,mvvm 模式的实现,数据双向绑定的文章一搜一大堆,不管写的谁好谁坏,都是写的自己的理解,我也发一篇文章记录自己的理解,如果对看官有帮助,那也是我莫大的荣幸,不过看完之后,你们以后如果再被面试官问...

    Pink 评论0 收藏0
  • 前端MVVM模式及其在Vue和React中的体现

    摘要:在模式中一般把层算在层中,只有在理想的双向绑定模式下,才会完全的消失。层将通过特定的展示出来,并在控件上绑定视图交互事件,一般由框架自动生成在浏览器中。三大框架的异同三大框架都是数据驱动型的框架及是双向数据绑定是单向数据绑定。 MVVM相关概念 1) MVVM典型特点是有四个概念:Model、View、ViewModel、绑定器。MVVM可以是单向绑定也可以是双向绑定甚至是不绑...

    沈建明 评论0 收藏0
  • 前端框架模式的变迁

    摘要:现在在前端的框架都是的模式,还有像和之类的变种独特的单向数据流框架。只要将数据流进行规范,那么原来的模式还是大有可为的。我们可以来看一下,框架的图示从图中,我们可以看到形成了一条到,再到,之后是的,一条单向数据流。 前言 前端框架的变迁,体系架构的完善,使得我们只知道框架,却不明白它背后的道理。我们应该抱着一颗好奇心,在探索框架模式的变迁过程中,体会前人的一些理解和思考 本篇将讲述的是...

    ssshooter 评论0 收藏0
  • VUE - MVVM - part5 - Observe

    摘要:具体代码执行方式进入到的目录下,命令行运行即可。确保为一个对象如果对象下有则不需要再次生成函数返回该对象的实例,这里判断了如果该对象下已经有实例,则直接返回,不再去生产实例。这就确保了一个对象下的实例仅被实例化一次。 看这篇之前,如果没有看过之前的文章,可拉到文章末尾查看之前的文章。 回顾 在 step4 中,我们大致实现了一个 MVVM 的框架,由3个部分组成: defineRe...

    xi4oh4o 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<