简单的实现vue3的响应式
简单的模拟vue3
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
<script type="module" src="./index.js"></script>
</html>
index.js
import {
ref,
reactive,
effect,
} from "./node_modules/@vue/reactivity/dist/reactivity.esm-browser.js";
// vue3 最小模型
const App = {
render(context) {
effect(() => {
document.querySelector("#app").innerHTML = "";
const div = document.createElement("div");
div.innerHTML = context.count.value;
document.querySelector("#app").append(div);
});
},
setup() {
// 在这里可以方便的在控制台通过修改count来观察变化
window.count = ref(0);
return { count };
},
};
App.render(App.setup());
结果
effect的原理
- 注意到上面的例子中,我们直接修改count.value就能直接引起dom的变化。这背后究竟是如何实现的?
- 我们都知道ref, reactive这两个都是创建响应式对象的api,而effect在这里的作用是收集依赖和触发依赖。那么effect是如何做到的?我们再看一个例子。
index.js
import {
ref,
reactive,
effect,
} from "./node_modules/@vue/reactivity/dist/reactivity.esm-browser.js";
const a = ref(5);
let b;
// 1.当程序第一次到这里时,会自动执行一次fn () => {...}
effect(() => {
b = a.value * 2;
console.log("a", a.value, "b", b);
});
// a.value的setter触发时,触发第二次fn
a.value = 10;
输出
a 5 b 10
a 10 b 20
- 在effect中,我们传入了一个函数
fn
,检测到这里有响应式数据a
,那么fn
被收集成为a
的依赖。fn
中的逻辑为a
的setter触发时具体要做的事情。
自己实现响应式
如果我们自己实现响应式,那么要做的事情有什么?
- 1、实现ref, reactivity
- 2、实现effect
- 3、收集依赖
- 4、触发依赖
为了实现对依赖(depend)的管理,我们可以定义一个Dep类来进行管理。
ref与effect
// currentEffect是全局变量,方便Dep类访问并收集依赖
let currentEffect = null;
class Dep {
#value;
constructor(value) {
this.#value = value;
// 我们要保证依赖不会重复
this.effects = new Set();
}
// getter触发收集依赖
get value() {
this.depend(currentEffect);
return this.#value;
}
// setter触发所有依赖
set value(newVal) {
this.#value = newVal;
this.notice();
}
// 收集依赖,即把effect中的fn收集起来
depend() {
// 判断currentEffect是否有值
currentEffect && this.effects.add(currentEffect);
}
// 触发依赖
notice() {
this.effects.forEach((effect) => {
effect();
});
}
}
function watchEffect(effect) {
currentEffect = effect;
effect();
currentEffect = null;
}
function ref(val) {
return new Dep(val);
}
const dep = ref(10);
watchEffect(() => {
// 触发getter, getter触发depend去收集依赖
console.log(dep.value);
});
// 触发setter
dep.value = 20;
Result
10
20
至此,我们已经实现了ref和effect的功能。
reactive与effect
- 注意到ref与effect中,一个value对应一个dep
- 如果我们的响应式数据是一个对象呢,里面具有多个key,那么是不是就要需要多个dep来管理依赖?
- 如果是对应多个dep,那么我们要选择怎样的数据结构去存储?
- 如果我们要去取这个值,那么如何去取?
接下来展示一下做法,利用Map和Proxy。
// currentEffect是全局变量,方便Dep类访问并收集依赖
let currentEffect = null;
class Dep {
constructor() {
// 我们要保证依赖不会重复
this.effects = new Set();
}
// 收集依赖,即把effect中的fn收集起来
depend() {
// 判断currentEffect是否有值
currentEffect && this.effects.add(currentEffect);
}
// 触发依赖
notice() {
this.effects.forEach((effect) => {
effect();
});
}
}
function watchEffect(effect) {
currentEffect = effect;
effect();
currentEffect = null;
}
在这里对Dep代码进行了部分的修改,因为在这里我们只需要Dep的收集依赖和触发依赖的功能。
// targetsMap储存所有的对象(即用Map包装过的dep)
// 数据结构
// targetsMap: Map { { name: 'xiaoming', age: 18 } => Map(0) {} }
// depsMap: Map { 'name' => Dep { effects: Set(0) {} } }
const targetsMap = new Map();
// 辅助函数,用来获取对应的dep
function getDep(target, key) {
let depsMap = targetsMap.get(target);
if (!depsMap) {
// 相当于初始化的操作
depsMap = new Map();
targetsMap.set(target, depsMap);
}
// targetsMap: Map { { name: 'xiaoming', age: 18 } => Map(0) {} }
let dep = depsMap.get(key);
if (!dep) {
// 收集的依赖必须有值
dep = new Dep();
depsMap.set(key, dep);
}
// depsMap: Map { 'name' => Dep { effects: Set(0) {} } }
return dep;
}
function reactive(raw) {
// 如何知道取raw中的哪个key?
// 通过Proxy
// Proxy是一个包含对象或函数并允许你对其拦截的对象
return new Proxy(raw, {
get(target, key) {
const dep = getDep(target, key);
// 收集依赖
dep.depend();
return Reflect.get(target, key);
},
set(target, key, value) {
const dep = getDep(target, key);
// Reflect.set有返回值
const result = Reflect.set(target, key, value);
// notice()必须要在赋新值之后操作
// 因为在赋新值前操作,你使用的还是旧值
dep.notice();
return result;
},
});
}
在reactive中,我们通过Proxy
和Reflect
进行操作,自然就不需要Dep中的getter和setter,Dep在这里的作用就是收集依赖和触发依赖。
const user = {
name: "xiaoming",
age: 18,
};
const userState = reactive(user);
watchEffect(() => {
console.log(userState.name);
});
userState.name = "xiaohong";
Result:
xiaoming
xiaohong
使用自定义函数实现响应式
首先我们把上述这些函数封装到文件夹/reactivity/index.js
中
// currentEffect是全局变量,方便Dep类访问并收集依赖
let currentEffect = null;
class Dep {
#value;
constructor(value) {
this.#value = value;
// 我们要保证依赖不会重复
this.effects = new Set();
}
// getter触发收集依赖
get value() {
this.depend(currentEffect);
return this.#value;
}
// setter触发所有依赖
set value(newVal) {
this.#value = newVal;
this.notice();
}
// 收集依赖,即把effect中的fn收集起来
depend() {
// 判断currentEffect是否有值
currentEffect && this.effects.add(currentEffect);
}
// 触发依赖
notice() {
this.effects.forEach((effect) => {
effect();
});
}
}
export function watchEffect(effect) {
currentEffect = effect;
effect();
currentEffect = null;
}
// targetsMap储存所有的对象
// 数据结构
// targetsMap: Map{key: depsMap<Map> }
// depsMap: Map {key: dep<Dep>}
const targetsMap = new Map();
function getDep(target, key) {
let depsMap = targetsMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetsMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Dep();
depsMap.set(key, dep);
}
return dep;
}
export function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
const dep = getDep(target, key);
dep.depend();
return Reflect.get(target, key);
},
set(target, key, value) {
const dep = getDep(target, key);
const result = Reflect.set(target, key, value);
dep.notice();
return result;
},
});
}
export function ref(val) {
return new Dep(val);
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
<script type="module" src="./index.js"></script>
</html>
index.js
import { ref, reactive, watchEffect } from "./reactivity/index.js";
// vue3 最小模型
const App = {
render(context) {
watchEffect(() => {
document.querySelector("#app").innerHTML = "";
const div = document.createElement("div");
div.innerHTML =
context.count.value + " " + context.user.name + " " + context.user.age;
document.querySelector("#app").append(div);
});
},
setup() {
window.count = ref(0);
window.user = reactive({ name: "hello", age: 18 });
return { count, user };
},
};
App.render(App.setup());
Result
- 到这里响应式基本完成,不过为了美观,我们需要抽离部分逻辑,实现解耦
index.js
import { createApp } from "./core/index.js";
import App from "./App.js";
// createApp -> 创建根组件
// mount -> 挂载组件
// 与vue3的用法保持一致
createApp(App).mount(document.querySelector("#app"));
App.js
import { ref, reactive } from "./core/reactivity/index.js";
export default {
// 提供渲染模板
render(context) {
const div = document.createElement("div");
div.innerHTML =
context.count.value + " " + context.user.name + " " + context.user.age;
return div;
},
// 提供响应式数据
setup() {
window.count = ref(0);
window.user = reactive({ name: "hello", age: 18 });
return { count, user };
},
};
/core/index.js
import { watchEffect } from "./reactivity/index.js";
// 一开始我们的用法是App.render(App.setup())
// 简单点说就是:
// setup()获取响应式数据 -> 丢给处理函数 -> watchEffect中渲染/更新视图
// 我们封装之后就变成了
// createApp(App) -> mount -> setup获取App的响应式数据 ->
// watchEffect -> 执行render(渲染/更新视图)
export function createApp(rootComponent) {
return {
// rootContainer 根容器
mount(rootContainer) {
// setupResult获取响应式数据
const setupResult = rootComponent.setup();
watchEffect(() => {
// 更新视图
const element = rootComponent.render(setupResult);
rootContainer.innerHTML = "";
rootContainer.append(element);
});
},
};
}
vdom
- 我们都vue和react都是基于virtual dom进行的。所以我们需要实现一个创建vdom的函数
h
- 以及将vdom渲染成真实dom的函数
mountElement
/core/h.js
// 作用是提供我们创建vdom所需要信息
export function h(type, props, children) {
return {
type,
props,
children,
};
}
/core/renderer.js
-- 将虚拟节点vdom转成真实的dom
// 虚拟节点转换为真实节点
// vnode: {type, props, children}
// children支持两种写法string与arrray
// h("div", null, [h("div", null, "hello")])
// h("div", null, "hi")
export function mountElement(vnode, container) {
const { type, props, children } = vnode;
const el = createElement(type);
// 有属性就给他挂上
if (props) {
for (const key in props) {
const val = props[key];
patchProps(el, key, null, val);
}
}
// 如果子节点只是文本的话,就直接添加到尾部
if (typeof children == "string" || typeof children == "number") {
const text = document.createTextNode(children);
el.append(text);
} else if (Array.isArray(children)) {
// 递归, 将其子节点挂在父节点上
children.forEach((node) => {
mountElement(node, el);
});
}
container.append(el);
}
// 创建dom元素的类型
function createElement(type) {
return document.createElement(type);
}
// 给元素添加属性
function patchProps(el, key, preValue, nextValue) {
el.setAttribute(key, nextValue);
}
/core/index.js
import { watchEffect } from "./reactivity/index.js";
import { mountElement } from "./renderer.js";
export function createApp(rootComponent) {
return {
// rootContainer 根容器
mount(rootContainer) {
// setupResult获取响应式数据
const setupResult = rootComponent.setup();
watchEffect(() => {
// 获取render里关于虚拟节点的详细信息
const subTree = rootComponent.render(setupResult);
rootContainer.innerHTML = "";
// 把subTree里虚拟节点的信息转换成真实的dom
mountElement(subTree, rootContainer);
});
},
};
}
app.js
import { ref, reactive } from "./core/reactivity/index.js";
import { h } from "./core/h.js";
export default {
render(context) {
return h("div", { id: "test" }, [
h("div", null, context.count.value),
h("div", null, context.user.name),
]);
},
setup() {
window.count = ref(0);
window.user = reactive({ name: "hello", age: 18 });
return { count, user };
},
};
Result:
diff
- 在没实现vdom之前,我们都是直接操作真实节点直接让innerHTML清空,这种做法太过暴力。而且无法判断节点是否更改或删除。
- 我们实现vdom之后,就可以在
renderer.js
里的diff
函数中进行判断。 diff
就是可以找出哪些节点改变,哪些没有改变。- 在这次中实现的diff算法很简陋,大概就从type, props, children这几个方面去考虑。
/core/renderer.js
// 虚拟节点转换为真实节点
// vnode: {type, props, children}
// children支持两种写法string与arrray
// h("div", null, [h("div", null, "hello")])
// h("div", null, "hi")
export function mountElement(vnode, container) {
const { type, props, children } = vnode;
const el = createElement(type);
vnode.el = el;
if (props) {
for (const key in props) {
const val = props[key];
patchProps(el, key, null, val);
}
}
if (typeof children == "string" || typeof children == "number") {
const text = document.createTextNode(children);
el.append(text);
} else if (Array.isArray(children)) {
// 递归, 将其子节点挂在父节点上
children.forEach((node) => {
mountElement(node, el);
});
}
container.append(el);
}
// 创建dom元素的类型
function createElement(type) {
return document.createElement(type);
}
// 给元素添加属性
function patchProps(el, key, preValue, nextValue) {
if (nextValue) {
el.setAttribute(key, nextValue);
} else {
// 删除属性
el.removeAttribute(key);
}
}
export function diff(prev, curr) {
// 对比type, 不一样的话可以全部换掉
if (curr.type != prev.type) {
prev.el.replaceWith(createElement(curr.type));
} else {
// props改变
// 主要是三种情况
// 1. 值改变 prev {id: "old"} curr: {id:"new"}
// 2. curr有新属性 prev {id: "old"} curr: {id:"old", test:"123"}
// 3. curr少了属性 prev: {id:"old", test:"123"} curr: {test: "123"}
const oldProps = prev.props || {};
const newProps = curr.props || {};
const el = (curr.el = prev.el);
// 处理新的props
Object.keys(newProps).forEach((key) => {
// 情况一: 值更新
// 情况二: 添加新值, 因为旧值中没有肯定是undefined
if (newProps[key] !== oldProps[key]) {
patchProps(el, key, oldProps[key], newProps[key]);
}
});
// 处理旧的props
// 情况三: 旧的有, 就需要删除
Object.keys(oldProps).forEach((key) => {
if (!newProps[key]) {
patchProps(el, key, oldProps[key], null);
}
});
// 处理children
// children -> string | array
// newChildren -> string | array
// 两两组合, 就会出现四种情况
const newChildren = curr.children || [];
const oldChildren = prev.children || [];
if (typeof newChildren == "string" || typeof newChildren == "number") {
if (typeof oldChildren == "string" || typeof oldChildren == "number") {
// 两个都是string, 对比一下是否不同再替换
if (newChildren !== oldChildren) {
el.textContent = newChildren;
}
} else if (Array.isArray(oldChildren)) {
// 新children是string, 旧的string是节点数组, 直接替换即可
el.textContent = newChildren;
}
} else if (Array.isArray(newChildren)) {
if (typeof oldChildren == "string" || typeof oldChildren == "number") {
// 有新的节点, 这时候要节点插入到旧节点的位置
el.innerHTML = ``;
newChildren.forEach((node) => {
mountElement(node, el);
});
} else if (Array.isArray(oldChildren)) {
// 考虑三种情况
// old的children与new的一样多
// old的比new的多
// old的比new的少
// 暴力算法,先算出公共长度,公共长度内的直接对比替换
// 多出来的直接删除
// 少的就添加
const commonLength = Math.min(newChildren.length, oldChildren.length);
for (let i = 0; i < commonLength; i++) {
const oldVnode = oldChildren[i];
const newVnode = newChildren[i];
diff(oldVnode, newVnode);
}
if (oldChildren.length > newChildren.length) {
for (let i = commonLength; i < oldChildren.length; i++) {
const vnode = oldChildren[i];
el.removeChild(vnode.el);
}
}
if (newChildren.length > oldChildren.length) {
for (let i = commonLength; i < newChildren.length; i++) {
const vnode = newChildren[i];
mountElement(vnode, el);
}
}
}
}
}
}
/core/index.js
import { watchEffect } from "./reactivity/index.js";
import { mountElement, diff } from "./renderer.js";
// 一开始我们的用法是App.render(App.setup())
// 简单点说就是:
// setup()获取响应式数据 -> 丢给render -> watchEffect-> 更新视图
// 我们封装之后就变成了
// App -> createApp(App) -> mount -> setup获取响应式数据 ->
// watchEffect -> 执行render(更新视图)
export function createApp(rootComponent) {
return {
// rootContainer 根容器
mount(rootContainer) {
// setupResult获取响应式数据
const setupResult = rootComponent.setup();
// 是否初始化
let isMounted = false;
let prevSubTree = null;
watchEffect(() => {
if (!isMounted) {
isMounted = true;
const subTree = rootComponent.render(setupResult);
mountElement(subTree, rootContainer);
prevSubTree = subTree;
} else {
// 初始化过了, 在这里实现diff
const subTree = rootComponent.render(setupResult);
diff(prevSubTree, subTree);
prevSubTree = subTree;
}
});
},
};
}
app.js
import { ref, reactive } from "./core/reactivity/index.js";
import { h } from "./core/h.js";
export default {
render(context) {
// const div = document.createElement("div");
// div.innerHTML =
// context.count.value + " " + context.user.name + " " + context.user.age;
return h("div", { id: "test" + context.count.value }, [
h("div", null, context.count.value),
h("div", null, [h("div", null, "我是干扰的")]),
h("div", null, context.user.name),
]);
},
setup() {
window.count = ref(0);
window.user = reactive({ name: "hello", age: 18 });
return { count, user };
},
};
result:
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!