动手实现简单Vue.js ,探索Vue原理

文章目录

前言

实现简单的 vue.js, 探索 vue 实现原理,这里仅实现 插值功能{``{xxx}}

index.html

html 复制代码
<!DOCTYPE html>
<html lang="">

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1.0" />
  <title>TestVue</title>
</head>

<body>
  <div id="app">
    <p>{{message}}--{{message}} </p>
  </div>

  <!-- vue2 开发环境版本: https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js -->
  <!-- 简版手动实现 -->
  <script src="zvue.js"></script>

  <script>
    const app = new Vue({
      el: '#app',
      data: {
        message: 'Hello Vue!'
      }
    });
  </script>
</body>

</html>

zvue.js

自己实现的简单 vue.js

js 复制代码
class Vue {
  constructor(options) {
    // $xx 表示对象
    this.$options = options || {};
    this.$data = options.data || {};

    // el 可能为字符串或对象
    this.el = options.el;

    // el 对象
    this.$el =
      typeof this.el == "string" ? document.querySelector(this.el) : this.el;

    //属性注入vue实例(setX),即data属性变成Vue的对象
    console.log("Vue.constructor -1- proxy(this, this.$data)", this);
    proxy(this, this.$data);

    //observer 观察data
    console.log("Vue.constructor -2- new Observer(this.$data)", this);
    new Observer(this.$data);

    //dom视图解析
    console.log("Vue.constructor -3- new Compiler(this, this.el)", this);
    new Compiler(this, this.el);
  }
}

// // data 来源于vue.$data
function proxy(target, data) {
  Object.keys(data).forEach((key) => {
    Object.defineProperty(target, key, {
      enumerable: true,
      configurable: true,
      //目的:this.xx==this.$data.xx
      get() {
        console.log("proxy -1- get data[key]:", data[key]);
        return data[key];
      },
      set(newVal) {
        console.log("proxy -2- set data[key]=", newVal);
        data[key] = newVal;
      },
    });
  });
}

class Observer {
  constructor(data) {
    console.log("Observer.constructor -1- data", data);
    this.data = data;
    // 注意: Dep  new 出来了
    this.dep = new Dep();
    // 重点
    this.walk(data, this.dep);
  }

  walk(data, dep) {
    console.log("Observer.walk -2- this:", this);
    //data中属性包装成对象
    Object.keys(data).forEach((key) => doDefProp(data, key, data[key], dep));
  }
}

// 会放到dep里面相关联,dep是传参进来的,与Observer 对应绑定
function doDefProp(data, key, val, dep) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get() {
      // Dep.target 是公用的
      console.log("doDefProp -1- get data[key] --> dep.addSub", Dep.target);
      Dep.target && dep.addSub(Dep.target);
      return val;
    },
    set(newVal) {
      console.log("doDefProp -2- set data[key]=", newVal);
      if (val === newVal) return;
      val = newVal;
      if (typeof val === "object" && val !== null) {
        new Observer(val);
      }
      dep.notify();
    },
  });
}

// 依赖收集器
class Dep {
  constructor() {
    console.log("Dep.constructor -1-");
    this.subs = [];
  }

  addSub(sub) {
    console.log("Dep.addsub -2- : ", sub);
    this.subs.push(sub);
  }

  notify() {
    console.log("Dep.notify -3- : ", this.subs);
    Array.from(this.subs).forEach((sub) => {
      sub.update();
    });
  }
}

Dep.target = null;

// 观察者
class Watcher {
  constructor(vm, key, callback) {
    this.vm = vm;
    this.key = key;
    this.callback = callback;

    console.log("Watcher.constructor -1-  Dep.target = this", this);
    Dep.target = this;
    console.log("Watcher.constructor -2-  this.oldVal = vm[key]: ", vm[key]);
    this.oldVal = vm[key];
    console.log("Watcher.constructor -3-  Dep.target = null");
    Dep.target = null;
  }
  update() {
    const newVal = this.vm[this.key];
    console.log("Watcher.update -4- newVal:", newVal);
    if (newVal === this.oldVal) return;
    this.callback(newVal);
    this.oldVal = newVal;
  }
}

// 编译模板
class Compiler {
  constructor(vm, el) {
    this.vm = vm;
    this.el = vm.$el;

    if (this.el) {
      this.compile(this.el);
    }
  }

  compile(el) {
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach((node) => {
      if (node.nodeType === 3) {
        // 插值形式,进行替换
        this.compileText(node);
      } else if (node.nodeType === 1) {
        // 元素类型,解析属性
      }

      // 递归解析
      if (node.childNodes && node.childNodes.length) {
        this.compile(node);
      }
    });
  }

  // 替换 {{key}} -> value
  compileText(node) {
    const reg = /\{\{(.+?)\}\}/g;
    const value = node.textContent.replace(/\s/g, "");
    const tokens = [];
    let result,
      index,
      lastIndex = 0;

    while ((result = reg.exec(value))) {
      console.log("Compiler.compileText -1- result:", result);
      index = result.index;
      if (index > lastIndex) {
        tokens.push(value.slice(lastIndex, index));
      }

      const key = result[1].trim();
      tokens.push(this.vm[key]);

      lastIndex = index + result[0].length;
      const pos = tokens.length - 1;

      console.log("Compiler.compileText -2- new Watcher");
      new Watcher(this.vm, key, (newVal) => {
        console.log(
          "Compiler.compileText -3- tokens[pos] = newVal",
          tokens,
          pos,
          newVal
        );
        tokens[pos] = newVal;
        node.textContent = tokens.join("");
      });
    }

    if (lastIndex < value.lenth) {
      tokens.push(value.slice(lastIndex));
    }

    if (tokens.length) {
      node.textContent = tokens.join("");
    }
  }
}

运行

动态响应


日志

初始运行

log 复制代码
zvue.js:15 Vue.constructor -1- proxy(this, this.$data) Vue {$options: {...}, $data: {...}, el: '#app', $el: div#app}
zvue.js:19 Vue.constructor -2- new Observer(this.$data) Vue {$options: {...}, $data: {...}, el: '#app', $el: div#app}
zvue.js:49 Observer.constructor -1- data {message: 'Hello world!'}
zvue.js:91 Dep.constructor -1-
zvue.js:58 Observer.walk -2- this: Observer {data: {...}, dep: Dep}
zvue.js:23 Vue.constructor -3- new Compiler(this, this.el) Vue {$options: {...}, $data: {...}, el: '#app', $el: div#app}
zvue.js:171 Compiler.compileText -1- result: (2) ['{{message}}', 'message', index: 0, input: '{{message}}--{{message}}', groups: undefined]
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub null
zvue.js:36 proxy -1- get data[key]: Hello world!
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub null
zvue.js:183 Compiler.compileText -2- new Watcher
zvue.js:117 Watcher.constructor -1-  Dep.target = this Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:96 Dep.addsub -2- :  Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:36 proxy -1- get data[key]: Hello world!
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:96 Dep.addsub -2- :  Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:119 Watcher.constructor -2-  this.oldVal = vm[key]:  Hello world!
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:96 Dep.addsub -2- :  Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:36 proxy -1- get data[key]: Hello world!
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:96 Dep.addsub -2- :  Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:121 Watcher.constructor -3-  Dep.target = null
zvue.js:171 Compiler.compileText -1- result: (2) ['{{message}}', 'message', index: 13, input: '{{message}}--{{message}}', groups: undefined]
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub null
zvue.js:36 proxy -1- get data[key]: Hello world!
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub null
zvue.js:183 Compiler.compileText -2- new Watcher
zvue.js:117 Watcher.constructor -1-  Dep.target = this Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:96 Dep.addsub -2- :  Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:36 proxy -1- get data[key]: Hello world!
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:96 Dep.addsub -2- :  Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:119 Watcher.constructor -2-  this.oldVal = vm[key]:  Hello world!
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:96 Dep.addsub -2- :  Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:36 proxy -1- get data[key]: Hello world!
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:96 Dep.addsub -2- :  Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:121 Watcher.constructor -3-  Dep.target = null

修改值 app.message=12345

log 复制代码
app.message=12345
zvue.js:40 proxy -2- set data[key]= 12345
zvue.js:77 doDefProp -2- set data[key]= 12345
zvue.js:101 Dep.notify -3- :  (8) [Watcher, Watcher, Watcher, Watcher, Watcher, Watcher, Watcher, Watcher]
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub null
zvue.js:36 proxy -1- get data[key]: 12345
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub null
zvue.js:126 Watcher.update -4- newVal: 12345
zvue.js:185 Compiler.compileText -3- tokens[pos] = newVal (3) ['Hello world!', '--', 'Hello world!'] 0 12345
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub null
zvue.js:36 proxy -1- get data[key]: 12345
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub null
zvue.js:126 Watcher.update -4- newVal: 12345
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub null
zvue.js:36 proxy -1- get data[key]: 12345
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub null
zvue.js:126 Watcher.update -4- newVal: 12345
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub null
zvue.js:36 proxy -1- get data[key]: 12345
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub null
zvue.js:126 Watcher.update -4- newVal: 12345
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub null
zvue.js:36 proxy -1- get data[key]: 12345
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub null
zvue.js:126 Watcher.update -4- newVal: 12345
zvue.js:185 Compiler.compileText -3- tokens[pos] = newVal (3) [12345, '--', 'Hello world!'] 2 12345
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub null
zvue.js:36 proxy -1- get data[key]: 12345
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub null
zvue.js:126 Watcher.update -4- newVal: 12345
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub null
zvue.js:36 proxy -1- get data[key]: 12345

总结

本文实现了一个简易版Vue.js,主要实现了插值功能{{xxx}}。

核心实现包括:

  1. Vue类初始化时进行数据代理响应式处理模板编译`;
  2. 通过Observer类实现数据劫持,使用Dep类管理依赖
  3. Watcher类作为观察者监听数据变化
  4. Compiler类解析DOM模板并处理插值表达式。

关键机制包括:数据代理使data属性可直接访问Object.defineProperty实现响应式(get/set),依赖收集和发布订阅模式实现数据变化时的视图更新。代码通过递归解析DOM节点,匹配{{}}表达式并替换为对应的值,从执行日志可以看出其调用的路径,下一篇就来深入解析其中的依赖关系与实现原理。

相关推荐
sniper_fandc4 小时前
Axios快速上手
vue.js·axios
哟哟耶耶4 小时前
Starting again-02
开发语言·前端·javascript
Apifox.4 小时前
Apifox 9 月更新| AI 生成接口测试用例、在线文档调试能力全面升级、内置更多 HTTP 状态码、支持将目录转换为模块
前端·人工智能·后端·http·ai·测试用例·postman
Kitasan Burakku5 小时前
Typescript return type
前端·javascript·typescript
叁佰万5 小时前
前端实战开发(一):从参数优化到布局通信的全流程解决方案
前端
笔尖的记忆5 小时前
js异步任务你都知道了吗?
前端·面试
光影少年5 小时前
react生态
前端·react.js·前端框架
golang学习记5 小时前
从0死磕全栈之Next.js 中的错误处理机制详解(App Router)
前端
力Mer5 小时前
console.log()控制台异步打印与对象展开后不一致问题
前端·javascript