一、三大核心系统
- Compiler模块:编译模板系统
主要用来将template模板转换为render渲染函数 - Runtime模块:也可以称之为Renderer模块,真正渲染的模块
主要用来将虚拟DOM转变为真实DOM,并且渲染到浏览器页面上 主要的实现原理是利用了snabbdom的思想,具体可以看我的另一篇转载文章,点下方 虚拟DOM原理 - Reactivity模块:响应式系统
主要用来当dom发生变化的时候,通过diff算法进行更新或者替换,然后再通过渲染系统渲染到浏览器页面上
对应源码位置 那么三个系统之间如何协同工作呢? 这里借用一张coderwey老师的制图
二、实现Mini-Vue
1、描述
这里我们实现一个简洁版的Mini-Vue框架,该Vue包括三个模块
2、渲染系统实现
- 功能一:h函数,用于返回一个VNode对象
- 功能二:mount函数,用于将VNode挂载到DOM上
- 功能三:patch函数,用于对两个VNode进行对比,决定如何处理新的VNode
h函数 – 生成VNode h函数的实现:
- 直接返回一个VNode对象即可
Mount函数 – 挂载VNode - mount函数的实现:
第一步 :根据tag,创建HTML元素,并且存储到vnode的el中;第二步 :处理props属性- 如果以on开头,那么监听事件;
- 普通属性直接通过 setAttribute 添加即可;
第三步 :处理子节点- 如果是字符串节点,那么直接设置textContent;
- 如果是数组节点,那么遍历调用 mount 函数;
Patch函数 – 对比两个VNode - patch函数的实现,分为两种情况
1、n1和n2是不同类型的节点: - 找到n1的el父节点,删除原来的n1节点的el;
- 挂载n2节点到n1的el父节点上;
2、n1和n2节点是相同的节点: 处理props的情况 - 先将新节点的props全部挂载到el上;
- 判断旧节点的props是否不需要在新节点上,如果不需要,那么删除对应的属性;
处理children的情况 - 如果新节点是一个字符串类型,那么直接调用 el.textContent = newChildren;
- 如果新节点不同一个字符串类型:
旧节点是一个字符串类型 - 将el的textContent设置为空字符串;
- 旧节点是一个字符串类型,那么直接遍历新节点,挂载到el上;
旧节点也是一个数组类型 - 取出数组的最小长度;
- 遍历所有的节点,新节点和旧节点进行path操作;
- 如果新节点的length更长,那么剩余的新节点进行挂载操作;
- 如果旧节点的length更长,那么剩余的旧节点进行卸载操作;
3、响应式系统实现
1、依赖收集系统的实现
思想:通过集合收集依赖,首先通过某个函数将依赖添加到集合中,然后当数据发生改变的时候,调用某个函数,重新遍历集合中的每一个函数,实现更新 从上面的代码,我们可以看出,存在很多弊端,需要我们手动调用addEffect函数才能将依赖添加进去,然后数据发生改变时,需要手动调用notify函数实现更新。
那么如何让它自动添加呢?
2、响应式系统Vue2的实现
class Dep {
constructor() {
this.subscribers = new Set();
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect);
}
}
notify() {
this.subscribers.forEach(effect => {
effect();
})
}
}
let activeEffect = null;
function watchEffect(effect) {
activeEffect = effect;
effect();
activeEffect = null;
}
const targetMap = new WeakMap();
function getDep(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Dep();
depsMap.set(key, dep);
}
return dep;
}
function reactive(raw) {
Object.keys(raw).forEach(key => {
const dep = getDep(raw, key);
let value = raw[key];
Object.defineProperty(raw, key, {
get() {
dep.depend();
return value;
},
set(newValue) {
if (value !== newValue) {
value = newValue;
dep.notify();
}
}
})
})
return raw;
}
const info = reactive({counter: 100, name: "why"});
const foo = reactive({height: 1.88});
watchEffect(function () {
console.log("effect1:", info.counter * 2, info.name);
})
watchEffect(function () {
console.log("effect2:", info.counter * info.counter);
})
watchEffect(function () {
console.log("effect3:", info.counter + 10, info.name);
})
watchEffect(function () {
console.log("effect4:", foo.height);
})
foo.height = 2;
这里通过两个使用示例进行说明如何响应式 正如代码里所看到的,当执行
watchEffect(function () {
console.log("effect1:", info.counter * 2, info.name);
})
这个函数的时候,会将
function () {
console.log("effect1:", info.counter * 2, info.name);
}
赋值给activeEffect,并且执行,看下面的图片 当执行的时候,会用到info.counter和info.name,而info这个对象通过reactive函数包裹,变成了响应式的对象,如下图 而通过reactive包裹,会进行数据劫持的操作,目的是为了只响应用到的dep依赖,而不是不管三七二十一都执行。
原理 就是通过遍历对象的每一个key,然后通过getDep函数(这个函数在下面讲解)获得对应的Map,然后通过Object.defineProperty进行相应的响应,如果是用,那就调用get方法,然后通过depend函数将依赖添加到收集系统,如果数据有更新,比如info.name = ‘123456’,那么会调用set方法,进而调用notify进行数据更新,如下图所示 接下来说明一下,在reactive中是如何根据getDep函数来获取相应的map的。 先说明一下这里的数据结构,其实就是在一个map里映射了一组一组的数据,而这一组组的数据又是映射成了一个map,这样就可以根据getDep函数来精确的找到对应的key的map。
所以当用到info.counter和info.name的时候,其实用的就是defineProperty的get方法,如下图所示
3、响应式系统vue3的实现
class Dep {
constructor() {
this.subscribers = new Set();
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect);
}
}
notify() {
this.subscribers.forEach(effect => {
effect();
})
}
}
let activeEffect = null;
function watchEffect(effect) {
activeEffect = effect;
effect();
activeEffect = null;
}
const targetMap = new WeakMap();
function getDep(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Dep();
depsMap.set(key, dep);
}
return dep;
}
function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
const dep = getDep(target, key);
dep.depend();
return target[key];
},
set(target, key, newValue) {
const dep = getDep(target, key);
target[key] = newValue;
dep.notify();
}
})
}
4、为什么Vue3选择Proxy呢?
1、Object.definedProperty 是劫持对象的属性,如果新增元素:
- 那么Vue2需要再次 调用definedProperty
- 而 Proxy 劫持的是整个对象,不需要做特殊处理
2、修改对象的不同:
- 使用 defineProperty 时,我们修改原来的 obj 对象就可以触发拦截
- 而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截
3、Proxy 能观察的类型比 defineProperty 更丰富
- has:in操作符的捕获器
- deleteProperty:delete 操作符的捕捉器
4、Proxy 作为新标准将受到浏览器厂商重点持续的性能优化 缺点 :Proxy 不兼容IE,也没有 polyfill, defineProperty 能支持到IE9
4、mini-vue的具体实现
<!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"></div>
<script src="./renderer.js"></script>
<script src="./reactive.js"></script>
<script src="./index.js"></script>
<script>
const App = {
data: reactive({
counter: 0
}),
render() {
return h("div", null, [
h("h2", null, `当前计数: ${this.data.counter}`),
h("button", {
onClick: () => {
this.data.counter++
console.log(this.data.counter);
}
}, "+1")
])
}
}
const app = createApp(App);
app.mount("#app");
</script>
</body>
</html>
function createApp(rootComponent) {
return {
mount(selector) {
const container = document.querySelector(selector);
let isMounted = false;
let oldVNode = null;
watchEffect(function() {
if (!isMounted) {
oldVNode = rootComponent.render();
mount(oldVNode, container);
isMounted = true;
} else {
const newVNode = rootComponent.render();
patch(oldVNode, newVNode);
oldVNode = newVNode;
}
})
}
}
}
|