代码仓库地址:https://github.com/141110126/write-vue/tree/main/jvue
一、MVVM框架
![](https://img-blog.csdnimg.cn/20210519170839920.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L01pZW1pZVdhbg==,size_16,color_FFFFFF,t_70)
- Object.defifineProperty()
- Proxy
- 插值:{{}}
- 指令:v-bind,v-on,v-model,v-for,v-if
- 模板 => vdom => dom
二、Vue响应式原理
利用Object.defineProperty()劫持data数据,里面每个属性都定义了get和set方法 监听属性的变化,
依赖收集:每个属性也都有个数组 保存着谁(视图watcher)依赖了它,当获取属性触发get时,收集了依赖,
依赖更新:当属性变化触发set函数时,通知依赖,通知视图(watcher),视图(watcher)开始更新。
- Vue:框架构造函数
- Observer: 执行数据响应化
- Compile:编译模板,初始化视图,收集依赖(更新函数、watcher创建)
- Watcher:执行更新函数
- Dep:管理多个Watcher,批量更新
三、 数据拦截与代理
Object.defineProperty():在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
get 值是一个函数,当属性被访问时,会触发 get 函数
set 值同样是一个函数,当属性被赋值时,会触发 set 函数
Vue 是怎么知道数据被调用或改变的?
Vue对data对象中的每个属性进行了get、set监听,当属性被调用时触发get方法,当属性被改变时触发set方法,所以Vue 能知道数据被调用或改变。
1.defineReactive():使用Object.defineProperty()对obj中的某一个属性进行拦截
// 对obj中的属性做拦截
defineReactive(obj, key, val) {
this.observe(val);
Object.defineProperty(obj, key, {
get () {
return val;
},
set (newValue) {
if(val === newValue) {
return;
}
val = newValue;
console.log(val);
}
})
}
2.observe(obj): 当obj中有多个属性要做拦截时,我们需要遍历obj中 的属性
创建Observer类:对传入的数据进行判断,再分类(Array、Object)进行操作。
// 遍历对象,给它的属性做响应式处理
function observe(obj){
// 判断obj是否为对象,若不是则返回
if(typeof obj !== "object" || obj == null) {
return obj;
}
// 创建Observer实例,传入obj进行判断 遍历
new Observer(obj);
}
// 判断是否为对象,若为对象再进行遍历
class Observer {
constructor(obj) {
if(Array.isArray(obj)){
// todo
} else{
this.walk(obj);
}
}
walk(obj) {
// 若是对象,则遍历其属性做拦截处理
Object.keys(obj).forEach(key => defineReactive(obj,key,obj[key]));
}
}
3.递归:当obj中的属性的属性值为对象时,需要递归遍历属性值中的属性,对深层的属性进行拦截。
当给属性重新赋值一个对象时,也需要对该对象中的属性做拦截处理
// 对obj中的属性做拦截
function defineReactive(obj, key, val) {
// 当val是一个对象时,需要递归遍历val中的属性做拦截处理
observe(val);
// vue2中用Object.defineProperty()对属性进行跟踪拦截
Object.defineProperty(obj, key , {
get() {
return val;
},
set(newVal) {
val = newVal;
// 当newVal为对象时,需要遍历newVal中的属性做拦截处理
observe(newVal);
// update()
}
})
}
4.set():给已有的对象动态添加新的响应式属性
Vue.$sets()实际上就是调用了defineReactive方法
// 给已有的对象动态添加新的响应式属性
function set(obj, key, val) {
defineReactive(obj, key, val);
}
5.Vue类初始化:获取data中的属性,进行拦截处理,并代理到vue上,这样才能直接从vue中获取和修改属性。
// 将传入对象中的所有属性代理到指定对象上
function Proxy(vm) {
// 遍历data中的属性,在vm中添加一样的属性,并做拦截处理
Object.keys(vm.$data).forEach(key => {
Object.defineProperty(vm, key, {
get() {
console.log(vm.$data[key]);
return vm.$data[key]
},
set(v) {
// 当修改vm中的属性时,data中的属性也跟着修改
vm.$data[key] = v
console.log(vm.$data[key]);
}
})
})
}
class JVue {
constructor(options) {
// 1.获取data对象
this.$options = options;
this.$data = options.data;
console.log(this.$data);
// 2.对data做拦截处理
observe(this.$data);
// 3.将data中的属性代理到vm中
Proxy(this);
}
}
6.测试
<!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>Document</title>
</head>
<body>
<div id="app">
<p>{{count}}</p>
</div>
<script src="./jvue.js"></script>
<script>
const app = new JVue({
el: "#app",
data: {
count: 1
}
})
setInterval(() => {
app.count ++
}, 1000)
</script>
</body>
</html>
7.数组响应式
解析: 数组中有7个方法会改变数组本身,所以在其改变自身时要通知更新,所以需要对这七个方法进行替换。
- 改变数组自身的7个方法:push,pop,unshift,shift,sort,reverse,splice
思路:创建7个新的方法,当传入的数据为数组时,将其原型替换
// 数组响应式
// 获取Array原型,备份一份,修改备份
const originalProto = Array.prototype;
const arrayProto = Object.create(originalProto);
// 设置七个新的方法
["push", "pop", "shifit", "unshift", "sort", "reverse", "splice"].forEach(
method => {
arrayProto[method] = function() {
// 原始操作
originalProto[method].apply(this, arguments);
// 覆盖:通知更新
console.log("数组执行" + method + "操作");
};
}
);
// 判断obj为数组还是对象,分类操作
class Observer {
constructor(obj) {
if (Array.isArray(obj)) {
// 数组类型
// 覆盖原型
obj.__proto__ = arrayProto;
// 递归遍历数组中的子元素
const keys = Object.keys(obj);
for (let i = 0; i < obj.length; i++) {
observe(obj[i]);
}
} else {
// 对象类型
this.walk(obj);
}
}
walk(obj) {
// 若是对象,则遍历其属性做拦截处理
Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]));
}
}
四、compile编译
遍历传入的子元素,分为节点和文本,再进行编译
// 编译
class Compile {
constructor(el, vm) {
this.$vm = vm;
const node = document.querySelector(el)
this.compile(node);
}
// 判断是节点还是文本
compile(node) {
// 获取el的子节点
const childNodes = node.childNodes;
// 遍历子节点并分类判断
Array.from(childNodes).forEach(n => {
// 元素节点
if(this.isElement(n)) {
// 当子节点中还有子节点时,进行递归
console.log(n);
if(n.childNodes.length > 0) {
this.compile(n)
}
// 编译节点
this.compileElement(n);
} else if(this.isInter(n)) {
// 插值表达式:编译文本
this.compileText(n)
console.log('文本',n.textContent);
}
})
}
// 元素节点
isElement(n) {
return n.nodeType === 1;
}
// 又是文本节点插值, 又是插值表达式,例:{{count}}
isInter(n) {
return n.nodeType === 3 && /\{\{(.*)\}\}/.test(n.textContent);
}
// 判断是否为j-指令
isDir(attrName) {
console.log(attrName);
return attrName.startsWith('j-');
}
// 编译初始化和更新函数
update(node, exp, dir) {
// 编译初始化
const fn = this[dir + 'Updater'];
fn && fn(node, this.$vm[exp]);
// 更新函数: watcher是在遍历节点去编译时创建的
new Watcher(this.$vm, exp, val => {
// 接收最新的值,更新
fn && fn(node, val);
});
}
// 编译插值表达式
compileText(n) {
// RegExp.$1:与上方正则表达式匹配的表达式,例:count
// 查找vm中匹配属性的值,赋给该节点
// n.textContent = this.$vm[RegExp.$1];
this.update(node, RegExp.$1, "text");
}
// 编译节点:属性分类:j-指令, @事件
compileElement(n) {
const attrs = n.attributes;
console.log(n.attributes);
// 遍历所有属性,进行分类
Array.from(attrs).forEach(attr => {
const attrName = attr.name;
const exp = attr.value;
// 若为j-指令
if(this.isDir(attrName)) {
// 截取j-后的指令,并执行编译对应指令
const dir = attrName.substring(2); //text,html,...
this[dir] && this[dir](n, exp);
} else {
// 若为@事件
}
})
}
// 编译j-text
text(n, exp) {
this.update(node, exp, 'text');
}
// 真正编译j-text的方法
textUpdater(node, val) {
node.textContent = val;
}
// 编译j-html
html(n, exp) {
this.update(node, exp, 'html');
}
htmlUpdate(n, exp) {
n.innerHTML = this.$vm[exp];
}
}
五、依赖收集、更新
1.watcher:依赖
实现思路:
1. defineReactive时为每⼀个key创建⼀个Dep实例
2. 初始化视图时读取某个key,例如name1,创建⼀个watcher1
3. 由于触发name1的getter⽅法,便将watcher1添加到name1对应的Dep中
4. 当name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新
watcher在编译时创建,应包含和哪个key有关,更新函数是谁:
// 负责dom更新
// watcher: watcher在编译时创建,和哪个key有关,更新函数是谁
class Watcher {
constructor(vm, key, updater) {
this.vm = vm;
this.key = key;
this.updater = updater;
}
// 将来会被Dep调用
update() {
this.updater.call(this.vm, this.vm[this.key]);
}
}
watcher是在编译时创建的,所以重构前面的Compile中的编译方法
// 编译
class Compile {
constructor(el, vm) {
this.$vm = vm;
const node = document.querySelector(el);
this.compile(node);
}
// 判断是节点还是文本
compile(node) {
// 获取el的子节点
const childNodes = node.childNodes;
// 遍历子节点并分类判断
Array.from(childNodes).forEach(n => {
// 元素节点
if (this.isElement(n)) {
// 当子节点中还有子节点时,进行递归
if (n.childNodes.length > 0) {
this.compile(n);
}
// 编译节点
this.compileElement(n);
} else if (this.isInter(n)) {
// 插值表达式:编译文本
this.compileText(n);
}
});
}
// 元素节点
isElement(n) {
return n.nodeType === 1;
}
// 又是文本节点插值, 又是插值表达式,例:{{count}}
isInter(n) {
return n.nodeType === 3 && /\{\{(.*)\}\}/.test(n.textContent);
}
// 判断是否为j-指令
isDir(attrName) {
return attrName.startsWith("j-");
}
// 编译初始化和更新函数
update(node, exp, dir) {
// 编译初始化
const fn = this[dir + "Updater"];
fn && fn(node, this.$vm[exp]);
// 更新函数: watcher是在遍历节点去编译时创建的
new Watcher(this.$vm, exp, val => {
// 接收最新的值,更新
fn && fn(node, val);
});
}
// 编译插值表达式
compileText(node) {
// RegExp.$1:与上方正则表达式匹配的表达式,例:count
// 查找vm中匹配属性的值,赋给该节点
// n.textContent = this.$vm[RegExp.$1];
this.update(node, RegExp.$1.trim(), "text");
}
// 编译节点:属性分类:j-指令, @事件
compileElement(n) {
const attrs = n.attributes;
// 遍历所有属性,进行分类
Array.from(attrs).forEach(attr => {
const attrName = attr.name;
const exp = attr.value;
// 若为j-指令
if (this.isDir(attrName)) {
// 截取j-后的指令,并执行编译对应指令
const dir = attrName.substring(2); //text,html,...
this[dir] && this[dir](n, exp);
} else if(isEvent(attrName)) {
// 若为@事件
// 获取事件名
const dir = attrName.substring(1);
this.eventHandler(n, exp, dir);
}
});
}
// 编译j-text
text(node, exp) {
this.update(node, exp, "text");
}
// 真正编译j-text的方法
textUpdater(node, val) {
node.textContent = val;
}
// 编译j-html
html(node, exp) {
this.update(node, exp, "html");
}
htmlUpdater(node, val) {
node.innerHTML = val;
}
// 判断是否为@指令
isEvent(attrName) {
return attrName.indexOf("@") == 0;
}
// 处理事件
eventHandler(node, exp, dir) {
const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp];
node.addEventListener(dir, fn.bind(this.$vm));
}
// 编译j-model:绑定value值,事件监听
model(node, exp) {
this.update(node, exp, "model");
console.log(11111111111);
node.addEventListener("input", e => {
// 新的值赋给数据
this.$vm[exp] = e.target.value;
});
}
modelUpdater(node, value) {
// 表单元素赋值
node.value = value;
}
}
2.Dep:收集依赖
依赖收集:data中每个属性都会有一个数组作为依赖收集器,当页面使用某个属性(get)时,页面的watcher就会被放到对应属性的依赖收集器中。
依赖更新:通知属性依赖收集器中的watcher进行更新
代码如下:
// Dep:保存watcher实例的依赖类
class Dep {
constructor() {
this.deps = [];
}
// 添加依赖:创建依赖关系时调用
addDep(dep) {
this.deps.push(dep);
}
// 通知deps数组中的依赖进行更新
notify() {
this.deps.forEach(dep => dep.update());
}
}
1.在defineReactive创建dep实例。
2.在getter中收集依赖,即将watcher放到该属性对应的deps数组中。
watcher如何放到deps中呢?在watcher中初始化时便将watcher放到全局变量Dep.target中,触发对应属性的getter, getter中将Dep.target放到deps数组中。
3.在setter中调用dep.notify()通知依赖更新
// 对obj中的属性做拦截
function defineReactive(obj, key, val) {
// 当val是一个对象时,需要递归遍历val中的属性做拦截处理
observe(val);
// 创建Dep实例
const dep = new Dep();
// vue2中用Object.defineProperty()对属性进行跟踪拦截
Object.defineProperty(obj, key, {
get() {
// 依赖收集
Dep.target && dep.addDep(Dep.target);
return val;
},
set(newVal) {
val = newVal;
// 当newVal为对象时,需要遍历newVal中的属性做拦截处理
observe(newVal);
// 通知更新
dep.notify();
}
});
}
// 负责dom更新
// watcher: watcher在编译时创建,和哪个key有关,更新函数是谁
class Watcher {
constructor(vm, key, updater) {
this.vm = vm;
this.key = key;
this.updater = updater;
// 将watcher放到Dep.target(任何一个全局变量都行),触发getter,getter中会将该Watcher放到该key所对应的Dep数组中
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
// 将来会被Dep调用
update() {
this.updater.call(this.vm, this.vm[this.key]);
}
}
六、整体代码
jvue.js:
// 数组响应式
// 获取Array原型,备份一份,修改备份
const originalProto = Array.prototype;
const arrayProto = Object.create(originalProto);
// 设置七个新的方法
["push", "pop", "shifit", "unshift", "sort", "reverse", "splice"].forEach(
method => {
arrayProto[method] = function() {
// 原始操作
originalProto[method].apply(this, arguments);
// 覆盖:通知更新
console.log("数组执行" + method + "操作");
};
}
);
// 对象响应式
function defineReactive(obj, key, val) {
// 当val是一个对象时,需要递归遍历val中的属性做拦截处理
observe(val);
// 创建Dep实例
const dep = new Dep();
// vue2中用Object.defineProperty()对属性进行跟踪拦截
Object.defineProperty(obj, key, {
get() {
// 依赖收集
Dep.target && dep.addDep(Dep.target);
return val;
},
set(newVal) {
val = newVal;
// 当newVal为对象时,需要遍历newVal中的属性做拦截处理
observe(newVal);
// 通知更新
dep.notify();
}
});
}
// 将传入对象中的所有属性代理到指定对象上
function Proxy(vm) {
// 遍历data中的属性,在vm中添加一样的属性,并做拦截处理
Object.keys(vm.$data).forEach(key => {
Object.defineProperty(vm, key, {
get() {
return vm.$data[key];
},
set(v) {
// 当修改vm中的属性时,data中的属性也跟着修改
vm.$data[key] = v;
}
});
});
}
// 遍历对象,给它的属性做响应式处理
function observe(obj) {
// 判断obj是否为对象,若不是则返回
if (typeof obj !== "object" || obj == null) {
return obj;
}
// 创建Observer实例,传入obj进行判断 遍历
new Observer(obj);
}
// 判断obj为数组还是对象,分类操作
class Observer {
constructor(obj) {
if (Array.isArray(obj)) {
// 数组类型
// 覆盖原型
obj.__proto__ = arrayProto;
// 递归遍历数组中的子元素
const keys = Object.keys(obj);
for (let i = 0; i < obj.length; i++) {
observe(obj[i]);
}
} else {
// 对象类型
this.walk(obj);
}
}
walk(obj) {
// 若是对象,则遍历其属性做拦截处理
Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]));
}
}
class JVue {
constructor(options) {
// 1.获取data对象
this.$options = options;
this.$data = options.data;
// 2.对data做拦截处理
observe(options.data);
// 3.将data中的属性代理到vm中
Proxy(this);
// 4.编译
new Compile(options.el, this);
}
}
// 编译
class Compile {
constructor(el, vm) {
this.$vm = vm;
const node = document.querySelector(el);
this.compile(node);
}
// 判断是节点还是文本
compile(node) {
// 获取el的子节点
const childNodes = node.childNodes;
// 遍历子节点并分类判断
Array.from(childNodes).forEach(n => {
// 元素节点
if (this.isElement(n)) {
// 当子节点中还有子节点时,进行递归
if (n.childNodes.length > 0) {
this.compile(n);
}
// 编译节点
this.compileElement(n);
} else if (this.isInter(n)) {
// 插值表达式:编译文本
this.compileText(n);
}
});
}
// 元素节点
isElement(n) {
return n.nodeType === 1;
}
// 又是文本节点插值, 又是插值表达式,例:{{count}}
isInter(n) {
return n.nodeType === 3 && /\{\{(.*)\}\}/.test(n.textContent);
}
// 判断是否为j-指令
isDir(attrName) {
return attrName.startsWith("j-");
}
// 编译初始化和更新函数
update(node, exp, dir) {
// 编译初始化
const fn = this[dir + "Updater"];
fn && fn(node, this.$vm[exp]);
// 更新函数: watcher是在遍历节点去编译时创建的
new Watcher(this.$vm, exp, val => {
// 接收最新的值,更新
fn && fn(node, val);
});
}
// 编译插值表达式
compileText(node) {
// RegExp.$1:与上方正则表达式匹配的表达式,例:count
// 查找vm中匹配属性的值,赋给该节点
// n.textContent = this.$vm[RegExp.$1];
this.update(node, RegExp.$1.trim(), "text");
}
// 编译节点:属性分类:j-指令, @事件
compileElement(n) {
const attrs = n.attributes;
// 遍历所有属性,进行分类
Array.from(attrs).forEach(attr => {
const attrName = attr.name;
const exp = attr.value;
// 若为j-指令
if (this.isDir(attrName)) {
// 截取j-后的指令,并执行编译对应指令
const dir = attrName.substring(2); //text,html,...
this[dir] && this[dir](n, exp);
} else if (this.isEvent(attrName)) {
// 若为@事件
const dir = attrName.substring(1);
this.eventHandler(n, exp, dir);
}
});
}
// 编译j-text
text(node, exp) {
this.update(node, exp, "text");
}
// 真正编译j-text的方法
textUpdater(node, val) {
node.textContent = val;
}
// 编译j-html
html(node, exp) {
this.update(node, exp, "html");
}
htmlUpdater(node, val) {
node.innerHTML = val;
}
// 判断是否为@指令
isEvent(attrName) {
return attrName.indexOf("@") == 0;
}
// 处理事件
eventHandler(node, exp, dir) {
const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp];
node.addEventListener(dir, fn.bind(this.$vm));
}
// 编译j-model:绑定value值,事件监听
model(node, exp) {
this.update(node, exp, "model");
console.log(11111111111);
node.addEventListener("input", e => {
// 新的值赋给数据
this.$vm[exp] = e.target.value;
});
}
modelUpdater(node, value) {
// 表单元素赋值
node.value = value;
}
}
// 负责dom更新
// watcher: watcher在编译时创建,和哪个key有关,更新函数是谁
class Watcher {
constructor(vm, key, updater) {
this.vm = vm;
this.key = key;
this.updater = updater;
// 将watcher放到Dep.target(任何一个全局变量都行),触发getter,getter中会将该Watcher放到该key所对应的Dep数组中
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
// 将来会被Dep调用
update() {
this.updater.call(this.vm, this.vm[this.key]);
}
}
// Dep:保存watcher实例的依赖类
class Dep {
constructor() {
this.deps = [];
}
// 添加依赖:创建依赖关系时调用
addDep(dep) {
this.deps.push(dep);
}
// 通知deps数组中的依赖进行更新
notify() {
this.deps.forEach(dep => dep.update());
}
}
jvue.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>Document</title>
</head>
<body>
<div id="app">
<p>{{ count }}</p>
<p j-text="count"></p>
<p j-html="exp"></p>
<button @click="add">增加</button>
<input j-model="value" />
<p>{{ value }}</p>
</div>
<script src="./jvue.js"></script>
<script>
const app = new JVue({
el: "#app",
data: {
count: 1,
exp: "<h3>j-html指令</h3>",
arr: [1, 2, 3],
value: ""
},
methods: {
add() {
this.count++;
}
}
});
// setInterval(() => {
// app.count++;
// app.arr.push(app.count);
// console.log(app.arr);
// }, 1000);
</script>
</body>
</html>
代码仓库地址:https://github.com/141110126/write-vue/tree/main/jvue