begger-vue组成
主要为以下几个核心函数:
render
和h
mount
patch
reactive
watchEffect
vue
最大的特点是数据响应式,数据驱动视图更新。意思是我每次修改了数据,能自动给我更新dom
。
render
和h
函数
我们在单文件组件定义下面template
:
<div>
<span id="foo">foo</span>
</div>
template
经过vue-loader
和vue-template-compiler
的解析后会变成一个render
函数。大概是这样子
function render () {
return h('div', {}, [h('span', { id: 'foo' }, 'foo')]);
}
这里我们定义的h
函数主要是用js描述了这个节点的结构:
function h(type, props, children) {
return {
type,
props,
children: Array.isArray(children) ? children : String(children),
};
}
那么上面的render
函数执行得到的一个结构是这样的,也就是我们所说的vnode
。当然实际上vnode
还有其他属性,我们的乞丐版不考虑。
{
"type": "div",
"props": {},
"children": [
{
"type": "span",
"props": {
"id": "foo"
},
"children": "foo"
}
]
}
自此我们就生成了vnode
,接下来是怎么把他渲染到页面上。
mount
拿到了vnode
,通过mount
函数渲染到页面,mount
主要是创建真实的dom
,处理属性,和递归遍历children
。
function mount(vnode: VNode, el: HTMLElement) :void {
const { type, props, children } = vNode;
// 把真实的dom元素记录在el属性上
const el = (vNode.el = document.createElement(type));
// 链接父元素
el.parent = container;
// 处理属性,事件或普通属性
Object.keys(props).forEach((key) => {
if (isEvent(key)) {
el.addEventListener(key.toLowerCase().substring(2), props[key]);
} else {
el.setAttribute(key, props[key]);
}
});
// 子元素是字符串,直接设置为文本
if (typeof children === "string") {
el.textContent = children;
} else {
children.forEach((child) => mount(child, el));
}
container.appendChild(el);
}
有了mount
函数,我们就可以挂载了。
mount(render(), document.getElementById('app'));
patch
patch
函数主要是深度遍历新旧vnode
,当比较发现有差异的时候会真实的操作dom
,根据差异来修改视图。这里不考虑diff
算法和节点复用,同层对比即可。
有如下两个新旧节点
// old vnode
{
"type": "div",
"props": {},
"children": [
{
"type": "span",
"props": {
"id": "foo"
},
"children": "foo"
}
]
}
// new vnode
{
"type": "div",
"props": {},
"children": [
{
"type": "span",
"props": {
"id": "baz"
},
"children": "baz"
}
]
}
可以看得出是span
标签的id
和textContent
发生了变化,那么patch
函数实现主要关注点如下:
- 如果标签不一致,那么移除旧元素,直接通过
mount
函数生成新的节点 - 如果属性不一致,需要区分事件和普通属性,逻辑分之有增删改操作
- 如果新
vnode
的chidlren
是字符串属性,删除原来子节点,改为设置textContent
,否则对比新旧children
,逻辑分之有处理新增子节点、删除子节点和修改子节点。
function isEvent(prop) {
return prop.startsWith("on");
}
function isProp(prop) {
return !isEvent(prop);
}
function patch(oldVnode, vnode) {
const {
type: oldType,
props: oldProps,
children: oldChildren,
el,
} = oldVnode;
const { type, props, children } = vnode;
vnode.el = el;
vnode.parent = vnode.parent;
// 完全不同的节点
if (oldType !== type) {
el.parent.removeChild(el);
mount(vnode, el.parent);
} else {
// 移除属性
Object.keys(oldProps)
.filter(isProp)
.filter((key) => !(key in props))
.forEach((key) => {
el.removeAttribute(key);
});
// 新增属性或者修改属性
Object.keys(oldProps)
.filter(isProp)
.filter((key) => oldProps[key] !== props[key])
.forEach((key) => {
el.setAttribute(key, props[key]);
});
// 移除事件
Object.keys(oldProps)
.filter(isEvent)
.filter((key) => !(key in props) || oldProps[key] !== props[key])
.forEach((key) => {
const eventType = key.toLowerCase().substring(2);
el.removeEventListener(eventType, oldProps[key]);
});
// 新增
Object.keys(oldProps)
.filter(isEvent)
.filter((key) => oldProps[key] !== props[key])
.forEach((key) => {
const eventType = key.toLowerCase().substring(2);
el.addEventListener(eventType, props[key]);
});
// 处理children
if (Array.isArray(children)) {
if (typeof oldChildren === "string") {
el.innerHTML = "";
children.forEach((child) => mount(child, el));
} else {
const commonLen = Math.min(children.length, oldChildren.length);
for (let i = 0; i < commonLen; i++) {
patch(oldChildren[i], children[i]);
}
// 删除多余节点
oldChildren.slice(commonLen).forEach(({ el, parent }) => {
parent.removeChild(el);
});
// 新增节点
children.slice(commonLen).forEach((newVnode) => {
mount(newVnode, el);
});
}
} else {
el.innerHTML = "";
el.textContent = children;
}
}
}
上面逻辑分之很多,不用太关注,只要知道patch会更新dom即可
Dep
和watchEffect
怎么在数据修改的时候,会自动修改视图呢,也就是每次修改数据会自动执行patch
函数。在之前,需要先了解Dep
和watchEffect
,它会告诉你,如果一个属性被修改了,怎么自动触发一段函数。
<script>
let activeEffect = null;
class Dep {
// 收集副作用
effects = new Set();
constructor(val) {
this._val = val;
}
get value() {
this.depend();
return this._val;
}
set value(newVal) {
if (newVal !== this.value) {
this._val = newVal;
this.notify();
}
return true;
}
depend() {
if (activeEffect && !this.effects.has(activeEffect)) {
this.effects.add(activeEffect);
}
}
notify() {
this.effects.forEach(efftct => efftct());
}
}
function ref(val) {
return new Dep(val);
}
let count = ref(0);
function watchEffect(effect) {
// 设置为当前运行中effect
activeEffect = effect;
// 立刻执行effect,和收集变量的effect
effect();
activeEffect = null;
}
watchEffect(() => {
console.log('count.value changed:', count.value);
})
</script>
简单来说只要劫持某个变量的getter
和setter
,在getter
收集依赖,vue3
把依赖叫做effect
副作用,然后在setter
触发副作用。
上面的代码运行后,控制台的输出:
每次在控制台更新count.value
就会执行effects
,每个变量可以收集多个effect
。
使用Proxy完成reactive
上面还是使用vue2
的方式去劫持某个属性的get、set
。有以下缺点
- 需要提前声明劫持属性,这就需要递归变量的每个层级,存在额外的性能开销
- 无法自动追踪数组的副作用方法,需要重写
shift、push、pop
等原型方法,且对于arr[0] = xxx
这种情况,无法自动跟踪。
基于Object.defineProperty
的一些弊端,vue3
已经使用Proxy
重写响应式部分,因此我们改造下上面的Dep
和新增reactive
。
let activeEffect = null;
class Dep {
effects = new Set();
depend() {
activeEffect &&
!this.effects.has(activeEffect) &&
this.effects.add(activeEffect);
}
notifiy() {
this.effects.forEach((effect) => effect());
}
}
// 这个map收集了每个响应式对象所有属性的dep
// 使用weakMap是为了更好的垃圾回收,具体可以参考高程4
const globalEffectMap = new WeakMap();
function getDep(target, key) {
let targetMap = globalEffectMap.get(target);
// 是否存在该变量的effects集合
if (!targetMap) {
targetMap = new Map();
globalEffectMap.set(target, targetMap);
}
let dep = targetMap.get(key);
// 是否存在该属性的effect
if (!dep) {
dep = new Dep();
targetMap.set(key, dep);
}
return dep;
}
const proxyHandler = {
get(target, key, receiver) {
const dep = getDep(target, key);
// 收集当前正在运行的effect
// 也就是watchEffect(effect)时,effect会立刻被执行,变成当前正在运行的effect
dep.depend();
return Reflect.get(...arguments);
},
set(target, key, value, receiver) {
const dep = getDep(target, key);
Reflect.set(...arguments);
dep.notifiy();
return true;
},
};
// 收集
function reactive(source) {
return new Proxy(source, proxyHandler);
}
使用Proxy
劫持get、set
后,就可以监听任意层级任意属性的变化了。
组合核心函数
结合上面的每个模块,我们把逻辑全部组合起来。
function createApp(App, containter) {
let mounted = false;
let vnode = null;
watchEffect(() => {
// 首次渲染
if (!mounted) {
vnode = App.render();
mount(vnode, containter);
mounted = true;
} else {
const newVnode = App.render();
patch(vnode, newVnode);
vnode = newVnode;
}
});
}
const proxy = reactive({
count: 1,
});
const App = {
render() {
return h("div", { tag: "div" }, [
// 每次点击button,新增一个p标签
h("button", { onClick: () => proxy.count++ }, proxy.count),
...Array.from({ length: proxy.count }).map((v, i) =>
h("p", { tag: i }, i)
),
]);
},
};
createApp(App, document.getElementById("app"));
来看下效果
点击按钮的时候, 新增一个p
标签
控制台改变数据的时候,视图也会同步
至此我们已经完成了一个乞丐版的vue3
,完整代码:
<div id="app"></div>
<script>
function h(type, props, children) {
return {
type,
props,
children: Array.isArray(children) ? children : String(children),
};
}
function mount(vNode, container) {
const { type, props, children } = vNode;
// 把真实的dom元素记录在el属性上
const el = (vNode.el = document.createElement(type));
// 链接父元素
el.parent = container;
// 处理属性,事件或普通属性
Object.keys(props).forEach((key) => {
if (isEvent(key)) {
el.addEventListener(key.toLowerCase().substring(2), props[key]);
} else {
el.setAttribute(key, props[key]);
}
});
if (typeof children === "string") {
el.textContent = children;
} else {
children.forEach((child) => mount(child, el));
}
container.appendChild(el);
}
function isEvent(prop) {
return prop.startsWith("on");
}
function isProp(prop) {
return !isEvent(prop);
}
function patch(oldVnode, vnode) {
const {
type: oldType,
props: oldProps,
children: oldChildren,
el,
} = oldVnode;
const { type, props, children } = vnode;
vnode.el = el;
vnode.parent = vnode.parent;
// 完全不同的节点
if (oldType !== type) {
const parentNode = el.parent;
parentNode.removeChild(el);
mount(vnode, parentNodet);
} else {
// 移除属性
Object.keys(oldProps)
.filter(isProp)
.filter((key) => !(key in props))
.forEach((key) => {
el.removeAttribute(key);
});
// 新增属性或者修改属性
Object.keys(oldProps)
.filter(isProp)
.filter((key) => oldProps[key] !== props[key])
.forEach((key) => {
el.setAttribute(key, props[key]);
});
// 移除事件
Object.keys(oldProps)
.filter(isEvent)
.filter((key) => !(key in props) || oldProps[key] !== props[key])
.forEach((key) => {
const eventType = key.toLowerCase().substring(2);
el.removeEventListener(eventType, oldProps[key]);
});
// 新增
Object.keys(oldProps)
.filter(isEvent)
.filter((key) => oldProps[key] !== props[key])
.forEach((key) => {
const eventType = key.toLowerCase().substring(2);
el.addEventListener(eventType, props[key]);
});
// 处理children
if (Array.isArray(children)) {
if (typeof oldChildren === "string") {
el.innerHTML = "";
children.forEach((child) => mount(child, el));
} else {
const commonLen = Math.min(children.length, oldChildren.length);
for (let i = 0; i < commonLen; i++) {
patch(oldChildren[i], children[i]);
}
// 删除多余节点
oldChildren.slice(commonLen).forEach(({ el, parent }) => {
parent.removeChild(el);
});
// 新增节点
children.slice(commonLen).forEach((newVnode) => {
mount(newVnode, el);
});
}
} else {
el.innerHTML = "";
el.textContent = children;
}
}
}
let activeEffect = null;
class Dep {
effects = new Set();
depend() {
activeEffect &&
!this.effects.has(activeEffect) &&
this.effects.add(activeEffect);
}
notifiy() {
this.effects.forEach((effect) => effect());
}
}
const globalEffectMap = new WeakMap();
function getDep(target, key) {
let targetMap = globalEffectMap.get(target);
if (!targetMap) {
targetMap = new Map();
globalEffectMap.set(target, targetMap);
}
let keyMap = targetMap.get(key);
if (!keyMap) {
keyMap = new Dep();
targetMap.set(key, keyMap);
}
return keyMap;
}
const proxyHandler = {
get(target, key, receiver) {
const keyMap = getDep(target, key);
keyMap.depend();
return Reflect.get(...arguments);
},
set(target, key, value, receiver) {
const keyMap = getDep(target, key);
Reflect.set(...arguments);
keyMap.notifiy();
return true;
},
};
function reactive(source) {
return new Proxy(source, proxyHandler);
}
function watchEffect(effect) {
activeEffect = effect;
effect();
activeEffect = null;
}
function createApp(App, containter) {
let mounted = false;
let vnode = null;
watchEffect(() => {
if (!mounted) {
vnode = App.render();
mount(vnode, containter);
mounted = true;
} else {
const newVnode = App.render();
patch(vnode, newVnode);
vnode = newVnode;
}
});
}
const proxy = reactive({
count: 1,
});
const App = {
render() {
return h("div", { tag: "div" }, [
h("button", { onClick: () => proxy.count++ }, proxy.count),
...Array.from({ length: proxy.count }).map((v, i) =>
h("p", { tag: i }, i)
),
]);
},
};
createApp(App, document.getElementById("app"));
</script>
思考
vue3
很大一部分的工作量都在
- 模板的编译优化,
patchFlag
的实现, 事件缓存等 - 更好的
diff
算法, 和更好的typescript
支持 Composition-API
在我们的demo
通通都没有, 甚至更新都是同步的,但是我们主要是学习核心思路,理解vue
的核心思想。后续的可以慢慢拓展
参考: