数据响应式演变
现在要实现如下功能。
b的值依赖于a的值,当a变化之后要重新获取b的值。
于是我们写下了下面的代码。
let a = 10
let b = a + 10 // b 的值依赖于 a 的值
console.log(b)
a = 20
b = a + 10
console.log(b)
我们发现在a更新完成之后,我们要重新给b进行赋值才可以拿到最近的b的值。于是我们再进行封装了。
let a = 10
let b = a + 10
console.log(b)
function update() {
b = a + 10
}
a = 20
// 现在每当我们改完 a 的值之后,就调用一下 update()方法。就可以得到最新的 b 的值了。
update()
console.log(b)
现在我们想想能不能有个操作当a变化之后可以自动的执行某一段函数。
// 用来临时存放依赖信息
let currentEffect;
// 新建一个依赖的类
class Dep {
constructor(val) {
// 存放依赖的地方
this.effects = new Set();
this._val = val
}
get value() {
return this._val
}
set value(newVal) {
this._val = newVal
}
// 添加依赖
depend() {
// 当前有依赖信息,再执执行添加操作。
if (currentEffect) {
this.effects.add(currentEffect);
}
}
// 触发更新
notice() {
// 循环遍历依赖信息
this.effects.forEach(effect => {
effect()
});
}
}
function effectWatch(effect) {
currentEffect = effect;
// 默认执行一次
effect()
dep.depend()
currentEffect = null
}
const dep = new Dep(10)
let b
effectWatch(()=>{
b = dep.value + 10
console.log(b)
})
// 值发生变化,触发更新
dep.value = 100
// 调用一下就可以收集依赖信息
dep.notice()
基于上面这代码,我们可以实现对一个对象的属性进行监测。
感受Vue3实现的响应式效果
// 好了现在我们来感受一下 vue3的响应式。
import {ref, effect} from '@vue/reactivity'
// ref 是产生一个响应式对象
// effect 是当这个对象变化之后的副作用
let num = ref(10)
// 这个副作用会默认执行一次
effect(()=>{
console.log(num.value)
})
num.value = 100
// 好了,基于以上的代码。我们想一想
// 依赖是什么?
// 副作用又是什么?
然后再基于Proxy实现对一个对象的拦截。代码如下
reactivity.js
let currentEffect;
// 新建一个依赖的类
class Dep {
constructor() {
// 存放依赖的地方
this.effects = new Set();
}
// 添加依赖
depend() {
// 当前有依赖信息,再执执行添加操作。
if (currentEffect) {
this.effects.add(currentEffect);
}
}
// 触发更新
notice() {
// 循环遍历依赖信息
this.effects.forEach((effect) => {
effect();
});
}
}
// Map(1) { { val: 100 } => Map(1) { 'val' => Dep { effects: [Set] } } }
const targetMap = new Map();
/**
*
* @param {依赖的对象} target
* @param {对象的key} key
*/
function getDep(target, key) {
// 我们知道要给对象的每个属性都进行依赖收集
// 先存一下不同对象的映射,然后存对象下面 key 映射的 dep
// 既每一个 key ---> 对应一个 dep
// 存每个对象的映射
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 存key 的映射
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) {
let dep = getDep(target, key);
// 收集依赖
dep.depend();
return Reflect.get(target, key);
},
set(target, key, value) {
// 获取到依赖 触发更新
let dep = getDep(target, key);
// 先设置值,后触发依赖
let result = Reflect.set(target, key, value);
dep.notice();
return result;
},
});
}
export function effectWatch(effect) {
currentEffect = effect;
// 默认执行一次
effect();
currentEffect = null;
}
// 收集的依赖一定是
// ref reactive 包装过的变量才收集依赖。
let obj = reactive({
val: 100,
});
let double
effectWatch(() => {
double = obj.val
// console.log(double)
});
obj.val = 999;
现在我们就实现了vue3中的 reactive 和 effect,假设我们在数据变化了之后还需要视图进行更新。下面我们模仿一下vue3中的创建组件,并把组件挂载到真实dom中的流程
初渲染
index.js
import { effectWatch } from "./reactivity.js";
import { diff, mountElement } from "./renderer.js";
// 把根组件 mount到那个节点上面。
export function createApp(rootComponent) {
return {
mount(rootContainer) {
const context = rootComponent.setup();
// 标记是否是初次渲染。
let isMounted = false;
let prevVnode
// 数据变了之后重新 render
effectWatch(() => {
if (!isMounted) {
rootContainer.innerHTML = "";
// let element = rootComponent.render(context);
// document.body.append(element)
let vnode = rootComponent.render(context);
// mount 的时候给虚拟节点添加了 el 属性。挂载了真实的节点。
mountElement(vnode, rootContainer);
prevVnode = vnode
isMounted = true
}
// 更新逻辑
else {
console.log('update')
let vnode = rootComponent.render(context)
// 老节点与新节点进行对比。
diff(prevVnode, vnode)
prevVnode = vnode
}
});
},
};
}
h.js
export function h(tag, props, children) {
return {
tag,
props,
children
}
}
简易diff
两个虚拟节点对比的时候分以下几种情况
先比较tag
- tag 不相同的时候,直接用新的节点替换老的节点
tag 相同
- 老的没有孩子,新的有孩子。用新的append到老的dom里面
- 新的没有孩子,老的有孩子。删除老的dom中的节点
- 两个子节点都是string ,用新的内容替换掉老的节点内容。
- 两个都有孩子(这个是核心)
renderer.js
/**
* 把vnode 转换成真实节点,并挂载到容器上。
* @param {*} vnode
* @param {*} container
*/
export function mountElement(vnode, container) {
let { tag, props, children } = vnode;
// 处理 tag
let element = document.createElement(tag);
// 处理 props
if (props) {
for (let key in props) {
// 给元素设置属性
element.setAttribute(key, props[key]);
}
}
// 检测一下 children 的类型
if (Array.isArray(children)) {
// 遍历子节点
children.forEach((v) => {
// 递归 添加儿子
mountElement(v, element);
});
} else {
element.innerText = children;
}
container.appendChild(element);
vnode.el = element;
}
/**
*
* @param {oldVnode} n1
* @param {newVnode} n2
*/
export function diff(n1, n2) {
console.log(n1, n2);
// tag
// 看两个虚拟节点的 tag 是否相同
if (n1.tag !== n2.tag) {
// 直接用新的节点替换掉老的节点
n1.el.replaceWith(document.createElement(n2.tag));
} else {
// 在每次 diff 之前先把老的虚拟节点的真实节点放在 新的虚拟节点下面
n2.el = n1.el;
//props
const { props: newProps } = n2;
const { props: oldProps } = n1;
// new {id: 'xxx', 'data-xxx':'xxx', data-b: 100}
// old {id: 'xxx', 'data-xxx':'xxx'}
// 1. 新节点的 props 比老节点的 props 多 用新的替换老的
if (newProps && oldProps) {
Object.keys(newProps).forEach((key) => {
const newVal = newProps[key];
const oldVal = oldProps[key];
if (newVal !== oldVal) {
// 用新的替换老的
n1.el.setAttribute(key, newVal);
}
});
}
// 2. 新节点的 props 比老节点的 props 少
if (oldProps && newProps) {
Object.keys(oldProps).forEach((key) => {
// 如果新的props 里面没有老的 key, 就从老的节点中删除这个属性。
if (!newProps[key]) {
n1.el.removeAttribute(key);
}
});
}
// children
const { children: oldChildren } = n1;
const { children: newChildren } = n2;
if (typeof newChildren === "string") {
// 1. 两个都是 字符串
if (typeof oldChildren === "string") {
if (newChildren !== oldChildren) {
n1.el.textContent = newChildren;
}
// 2. 新节点是字符串, 老节点是数组
else if (Array.isArray(oldChildren)) {
n1.el.textContent = newChildren;
}
}
} else if (Array.isArray(newChildren)) {
// 新的是数组, 老的是字符串
if (typeof oldChildren === "string") {
n1.el.innerText = "";
// 遍历新的孩子加入到原来的节点中
mountElement(n2, n1.el);
}
// 两个都是数组
// [1,2,3,4]
// [1,2,3]
else if (Array.isArray(oldChildren)) {
// 取一个公共长度
let length = Math.min(newChildren.length, oldChildren.length);
// 处理公共的 length
for (let index = 0; index < length; index++) {
const newVal = newChildren[index];
const oldVal = oldChildren[index];
// 再对比公共的 length
diff(oldVal, newVal);
}
// 如果新的长度大于公共长度 , 在老节点里面追加元素
if (newChildren.length > length) {
for (let i = length; i < newChildren.length; i++) {
const newVnode = newChildren[i]
mountElement(newVnode);
}
}
// 如果新的长度小于公共长度 ,删除老节点里面的元素
if (newChildren.length < length) {
for(let i = length; i < oldChildren.length; i++) {
const oldVnode = oldChildren[i]
// 从老的里面删除老的
oldVnode.el.parent.removeChild(oldVnode.el);
}
}
}
}
}
}
组件
App.js
import { h } from './core/h.js';
import {reactive} from './core/reactivity.js'
export default {
// 根据数据创建出最新的视图节点
// 这里先不考虑 vnode 和 patch
render(context) {
return h(
"div",
{
class: "class----"+String(context.state.count),
},
[h("p", null, "我是第一个儿子"+ context.state.count), h("p", null, "我是第二个儿子")]
);
},
setup() {
const state = reactive({
count: 0,
});
window.state = state;
return {
state,
};
},
};
代码入口
index.js
import { createApp } from "./core/index.js";
import App from './App.js'
const app = createApp(App)
.mount(document.querySelector('#app'))
这样我们就可以实现了一个简易的Vue数据响应式,以及 render函数到页面的渲染过程。
还需要更进一步的了解的就是 在template如转换成render函数。
以及通过render函数如何生成的vnode
diff中当两个vnode的子节点都是数组的情况下,里面的diff流程。
还有根组件与子组件之间的渲染流程。
代码目录
{
"name": "up-mini-vue",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"type": "module",
"dependencies": {
"@vue/reactivity": "^3.2.23"
}
}
根据以上的代码,可以先感受一下vue内部为我们不手动操作dom背后所做的事情吧。