续上文:
Vue3源码学习之旅(1)–自己实现一个简单的渲染系统-render/h/patch函数
依赖收集系统
// 收集依赖
/**
* 收集依赖的类
*/
class Dep {
constructor() {
// 发布订阅模式
// 订阅者 将其都加入进来
this.subscribers = new Set();
}
// 收集副作用
// addEffect(effect) {
// this.subscribers.add(effect);
// }
/* 自动收集副作用 */
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
// 通知,进行回调执行
notify() {
this.subscribers.forEach(effect => {
effect();
})
}
}
let activeEffect = null;
/**
* 帮助收集依赖
* @param {*} effect 数据发送改变后要执行的副作用函数
*/
function watchEffect(effect) {
activeEffect = effect;
// 用原始数据执行一次
effect()
activeEffect = null;
}
/*
Map: {key:value} key: 字符串 value任意
WeakMap: key 是一个对象,弱引用 value任意
*/
const targetMap = new WeakMap();
/**
*
* @param {*} target 目标对象
* @param {*} key 目标对象的属性
*/
function getDep(target, key) {
// 1. 根据目标对象 取出对应的Map对象
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;
}
数据劫持
响应式系统Vue2实现
**
* vue2对raw进行数据劫持
* @param {*} raw 原始数据
*/
function reactive(raw) {
Object.keys(raw).forEach(key => {
// 收集依赖
const dep = getDep(raw, key);
let value = raw[key];
// 劫持的对象,对象的属性,属性的描述
Object.defineProperty(raw, key, {
// 获取数据 会执行get函数
get() {
// 添加依赖
dep.depend()
return value;
},
// 设置数据
set(newValue) {
if (value !== newValue) {
value = newValue;
dep.notify();
}
}
})
})
return raw;
}
响应式系统Vue3实现
**
* vue3对raw进行数据劫持
* @param {*} raw 原始数据
*/
function reactive(raw) {
// 劫持的对象 对代理对象的数据劫持
return new Proxy(raw, {
// 获取数据时调用
// target 参数 就是 我们劫持的对象 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();
}
});
}
响应式数据的测试
// ----------------测试----------------------
// 数据
const info = reactive({ name: '毛毛', age: 21 })
// 会自动收集依赖
watchEffect(() => {
console.log(info.age * 2);
})
// 数据发送改变 自动执行副作用函数
info.age++;
let obj = reactive({name:'hah ',counter:1});
watchEffect(()=>{
console.log("name:",obj.name);
})
watchEffect(()=>{
console.log("counter:",obj.counter);
})
obj.counter++
为什么Vue3选择Proxy呢?
Object.definedProperty 是劫持对象的属性时,如果新增元素:
那么Vue2需要再次 调用definedProperty,而 Proxy 劫持的是整个对象,不需要做特殊处理;
修改对象的不同:
- 使用 defineProperty 时,我们修改原来的 obj 对象就可以触发拦截;
- 而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截;
Proxy 能观察的类型比 defineProperty 更丰富
- has:in操作符的捕获器;
- deleteProperty:delete 操作符的捕捉器;
- 等等其他操作;
框架外层API设计
这样我们就知道了,从框架的层面来说,我们需要 有两部分内容:
-
createApp用于创建一个app对象;
-
该app对象有一个mount方法,可以将根组件挂 载到某一个dom元素上;
/**
*
* @param {*} rootComponent 根组件(元素)
* @returns
*/
function createApp(rootComponent) {
return {
mount(selector) {
const container = document.querySelector(selector);
// 是否挂载过
let isMounted = false;
// 数据未更新前的vnode
let oldVNode = null;
watchEffect(() => {
if (!isMounted) {
// 没有挂载过
// 进行挂载
oldVNode = rootComponent.render();
mount(oldVNode, container);
isMounted = true;
} else {
// 已经挂载过
// 进行patch比对
const newVNode = rootComponent.render()
patch(oldVNode, newVNode);
// 保存这次新的vnode
oldVNode = newVNode;
}
})
}
}
}
测试
// 根组件
const App = {
data: reactive({
counter: 0
}),
render() {
return h("div", { style: "color:red" }, [
h("h2", null, `当前计数:${this.data.counter}`),
h("button", {
onClick: () => {
this.data.counter++
}
}, "+1")
])
}
}
// 挂载根组件
const app = createApp(App)
app.mount("#app")