Implementing a Mini-Vue can deepen the understanding of vue, including rendering system modules, responsive system modules, and application entry modules (without implementing the compilation system module).
vue core module
Vue has three core modules, a compilation system, a rendering system, and a responsive system.
build system
- Parse the template template into an abstract syntax tree (AST).
- Optimize the AST.
- Generate a render function from the AST.
rendering system
- reder function returns vnode
- A tree structure vdom will be formed between vnodes
- Generate real dom based on vdom and render to browser
Responsive system
- Compare the old and new vnodes using the diff algorithm
- The rendering system regenerates the DOM based on the vnode and renders it to the browser
rendering system
Generate vnode by h function
The h function includes 3 parameters, element, attribute, child element. The resulting vnode is a javascript object
function h(tag, props, children) {
// vnode --> javascript对象
return {
tag,
props,
children
}
}
复制代码
// 1.通过h函数创建vnode
const vnode = h("div", {class: 'lin'}, [
h("span", null, '我是靓仔'),
h("button", {onClick: function() {}}, 'change')
])
复制代码
Documentation: H-function
Mount vnode to div#app through the mount function
mount(vnode, document.querySelector("#app"))
const h = (tag, props, children) => {
// vnode --> javascript对象
return {
tag,
props,
children
}
}
const mount = (vnode, container) => {
// 1. 创建出真实的元素, 并且在vnode上保存el
const el = vnode.el = document.createElement(vnode.tag)
// 2. 处理props
if (vnode.props) {
for (let key in vnode.props) {
const value = vnode.props[key]
if (key.startsWith("on")) { // 是否是事件
el.addEventListener(key.slice(2).toLowerCase(), value)
} else {
el.setAttribute(key, value)
}
}
}
// 3. 处理子元素 字符串,数组
if (vnode.children) {
if (typeof vnode.children === "string") {
el.textContent = vnode.children
} else {
// 数组 多个子元素 递归
vnode.children.forEach(element => {
mount(element, el)
})
}
}
// 4.将el挂载到container上
container.appendChild(el)
}
复制代码
patch compares old and new nodes
When a node changes, compare the old and new nodes and update them. Based on the new VNode, transform the old VNode to be the same as the new VNode (patch)
setTimeout(() => {
const vnode2 = h("div", {class: 'jin', onClick: function() {console.log("我是靓仔")}}, [
h("button", {class: "zhang"}, '按钮')
])
patch(vnode, vnode2)
}, 2000)
复制代码
const patch = (n1, n2) => {
// n1旧 n2新
// 如果父元素不一样, 直接替换
if (n1.tag !== n2.tag) {
// 获取父元素
const n1Elparent = n1.el.parentElement
// 移除旧节点
n1Elparent.removeChild(n1.el)
// 重新挂载新节点
mount(n2, n1Elparent)
} else {
// 引用, 修改一个另一个也会改变, n1.el 在n1挂载(mount)时赋值
const el = n2.el = n1.el
// 对比props
const oldProps = n1.props || {}
const newProps = n2.props || {}
// 把新的props添加到el上
for (let key in newProps) {
const oldValue = oldProps[key]
const newValue = newProps[key]
if (newValue !== oldValue) {
if (key.startsWith("on")) {
el.addEventListener(key.slice(2).toLowerCase(), newValue)
} else {
el.setAttribute(key, newValue)
}
}
}
// 删除旧的props 移除监听器, 属性
for (let key in oldProps) {
if (key.startsWith("on")) {
el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key])
} else {
if (!key in newProps) {
el.removeAttribute(key)
}
}
}
// 对比children
// children 可能是字符串, 数组, 对象(插槽), 字符串跟数组比较常见
// n1 [v1, v2, v3, v4, v5]
// n2 [v1, v7, v8]
const oldChildren = n1.children || []
const newChidlren = n2.children || []
if (typeof newChidlren === 'string') { // 如果newChidlren是字符串
if (typeof oldChildren === "string") {
if (newChidlren !== oldChildren) {
// textContent属性表示一个节点及其后代的文本内容
el.textContent = newChidlren
}
} else {
// innerHTML 返回 HTML textContent 通常具有更好的性能,因为文本不会被解析为HTML 使用 textContent 可以防止 XSS 攻击。
el.innerHtml = newChidlren
}
} else { // 如果newChidlren是数组
const oldLength = oldChildren.length
const newLength = newChidlren.length
const minLength = Math.min(oldLength, newLength)
// 先对比相同长度的部分
for (let i = 0; i < minLength; i++) {
patch(oldChildren[i], newChidlren[i])
}
// 如果新的比较长, 则mount新增的节点
if (newLength > oldLength) {
newChidlren.slice(minLength).forEach(item => {
mount(item, el)
})
}
// 如果旧的比较长, 则移除多余节点
if (newLength < oldLength) {
oldChildren.slice(minLength).forEach(item => {
el.removeChild(item.el)
})
}
}
}
}
复制代码
Responsive system
When the data changes, everything that uses the data should also change
let obj = {
name: 'lin'
}
const change = () => {
console.log('输出为:', obj.name)
}
change()
obj.name = 'jin'
// 当obj发生变化时,有使用到到obj的地方也会发生相应的改变
change()
复制代码
Define a dependency collection class
class Depend {
constructor() {
// Set对象允许你存储任何类型的唯一值,不会出现重复
this.reactiveFns = new Set()
}
addDepend(reactiveFn) {
this.reactiveFns.add(reactiveFn)
}
notify() {
this.reactiveFns.forEach(item => {
item()
})
}
}
let obj = {
name: 'lin'
}
const change = () => {
console.log('输出为:', obj.name)
}
const dep = new Depend()
dep.addDepend(change)
obj.name = 'jin'
dep.notify()
复制代码
Automatically monitor object changes
Every time the object changes, we have to call the notify
method again, and we can use the proxy to monitor the changes of the object.
reactive function
Not every function needs to be made reactive, we can define a function to receive functions that need to be made reactive.
let activeReactiveFn = null
function watchFn(fn) {
activeReactiveFn = fn
fn() // 调用一次触发get(看下面代码)
activeReactiveFn = null
}
复制代码
Monitor object changes
Vue2 and Vue3 are implemented differently:
- Vue2 uses Object.defineProperty() to hijack the object to monitor data changes
- Can't monitor array changes
- Each property of the object must be traversed
- Nested objects must be deeply traversed
- vue3 uses proxy to monitor object changes
- For objects: For the entire object, not a property of the object, so there is no need to traverse the keys.
- Support for arrays: Proxy does not need to overload the methods of arrays, which saves many hacks, reduces the amount of code, and reduces maintenance costs, and the standard is the best.
- The second parameter of Proxy can have 13 interception methods, which is richer than Object.defineProperty()
- Proxy, as a new standard, has been focused on and optimized by browser manufacturers. In contrast, Object.defineProperty() is an existing old method.
const reactive = (obj) => {
let depend = new Depend()
// 返回一个proxy对象, 操作代理对象, 如果代理对象发生变化, 原对象也会发生变化
return new Proxy(obj, {
get: (target, key) => {
// 收集依赖
depend.addDepend()
return Reflect.get(target, key)
},
set: (target, key, value) => {
Reflect.set(target, key, value)
// 当值发生改变时 触发
depend.notify()
}
})
}
// 修改 Depend类中的addDepend方法
// addDepend() {
// if (activeReactiveFn) {
// this.reactiveFns.push(activeReactiveFn)
// }
// }
let obj = {
name: 'lin'
}
let proxyObj = reactive(obj)
const foo = () => {
console.log(proxyObj.name)
}
watchFn(foo)
proxyObj.name = 'jin'
复制代码
Documentation:
Correctly collect dependencies
Whenever we change the proxy object (vue2 object), for example we add a age
property, even if change
it is not used in the function age
, we will trigger the change
function. So we need to collect dependencies correctly, how to collect dependencies correctly.
- Different objects are stored separately
- Different properties of the same object should also be stored separately
- To store objects we can use WeakMap
WeakMap
An object is a collection of key/value pairs, where the keys are weak references (which can be garbage collected when the original object is destroyed). Its key must be对象
, and the value can be arbitrary.
- You can use Map to store different properties of objects
Map
Objects hold key-value pairs and are able to remember the original insertion order of keys. Any value (object or primitive ) can be used as a key or a value.
const targetMap = new WeakMap()
const getDepend = (target, key) => {
// 根据target对象获取Map
let desMap = targetMap.get(target)
if (!desMap) {
desMap = new Map()
targetMap.set(target, desMap)
}
// 根据key获取 depend类
let depend = desMap.get(key)
if (!depend) {
depend = new Depend()
desMap.set(key, depend)
}
return depend
}
复制代码
const reactive = (obj) => {
return new Proxy(obj, {
get: (target, key) => {
// 收集依赖
const depend = getDepend(target, key)
depend.addDepend()
return Reflect.get(target, key)
},
set: (target, key, value) => {
const depend = getDepend(target, key)
Reflect.set(target, key, value)
// 当值发生改变时 触发
depend.notify()
}
})
}
复制代码
application entry module
Create a new html file and import all the js files where the created function is located
<script>
// 创建根组件
const App = {
// 需要进行响应式的数据
data: reactive({
counter: 0
}),
render() {
// h函数渲染节点
return h("div", {class: 'lin'}, [
h("div", {class: 'text'}, `${this.data.counter}`),
h("button", {onClick: () => {
this.data.counter++
console.log(this.data.counter)
}}, '+')
])
}
}
// 挂载根组件
const app = createApp(App)
app.mount("#app")
</script>
复制代码
Create a new js file save createApp
function, this function returns an object, and there is a mount
method in the object
const createApp = (rootComponent) => {
return {
mount(selector) {
const container = document.querySelector(selector)
// 响应式函数
watchEffect(function() {
const vNode = rootComponent.render()
// 把节点挂载到 #div
mount(vNode, container)
})
}
}
}
复制代码
There is a little problem with this, every time the +
button is clicked, a new node will be added
First mount (mount) --> value change --> patch
const createApp = (rootComponent) => {
return {
mount(selector) {
const container = document.querySelector(selector)
// isMounted 是否已经挂载
let isMounted = false
let oldVNode = null
watchEffect(function() {
if (!isMounted) {
oldVNode = rootComponent.render()
// 第一次挂载
mount(oldVNode, container)
isMounted = true
} else {
const newVNode = rootComponent.render()
// 对比新旧节点
patch(oldVNode, newVNode)
oldVNode = newVNode
}
})
}
}
}
复制代码
The writing is very good, and I will come back to improve it when I become bald! ! !
Reference documentation
王红元 《深入vue3 + typescript》