1. 前言
从vue源码中可以看出vue主要包含下面三大核心模块:
- Compiler模块:编译模块 (将template中的模板编译成render渲染函数)
- Renderer模块:渲染模块 (将Compiler模块编译后的结果真正渲染到页面上)
- Reactivity模块:响应式模块 (当数据发生改变,响应变化)
本篇文章来实现一个简洁版的Mini-Vue框架,该Vue包括三个模块:
- 渲染系统模块 (runtime -> vnode -> 真实DOM)
- 可响应式模块 (reactive)
- 应用程序入口模块 (createApp)
2.渲染系统实现
该模块主要包含三个功能:
- 功能一、h函数,用于返回一个VNode对象
- 功能二、mount函数,用于将VNode挂载到DOM上
- 功能三、patch函数,用于对两个VNode进行对比,决定如何处理新的VNode
2.1 h函数的实现
- h函数:h函数的作用是返回一个
虚拟节点
,通常缩写为 VNode接收三个参数:type
,props
和children
,虚拟节点组成虚拟DOM。
虚拟DOM是轻量级的 JavaScript 对象,由渲染函数创建。它包含三个参数:元素,具有数据、prop、attr 等的对象,以及一个数组。数组是我们传递子级的地方,子级也具有所有这些参数,然后它们也可以具有子级,依此类推,直到我们构建完整的元素树为止。
function h(type, props, children) {
return {
type,
props,
children
}
}
2.2 mount函数的实现
- mount函数的作用是将虚拟节点挂载的页面上。它接收两个参数,第一个是需要挂载的vnode,第二个是被挂载的真实dom节点
function mount(vnode, container) {
// 1. 创建出真实的元素,并且在vnode上保留el
const el = vnode.el = document.createElement(vnode.type)
// 2. 处理props
if(vnode.props) {
for(const key in vnode.props) {
const value = vnode.props[key]
// 判断属性是否是事件属性
if(key.startsWith('on')) {
el.addEventListener(key.slice(2).toLocaleLowerCase(), value)
} else {
el.setAttribute(key, value)
}
}
}
// 3. 处理children
if(vnode.children) {
if(typeof vnode.children === 'string') {
el.textContent = vnode.children
} else if(vnode.children instanceof Array) {
vnode.children.forEach(item => {
mount(item, el)
})
} else if (typeof vnode.children === 'object') {
mount(vnode.children, el)
}
}
// 4. 挂载到container上
container.appendChild(el)
}
2.3 patch函数的实现
- patch函数的作用是对比两个vnode,决定如何处理新的VNode。它接收两个参数,第一个是旧的vnode,第二个是新的vnode
function patch(n1, n2) {
if(n1.type !== n2.type) {
// 1. 标签名不相同 直接移除原来的元素 挂载新的vnode
const n1ParentEl = n1.el.parentElement
n1ParentEl.removeChild(n1.el)
mount(n2, n1ParentEl)
} else {
// 2 标签名相同
const el = n2.el = n1.el
// 2.1 处理props
const oldProps = n1.props || {}
const newProps = n2.props || {}
// 添加新的属性
for(const key in newProps) {
const oldValue = oldProps[key]
const newValue = newProps[key]
if(oldValue !== newValue) {
if(key.startsWith('on')) {
el.addEventListener(key.slice(2).toLocaleLowerCase(), oldValue)
} else {
el.setAttribute(key, newValue)
}
}
}
// 删除旧的属性
for(const key in oldProps) {
const oldValue = oldProps[key]
const newValue = newProps[key]
if(oldValue !== newValue) {
if(key.startsWith('on')) {
el.removeEventListener(key.slice(2).toLocaleLowerCase(), value)
} else {
el.removeAttribute(key)
}
}
}
// 2.2 处理children
const oldChildren = n1.children
const newChildren = n2.children
// 情况一:新的children是字符串
if(typeof newChildren === 'string') {
if(newChildren !== oldChildren) {
el.textContent = newChildren
}
}
// 情况二:新的children是数组
else if (newChildren instanceof Array) {
if(typeof oldChildren === 'string') {
newChildren.forEach(item => {
mount(item, el)
})
} else {
// 新旧都是数组(不考虑key的情况)
const commonLength = Math.min(oldChildren.length, newChildren.length)
for(let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i])
}
if(newChildren.length > oldChildren) {
newChildren.slice(oldChildren.length).forEach(item => {
mount(item, el)
})
}
if(newChildren.length < oldChildren) {
oldChildren.slice(oldChildren.length).forEach(item => {
el.removeChild(item.el)
})
}
}
}
}
}
2.4 演示
测试代码
<div id="app"></div>
<button id="btn">CHANGE</button>
<!-- renderer.js 包含上述h、mount、patch函数 -->
<script src="./renderer.js"></script>
<script>
// 1.通过h函数来创建一个vnode
const vnode1 = h("div", { class: "coder" }, [
h("h2", null, "当前计数: 100"),
h("button", null, "+1"),
]);
// 2.通过mount函数,将vnode挂载到div#app上
mount(vnode1, document.getElementById("app"));
// 3.创建新的vnode2
const vnode2 = h(
"div",
{ class: "coder", style: "font-weight: 700; font-size: 30px;" },
[h("h3", null, "哈哈哈"), h("b", null, "嘿嘿嘿")]
);
const btn = document.getElementById("btn");
btn.addEventListener("click", (e) => {
patch(vnode1, vnode2);
});
</script>
结果
3. 响应式系统
响应式我在我之前的一篇文章有讲解过
简单实现vue中的响应式系统
- 下面贴一下代码
// 保存当前需要收集的响应式函数
let activeReactiveFn = null
class Depend {
constructor() {
// 使用Set来保存依赖函数, 而不是数组[]
this.reactiveFns = new Set()
}
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
depend() {
if(activeReactiveFn) {
this.reactiveFns.add(activeReactiveFn)
}
}
}
// WeakMap({key(对象): value}), key是个对象,弱引用(当将key设置为null时,key被垃圾回收机制回收,对应的value也会被回收)
const targetMap = new WeakMap()
// 封装一个获取depend函数
function getDepend(target, key) {
// 根据target对象获取map的过程
let map = targetMap.get(target)
if(!map) {
map = new Map()
targetMap.set(target, map)
}
// 根据key获取depend对象
let depend = map.get(key)
if(!depend) {
depend = new Depend()
map.set(key, depend)
}
return depend
}
// 封装一个响应式的函数
function watchEffect(fn) {
activeReactiveFn = fn
fn()
activeReactiveFn = null
}
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
// 根据target.key获取对应的depend
// 做依赖收集
const depend = getDepend(target, key)
depend.depend()
return Reflect.get(target, key, receiver)
},
set(target, key, newValue , receiver) {
Reflect.set(target, key, newValue, receiver)
// 监听对象变化做出响应
const depend = getDepend(target, key)
depend.notify()
}
})
}
4. createApp实现
- createApp:要求传入一个根组件实例,并且需要提供一个mount方法挂载函数
function createApp(rootComponent) {
return {
mount(selector) {
const container = document.querySelector(selector);
let isMounted = false;
let oldVNode = null;
// 监听counter变化做页面的更新
watchEffect(function() {
if (!isMounted) {
// 第一次做mount操作
oldVNode = rootComponent.render();
mount(oldVNode, container);
isMounted = true;
} else {
// 数据发生更新做patch操作
const newVNode = rootComponent.render();
patch(oldVNode, newVNode);
oldVNode = newVNode;
}
})
}
}
}
5.mini-vue框架最终演示
- 测试代码
<div id="app"></div>
<script src="./renderer.js"></script>
<script src="./reactive.js"></script>
<script src="./createApp.js"></script>
<script>
const vnode1 = h("div", { class: "coder" }, [
h("h2", null, "当前计数: 100"),
h("button", null, "+1"),
]);
const App = {
data: reactive({
counter: 0,
}),
render() {
return h("div", { class: "coder" }, [
h("h2", null, `当前计数: ${this.data.counter}`),
h("button",{
onClick: () => {
this.data.counter--;
},
},"-1"
),
h("button",{
onClick: () => {
this.data.counter++;
},
},"+1"
),
]);
},
};
const app = createApp(App);
app.mount("#app");