响应式(reactivity)的实现 -> vue3的ref,reactive和watchEffect
- 从简单版开始(监控一个简单变量的变化->ref) 预期目标:通过watchEffect注册变量值改变后的回调函数,当变量值改变时触发。举个简单例子,下面的代码输出10 20
let a = ref(10);
watchEffect(() => {
console.log(a.value);
})
a.value = 20;//输出10 20
具体实现:
- 创建类Dep,一个变量对应一个Dep。监控的值保存在其value属性上,回调函数保存在其effects属性上,然后使用getter,setter进行拦截;
- 调用watchEffet时将依赖保存在一个全局变量上(以便在类中调用)
- getter时收集依赖,setter时触发依赖
let currentEffect = null;
class Dep {
constructor(val) {
this._val = val;//_val用value也可
this.effects = new Set();//存放所有的依赖
}
//监视的值放到value变量上,然后监视value的值的变化
get value() {
this.depend();
return this._val;
}
set value(newVal) {
this._val = newVal;
this.notify();
}
depend() {
//收集依赖
if (currentEffect) {
this.effects.add(currentEffect);
}
}
notify() {
//触发依赖
this.effects.forEach((effect) => {
effect();
})
}
}
function watchEffect(effect) {
currentEffect = effect;
effect();
currentEffect = null;
}
let dep = new Dep(10);
watchEffect(() => {
console.log(dep.value);
})
dep.value = 20;//输出10 20
后面的事就简单了:
function ref(value) {
let dep = new Dep(value);
return dep;
}
let a = ref(10);
watchEffect(() => {
console.log(a.value);
})
a.value = 20;//输出10 20
- 进阶版,如果需要监视一个对象内的属性值的改变呢?(->reactive) 预期目标:输出xiaohong xiaohei
const user = {
name: "xiaohong",
age: 18,
};
const userState = reactive(user);
watchEffect(() => {
console.log(userState.name);
});
userState.name = "xiaohei";
实际上,简单想法就是:对象中的每一个属性对应一个dep(先不考虑深层监控),同时为每一个属性设置getter和setter进行拦截,vue2中Object.defineProperty实现,vue3中采用Proxy。
let targetsMap = new Map;//targetsMap = {target1:{key1:dep1,key2:dep2,...},target2:{},...}
vue2
function reactive(target) {
for (let key in target) {
let oldVal = target[key];//暂存,防止爆栈
Object.defineProperty(target, key, {
get() {
const dep = getDep(target, key);
dep.depend();
return oldVal;
},
set(newVal) {
const dep = getDep(target, key);
oldVal = newVal;//为什么改变oldVal,就能改变target[key]???
dep.notify();
}
})
}
return target;
}
vue3
function reactive(target) {
return new Proxy(target, {
get(target, key) {
const dep = getDep(target, key);
dep.depend();
return Reflect.get(target, key);
},
set(target, key, newVal) {
const dep = getDep(target, key);
const result = Reflect.set(target, key, newVal);
dep.notify();
return result;
}
})
}
getDep实际上就是从targetsMap对应target的对应key的dep:
function getDep(target, key) {
let depsMap = targetsMap.get(target);
// 不能取出来,是因为我们之前都没有存过
if (!depsMap) {
// 存一下呗
depsMap = new Map();
targetsMap.set(target, depsMap);
}
// dep
//只存一次,之后从targetsMap中取!!!
let dep = depsMap.get(key);
if (!dep) {
dep = new Dep();
depsMap.set(key, dep);
}
return dep;
}
最后,删除Dep类中的_val和gettter,setter
整体流程
(简单版-没有实现虚拟dom)
入口文件index.js
//import { createApp } from "vue";
//改为从自己写的文件中导入createApp
import { createApp } from "./my/index.js";;
import App from "./App.js";
createApp(App).mount("#app");
主要做的就是通过,然后挂(mount)到根节点上
./App.js:
import { reactive } from "./my/reactivity/index.js";
export default {
//渲染
render(context) {
const div = document.createElement("div");
div.innerText = context.state.count;
return div;
},
//数据
setup() {
const state = reactive({
count: 1,
});
window.state = state;
// count -> change
return {
state,
};
},
};
./my/reactivity/index.js:
import { watchEffect } from "./reactivity/index.js";
export function createApp(rootComponent) {
return {
mount(rootSelector) {
let rootContainer = document.querySelector(rootSelector);
const setupResult = rootComponent.setup();
watchEffect(() => {
let element = rootComponent.render(setupResult);
rootContainer.innerHTML = ``;
rootContainer.append(element);
})
},
};
}
虚拟DOM
(进阶版-实现虚拟dom)
首先,简单介绍虚拟DOM的概念,虚拟DOM实际上就是描述节点的对象:
export function h(type, props, children) {
return {
type,
props,
children,
};
}
比如说有如下DOM结构:
<div class="test" id="test"></div>
转换为虚拟DOM就是:
{
type:"div",
props:{class:"test",id="test"},
//children为数组或字符串类型
children:[]
}
将虚拟DOM转为真实DOM
function createElement(type) {
return document.createElement(type);
}
function patchProps(el, key, preValue, nextValue) {
el.setAttribute(key, nextValue);
}
function insertElement(parent, child) {
parent.append(child);
}
export function mountElement(vnode, container) {
const { type, props, children } = vnode;
const el = (vnode.el = createElement(type));//vnode.el是在下面的diff算法中会用到
if (props) {
for (const key in props) {
const val = props[key];
patchProps(el, key, null, val);
}
}
if (typeof children === 'string') {
const text = document.createTextNode(children);
insertElement(el, text);
} else if (Array.isArray(children)) {
children.forEach((child) => mountElement(child, el));
}
insertElement(container, el);
}
diff算法
(vue3版原理)这里只实现了一个简易版的,完整版以及原理可参考 www.jb51.net/article/189…
export function diff(prev, cur) {
if (prev.type !== cur.type) {
prev.el.replaceWith(createElement(cur.type));
} else {
const el = (cur.el = prev.el)
//处理props
const oldProps = prev.props || {};
const newProps = cur.props || {};
//新的替换
Object.keys(newProps).forEach((key) => {
if (newProps[key] !== oldProps[key]) {
patchProps(el, key, oldProps[key], newProps[key]);
}
})
//不存在了的删除
Object.keys(oldProps).forEach((key) => {
if (newProps[key] !== oldProps[key]) {
patchProps(el, key, oldProps[key], null);
}
})
//处理children
const oldChildren = prev.children || [];
const newChildren = cur.children || [];
if (typeof newChildren === 'string') {
if (newChildren !== oldChildren) {
el.textContent = newChildren;
}
} else {
if (!Array.isArray(oldChildren)) {
el.innerHTML = ``;
newChildren.forEach((child) => {
mountElement(el, child);
})
} else {
//两个都是数组
//简单暴力法,源码中做了很多优化!!!
let oldLen = oldChildren.length;
let newLen = newChildren.length;
let len = Math.min(oldLen, newLen);
for (let i = 0; i < len; i++) {
let _1 = oldChildren[i];
let _2 = newChildren[i];
diff(_1, _2);
}
//老的多需要删除
if (oldLen > len) {
for (let i = len + 1; i < oldLen; i++) {
el.removeChild(oldChildren[i].el);
}
}
//新的多需要增加
if (newLen > len) {
for (let i = len + 1; i < newLen; i++) {
mountElement(newChildren[i].el, el);
}
}
}
}
}
}