Vue3_14(虚拟DOM | 三大核心系统:Compiler、Runtime、Reactivity)
虚拟DOM的优势
对真实的元素节点进行抽象,抽象成VNode(虚拟节点),方便对其进行各种操作:
直接操作DOM来说是有很多的限制的,比如diff、clone等等;
可以使用JavaScript来表达非常多的逻辑,而对于DOM本身来说是非常不方便的;
其次是方便实现跨平台,包括你可以将VNode节点渲染成任意你想要的节点:
如渲染在canvas、WebGL、SSR、Native(iOS、Android)上;
并且Vue允许你开发属于自己的渲染器(renderer),在其他的平台上渲染;
虚拟DOM的渲染过程

VUE三大核心系统
Compiler模块:编译模板系统;
Runtime模块:也可以称之为Renderer模块,真正渲染的模块;
Reactivity模块:响应式系统;

简洁版的Mini-Vue框架,该Vue包括三个模块:渲染系统模块;可响应式系统模块;应用程序入口模块;
渲染系统实现
功能一:h函数,用于返回一个VNode对象;
功能二:mount函数,用于将VNode挂载到DOM上;
功能三:patch函数,用于对两个VNode进行对比,决定如何处理新的VNode;
h函数的实现:直接返回一个VNode对象即可
const h = (tag, props, children) => {
return {
tag,
props,
children
}
}
mount函数的实现:
第一步:根据tag,创建HTML元素,并且存储到vnode的el中;
第二步:处理props属性
如果以on开头,那么监听事件;
普通属性直接通过 setAttribute 添加即可;
第三步:处理子节点
如果是字符串节点,那么直接设置textContent;
如果是数组节点,那么遍历调用 mount 函数;
const mount = (vnode, container) => {
const el = (vnode.el = document.createElement(vnode.tag))
if (vnode.props) {
for (const 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)
}
}
}
if (vnode.children) {
if (typeof vnode.children === 'string') {
el.textContent = vnode.children
} else {
vnode.children.forEach((item) => {
mount(item, el)
})
}
}
container.appendChild(el)
}
Patch函数 – 对比两个VNode
n1和n2是不同类型的节点:
找到n1的el父节点,删除原来的n1节点的el;
挂载n2节点到n1的el父节点上;
n1和n2节点是相同的节点:
处理props的情况
先将新节点的props全部挂载到el上;
判断旧节点的props是否不需要在新节点上,如果不需要,那么删除对应的属性;
处理children的情况
如果新节点是一个字符串类型,那么直接调用 el.textContent = newChildren;
如果新节点不同一个字符串类型:
旧节点是一个字符串类型
将el的textContent设置为空字符串;
旧节点是一个字符串类型,那么直接遍历新节点,挂载到el上;
旧节点也是一个数组类型
取出数组的最小长度;
遍历所有的节点,新节点和旧节点进行path操作;
如果新节点的length更长,那么剩余的新节点进行挂载操作;
如果旧节点的length更长,那么剩余的旧节点进行卸载操作;
const patch = (n1, n2) => {
if (n1.tag !== n2.tag) {
const p = n1.el.parentNode
p.removeChild(n1.el)
mount(n2, p)
} else {
const el = (n2.el = n1.el)
//对象diff
const oldProps = n1.props || {}
const newProps = n2.props || {}
//新对象覆盖原对象,新增或更新key
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).toLowerCase(), newValue)
} else {
el.setAttribute(key, newValue)
}
}
}
//原对象对比新对象,删除多出key
for (const key in oldProps) {
if (key.startsWith('on')) {
const value = oldProps[key]
el.removeEventListener(key.slice(2).toLowerCase(), value)
}
if (!(key in newProps)) {
// if (key.startsWith('on')) {
// el.removeEventListener(key.slice(2).toLowerCase(), value)
// } else {
el.removeAttribute(key)
// }
}
}
const oldChildren = n1.children || []
const newChildren = n2.children || []
// 情况一: newChildren本身是一个string
if (typeof newChildren === 'string') {
if (typeof oldChildren === 'string') {
if (newChildren != oldChildren) {
el.textContent = newChildren
}
} else {
el.textContent = newChildren
}
} else {
// 情况二: newChildren本身是一个数组
if (typeof oldChildren === 'string') {
el.textContent = ''
newChildren.forEach((item) => {
mount(item, el)
})
} else {
//数组diff
const oldLen = oldChildren.length
const newLen = newChildren.length
const commonLen = Math.min(oldLen, newLen)
// oldChildren: [v1, v2, v3, v8, v9]
// newChildren: [v1, v5, v6]
//前面有相同节点的原生进行patch操作
for (let i = 0; i < commonLen; i++) {
patch(oldChildren[i], newChildren[i])
}
//新数组 比 旧数组多,则新增新
if (oldLen < newLen) {
newChildren.slice(commonLen).forEach((item) => mount(item, el))
}
//新数组比旧数组少,则删除旧
if (oldLen > newLen) {
oldChildren
.slice(commonLen)
.forEach((item) => el.removeChild(item.el))
}
}
}
// console.log(el)
}
}
可响应式系统
1、依赖收集
class Dep {
constructor() {
this.subscribes = new Set()
}
depend() {
if (activeEffect) {
this.subscribes.add(activeEffect)
}
}
notify() {
this.subscribes.forEach((effect) => effect())
}
}
let activeEffect = null
function watchEffect(effect) {
activeEffect = effect
dep.depend()
effect()
activeEffect = null
}
const dep = new Dep()
watchEffect(function () {
console.log('effect1:', info.counter * 2, info.name)
})
dep.notify()
2、响应式系统Vue2实现
const targetMap = new WeakMap()
const getDep = (target, key) => {
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对raw进行数据劫持
const reactive = (raw) => {
Object.keys(raw).forEach((key) => {
const dep = getDep(raw, key)
let value = raw[key]
Object.defineProperty(raw, key, {
get() {
dep.depend()
return value
},
set(newValue) {
value = newValue
dep.notify()
}
})
})
return raw
}
3、响应式系统Vue3实现
// vue3对raw进行数据劫持
const reactive = (raw) => {
return new Proxy(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()
}
})
}
完整响应式系统
let activeEffect = null
const targetMap = new WeakMap()
const getDep = (target, key) => {
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对raw进行数据劫持
// const reactive = (raw) => {
// Object.keys(raw).forEach((key) => {
// const dep = getDep(raw, key)
// let value = raw[key]
// Object.defineProperty(raw, key, {
// get() {
// dep.depend()
// return value
// },
// set(newValue) {
// value = newValue
// dep.notify()
// }
// })
// })
// return raw
// }
// vue3对raw进行数据劫持
const reactive = (raw) => {
return new Proxy(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()
}
})
}
function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}
class Dep {
constructor() {
this.subscribes = new Set()
}
depend() {
if (activeEffect) {
this.subscribes.add(activeEffect)
}
}
notify() {
this.subscribes.forEach((effect) => effect())
}
}
const info = reactive({ counter: 100, name: 'why' })
// watchEffect1
watchEffect(function () {
console.log('effect1:', info.counter * 2, info.name)
})
// watchEffect2
// watchEffect(function () {
// console.log('effect2:', info.counter * info.counter)
// })
// // watchEffect3
// watchEffect(function () {
// console.log('effect3:', info.counter + 10, info.name)
// })
// watchEffect(function () {
// console.log('effect4:', foo.height)
// })
// info.counter++
info.name = 'why123'
// foo.height = 2
为什么Vue3选择Proxy呢?
Object.definedProperty 是劫持对象的属性时,如果新增元素:
那么Vue2需要再次 调用definedProperty,而 Proxy 劫持的是整个对象,不需要做特殊处理;
修改对象的不同:
使用 defineProperty 时,我们修改原来的 obj 对象就可以触发拦截;
而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截;
Proxy 能观察的类型比 defineProperty 更丰富
has:in操作符的捕获器;
deleteProperty:delete 操作符的捕捉器;
等等其他操作;
Proxy 作为新标准将受到浏览器厂商重点持续的性能优化;
缺点:Proxy 不兼容IE,也没有 polyfill, defineProperty 能支持到IE9
应用程序入口
createApp用于创建一个app对象;
该app对象有一个mount方法,可以将根组件挂载到某一个dom元素上;
function createApp(rootComponent) {
return {
mount(selector) {
const container = document.querySelector(selector)
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
}
})
}
}
}

浙公网安备 33010602011771号