Vue简易源码实现(MVVM)

概述

  • Vue响应式数据实现: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。
  • 实现思路:首先通过compile类,对处在根元素中的子节点进行扫描、编译,通过keydata中获取对应的值追加到页面上。实现一个observe类,getdata中的数据设置gettersetter方法,数据劫持data中数据的变化做对应的更新操作。页面中每一个属性对应一个watcher,通过watcher连接compileobserve,监听页面的变化来改变数据。
    image.png

实现

Compile

compile主要是编译页面中的指令模板,将模板中的变量替换成数据,然后初次渲染视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。

注意事项:

  • 由于页面的DOM操作非常耗性能,所以我们将DOM转换成fragment文档碎片,解析完成后再讲fragment添加会DOM,提高性能和效率。
  • 对编译的每一个属性,需要指定一个watcher,页面上的每一个属性对应一个watcher。


    image.png
// 编译工具类
const compileUtil = {
  // 从 data:{} 中获取值
  getValue(expr, vm) {
    // [person,name]  通过这个方法可以获取对象中的值  data:{obj:{name:'jack'}}
    // console.log(expr.trim().split(".")); ["msg"] ["person","name"]
    return expr
      .trim()
      .split(".")
      .reduce((data, currentValue) => {
        return data[currentValue];
      }, vm.$data);
  },
  setVal(expr, vm, value) {
    return expr
      .trim()
      .split(".")
      .reduce((data, currentValue) => {
        data[currentValue] = value;
      }, vm.$data);
  },
  // 处理这种情况{{ person.name }} {{ person.age }}
  getContentValue(expr, vm) {
    return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
      return this.getValue(args[1].trim(), vm);
    });
  },
  text(node, expr, vm) {
    // console.log(expr)
    //exp:msg  exp:person.name
    let value;
    if (expr.indexOf("{{") !== -1) {
      value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
        new Watcher(vm, args[1], newVal => {
          // console.log(this.getContentValue(expr, vm))
          // this.getContentValue(expr, vm) => jack===6666
          this.updater.textUpdater(node, this.getContentValue(expr, vm));
        });
        //  new Watcher(vm, args[1], newVal => {
        // newVal:6666
        //   this.updater.textUpdater(node, newVal);
        // });
        return this.getValue(args[1].trim(), vm);
      });
    } else {
      value = this.getValue(expr, vm);
    }
    this.updater.textUpdater(node, value);
  },
  html(node, expr, vm) {
    const value = this.getValue(expr, vm);
    new Watcher(vm, expr, newVal => {
      this.updater.htmlUpdater(node, newVal);
    });
    this.updater.htmlUpdater(node, value);
  },
  model(node, expr, vm) {
    const value = this.getValue(expr, vm);
    // 绑定更新视图
    new Watcher(vm, expr, newVal => {
      this.updater.modelUpdater(node, newVal);
    });
    // 视图更新数据
    node.addEventListener("input", e => {
      this.setVal(expr, vm, e.target.value);
    });
    this.updater.modelUpdater(node, value);
  },
  on(node, expr, vm, eventName) {
    //expr:'click'
    const fn = vm.$options.methods[expr];
    node.addEventListener(eventName, fn.bind(vm));
  },
  updater: {
    htmlUpdater(node, value) {
      node.innerHTML = value;
    },
    textUpdater(node, value) {
      node.textContent = value;
    },
    modelUpdater(node, value) {
      node.value = value;
    }
  }
};
// 编译类 编译指令模板 v-text {{ msg }} v-on:click
class Compile {
  constructor(el, vm) {
    // 判断传过来是节点还是选择器
    this.el = this.isElementNode(el) ? el : document.querySelector(el);
    this.vm = vm;
    // 获取文档碎片对象 存入内存中 操作DOM时减少页面的回流和重绘
    const fragment = this.node2Fragment(this.el);
    // 编译模板
    this.compile(fragment);
    // 追加到根元素上
    this.el.appendChild(fragment);
  }
  compile(fragment) {
    // 获取所有的子节点
    const childNodes = fragment.childNodes;
    // 遍历所有的子节点 针对不同的节点 作不同的编译工作
    [...childNodes].forEach(child => {
      // 元素节点 <p></p> <h1></h1>
      if (this.isElementNode(child)) {
        // console.log(child)
        // console.log('元素节点',child)
        this.compileElement(child);
      } else {
        // 文本节点 {{ person.name }} {{ msg }}
        // console.log('文本节点',child)
        this.compileText(child);
      }
      // 递归调用子节点
      if (child.childNodes && child.childNodes.length) {
        this.compile(child);
      }
    });
  }
  compileElement(node) {
    // console.log(node)  v-text v-html v-model
    const attributes = node.attributes;
    // console.log(attributes);
    [...attributes].forEach(attr => {
      // 获取属性和值
      const { name, value } = attr;
      // 判断是否是指令
      if (this.isDirective(name)) {
        // 获取对应的指令的名字 html text model
        const [, directive] = name.split("-"); //text html model on:click
        // 对于是v-on:click这种情况需要做进一步处理  不
        const [dirName, eventName] = directive.split(":"); //text html model (on,eventName)
        // console.log(dirName,eventName) 是事件的返回 text:undefined
        // 数据驱动视图 node:节点  value:指令的名称
        compileUtil[dirName](node, value, this.vm, eventName);
        // 删除V-text等指令属性
        node.removeAttribute(`v-${directive}`);
      } else if (this.isEventName(name)) {
        let [, eventName] = name.split("@");
        compileUtil["on"](node, value, this.vm, eventName);
      }
    });
  }
  compileText(node) {
    const content = node.textContent;
    if (/\{\{(.*)\}\}/.test(content)) {
      compileUtil["text"](node, content, this.vm);
    }
  }
  node2Fragment(el) {
    // 创建文档碎片
    const fragment = document.createDocumentFragment();
    let firstChild;
    while ((firstChild = el.firstChild)) {
      fragment.appendChild(firstChild);
    }
    return fragment;
  }
  isEventName(attrName) {
    return attrName.startsWith("@");
  }
  isDirective(attrName) {
    return attrName.startsWith("v-");
  }
  isElementNode(el) {
    return el.nodeType === 1;
  }
}
class KVue {
  constructor(options) {
    // 全局保存数据
    this.$el = options.el;
    this.$data = options.data;
    this.$options = options;
    if (this.$el) {
      // 1、实现一个数据观察者
      new Observer(this.$data);
      // 2、实现一个编译器
      new Compile(this.$el, this);
      // 3、将data数据代理到当前实例上
      this.proxyData(this.$data);
    }
  }
  proxyData(data) {
    Object.keys(data).forEach(key => {
      Object.defineProperty(this, key, {
        get() {
          return data[key];
        },
        set(newVal) {
          data[key] = newVal;
        }
      });
    });
  }
}

Observe

Observe主要使用Obeject.defineProperty()是对data做监听,给data中的每个属性加上getset方法,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化。

// 观察者 页面上 每一个属性都对应一个watcher 通知页面更新  存UI中的依赖,实现update函数更新页面
class Watcher {
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr;
    // 通过回调去更新页面
    this.cb = cb;
    //初始化的保存旧值 便于后面对比 并且把当前实例指向Dep.target静态对象
    this.oldVal = this.getOldValue();
  }
  getOldValue() {
    Dep.target = this;
    const oldVal = compileUtil.getValue(this.expr, this.vm);
    Dep.target = null;
    return oldVal;
  }
  update() {
    const newVal = compileUtil.getValue(this.expr, this.vm);
    if (newVal !== this.oldVal) {
      this.cb(newVal);
    }
  }
}
// 存储watcher的容器
class Dep {
  constructor() {
    this.deps = [];
  }
  addDep(watcher) {
    this.deps.push(watcher);
  }
  notify() { //通知watcher刦更新页面
    this.deps.forEach(watcher=>watcher.update())
  }
}
// 监听data中各个属性的变化 给data中的数据添加getter setter方法
class Observer {
  constructor(data) {
    this.observe(data);
  }
  // 监听data中的数据
  observe(data) {
    if (data && typeof data === "object") {
      Object.keys(data).forEach(key => {
        // 定义响应式
        this.defineReactive(data, key, data[key]);
      });
    }
  }
  defineReactive(obj, key, value) {
    if (typeof value === "object") {
      this.observe(value);
    }
    // 创建Dep实例,Dep和key是一一对应的
    const dep = new Dep();
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: false,
      set: newVal => {
        // 解决由于新增对象而无法监听问题 vm.$data.person = {a:1}
        this.observe(newVal);
        if (newVal !== value) {
          value = newVal;
          dep.notify()
        }
      },
      get() {
        // 页面中每使用一次data中的属性  都需要使用watcher监视 并添加到dep中
        Dep.target && dep.addDep(Dep.target);
        return value;
      }
    });
  }
}

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>MVVM</title>
  </head>
  <body>
    <div id="app">
      <p>{{ msg }}</p>
      <p>{{ person.name }}=={{ person.age }}</p>
      <p v-text="msg"></p>
      <p v-text="person.age"></p>
      <p v-text="htmlStr"></p>
      <p v-html="htmlStr"></p>
      <ul>
        <li>{{msg}}</li>
        <li>{{htmlStr}}</li>
      </ul>
      <input type="text" v-model="msg" />
      <button v-on:click="clickHandle">click</button>
      <button @click="clickHandle">click</button>
    </div>
  </body>
  <script src="./observer.js"></script>
  <script src="./kVue.js"></script>
  <script>
    let app = new KVue({
      el: '#app',
      data: {
        msg: 'this is msg',
        person: {
          name: 'jack',
          age: 21
        },
        htmlStr: '<b>this is html string</b>'
      },
      methods: {
        clickHandle(){
          console.log(this.$data.msg)
        }
      }
    })
  </script>
</html>

效果图

image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容