理解Vue的设计思想
将视图View的状态和行为抽象化,让我们将视图UI和业务逻辑分开
Vue是MVVM框架的实现,其包括三大要素:数据响应式、模板引擎和渲染
数据响应式:监听数据变化并在视图中更新
- Object.defineProperty()(Vue2)
- Proxy(Vue3)
模板引擎:提供描述视图的模板语法
- 插值:{{}}
- 指令:v-bind, v-on, v-model, v-for, v-if
渲染:如何将模板转换为html
- 模板=>vdom=>dom
数据响应式原理
数据变更能够响应在视图中,就是数据响应式,Vue2中利用Object.defineProperty()实现变更检测
简单实现,01-reactive.js
// Object.defineProperty()
// 将传入的obj,动态的设置一个key,它的值为val
// /* eslint-disable-next-line */
function defineReactive(obj, key, val) {
// 对val判断是否为对象,递归
observe(val);
Object.defineProperty(obj, key, {
get() {
console.log("get", key);
return val;
},
set(v) {
if (val !== v) {
console.log("set", key);
// 如果新传入的v是个对象
observe(v);
val = v;
}
}
});
}
// 递归遍历对象,对每个key属性进行响应式拦截getter,setter
function observe(obj) {
// 判断是否为对象,不是跳出
if (typeof obj !== "object" || obj === null) return;
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
// 类似于vue中的$set
function set(obj, key, val) {
defineReactive(obj, key, val);
}
const obj = {
foo: "foo",
bar: "bar",
baz: {
a: 1
}
};
// defineReactive(obj, "foo", "foo");
observe(obj); // 递归遍历
// obj.foo;
// obj.foo = "fooooo";
// obj.baz.a;
// obj.baz = { b: 1 };
// console.log(obj.baz.b);
set(obj, "dong", "dong");
obj.dong;
结合视图,02-reactive.html
<div id="app"></div>
<script>
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
return val;
},
set(v) {
if (val !== v) {
val = v; // val更新,下次触发get为新的值v
updater();
}
}
});
}
function updater() {
app.innerText = obj.foo; // 触发get
}
const obj = {};
defineReactive(obj, "foo", "foo");
// 设置定时器
setInterval(() => {
obj.foo = new Date().toLocaleTimeString(); // 触发set
}, 1000);
</script>
递归遍历需要响应化的对象
// 递归遍历对象,对每个key属性进行响应式拦截getter,setter
function observe(obj) {
// 判断是否为对象,不是跳出
if (typeof obj !== "object" || obj === null) return;
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
解决嵌套对象的问题
function defineReactive(obj, key, val) {
// 对val判断是否为对象,递归
observe(val);
Object.defineProperty(obj, key, {
get() {
console.log("get", key);
return val;
},
set(v) {
if (val !== v) {
console.log("set", key);
// 如果新传入的v是个对象
observe(v);
val = v;
}
}
});
}
如果赋值是对象
obj.baz = {a:1}
// 在set操作函数里
set(v) {
if (val !== v) {
console.log("set", key);
// 如果新传入的v是个对象
observe(v);
val = v;
}
}
添加新属性无法检测
// 类似于vue中的$set
function set(obj, key, val) {
defineReactive(obj, key, val);
}
Vue中的数据响应化
目标代码,kvue.html,手动写一个vue实现响应式,包括编译模板,让其正常运行
<div id="app">
<p>{{counter}}</p>
</div>
<script src="./kvue.js"></script>
<script>
const app = new Vue({
el: "#app",
data: {
counter: 1
}
});
setInterval(() => {
app.counter++;
}, 1000);
</script>
原理分析
new Vue()首先执行初始化,对data执行响应化处理,这个过程发生在Observer类中- 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile类中
- 同时定义个更新函数和Watcher类,将来对映数据变化时Watcher会调用更新函数
- 由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个Watcher,key与Dep是一对一关系,Dep与Watcher是一对多关系
- 将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数
涉及类及方法介绍
- KVue:框架构造函数
- Observer:执行数据响应化(分辨数据是对象还是数组)
- Compile:编译模板,初始化视图,收集依赖(更新函数、watcher创建)
- Dep:管理多个Watcher,批量更新
KVue-数据响应式
框架构造函数初始化,对data执行响应化处理
// 递归遍历对象,对每个key属性进行响应式拦截getter,setter
function observe(obj) {
// 判断是否为对象,不是跳出
if (typeof obj !== "object" || obj === null) return;
// 每出现一个对象,创建一个Ob实例
new Observer(obj); // __ob__
}
// Observer:判断传入obj类型,做对应的响应式处理
class Observer {
constructor(obj) {
this.value = obj;
// 判断对象类型
if (Array.isArray(obj)) {
// todo待定
} else {
this.walk(obj);
}
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
}
为$data做代理
class KVue {
constructor(options) {
// 1.保存选项
this.$options = options;
this.$data = options.data;
// 2.响应式处理
observe(this.$data);
// 3.代理data到KVue实例上
proxy(this);
}
}
function proxy(vm) {
Object.keys(vm.$data).forEach(key => {
Object.defineProperty(vm, key, {
get() {
return vm.$data[key];
},
set(newValue) {
vm.$data[key] = newValue;
}
});
});
}
Compile-编译
编译模板中Vue特殊语法,初始化视图、更新视图
class KVue {
constructor(options) {
// 1.保存选项
this.$options = options;
this.$data = options.data;
// 2.响应式处理
observe(this.$data);
// 3.代理data到KVue实例上
proxy(this);
// 4.编译
new Compile(options.el, this);
}
}
class Compile {
constructor(el, vm) {
// el-宿主,vm-KVue实例
this.$vm = vm;
this.$el = document.querySelector(el);
this.compile(this.$el);
}
compile(el) {
// 遍历el dom树
el.childNodes.forEach(node => {
if (this.isElement(node)) {
// element
// 需要处理属性和子节点
// console.log("编译元素", node.nodeName);
this.compileElement(node);
// 递归子节点
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node);
}
} else if (this.isInter(node)) {
// console.log("编译插值表达式", node.textContent);
// 获取表达式的值并赋值给node
this.compileText(node);
}
});
}
isElement(node) {
return node.nodeType === 1;
}
// 插值{{xxx}}
isInter(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
// 指令
isDir(attrName) {
// 是否以k-开头的
return attrName.startsWith("k-");
}
// 编译文本,将{{ooxx}}
compileText(node) {
node.textContent = this.$vm[RegExp.$1];
}
// 处理元素所有动态属性
compileElement(node) {
Array.from(node.attributes).forEach(attr => {
console.log(attr);
const attrName = attr.name; // 属性名
const exp = attr.value; // 表达式
// pd是否是一个指令
if (this.isDir(attrName)) {
// 执行指令处理函数
// k-text,关心text
const dir = attrName.substring(2);
this[dir] && this[dir](node, exp);
}
});
}
// k-text处理函数
text(node, exp) {
node.textContent = this.$vm[exp];
}
// k-html
html(node, exp) {
node.innerHTML = this.$vm[exp];
}
}
依赖收集
视图中会用到data中某key,这称为依赖。同一个key可能会出现多次,每次都需要收集出来用一个Watcher来维护它们,此过程称为依赖收集
多个Watcher需要一个Dep来管理,需要更新时由Dep统一通知
如下图:
实现思路
- defineReactive时为每个key创建一个Dep实例
- 初始化视图时读取某个key,例如name1,创建一个watcher1
- 由于触发name1的getter方法,便将watcher1添加到name1对应的Dep中
- 当name1更新,setter触发时,便可通过对应的Dep通知管理所有Watcher更新
创建Watcher
// 小秘书:做dom更新
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm;
this.key = key;
this.updateFn = updateFn;
// 读取以下key的值,触发其get,从而收集依赖
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
update() {
this.updateFn.call(this.vm, this.vm[this.key]);
}
}
编写更新函数、创建watcher
// 更新函数
update(node, exp, dir) {
const fn = this[dir + "Updater"];
fn && fn(node, this.$vm[exp]);
// update:创建Watcher
new Watcher(this.$vm, exp, function(val) {
fn && fn(node, val);
});
}
// 编译文本,将{{ooxx}}
compileText(node) {
this.update(node, RegExp.$1, "text");
}
// 实操函数
textUpdater(node, val) {
node.textContent = val;
}
// k-text处理函数
text(node, exp) {
this.update(node, exp, "text");
}
// k-html
html(node, exp) {
this.update(node, exp, "html");
}
htmlUpdater(node, val) {
node.innerHTML = val;
}
声明Dep
/ 依赖:和响应式对象的每个key一一对应
class Dep {
constructor() {
this.deps = [];
}
addDep(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach(dep => dep.update());
}
}
创建watcher时触发get
class Watcher{
constructor(vm,key,updateFn){
// 读取以下key的值,触发其get,从而收集依赖
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
}
依赖收集,创建Dep实例
function defineReactive(obj, key, val) {
// 对val判断是否为对象,递归
observe(val);
// Dep在这创建
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
console.log("get", key);
// 依赖收集
Dep.target && dep.addDep(Dep.target);
return val;
},
set(v) {
if (val !== v) {
console.log("set", key);
// 如果新传入的v是个对象
observe(v);
val = v;
// 通知更新
dep.notify();
}
}
});
}