总的来说实现 mini-vue 需要实现以下几个部分。
首先需要搞清楚 VNode 是什么。VNode 的 全称叫 Virtual Node,顾名思义,指的是虚拟 Node(DOM)。在 JavaScript 中,VNode 实际就是一个 JS 对象。这个对象通过 mount 函数转换成真实 DOM,挂载在 DOM 树中。
分析 VNode 需要的属性
于是我们可以写出 h 函数:
const h = (tag, props, children) => {
return {
tag,
props,
children,
el: document.createElement(tag)
}
}
可以看到这个 h 函数实际就是对传入的参数进行了封装。各个参数的含义如下:
mount 函数主要解决一个问题:把 VNode 挂载到真实 DOM 上。可以得出这个函数至少需要两个参数:
又因为 VNode 的 children 也是 VNode 类型,所以可以得出 mount 函数有可能会递归:创建父 VNode 后递归创建这个父 VNode 的子 VNode。
// vnode 一个 js 对象
// container vnode 创建出来后挂载到的真实 DOM
const mount = (vnode, container) => {
const el = vnode.el;
if (vnode.props) {
for (key in vnode.props) {
const val = vnode.props[key];
if (key.startsWith('on')) {
el.addEventListener(key.slice(2).toLowerCase(), val)
} else {
el.setAttribute(key, val);
}
}
}
if (typeof vnode.children === 'string') {
el.textContent = vnode.children;
} else if (vnode.children) {
vnode.children.forEach(item => {
if (typeof item === 'string') {
el.textContent = item;
} else {
mount(item, el);
}
});
}
container.appendChild(el);
}
先记下思路:
function patch(oldn, newn) {
// 新旧节点 tag 不同:直接把旧节点替换成新节点
if (oldn.tag !== newn.tag) {
const parentEle = oldn.el.parentElement;
parentEle.removeChild(oldn.el);
mount(newn, parentEle);
return;
}
const el = newn.el = oldn.el;
// 新旧节点 tag 相同,比较 props 和 children
// 先处理 props
const newProps = newn.props || {}
const oldProps = oldn.props || {}
// 两种情况:
// 1. 新属性旧元素没有:直接添加
// 2. 新属性旧元素有:替换
for (key in newProps) {
// 更新 el 的 attrs;具体判断可以看 mount 函数,这里简写
const val = newProps[key];
el.setAttribute(key, val);
}
for (key in oldProps) {
// 删除旧元素中不在新属性的属性
if (!(key in newProps)) {
el.removeAttribute(key);
}
}
// 处理 children
const newChildren = newn.children || [];
const oldChildren = oldn.children || [];
if (!newChildren) {
// 最简单的情况:新元素没有 children,直接把旧元素的 innerHTML 清空
el.innerHTML = "";
} else if (typeof newChildren === 'string') {
// 另一种简单情况:新元素的 children 是个字符串
el.textContent = newChildren;
} else {
const newChildrenLength = newChildren.length;
const oldChildrenLength = oldChildren.length;
const commLength = Math.min(newChildrenLength, oldChildrenLength);
// 如果老元素的子元素是个字符串,但新元素是数组
if (typeof oldChildren === 'string') {
el.innerHTML = '';
for (let i = 0; i < newChildrenLength; i++) {
mount(newChildren[i], el);
}
return ;
}
// 新元素是个数组: 递归的逐个比较新旧 children
// 这里我们认为新旧元素的数组是有序的,比如
// 旧: [1, 2, 3]
// 新:[2, 4, 5, 7]
// 这样比较
// 比较都有的前 n 个
for (let i = 0; i < commLength; i++) {
patch(oldChildren[i], newChildren[i]);
}
// 这里有两种情况:
// 1. 新元素数组比老元素数组长 直接把新的加到旧元素里
for (let i = commLength; i < newChildrenLength; i++) {
mount(newChildren[i], el);
}
// 2. 旧元素数组比新元素数组长 删除这些多余的元素
for (let i = commLength; i < oldChildrenLength; i++) {
el.removeChild(oldChildren[i].el);
}
}
}
先简单介绍下什么是响应式:当一个对象的值发生改变的时候,所有依赖该值的函数应该发送更新(被执行)。
实现响应式的两个重点是:
我们先定一个类,该类主要实现两个方法:收集依赖和通知更新依赖。
// 全局变量 用于保存当前的依赖函数
let activeEffect = null;
class Dep {
constructor() {
// 用 Set 收集依赖,以免依赖重复
this.deps = new Set();
}
depend() {
// 判断 activeEffect 是否存在,存在才添加
if (activeEffect) {
this.deps.add(activeEffect);
}
}
notify() {
// 遍历依赖 Set,执行函数
this.deps.forEach(effect => {
effect();
})
}
}
这里的依赖是针对某个对象的某个属性而言的。比如现在有个 obj,其属性有 age 和 name 两个,那么当 obj.age 变化时,我们应该更新所有使用了 obj.age 的函数,而没使用 obj.age 的函数不会被更新。
考虑下,我们会有很多不同的对象,每个对象又有很多不同的属性。响应式依赖类针对的是某个对象的某个属性,不难想出,我们需要对每个对象的每个属性创建 Dep 实例。于是我们需要一个数据结构保存对象和属性到 Dep 实例的映射。这里使用两个 Map:
const person = {
name: 'www',
age: 19
}
// 保存对象
const targetMap = new WeakMap();
// 保存属性到 Dep 实例的映射
const personDepMap = new Map();
Object.keys(person).forEach(key => {
const value = person[key];
personDepMap.set(key, new Dep());
})
targetMap.set(person, personDepMap)
上面是最基本的代码,封装成函数的,不难想出这个函数需要两个参数:
const targetMap = new WeakMap();
function getDep(target, key) {
let t = targetMap.get(target);
// target 不在 target map 中,添加
if (!t) {
t = new Map();
targetMap.set(target, t);
}
let dep = t.get(key);
// dep 不在 dep map 中,添加
if (!dep) {
dep = new Dep();
t.set(key, dep);
}
return dep;
}
补充:Map 和 WeakMap 的区别
Map 的 key 是一个字符串
WeakMap 的 key 是个对象。并且这个对象只是引用。这意味当我们删除这个对象的时候,WeakMap 的对应 key 也会消失,所以无需额外清除 WeakMap 的 key。
通过 Object.defineProperty
函数劫持对象的属性的 get 和 set 方法,在 get 方法中,添加依赖;在 set 方法中,更新依赖。
function reactive(target) {
Object.keys(target).forEach(key => {
let value = target[key];
const dep = getDep(target);
Object.defineProperty(target, key, {
get() {
dep.depend();
return value;
},
set(newValue) {
value = newValue;
dep.notify()
}
})
})
return target;
}
function watchEffect(effect) {
activeEffect = effect;
// 执行 effect 函数时会触发这个函数内的对象的 getter 或者 setter 方法
// 于是会执行 dep.depend 收集依赖(通过 activeEffect 这个全局变量
effect();
activeEffect = null;
}
reactive_vue2.js
class Dep {
constructor() {
this.deps = new Set();
}
depend() {
if (activeEffect) {
this.deps.add(activeEffect);
}
}
notify() {
this.deps.forEach(effect => {
effect()
})
}
}
let activeEffect = null;
// key 是个对象
const depMap = new WeakMap();
function getDep(target, key) {
let targetMap = depMap.get(target);
if (!targetMap) {
targetMap = new Map();
depMap.set(target, targetMap);
}
let dep = targetMap.get(key);
if (!dep) {
dep = new Dep();
targetMap.set(key, dep);
}
return dep;
}
function reactive(target) {
Object.keys(target).forEach(key => {
let val = target[key];
const dep = getDep(target, key);
Object.defineProperty(target, key, {
get() {
dep.depend();
return val;
},
set(newValue) {
if (val !== newValue) {
val = newValue;
dep.notify();
}
}
})
});
return target;
}
function watchEffect(effect) {
activeEffect = effect;
effect();
activeEffect = null;
}
const info = reactive({
count: 10,
name: 'hhh'
})
watchEffect(() => {
console.log(info.count * info.count)
})
watchEffect(() => {
console.log(info.name)
})
Vue3 响应式代码的区别在于创建响应式对象函数reactive
,其它的都可以直接拿来用:
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();
}
})
}
修改对象的区别
Proxy 返回的是一个 Proxy 对象,直接操作这个对象并不会把结果反应到原对象上
defineProperty 是对原对象进行修改,返回的还是原对象
Proxy 的优点
Proxy 的缺点