前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Vue学习-学习源码手撸简易Vue

Vue学习-学习源码手撸简易Vue

作者头像
Caleb
发布2020-12-14 17:11:49
7310
发布2020-12-14 17:11:49
举报
文章被收录于专栏:乐赞分享乐赞分享

本期内容是带着大家熟悉 Vue 的基本组成逻辑,并手把手的帮助大家完成一个简易版本的 Vue。

内容篇幅较长,请耐心观看。

? 演示

? 准备工作

创建好文件夹,起名叫做 Mini_Vue。再在文件夹中分别创建好 index.htmljs 文件夹。在 js 文件下创建如下内容:

Mini_Vue
--------
├─ js
│  ├─vue.js
│  ├─observer.js
│  ├─compiler.js
│  ├─dep.js
│  └─watcher.js
└─ index.html

需要在index.html中写入数据。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Mini Vue</title>
  </head>

  <body>
    <div id="app">
      <h1>差值表达式</h1>
      <h3>{{msg}}</h3>
      <h3>{{count}}</h3>
      <h1>v-text</h1>
      <div v-text="msg"></div>
      <h1>v-model</h1>
      <input type="text" v-model="msg" />
      <input type="text" v-model="count" />
    </div>

    <script src="./js/dep.js"></script>
    <script src="./js/watcher.js"></script>
    <script src="./js/compiler.js"></script>
    <script src="./js/observer.js"></script>
    <script src="./js/vue.js"></script>

    <script>
      let vm = new Vue({
        el: "#app",
        data: {
          msg: "Hello Mini Vue",
          count: 12,
          person: {
            name: "xiao",
          },
        },
      });
      console.log(vm);
    </script>
  </body>
</html>

准备好文件后,我们开始逐一分析。

? 整体分析

Vue

  • 把 data 中的成员注入到 Vue 实例,并且把 data 中的成员转成 getter/setter

Observer

  • 能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知 Dep

Compiler

  • 解析每个元素中的指令/插值表达式,并替换成相应的数据

Dep

  • 添加观察者(watcher),当数据变化通知所有观察者

Watcher

  • 数据变化更新视图

? Vue

功能

  • 负责接收初始化的参数(选项)
  • 负责把 data 中的属性注入到 Vue 实例,转换成 getter/setter
  • 负责调用 observer 监听 data 中所有属性的变化
  • 负责调用 compiler 解析指令/插值表达式

结构 大致内容

Vue
|
├─ $options
├─ $el
├─ $data
├─ _proxyData()
├─ ..... 等等属性

梳理

先行解读 Vue 模块中的参数:

  • $options 此属性代指初始化 Vue 时(new Vue())传入的自定义属性数据。例如传入 router、store、render()、i18n。
  • $el Vue 实例绑定的 DOM 节点
  • $data 读取数据属性对象
  • _proxyData() 我们理解为对 data 进行数据劫持。

简单的了解参数后,实现功能:

  • 接收初始化的参数。首先应该将需要的属性进行声明,而属性的值都来自于传入的 option
  • 通过Object.defineProperty将 data 转换成 getter/setter

代码

首先打开 vue.js 文件,我们开始 Vue 类的建立。

  1. 实现 Vue 属性的加载
class Vue {
  constructor(option) {
    // 1.通过属性保存选项的数据
    this.$option = option || {};
    this.$data = option.data || {};
    this.$el =
      typeof option.el === "string"
        ? document.querySelector(option.el)
        : option.el;
    // 2.把data中的成员转换成getter和setter,并注入到Vue实例中
    this._proxyData(this.$data);
    // 3.调用observer对象,监听数据变化
    // 4.调用compiler对象,解析指令和差值表达式
  }
}

在如上代码中可以看到,我们在第二步调用了 this._proxyData()函数,目的是为了将 data 中的成员转换成 getter 和 setter。现在开始实现。

  1. 实现 this._proxyData()
class Vue {
  // 此处与第一步中的一致,不再复制
  constructor(option) {
    //   xxxxxxx
  }

  _proxyData(data) {
    // 遍历data中的所有属性,把data的所有属性注入到实例中
    Object.keys(data).forEach((key) => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          return data[key];
        },
        set(newValue) {
          if (newValue === data[key]) {
            return;
          }
          data[key] = newValue;
        },
      });
    });
  }
}

如果对此方法不熟悉的,可以先去看看这篇文章。

? Vue 学习-数据响应式原理

此时我们对 Vue 类进行了简单的处理。但是还有两个功能并没有实现,我们先放着,接着往下走。

? Observer

功能

  • 负责把 data 选项中的属性转换成响应式数据
  • data 中的某个属性也是对象,把该属性转换成响应式数据
  • 数据变化发送通知

结构 大致内容

Observer
|
├─ walk(data)
├─ defineReactive(data,key,value)
├─ .....

梳理

  • walk() 用来判断传入的值是否为对象。如果不是对象就返回,是对象的话遍历对象的所有属性调用defineReactive()来转换为 getter/setter
  • defineReactive() 将传入的对象进行转换,并进行递归操作。

代码

打开 observe.js。

// 负责数据劫持
// 把 $data 中的成员转换成 getter/setter
class Observer {
  constructor(data) {
    this.walk(data);
  }

  // 1.负责把 data 选项中的属性转换成响应式数据
  walk(data) {
    // a.判断data是否是对象
    if (!data || typeof data !== "object") {
      return;
    }
    // b.遍历data对象的所有属性
    Object.keys(data).forEach((key) => {
      this.defindReactive(data, key, data[key]);
    });
  }

  defindReactive(obj, key, val) {
    var this_ = this;
    // 2.如果val是对象,把对象中的属性也转换成响应式数据
    this.walk(val);
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        Dep.target && dep.addSub(Dep.target);
        return val;
      },
      set(newValue) {
        if (newValue === val) {
          return;
        }
        val = newValue;
        this_.walk(newValue);
      },
    });
  }
}

此时完成了前两个功能,已经拥有了简单的处理能力。数据变化发送通知需要在后面进行处理。

我们需要把 Observer 类实例化,此时需要在 Vue 类的第三个功能下 new Observer()并传入 this.$data.

打开 vue.js 文件,在第三步中调用。

class Vue {
  constructor(option) {
    // 1.通过属性保存选项的数据
    this.$option = option || {};
    this.$data = option.data || {};
    this.$el =
      typeof option.el === "string"
        ? document.querySelector(option.el)
        : option.el;
    // 2.把data中的成员转换成getter和setter,并注入到Vue实例中
    this._proxyData(this.$data);
    // 3.调用observer对象,监听数据变化
    new Observer(this.$data);
    // 4.调用compiler对象,解析指令和差值表达式
  }
}

? Compiler

功能

  • 负责编译模板,解析指令/插值表达式
  • 负责页面的首次渲染
  • 当数据变化后重新渲染视图

结构 大致内容

Compiler
|
├─ el
├─ vm
├─ compile(el)
├─ compilerElement(node)
├─ compilerText(node)
├─ isDirective(attrName)
├─ isTextNode(node)
├─ isElementNode(node)
├─ .....

梳理

属性介绍:

  • el Vue 实例化的初始 DOM 对象
  • vm vue 实例
  • compile(el) 顾名思义编译 DOM 节点,判断 el 下的内容是元素节点还是文本节点,进行对应的操作
  • compilerElement(node) 解析元素节点的内容
  • compilerText(node) 解析文本节点的内容
  • isDirective(attrName) 判断元素节点里的属性是否为 Vue 属性,以"v-"开头
  • isTextNode(node) 判断元素是否为文本节点
  • isElementNode(node) 判断元素是否为元素节点

代码

打开 compiler.js 文件。

class Compiler {
  constructor(vm) {
    this.el = vm.$el;
    this.vm = vm;
    this.compiler(this.el);
  }
  // 编译模板,出来文本节点和元素节点
  compiler(el) {
    let childNodes = el.childNodes;
    Array.from(childNodes).forEach((node) => {
      if (this.isTextNode(node)) {
        // 处理文本节点
        this.compilerText(node);
      } else if (this.isElementNode(node)) {
        // 处理元素节点
        this.compilerElement(node);
      }

      // 判断node节点,是否有子节点 如果有子节点 要递归处理compliler
      if (node.childNodes && node.childNodes.length) {
        this.compiler(node);
      }
    });
  }

  // 编译文本节点
  compileText(node) {}

  // 编译属性节点
  compileElement(node) {}

  // 判断元素属性的名字是否是指令
  isDirective(attrName) {
    return attrName.startsWith("v-");
  }
  // 判断元素是否为文本节点
  isTextNode(node) {
    return node.nodeType === 3;
  }
  // 判断元素是否为元素节点
  isElementNode(node) {
    return node.nodeType === 1;
  }
}

分步实现两个未定义的函数。

compileText()

  • 负责编译插值表达式

此步骤是用来提取页面中被 {{}} 包裹的参数。

// 编译文本节点
compileText(node) {
  const reg = /\{\{(.+)\}\}/;
  // 获取文本节点的内容
  const value = node.textContent;
  if (reg.test(value)) {
    // 插值表达式中的值就是我们要的属性名称
    const key = RegExp.$1.trim();
    // 把插值表达式替换成具体的值
    node.textContent = value.replace(reg, this.vm[key]);

    // 未来实现数据响应式。。。。。
  }
}

compileElement()

  • 负责编译元素的指令
  • 处理 v-text 的首次渲染
  • 处理 v-model 的首次渲染

我们在这里先实现 v-text v-model, 可以理解在初始化时,如果元素节点中绑定了指令,那么在解析它时必须先对 attr 进行遍历,拿到带有指令的属性。然后建立对应的指令方法,将指令功能实现。

compileElement(node) {
  // 遍历元素节点中的所有属性,找到指令
  Array.from(node.attributes).forEach((attr) => {
    // 获取元素属性的名称
    let attrName = attr.name;
    // 判断当前的属性名称是否是指令
    if (this.isDirective(attrName)) {
      // attrName 的形式 v-text  v-model
      // 截取属性的名称,获取 text model
      attrName = attrName.substr(2);
      // 获取属性的名称,属性的名称就是我们数据对象的属性 v-text="name",获取的是name;
      const key = attr.value;
      // 处理不同的指令
      this.update(node, key, attrName);
    }
  });
}
// 负责更新 DOM
update(node, key, attrName) {
  // node 节点,key 数据的属性名称,dir 指令的后半部分
  const updaterFn = this[attrName + "Updater"];
  updaterFn && updaterFn(node, this.vm[key]);
}
// v-text 指令的更新方法
textUpdater(node, value) {
  node.textContent = value;
}
// v-model 指令的更新方法
modelUpdater(node, value) {
  node.value = value;
}

此时我们需要将 Compiler 类实例化到 Vue 类的第四个功能当中。

打开 vue.js,在第四步中实例化 Compiler。

class Vue {
  constructor(option) {
    // 1.通过属性保存选项的数据
    this.$option = option || {};
    this.$data = option.data || {};
    this.$el =
      typeof option.el === "string"
        ? document.querySelector(option.el)
        : option.el;
    // 2.把data中的成员转换成getter和setter,并注入到Vue实例中
    this._proxyData(this.$data);
    // 3.调用observer对象,监听数据变化
    new Observer(this.$data);
    // 4.调用compiler对象,解析指令和差值表达式
    new Compiler(this);
  }
}

下面这部分可能会有点绕,此处使用了观察者模式。对设计模式陌生的同学,请先阅读第二章内容。

? Vue 学习-设计模式探索

? Dep(Dependency)

功能

  • 收集依赖,添加观察者(watcher)
  • 通知所有观察者

结构 大致内容

Dep
|
├─ subs
├─ addSub(sub)
├─ notify()
├─ .....

梳理

解读属性:

  • subs 存放所有的观察者
  • addSub(sub) 添加观察者
  • notify() 通知所有的观察者

代码

打开 dep.js 文件。

class Dep {
  constructor() {
    // 存储所有的观察者
    this.subs = [];
  }

  // 添加观察者
  addSub(sub) {
    if (sub && sub.update) {
      this.subs.push(sub);
    }
  }

  // 通知所有观察者
  notify() {
    this.subs.forEach((sub) => {
      sub.update();
    });
  }
}

简单的 Dep 订阅者已经建立好了,这时候需要在数据监听的地方将订阅者激活,也就是数据劫持 Observe 类。

所以需要将原本在 Observe 中的defindReactive进行部分改写。

defindReactive(obj, key, val) {
  var this_ = this;
  // 负责收集依赖,并发送通知
  let dep = new Dep();
  // 2. 如果val是对象,把对象中的属性也转换成响应式数据
  this.walk(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // 获取数据的时候将观察者添加进来
      Dep.target && dep.addSub(Dep.target);
      return val;
    },
    set(newValue) {
      if (newValue === val) {
        return;
      }
      val = newValue;
      this_.walk(newValue);

      // 发送通知
      dep.notify();
    },
  });
}

需要将 Dep 的方法注册在数据劫持函数内,已达到全局的订阅中心。

? Watcher

功能

  • 当数据变化触发依赖,dep 通知所有的 Watcher 实例更新视图
  • 自身实例化的时候往 dep 对象中添加自己

结构 大致内容

Watcher
|
├─ vm
├─ key
├─ cb
├─ oldValue
├─ update()
├─ .....

梳理

  • vm vue 实例
  • key data 中属性名称
  • cb 回调函数,负责更新视图
  • oldValue 用来触发 Observe 中定义的 get 方法,调用 Dep 的 addSub 方法。
  • update() 当数据发生变化的时候更新视图

可以大致理解,Watcher 干的事情就是负责处理视图变化,由 Dep 在数据更新的时候告诉它,调用它的 update 方法,然后通过回调函数来更新视图。

代码

打开 watcher.js 文件。

class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    // data中的属性名称
    this.key = key;
    // 回调函数 负责更新视图
    this.cb = cb;

    // 把watcher对象记录到Dep类的静态属性target
    Dep.target = this;
    // 触发get方法,在get方法中会调用addSub
    this.oldValue = vm[key];
    // 添加后清除当前Watcher
    Dep.target = null;
  }
  // 当数据发生变化的时候更新视图
  update() {
    let newValue = this.vm[this.key];
    if (this.oldValue === newValue) {
      return;
    }
    this.cb(newValue);
  }
}

那么需要在哪里调用 Watcher 类呢? 当然是在 Compiler 类中解析元素数据的时候,比如指令的对应函数,解析文本节点时。所以就需要对原本的函数进行处理升级。

打开 compiler.js 文件。

compilerText()

compilerText(node) {
    // console.dir(node);
    // {{ msg }}
    let reg = /\{\{(.+?)\}\}/;
    let val = node.textContent;
    if (reg.test(val)) {
        let key = RegExp.$1.trim()
        node.textContent = val.replace(reg, this.vm[key])

        // 添加 观察者
        new Watcher(this.vm, key, (newValue) => {
            node.textContent = newValue
        })
    }
}

textUpdater()

// 处理v-text
textUpdater(node, value, key) {
    node.textContent = value;

    // 添加 观察者
    new Watcher(this.vm, key, (newValue) => {
        node.textContent = newValue
    })
}

modelUpdater()

我们假设目前只给 input 做双向数据绑定,那个监听它的 input 事件,更新数据就可以触发一圈内容。

// 处理v-model
modelUpdater(node, value, key) {
    node.value = value;

    // 添加 观察者
    new Watcher(this.vm, key, (newValue) => {
        node.value = newValue
    })
    // 双向绑定
    node.addEventListener('input', () => {
        this.vm[key] = node.value
    })
}

至此简单的 Vue 就封装完毕了,这时候我们就需要调试了,但是作为文章显示,不是特别好做调试演示,就将这块省略了。

感谢能阅读到这里。如果有什么问题,可以自行调试排查或者在文字底部留言。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-12-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ? 演示
  • ? 准备工作
  • ? 整体分析
    • ? Vue
      • ? Observer
        • ? Compiler
          • ? Dep(Dependency)
            • ? Watcher
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档