? ? ? ? 在前端发展的过程中,vue这个框架,成为了越来越多公司及工作者的选择。笔者认为vue设计最棒的一点,就在于他是响应式的,即数据变化,视图也跟着一起变化,那么vue的响应式,是如何实现的呢?
? ? ? ? 这篇文档会带着读者一步步实现一个vue的双向绑定,这篇文章会解开你对vue双向绑定的所有雾水,下面就让我们一步步实现它
一、实现目标及大体思路
? ? ? ? 1. 目标:其实目标非常简单,就是数据发生变化的时候,视图也跟着变化
? ? ? ? 2. 大体思路:首先解析到HTML模板,判断节点是文本节点还是dom节点,
? ? ? ? ? ? ? ? ①、如果是文本节点,则看看文本节点的value值是不是可以与vue数据响应模板语法{{? }}匹配,如果匹配,则获取data中对应该节点的value替换html中的文本,并对其进行监听,如果发生变化,则再次对html中的文本进行更新
? ? ? ? ? ? ? ? ②、如果是元素节点,则看看他的attribute属性中,存不存在vue指令,如v-model等,如果存在,则执行对应的方法
二、双向绑定用到的所有类及他们的作用
? ? ? ? 1. Compile: 解析HTML模板,通过递归的方式获取他们所有的Dom节点
????????????????①如果是文本节点并且节点的值匹配上vue响应数据的固定格式({{? }}形式),则为它创建一个观察者watcher,如果数据发生变化,则更新这个节点
? ? ? ? ? ? ? ? ②如果是dom节点类型的,则获取他们的attribute属性,看看是不是存在v-model、v-on等vue指令,如果存在则执行各个指令所对应的函数
? ? ? ? 2. Observer:通过Object.defineProperty方法,为data中的每个元素增加一个监听器,元素被获取或者改变,分别执行对应的get、set方法
? ? ? ? 3. Watcher:观察数据是否变化,数据发生变化,则提示视图进行更新
? ? ? ? 4. Dep:订阅者,作为Watcher和Observer的桥梁,收集所有订阅者
? ? ? ? 5. selfVue: vue实例,使用方法和new vue一样,都是传递一个配置项(data,el等)
????????很多人对Dep订阅者,存在很大的疑问,为什么要增加一个订阅者呢??其实原因很简单,因为vue是响应式的,数据发生变化,则所有依赖这个数据的地方,都要做出变化;
????????那假如只有一个数据发生变化,反而要更新所有dom节点的值,是不是很浪费性能呢?原因是会的,但是我们在Watcher上做下手脚呢??如果数据没有发生变化,watcher就不去更新dom,直接return出去不就解决了嘛
三、实现过程
????????在实现之前,咱们先看下咱们自己实现的vue响应式该如何使用
import { Observer } from "../responsive/Observer.js";
import { Compile } from "../responsive/Compile.js";
class SelfVue {
constructor(options) {
// option为vue的配置项,其中包括了el,data等
this.data = options.data;
this.el = options.el;
// 监听器,在vue创建的时候,对所有的数据都进行监控,触发get和set的时候对应执行响应的方法
new Observer(this.data);
// 解析器
new Compile(this.el, this);// 这里的this为当前vue实例,感兴趣的可以自行打印下看看
}
}
export { SelfVue };
? ? ? ? 是不是看着很简单!确实也是真的很简单,别慌!下面就一步步实现它
1. 实现一个Observer,能对HTML文本节点解析出来的value值进行监听.他的入参只有一个,即new self的时候传递的data
import { Dep } from "./Dep.js";
class Observer {
constructor(data) {
this.data = data;
this.observer(data);
// Dep为订阅者,关于Dep的使用,在下文详解
this.dep = new Dep();
}
observer(data) {
if (!data || typeof data !== "object") {
// data不存在,或者data为基础数据类型
return;
}
Object.keys(data).forEach((key) => {
this.defineReactive(data, key, data[key]);
});
}
defineReactive(data, key, val) {
let self = this;
// 递归
this.observer(val);
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
// Dep.target为当前Watcher的实例,它具有update,和get方法
if (Dep.target) {
// 将当前实例化的watcher,保存到Dep订阅者数组中
self.dep.addSub(Dep.target);
}
return val;
},
set(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 下文详解Dep作用,现在不用关心,到此知道这个属性被监听到了即可
self.dep.notify();
},
});
}
}
export { Observer };
2. 实现一个观察者Watcher
????????使用场景:解析HTML模板的时候,为每个匹配到vue响应式数据格式的节点创建一个watcher,对应data值变化的时候,watcher会对其进行更新,对于以下代码的Dep和callback回调函数部分依然不用纠结,不久后,咱们会一起揭开它神秘的面纱!
import { Dep } from "../responsive/Dep.js";
class Watcher {
// vm当前实例(包括了method,data等),exp监听的那个属性,callback回调函数,返回改变后的值,更新视图
constructor(vm, exp, callback) {
this.vm = vm;
this.exp = exp;
this.callback = callback;
this.value = this.get(); // 这里的value是data中真实的数据
}
get() {
// this当前watcher实例,监听的是HTML模板中某一项可匹配的value值,即以{{ }}形式出现的
Dep.target = this; // 这里的this为当前watcher的实例
let value = this.vm.data[this.exp]; // 注意!!!这里会触发Object.defineProperty的get方法
Dep.target = null; // 释放自己
return value;
}
update() {
this.run();
}
run() {
// 现在this.vm.data[this.exp]中的值已经为最新的
this.value = this.vm.data[this.exp];
this.callback(this.value);
}
}
export { Watcher };
? ? ? ? 3. 实现一个Dep订阅者
????????????????使用场景:Dep的作用只有一个,存放所有的watcher实例,数据改变的时候,执行所有的watcher实例,更新视图
// Dep存放的是所有的watcher
class Dep {
constructor() {
this.subs = [];
}
static target = null;
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach((sub) => {
// 每个sub都是对应HTML模板的watcher实例
sub.update();
});
}
}
export { Dep };
? ? ? ? 4. 实现一个Compile节点处理解析器
? ? ? ? ? ? ? ? Compile是整个vue响应式执行的第一步,如果上面的代码看的也是带有疑惑,可根据注释中的提示,在看一遍,相信这个提示,具有醍醐灌顶的功能
import { Watcher } from "./Watcher.js";
class Compile {
constructor(el, vm) {
this.ele = document.querySelector(el);
this.vm = vm;
this.fragment = null; // createDocumentFragment创建的虚拟节点片段
this.init();
}
init() {
if (this.ele) {
this.fragment = this.nodeToFragment(this.ele);
this.compileElement(this.ele);
// 将Fragment虚拟节点加入HTML结构中
this.ele.appendChild(this.fragment);
} else {
console.error("el不存在");
}
}
nodeToFragment(ele) {
// 递归获取所有子节点
/*
知识补充
1. createDocumentFragment方法在遍历原来子节点并将原来子节点添加进fragment的时候,会删除要添加的节点
2. createDocumentFragment()方法,是用来创建一个虚拟的节点对象,或者说,是用来创建文档碎片节点。它可以包含各种类型的节点,在创建之初是空的。
3. DocumentFragment节点不属于文档树,继承的parentNode属性总是null
4. 当请求把一个DocumentFragment节点插入文档树时,插入的不是DocumentFragment自身,而是它的所有子孙节点,即插入的是括号里的节点
*/
let fragment = document.createDocumentFragment();
let child = ele.firstChild;
while (child) {
fragment.appendChild(child);
child = child.firstChild;
}
return fragment;
}
compileElement(ele) {
let childNodes = ele.childNodes;
childNodes.forEach((node) => {
const reg = /\{\{(.*)\}\}/;
let text = node.textContent;
if (this.isElementNode(node)) {
// 元素节点
this.compileNode(node);
} else if (this.isTextNode(node) && reg.test(text)) {
// 当前节点类型是文本节点,并且节点的value匹配上了{{ }}格式
// 文本节点
this.compileText(node, reg.exec(text)[1]);
}
// 递归遍历所有子节点
if (node.childNodes && node.childNodes.length) {
this.compileElement(node);
}
});
}
compileNode(node) {
// 主要针对node节点的attribute属性,判断有没有v-model等属性
let nodeAttrs = node.attributes;
Array.prototype.forEach.call(nodeAttrs, (attr) => {
if (this.isDirective(attr.name)) {
// 含有vue自带指令
if (this.isModelAttr(attr.name)) {
this.compileModel(node, attr.value);
}
}
});
}
compileText(node, exp) {
exp = exp.trim();
let initText = this.vm.data[exp];
this.updateText(node, initText);
// 为当前节点创建一个watcher观察者实例,如果数据改变,则更新对应的节点值
// 提示:可以在这里看看watcher是怎么实现的,在留意下Dep中target属性
new Watcher(this.vm, exp, (value) => {
this.updateText(node, value);
});
}
updateText(node, value) {
// 更新文本节点的value值
node.textContent = typeof value == "undefined" ? "" : value;
}
compileModel(node, exp) {
let val = this.vm.data[exp];
// 执行v-model对应的方法
this.modelUpdate(node, exp, val);
new Watcher(this.vm, exp, (value) => {
this.modelUpdate(node, exp, value);
});
// input 的双向绑定
node.addEventListener("input", (e) => {
let newVal = e.target.value;
if (val === newVal) {
return;
}
this.vm.data[exp] = newVal;
val = newVal;
});
}
modelUpdate(node, exp, value) {
node.value = typeof this.vm.data[exp] == "undefined" ? "" : value;
}
isModelAttr(attrName) {
return (
attrName.split("-").length === 2 && attrName.split("-")[1] === "model"
);
}
isDirective(attrName) {
return attrName.indexOf("v-") === 0;
}
isElementNode(node) {
return node.nodeType === 1;
}
isTextNode(node) {
return node.nodeType === 3;
}
}
export { Compile };
到此一个完整的vue响应式就已经实现了,如有不对之处还望之处
????????创作不易,如果对您有一些帮助或者启发,麻烦留个赞在离开~~,后续会持续更新vue的一些实现原理,如果喜欢的话,关注不迷路!
参考链接:vue的双向绑定原理及实现 - canfoo#! - 博客园
|