零源码认识Vue响应式原理

963 阅读3分钟

为什么要零源码认识Vue响应式原理?

  • 所有的框架都是对JavaScript的封装
  • 根据框架应用反推代码实现,可以培养逆向思维
  • 通过代码比对,提升代码质量
  • 好处多多, 不再列举......

Vue的初步分析结构如下:

1. Vue简单应用及分析

1.1 Vue入门使用

image.png

<!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>vue2响应式原理基础版</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="../../node_modules/vue/dist/vue.js"></script>
		<script>
			let vm = new Vue({
				el: "#app",
				data: {
					msg: "hello",
					count: 12,
				},
			});
			setTimeout(() => {
				vm.msg = "hello world";
			}, 2000);
		</script>
	</body>
</html>

1.2 Vue基本分析

功能分析如下

  • 负责接收初始化的对象选项参数 ==> new Vue(options)
  • 将data中的属性注入到Vue实例上,并转为getter/setter方法 ==> vm.msg
  • 负责调用observer监听data中所有属性的变化
  • 负责调用compile解析页面标签、指令和插值表达式

2. Vue基本结构

根据上述分析,需要给Vue安排如下内容:

  1. 对象选项参数的初始化: $options
  2. 页面挂载位置:$el
  3. 数据初始化: $data
  4. 数据代理到Vue实例上: _proxyData
  5. 调用特定的方法进行处理所有的data属性并解析页面

class Vue {
	constructor(options) {
		// 保存选项中的数据, 初始化基本数据 $options, $data和$el
		this.$options = options || {};
		this.$data = options.data || {};
		const el = options.el;
		this.$el = typeof el === "string" ? document.querySelector(el) : el;
		
		// 将data中的数据代理到vue实例上  (注入到vue实例上)
		this._proxyData(this.$data);
		
		// 使用observer对data数据进行劫持(待完成 a.数据劫持)
		// 使用compiler对页面进行解析,解析dom、插值表达式和指令等;(待完成 b.解析html)
	}
	
	_proxyData(data) {
		// 遍历所有的data属性, 将其代理到vm上
		Object.keys(data).forEach((key) => {
			Object.defineProperty(this, key, {
				get() {
					return data[key];
				},
				set(newValue) {
					if (data[key] === newValue) {
						return;
					}
					data[key] = newValue;
				},
			});
		});
	}
}

3. 数据劫持

3.1 功能分析

  • 负责将data选项中的属性转换为响应式数据 ==> defineReactive
    • 此处需要递归处理data中的属性
  • 使用数据时,在 发布者 中注册 观察者 (相关概念见发布者和观察者
  • 数据变化发送更新通知

3.2 结构分析

3.3 代码实现

// 负责数据劫持
// 将$data中的成员转化为 getter/setter
class Observer {
  constructor(data) {
    this.walk(data);
  }
  // 遍历数据
  walk(data) {
    // 1. 判断是否是对象,如果不是对象直接返回
    // 2. 如果是对象,就遍历对象的所有属性,设置getter/setter
    if (!data || typeof data !== "object") {
      return;
    }

    Object.keys(data).forEach((key) => {
      this.defineReactive(data, key, data[key]);
    });
  }
  // 定义响应式成员
  defineReactive(data, key, val) {
    const self = this;
    // 如果val是对象, 继续将其内部的属性设置为响应式数据
    this.walk(val);
    // 创建dep对象收集依赖(待完成,c.注册发布者)
    Object.defineProperty(data, key, {
      configurable: true,
      enumerable: true,
      get() {
        // 使用数据时,进行收集依赖(待完成,d.发布者进行收集依赖)
        return val;
      },
      set(newValue) {
        if (newValue === val) {
          return;
        }
        // 如果newValue是对象, 继续将其内部的属性设置为响应式数据
        self.walk(newValue);
        val = newValue;
        // 数据变化后,发送更新通知(待完成,e.发布者通知观察者)
      },
    });
  }
}

4. 解析html

4.1 功能分析:

  • 负责编译html,负责解析指令和插值表达式
  • 负责页面的首次渲染
  • 负责重新渲染视图

4.2 结构分析

4.3 代码实现

// 负责解析指令、插值表达式、dom等
class Compiler {
  constructor(vm) {
    this.vm = vm;
    this.el = vm.$el;
    // 编译模板
    this.compile(this.el);
  }
  // 处理各种节点
  compile(el) {
    const nodes = el.childNodes;
    // 将类数组转为真实数组
    Array.from(nodes).forEach((node) => {
      // 判断是否是文本节点还是元素节点
      if (this.isTextNode(node)) {
        this.compileText(node);
      } else if (this.isElementNode(node)) {
        this.compileElement(node);
      }

      if (node.childNodes && node.childNodes.length) {
        // 如果有子节点,递归编译
        this.compile(node);
      }
    });
  }
  // 判断文本节点
  isTextNode(node) {
    return node.nodeType === 3;
  }
  // 判断元素节点
  isElementNode(node) {
    return node.nodeType === 1;
  }
  // 判断指令
  isDirective(attrName) {
    return attrName.startsWith("v-");
  }
  // 编译文本节点
  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]);
			// 文本节点中使用到数据需要一个渲染watcher,观察数据的变化(待完成,f.注册观察者)
    }
  }
  // 编译元素节点
  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、v-model
        const key = attr.value;
        // 处理不同的指令
        this.update(node, key, attrName);
      }
    });
  }
  // 负责初始化和更新dom
  // 创建 渲染watcher
  update(node, key, attrName) {
    const updateFn = this[attrName + "Updater"];
    updateFn && updateFn.call(this, node, this.vm[key], key);
  }
  // v-text的处理方法
  textUpdater(node, value, key) {
    node.textContent = value;
    // 每个指令中创建一个watcher, 观察数据的变化(待完成,g.注册观察者)
  }
  // v-model的处理方法
  modelUpdater(node, value, key) {
    node.value = value;
    // 每个指令中创建一个watcher, 观察数据的变化(待完成,h.注册观察者)
    // 监听视图的变化(待完成,i.处理双向绑定)
  }
}

5. 发布者

5.1 功能分析

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

5.2 结构分析

(见发布者和观察者

5.3 代码实现

class Dep {
  constructor() {
    // 存储所有的观察者
    this.subs = [];
  }
  // 添加观察者
  addSub(sub) {
    if (sub && sub.update) {
      this.subs.push(sub);
    }
  }
  // 通知观察者更新
  notify() {
    this.subs.forEach((sub) => {
      sub.update();
    });
  }
}

5.4 代码补充

// 3. 数据劫持 ==> defineReactive ==> c.注册发布者 ==> 增加如下代码
const dep = new Dep();
// 3. 数据劫持 ==> defineReactive ==> getter中 ==> d.发布者进行收集依赖 ==> 增加如下代码
Dep.target && dep.addSub(Dep.target);
// 3. 数据劫持 ==> defineReactive ==> setter中 ==> e.发布者通知观察者 ==> 增加如下代码
dep.notify();

6. 观察者

6.1 功能分析

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

6.2 结构分析

6.3 代码实现

class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    // data中的属性名称
    this.key = key;
    // 在数据变化时,调用cb进行更新视图
    this.cb = cb;
    // 在Dep的静态属性上记录当前的watcher对象,当访问数据的时候可以添加到subs中
    Dep.target = this;
    // 触发一次getter, 让dep为当前的key记录watcher
    this.oldValue = vm[key];
    // 清空target
    Dep.target = null;
  }

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

6.4 代码补充

// 4. 解析html ==> compileText ==> f.注册观察者 ==> 增加如下代码
new Watcher(this.vm, key, (value) => {
	node.textContent = value.replace(reg, this.vm[key]);
});
// 4. 解析html ==> textUpdater(处理v-text指令) ==> g.注册观察者 ==> 增加如下代码
new Watcher(this.vm, key, (value) => {
	node.textContent = value;
});
// 4. 解析html ==> modelUpdater(处理v-model指令) ==> h.注册观察者 ==> 增加如下代码
new Watcher(this.vm, key, (value) => {
	node.value = value;
});
// 4. 解析html ==> modelUpdater ==> i.处理双向绑定 ==> 增加如下代码
if (
	node.tagName.toLowerCase() === "input" &&
	node.type.toLowerCase() === "text"
) {
	node.addEventListener("input", (e) => {
		this.vm[key] = node.value;
	});
}

7. 总结

Vue响应式原理的整体流程回顾:

  • Vue
    • 记录传入的选项,设置私有属性data/data/el等
    • 将data数据注入到Vue实例中
    • 负责调用Observer实现数据响应式处理
    • 负责调用Compiler编译html页面
  • Observer
    • 数据劫持
      • 负责将data中的成员递归转为getter/setter
      • 对新对象递归转为getter/setter
    • 添加Dep和Watcher的依赖关系
    • 数据变化发送通知
  • Compiler
    • 负责编译html页面,解析指令/插值表达式等
    • 负责页面的首次渲染过程
    • 当数据变化后重新渲染
  • Dep
    • 收集依赖,添加订阅者(watcher)
    • 通知所有订阅者
  • Watcher
    • 自身实例化的时候 向dep对象上添加自己
    • 当数据变化时被dep通知 watcher进行更新视图

后续将进行源码阅读,对照自己完成的响应式原理,分析差别并进行学习源码。[如果需要源码,请留言一起学习学习]