Vue3 响应式原理
1.vue3响应式原理的实现思路:
(1)创建一个依赖管理对象用于添加依赖和触发依赖(dep)
(2)创建一个reactive函数用于代理传入的对象或数组
(3)创建一个全局的Map,用于存储被代理对象和依赖函数的对应关系
(4)设置代理对象属性描述符的get函数,当触发get时,给全局Map的对应对象添加函数依赖
(5)设置代理对象属性描述符的set函数,当触发set时,触发全局Map的对应对象的依赖
(6)创建watchEffect
函数,用于自动侦听响应式数据的读取,如果传入的回调函数读取了全局Map里面的响应式对象的属性,则说明该函数以来了响应式对象,把传入的回调函数赋值给currenteffect,对该函数依赖进行收集
// 当前依赖设置为全局变量,用于解决跨作用域读取的问题
let curentEffect = null
// 依赖管理对象
class Dep{
constructor(value){
this.effects = new Set()
this._val = value
}
addDep(){
if(curentEffect){
this.effects.add(curentEffect)
}
}
notify(){
this.effects.forEach(item => item(this._val))
}
}
let targetMap = new Map()
function handleTargetGet(target, key){
let targetDep = targetMap.get(target)
if(!targetDep){
targetDep = new Dep()
targetMap.set(target, targetDep)
}
targetDep.addDep(curentEffect)
return Reflect.get(target, key)
}
function handleTargetSet(target, key, value){
Reflect.set(target, key, value)
const targetDep = targetMap.get(target)
targetDep._val = value
if(targetDep){
targetDep.notify()
}
}
function reactive(target) {
return new Proxy(target, {
get: handleTargetGet,
set: handleTargetSet
})
}
function effectWatch(effect) {
curentEffect = effect
effect()
curentEffect = null
}
const proxyObj = reactive({})
effectWatch((value) => {
console.log('test:', value);
proxyObj.a
})
proxyObj.a = 1
proxyObj.a = 2
// 打印结果:
// test: undefined
// test: 1
// test: 2
2.参考资料
1.跟尤雨溪一起解读Vue3源码【中英字幕】- Vue Mastery_哔哩哔哩_bilibili
3.gpt问答
创建虚拟dom
1.vue使用虚拟dom的原因是设计机制导致的,vue的设计机制是数据驱动视图,当数据改变的时候要自动改变视图,如果没有虚拟dom做前后对比,就不知道哪里的数据发生了改变,只能全量更新了,因此vue引入了虚拟dom和diff算法,这样就可以判断出哪里的数据发生了变动,针对变动进行真实dom的更新
function createVNode(tag, props, children) {
return { tag, props, children }
}
虚拟dom转真实dom
function mountElement(vnode, container) {
const { tag, props, children } = vnode;
// 比较重要的一部,需要把dom元素的地址保存在虚拟dom上,diff操作的时候,挂载新增、删除和修改真实dom的时候要用
const el = (vnode.el = document.createElement(tag));
if (props) {
// 这里还可以判断是否要绑定属性和事件,通过正则匹配指定字符例如 :和 @ 字符
for (let key in props) {
const prop = props[key];
el.setAttribute(key, prop);
}
}
if (typeof children == "string") {
el.innerText = children;
} else {
if (Array.isArray(children)) {
for (let vnode of children) {
mountElement(vnode, el);
}
}
}
container.appendChild(el);
}
function render(context) {
return _h("ul", {}, [
_h("li", { id: context?.isShow?.value }, context.user.username),
_h("li", {}, context.user.username),
_h("li", {}, "赵六"),
]);
}
对比虚拟dom(diff)
// 暴力解法做了简化,children长度不同,直接操作多出的部分或减少的部分,不进行精细化的对比判断
function diff(n1, n2) {
let el = (n2.el = n1.el);
if (n1.tag !== n2.tag) {
el.replaceWith(document.createElement(n2.tag));
} else {
const { props: oldProps } = n1;
const { props: newProps } = n2;
if (oldProps && newProps) {
Object.keys(newProps).forEach((key) => {
const newValue = newProps[key];
const oldValue = oldProps[key];
if (newValue !== oldValue) {
el.setAttribute(key, newValue);
}
});
}
if (oldProps) {
Object.keys(oldProps).forEach((key) => {
if (!newProps[key]) {
el.removeAttribute(key);
}
});
}
}
const { children: oldCildren } = n1;
const { children: newCildren } = n2;
if (typeof newCildren == "string") {
el.textContent = newCildren;
} else if (Array.isArray(newCildren)) {
if (typeof oldCildren == "string") {
el.innerText = "";
mountElement(n2, el);
} else if (Array.isArray(oldCildren)) {
const length = Math.min(oldCildren.length, newCildren.length);
for (let index = 0; index < length; index++) {
const oldVnode = oldCildren[index];
const newVnode = newCildren[index];
diff(oldVnode, newVnode);
}
if (newCildren.length > length) {
for (let index = length; index < newCildren.length; index++) {
const newVnode = newCildren[index];
mountElement(newVnode, el);
}
}
if (oldCildren.length > length) {
for (let index = length - 1; index < newCildren.length; index++) {
const oldVnode = oldCildren[index];
el.removeChild(oldVnode.el);
}
}
}
}
}
miniVue完整代码
1.index.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>
<script src="./reactivity.js"></script>
<script src="./render.js"></script>
</head>
<body>
<div id="app"></div>
</body>
<script>
const createComponentFn = function (el) {
function setup() {
const user = reactive({
username: "张三",
});
const isShow = reactive({ value: true });
setTimeout(() => {
user.username = "李四";
}, 3000);
return { user, isShow };
}
return {
context: setup(),
mount(el) {
let isMounted = false;
let preVnode;
watchEffect(() => {
if (!isMounted) {
el.innerHTML = "";
const vnode = render(this.context);
mountElement(vnode, el);
preVnode = vnode;
isMounted = true;
} else {
const vnode = render2(this.context);
diff(preVnode, vnode);
preVnode = vnode;
}
});
},
};
};
const el = document.getElementById("app");
const app = createComponentFn();
app.mount(el);
</script>
<style>
#app {
display: flex;
align-items: center;
justify-content: center;
}
</style>
</html>
2.reactivity.js
(() => {
// 当前依赖设置为全局变量,用于解决跨作用域读取的问题
let curentEffect = null;
// 依赖管理对象
class Dep {
constructor(value) {
this.effects = new Set();
this._val = value;
}
addDep() {
if (curentEffect) {
this.effects.add(curentEffect);
}
}
notify() {
this.effects.forEach((item) => item(this._val));
}
}
let targetMap = new Map();
function handleTargetGet(target, key) {
let targetDep = targetMap.get(target);
if (!targetDep) {
targetDep = new Dep();
targetMap.set(target, targetDep);
}
targetDep.addDep(curentEffect);
return Reflect.get(target, key);
}
function handleTargetSet(target, key, value) {
Reflect.set(target, key, value);
const targetDep = targetMap.get(target);
if (!targetDep) throw new Error("依赖未收集");
targetDep._val = value;
if (targetDep) {
targetDep.notify();
}
}
function reactive(target) {
return new Proxy(target, {
get: handleTargetGet,
set: handleTargetSet,
});
}
function watchEffect(effect) {
curentEffect = effect;
effect();
curentEffect = null;
}
window.reactive = reactive;
window.watchEffect = watchEffect;
})();
3.render.js
(() => {
function _h(tag, props, children) {
return {
tag,
props,
children,
};
}
// 暴力解法做了简化,children长度不同,直接操作多出的部分或减少的部分,不进行精细化的对比判断
function diff(n1, n2) {
let el = (n2.el = n1.el);
if (n1.tag !== n2.tag) {
el.replaceWith(document.createElement(n2.tag));
} else {
const { props: oldProps } = n1;
const { props: newProps } = n2;
if (oldProps && newProps) {
Object.keys(newProps).forEach((key) => {
const newValue = newProps[key];
const oldValue = oldProps[key];
if (newValue !== oldValue) {
el.setAttribute(key, newValue);
}
});
}
if (oldProps) {
Object.keys(oldProps).forEach((key) => {
if (!newProps[key]) {
el.removeAttribute(key);
}
});
}
}
const { children: oldCildren } = n1;
const { children: newCildren } = n2;
if (typeof newCildren == "string") {
el.textContent = newCildren;
} else if (Array.isArray(newCildren)) {
if (typeof oldCildren == "string") {
el.innerText = "";
mountElement(n2, el);
} else if (Array.isArray(oldCildren)) {
const length = Math.min(oldCildren.length, newCildren.length);
for (let index = 0; index < length; index++) {
const oldVnode = oldCildren[index];
const newVnode = newCildren[index];
diff(oldVnode, newVnode);
}
if (newCildren.length > length) {
for (let index = length; index < newCildren.length; index++) {
const newVnode = newCildren[index];
mountElement(newVnode, el);
}
}
if (oldCildren.length > length) {
for (let index = length - 1; index < newCildren.length; index++) {
const oldVnode = oldCildren[index];
el.removeChild(oldVnode.el);
}
}
}
}
}
function mountElement(vnode, container) {
const { tag, props, children } = vnode;
const el = (vnode.el = document.createElement(tag));
if (props) {
// 这里还可以判断是否要绑定事件
for (let key in props) {
const prop = props[key];
el.setAttribute(key, prop);
}
}
if (typeof children == "string") {
el.innerText = children;
} else {
if (Array.isArray(children)) {
for (let vnode of children) {
mountElement(vnode, el);
}
}
}
container.appendChild(el);
}
function render(context) {
return _h("ul", {}, [
_h("li", { id: context?.isShow?.value }, context.user.username),
_h("li", {}, context.user.username),
_h("li", {}, "赵六"),
]);
}
function render2(context) {
return _h("ul", {}, [
_h("li", { id: context?.isShow?.value }, context.user.username),
_h("li", {}, "赵六2"),
]);
}
window.render = render;
window.mountElement = mountElement;
window.diff = diff;
window.render2 = render2
})();
Vue2响应式原理
vue2和vue3响应式的差别主要是definedProperty和proxy实现属性拦截的区别,definedProperty是重写对象属性描述符的get函数和set函数,因为是直接在对象上进行的操作,因此无法监听属性的新增,而proxy用于生成一个代理目标对象的代理对象,对象进行的读取和赋值操作都会被代理对象拦截,因此可以监听属性的新增
1.index.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>Vue2</title>
<script src="./reactive.js"></script>
<script src="./vnode.js"></script>
</head>
<body>
<div id="app"></div>
</body>
<script>
const vm = {
data() {
return {
user: {
username: "张三",
sex: "男",
age: '23',
},
role: {
roleId: "admin",
roleName: "管理员",
},
};
},
mount() {
const data = this.data();
window.obj = data;
reactive(data);
let isMounted = false;
let oldVnode = null;
watch(() => {
if (!isMounted) {
const vnode = render(data);
// 生成一份虚拟dom,用于控制台修改进行diff功能验证
window.vnode = render(data);
const el = document.getElementById("app");
createElement(vnode, el);
oldVnode = vnode;
isMounted = true
} else {
diff(oldVnode, window.vnode);
oldVnode = window.vnode;
}
});
},
};
vm.mount();
</script>
</html>
2.reactive.js
function reactive(data) {
if (typeof data != "object") {
throw new Error("请传入对象或数组");
}
if (!Array.isArray(data)) {
Object.keys(data).forEach((key) => {
let value = data[key];
if (typeof value == "object") {
reactive(value);
}
const dep = new Dep();
Object.defineProperty(data, key, {
get() {
dep.add(currentSub);
return value;
},
set(newVal) {
value = newVal;
dep.update(newVal);
},
});
});
}
}
class Dep {
constructor() {
this.subs = new Array();
}
add(fn) {
if (typeof fn == "function") {
this.subs.push(fn);
}
}
update(value) {
this.subs.forEach((sub) => {
sub(value);
});
}
}
// 使用全局变量实现跨作用域数据传递,这样就不需要在watch函数传入data和key了
// 只要fn使用了响应式对象就可以自动把currentSub添加到对应的依赖dep中
let currentSub = null;
function watch(fn) {
currentSub = fn;
fn();
currentSub = null;
}
window.reactive = reactive;
window.watch = watch;
3.vnode.js
function render(data) {
return _h("ul", {}, [
_h("li", {}, data.user.username),
_h("li", {}, data.user.sex),
_h("li", {}, data.user.age),
_h("li", {}, data.role.roleId),
_h("li", {}, data.role.roleName),
]);
}
function _h(tag, props, children) {
return {
tag,
props,
children,
};
}
function createElement(vnode, parentEl) {
const { tag, props, children } = vnode;
const el = document.createElement(tag);
vnode.el = el;
for (let key in props) {
el.setAttribute(key, props[key]);
}
if (typeof children === "string") {
el.innerText = children;
}
if (Array.isArray(children)) {
for (let vnode of children) {
createElement(vnode, el);
}
}
parentEl.appendChild(el);
}
// 对比属性
function propsDiff(oldProps, newProps, el) {
for (let key in oldProps) {
if (!newProps[key]) {
el.removeAttribute(key);
}
}
for (let key in newProps) {
if (!oldProps[key]) {
const oldVal = oldProps[key];
const newVal = newProps[key];
if (oldVal != newVal) {
el.setAttribute(key, newVal);
}
}
}
}
function diffChildren(oldChildren, newCildren, el) {
if (typeof newCildren == "string") {
el.innerText = newCildren;
return;
}
if (typeof newCildren !== "string" && typeof oldChildren == "string") {
for (let i = 0; i < newCildren.length; i++) {
const newNode = newCildren[i];
createElement(newNode, el);
}
return;
}
const length = Math.min(oldChildren.length, newCildren.length);
function updateNode() {
for (let i = 0; i < length; i++) {
const oldNode = oldChildren[i];
const newNode = newCildren[i];
diff(oldNode, newNode);
}
}
function removeNode() {
for (let i = length; i < oldChildren.length; i++) {
const oldNode = oldChildren[i];
el.removeChild(oldNode.el);
}
}
function appendNode() {
for (let i = length; i < newCildren.length; i++) {
const newNode = newCildren[i];
createElement(newNode, el);
}
}
if (Array.isArray(newCildren)) {
// 不需要新增删除节点
if (oldChildren.length == newCildren.length) {
updateNode();
}
// 需要删除节点
if (oldChildren.length > newCildren.length) {
updateNode();
removeNode();
}
// 需要新增节点
if (newCildren.length > oldChildren.length) {
updateNode();
appendNode();
}
}
}
// 实现一个最简化的diff算法
function diff(n1, n2) {
let el = (n2.el = n1.el);
if (n1.tag != n2.tag) {
const el2 = document.createElement(n2.tag);
el.replaceWith(el2);
el = el2;
}
propsDiff(n1.props, n2.props, el);
diffChildren(n1.children, n2.children, el);
}
window.render = render;
window.createElement = createElement;
window.diff = diff;