我们传统的前端开发中,我们是编写自己的HTML,最终被渲染到浏览器上的
但是这样存在一个问题,就是dom元素本身是非常庞大的,每一个node节点上的属性是非常多的。
频繁操作dom必然是比较损耗性能 ,所以目前框架都会引入虚拟DOM来对真实的DOM进行抽象
VDOM
-
vdom和dom中的每一个节点都是一一映射的,每一个vnode对应一个node
-
vdom是使用JavaScript表示的树结构,所以相比直接操作dom,使用JavaScript来操作vdom更为的灵活,限制更少
更方便对节点进行逻辑操作
-
每一个vnode上只存在必要的属性,所以相对于node而言,vnode是十分轻量级的 -
vdom本质上是一个中间代码,我们可以将VNode节点渲染成任意你想要的节点- 如渲染在canvas、WebGL、SSR、Native(iOS、Android),浏览器上
- Vue允许你开发属于自己的渲染器(renderer),在其他的平台上渲染
三大核心系统
-
Compiler模块:编译模板系统
-
Runtime模块:
- 在运行的时候,将vdom转换为具体的dom或控件等
- 也可以称之为Renderer模块,真正渲染的模块
-
Reactivity模块:响应式系统
真实DOM渲染
runtime.js
// 生成对应的vnode
const h = (tag, props, children) => ({
tag,
props,
children
})
// vdom => dom
function 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]
// 事件处理,例如onClick
if (key.startsWith('on')) {
const eventName = key.slice(2).toLowerCase()
// 添加事件
el.addEventListener(eventName, 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))
}
}
// 将生成的真实DOM 挂载到 挂载点
container.appendChild(el)
}
使用
<body>
<div id="app"></div>
<script src="./runtime.js"></script>
<script>
// 虚拟dom
const vnode = h('div', { class: 'foo' }, [
h('div', null, '当前计数: 0'),
h('button', {
onClick: () => console.log('clicked')
}, 'increment')
])
// 挂载
mount(vnode, document.getElementById('app'))
</script>
</body>
渲染器
更新操作
// 过2s后,修改vnode数据,进行patch操作
setTimeout(() => {
const newVnode = h('div', { class: 'baz', id: 'foo' }, [
h('h2', null, 'klaus'),
h('button', {
onClick: () => console.log('clicked')
}, 'click me')
])
patch(vNode, newVnode)
}, 2000)
patch函数
// patch --- 更新节点
function patch(oldNode, newNode) {
// 新旧节点类型不同 直接替换
if (oldNode.tag !== newNode.tag) {
const parent = oldNode.el.parentElement
parent.removeChild(oldNode.el)
mount(newNode, parent)
} else {
const el = newNode.el = oldNode.el
// patch props
const newProps = newNode.props || {}
const oldProps = oldNode.props || {}
// 新节点属性 一定在patch后的元素上
for (const key in newProps) {
const newValue = newProps[key]
const oldValue = oldProps[key]
// 属性值可能是函数,此时newValue 和 oldValue恒不等
// 因此需要将其转换为字符串后再判断前后函数的函数体是否一致
if (newValue.toString() !== oldValue.toString()) {
if (key.startsWith('on')) {
const eventName = key.slice(2).toLowerCase()
el.addEventListener(eventName, newValue)
} else {
el.setAttribute(key, newValue)
}
}
}
// 移除无用的旧节点
for(const key in oldProps) {
if (!(key in newProps)) {
if (key.startsWith('on')) {
const eventName = key.slice(2).toLowerCase()
el.removeEventListener(eventName, oldProps[key])
} else {
el.removeAttribute(key)
}
}
}
// patch children
if(typeof newNode.children === 'string') {
// 排除类似 newValue.children = 'aaa'
// oldNode.children = 'aaa' 的情况
if (newNode.children !== oldNode.children) {
el.innerHTML = newNode.children
}
} else {
const newChildren = newNode.children || []
const oldChildren = oldNode.children || []
// newNode.children是数组
const length = Math.min(newChildren.length, oldChildren.length)
for(let i = 0; i < length; i++) {
patch(oldChildren[i], newChildren[i])
}
if (newChildren.length > oldChildren.length) {
newChildren.slice(oldChildren.length).forEach(node => mount(node, el))
} else {
oldChildren.slice(newChildren.length).forEach(node => el.removeChild(node.el))
}
}
}
}
响应式系统
v1
class Dep {
constructor() {
// 保证所有的依赖都只会被添加一次
// 避免因为依赖的重复添加,导致数据发生重复更新
this.subscribers = new Set()
}
// 值的改变会引起依赖的改变
// 所以可以认为 依赖随值的改变 是 值改变产生的副作用
addEffect(effect) {
this.subscribers.add(effect)
}
// 更新所有依赖
notify() {
this.subscribers.forEach(effect => effect())
}
}
const dep = new Dep()
// 测试代码
let info = { num: 0, name: 'coderwxf' }
function double() {
console.log(info.num * 2)
}
function power() {
console.log(info.num * info.num)
}
function changeName() {
info.name = 'Klaus'
console.log(info.name)
}
// 手动添加依赖
dep.addEffect(double)
dep.addEffect(power)
// 因为只有一个dep,所以所有的依赖都会添加到一个dep中
// 当num发生改变的时候,changeName这个副作用函数是不应该被执行的
// 但是此时却被执行了,所以不同的依赖应该添加到不同的Dep实例中
dep.addEffect(changeName)
info.num++
dep.notify()
v1存在着几个问题:
-
所有的依赖都是要手动添加的,而且更新的时候也都是要手动通知更新依赖
-
一个副作用函数中,可能存在不止一个依赖,不同依赖发生改变的时候,需要更新的副作用函数也是不一样的
但是
v1中如果某一个依赖发生了改变,那么所有的副作用函数都会重新被触发
V2
class Dep {
constructor() {
// 收集依赖的set集合
this.subscribers = new Set()
}
depend() {
// 如果有副作用函数,那么就存储副作用函数
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
notify() {
// 通知所有的依赖,更新数据
this.subscribers.forEach(effect => effect())
}
}
// 设置全局effect,目的是为了让dep和副作用函数解耦
let activeEffect = null
function watchEffect(effect) {
activeEffect = effect
// 刚开始的时候就要执行一次,以便于收集所有的依赖
effect()
activeEffect = null
}
// 最外层需要定义为WeakMap,以便于对应的map不会成为target在gc中的依赖
// 也就是说方便我们直接使用类似info = null的方式去移除数据,
// 而不会因为targetMap中存在依赖,而无法移除数据
// 另一方面 weakMap的key需要设置成对象,而target本身就是一个对象
const targetMap = new WeakMap()
// 获取dep实例
function getDep(target, key) {
let depMap = targetMap.get(target)
if (!depMap) {
depMap = new Map()
targetMap.set(target, depMap)
}
let dep = depMap.get(key)
if (!dep) {
dep = new Dep()
depMap.set(key, dep)
}
return dep
}
// vue2的方式,使用defineProperty
// 使用劫持后的数据去覆盖原始数据
// 这样当被劫持的数据发生更新和修改的时候,
// 可以对数据进行二次加工操作
// 即可以通过数据劫持 自动去收集依赖,自动去更新依赖
function 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) {
if (newValue !== value) {
value = newValue
dep.notify()
}
}
})
})
return raw
}
// test code
let info = reactive({ num: 0, name: 'coderwxf' })
let foo = reactive({ width: 200, height: 300 })
watchEffect(function () {
console.log("effect1:", info.num * 2)
})
watchEffect(function () {
console.log("effect2:", info.num * info.num)
})
watchEffect(function () {
console.log("effect3:", info.name)
})
// info.num++
info.name = 'Alex'
在vue3, 使用proxy来取代了definedProperty
function reactive(raw) {
// 返回的是一个新的代理对象
return new Proxy(raw, {
// target 指代的就是代理对象,在这里就是raw对象
// key是对象的每一个属性
get(target, key) {
const dep = getDep(target, key)
dep.depend()
// 因为返回的是原始对象上的值,而不是代理对象上的值,因此不会产生死循环
return target[key]
},
set(target, key, newV) {
const dep = getDep(target, key)
// 修改完值以后,再更新数据
target[key] = newV
dep.notify()
}
})
}
框架外层API设计
index.js
function createApp(root) {
return {
mount (selector) {
const el = document.querySelector(selector)
let isMounted = false
let oldNode = null
// 设置watch监听
// 第一次 --- mount
// 第二次 --- patch
watchEffect(() => {
if (!isMounted) {
oldNode = root.render()
mount(oldNode, el)
isMounted = true
} else {
const newNode = root.render()
patch(oldNode, newNode)
}
})
}
}
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mini Vue</title>
</head>
<body>
<div id="app"></div>
<script src="./runtime.js"></script>
<script src="./reactive.js"></script>
<script src="./index.js"></script>
<script>
const App = {
data: reactive({
count: 0
}),
// 使用render函数来替换template
render() {
return h('div', { class: 'old', id: 'foo' }, [
h('div', null, `当前计数: ${this.data.count}`),
h('button', {
// 事件是被绑定到元素上的
// 所以为了获取正确的this,这里需要使用箭头函数
onClick: () => this.data.count++
}, 'increment')
])
}
}
const app = createApp(App)
app.mount('#app')
</script>
</body>
</html>