likes
comments
collection
share

拆解Vue2核心模块实现(1)

作者站长头像
站长
· 阅读数 17

前言

之前写过一篇 为什么 Vue2 this 能够直接获取到 props、 data 、computed和 methods 源码分析,看源码过程中发现一些核心概念可以抽离出来方便理解,于是就有了这篇

初始化项目

  1. 新建项目文件夹 toy-vue
  2. 进入toy-vue 文件夹,执行 npm init 初始化 package.json
  3. 新建 index.jsindex.html 文件,新建 lib 文件夹
  4. 执行 npm i -S vite 安装 vite

初始化文件

第一阶段先实现v-model指令、{{}}表达式,新建 index.jsindex.html文件分别写入如下内容

  • index.js
import Vue from './lib'

new Vue({
  el: '#app',
  data: {
    msg: 'msg',
  }
})

  • index.html
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta http-equiv="X-UA-Compatible" content="IE=edge" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>toy vue</title>
	</head>
	<body>
		<div id="app">
			<div>{{msg}}</div>
			<input v-model="msg" type="text" />
		</div>
		<script type="module" src="/index.js"></script>
	</body>
</html>

核心模块

  • Vue,负责主流程管理
  • Compiler,负责模板解析,包括指令、插值表达式解析,还有页面渲染
  • 双向绑定:Dep、 Observer、 Watcher
  • util 工具集

工具集

  • lib/util.js
/**
 * 判断元素属性是否为指令
 * @param attrName
 */
export function isDirective(attrName) {
  return attrName.startsWith('v-')
}
/**
 * 判断元素节点是否为文本节点
 * @param node
 */
export function isTextNode(node) {
  return node.nodeType === 3
}
/**
 * 判断元素节点是否为元素节点
 * @param node
 */
export function isElementNode(node) {
  return node.nodeType === 1
}
/**
 * 是否对象
 * @param obj
 */
export function isObject(obj) {
  return obj !== null && typeof obj === "object";
}
const _toString = Object.prototype.toString
/**
 * 是否为纯对象
 * @param obj 
 */
export function isPlainObject(obj) {
  return _toString.call(obj) === "[object Object]";
}
/**
 * 警告
 * @param msg 
 */
export function wranLog(msg) {
  console.warn(`vue wran: ${msg}`);
}

Vue 主类实现

  • lib/Vue.js
import Observer from "./Observer";
import Compiler from "./Compiler";
import { isPlainObject } from "./util";

class Vue {
  constructor(options) {
    const { data, el } = options || {};
    this.$options = options || {};
    this.$data = data;
    this.$el = typeof el === "string" ? document.querySelector(el) : el;
    this._proxyData(this.$data);
    // data数据双向绑定
    new Observer(this.$data);
    // 模板解析、以及页面渲染
    new Compiler(this);
  }
  /**
   * 将data属性代理到this上
   * @param data
   */
  _proxyData(data) {
    Object.keys(data).forEach((key) => {
      Object.defineProperty(this, key, {
        enumerableL: true,
        configurable: true,
        get() {
          return data[key];
        },
        set(val) {
          if (val === data[key]) {
            return;
          }
          data[key] = val;
        },
      });
    });
  }
}

export default Vue;

双向绑定实现

双向绑定基于观察者模式和+发布订阅模式实现

update data
notify
update
subscribe
change data
Observer
Dep
Watcher
View

Dep

依赖收集器,Observer和Watcher的纽带

  • lib/Dep.js
class Dep {
  constructor () {
    this.subs = []
  }
  addSub (sub) {
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  notify () {
    this.subs.forEach(sub => {
      sub.update();
    })
  }
}

export default Dep

Observer

数据观察者,将数据操作归置到自己的监管之下

  • lib/Observer.js


import Dep from './Dep'
import { isPlainObject } from './util'

class Observer {
  constructor(data) {
    this.walk(data)
  }
  walk(data) {
    if (!isPlainObject(data)) {
      return
    }
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key]);
    })
  }
  defineReactive(obj, key, val) {
    const that = this;
    const dep = new Dep()
    // 递归处理
    this.walk(val);
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 当 target 不为空时才需要订阅
        // Watcher 初始化后会将 target 设为null,防止多次订阅
        Dep.target && dep.addSub(Dep.target)
        return val
      },
      set(newValue) {
        if (newValue === val) {
          return
        }
        val = newValue
        // 新的数据需要重新设置观察者
        that.walk(newValue);
        // 有数据变化,通知订阅者更新
        dep.notify()
      }
    })
  }
}
export default Observer

Watcher

订阅者,数据变化时由 Watcher 进行相应操作

  • lib/Watcher.js
import Dep from "./Dep";

class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;
    // 这里这样写是为了触发订阅,同时保证只订阅一次
    Dep.target = this;
    this.oldValue = vm[key];
    Dep.target = null;
  }

  update() {
    const newValue = this.vm[this.key];
    if (this.oldValue === newValue) {
      return;
    }
    this.cb(newValue);
  }
}

export default Watcher;

Compiler 类实现

Compiler 类负责模板解析处理

  • lib/Compiler.js
import Watcher from './Watcher'
import { isDirective, isTextNode, isElementNode } from './util'

class Compiler {
  constructor(vm) {
    this.el = vm.$el;
    this.vm = vm;
    this.compile(this.el)
  }
  /**
   * 编译模板,处理节点
   * @param el 
   */
  compile(el) {
    const childNodes = el.childNodes
    Array.from(childNodes).forEach(node => {
      
      if (isTextNode(node)) {
        // 文本节点
        this.compileText(node)
      } else if (isElementNode(node)) {
        // 标签节点
        this.compileElement(node)
      }
      // 处理子节点
      if (node.childNodes && node.childNodes.length) {
        this.compile(node)
      }
    })
  }
  /**
   * 编译元素节点,处理指令
   * @param node 
   */
  compileElement(node) {
    Array.from(node.attributes).forEach(attr => {
      let attrName = attr.name;
      if (isDirective(attrName)) {
        attrName = attrName.substr(2)
        const key = attr.value
        this.update(node, key, attrName)
      }
    })
  }
  /**
   * 更新操作
   */
  update(node, key, attrName) {
    const updateFn = this[`${attrName}Updater`];
    updateFn && updateFn.call(this, node, this.vm[key], key)
  }
  /**
   * v-model 指令
   */
  modelUpdater(node, value, key) {
    node.value = value
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue
    })
    node.addEventListener('input', (a) => {
      this.vm[key] = node.value
    })
  }
  /**
   * 文本节点,处理插值表达式
   * @param node 
   */
  compileText(node) {
    // .:匹配任意字符,():提取内容
    const reg = /\{\{(.+?)\}\}/g;
    const value = node.textContent
    if (reg.test(value)) {
      const key = RegExp.$1.trim()
      node.textContent = value.replace(reg, this.vm[key])
      new Watcher(this.vm, key, (newValue) => {
        node.textContent = newValue
      })
    }
  }
}
export default Compiler

测试

package.json 新增 scripts.dev 配置项,写入启动命令 vite --open

  • package.json
    "scripts": {
        "dev": "vite --open",
        "test": "echo \"Error: no test specified\" && exit 1"
    },
  • 执行 pnpm dev 可看到如下页面,输入框输入会有联动效果

拆解Vue2核心模块实现(1)

以上代码在线运行地址:code.juejin: toy-vue(1)

总结

本次 toy-vue 实现了双向绑定、v-model 指令、插值表达式,之后可以在这基础之上增加生命周期、组件支持、复杂表达式支持等