深入理解 Vue.js 原理

文章目录

前言

前一篇 简单实现vue.js 一文中,我们实现一个简化版的 Vue.js(zvue.js),本文来通过这个 zvue.js 来一窥 vue 结构及实现原理

zvue.js 架构图

先上一张 zvue.js 架构图,本文我们就对此详细展开研究:

各个类的主要功能如下:

  1. VUE: 主类,执行入口

  2. Observer: 给 vue.$data 下每个属性都包装成一个有 get/set 方法的对象,并在 get/set 方法中夹带私货(Dep 与 Watcher之间的互动)

  3. Compiler: Dom 视图解析成 token 流,把 html 中有 vue相关的部分找出来,每个位置都记录下来并分配一个 Watcher 进行监管,一旦数据有变,最终会执行到 Watcher 的更新,从而使页面更新

  4. Dep: 管理观察者(Watcher)列表,并通知全部的 Watcher 对象

  5. Watcher: 观察者模式中的实体观察者,一旦数据有变,最终会执行到 Watcher 的更新,从而使页面实时更新

Object.defineProperty()

在上面代码中,有两处使用到 Object.defineProperty() 方法,一个是 Vue 类中的 proxy() 方法,一个是 Observer 类中的 doDefProp(),其本质上是一个装饰器,给一个数据包装成一个有 get/set 方法的对象,并在 get/set 方法中加入装饰逻辑,一旦有 读取或更新 数据时触发执行到装饰逻辑。

Vue 类中的 proxy() 方法比较简单,没有什么装饰逻辑,其实就是想要 无中生有,把 Vue.$data 下的属性当作是 Vue 的自己属性,即实现 vm.xx==vm.$data.xx,这里起到的就是代理作用,简化省掉$data而已。

类似 java 中的 getXXX/setXXX() 方法,可以没有 XXX 字段

js 复制代码
  // data 来源于vue.$data
  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;
        },
      });
    });
  }

Observer 类中的 doDefProp(),是给 data 每个 key 对应的数据 value 进行封装成一个对象,有 get/set 逻辑

例如 value 本身是一个姓名字符串 "张三",经过 Object.defineProperty 摇身一变成了一个 Person 对象,取值设置要调用里面的 get/set 方法

并且还可以夹带私货(装饰逻辑),例如代码中的 Dep.target && dep.addSub(Dep.target);,这个可以说是 Vue 的灵魂所在,后面我们会看到这里 target 其实就是 Watcher 对象,data 的每个 key 值都有一个 Watcher 监视器,监听 value 的变化。

js 复制代码
  // 会放到dep里面相关联,dep是传参进来的,与Observer 对应绑定
  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);
        }
        // 通知监视器更新 token 流,实现页面渲染更新
        dep.notify();
      },
    });
  }

注意 这里要划重点了,在 读取 key(get) 时添加 监视器 Watcher 到观察者列表

js 复制代码
        // 加入列表
        Dep.target && dep.addSub(Dep.target);

更新 key(set)时候通知 监视器 Watcher 进行 token 流替换,从而实现页面渲染更新。

js 复制代码
// 通知监视器更新 token 流,实现页面渲染更新
        dep.notify();

在浏览器中可以查看到 $data 的信息,一点开里面的 message,立刻执行了装饰逻辑,打印出日志 doDefProp -1- get data[key] --> dep.addSub null

读取数据:vm[key]

Watcher 构造函数中 有一句 this.oldVal = vm[key];

js 复制代码
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]");
    this.oldVal = vm[key];
    console.log("Watcher.constructor -3-  Dep.target = null");
    Dep.target = null;
  }
  ...
}

通过日志可以看出其调用的路径,vm[key] -> get -> dep.addSub

log 复制代码
zvue.js:119 Watcher.constructor -2-  this.oldVal = vm[key]
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=1234,页面渲染马上变成 1234--1234,执行路径为 set -> Dep.notify -> Watcher.update,到此就完成页面的更新了

完整 zvue.js 代码

zvue.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);
    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
  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) =>
      this.doDefProp(data, key, data[key], dep)
    );
  }
  // 会放到dep里面相关联,dep是传参进来的,与Observer 对应绑定
  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 = 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("");
    }
  }
}
相关推荐
西西学代码3 小时前
Flutter---showCupertinoDialog
java·前端·flutter
你的眼睛會笑3 小时前
vue3 使用html2canvas实现网页截图并下载功能 以及问题处理
前端·javascript·vue.js
ZTLJQ3 小时前
植物大战僵尸HTML5游戏完整实现教程
前端·游戏·html5
无光末阳4 小时前
vue 环境下多个定时器的创建与暂停的统一封装
前端·vue.js
Hilaku4 小时前
技术Leader的“第一性原理”:我是如何做技术决策的?
前端·javascript·面试
liyf4 小时前
发布-订阅(Publish–Subscribe) vs 观察者模式(Observer Pattern)
前端
云中雾丽4 小时前
Flutter 里的 Riverpod 用法解析
前端
前端snow4 小时前
记录:非常典型的一个redux问题
前端
慧一居士4 小时前
src/App.vue 和 public/index.html 关系和区别
前端·vue.js