
写了两年的vue一直没有对它进行过总结,现在闲下来可以填填坑了。
VDOM
什么是虚拟DOM?虚拟DOM的作用。
老生常谈的问题了。如果说真实的DOM是一个大厦,那么说虚拟DOM就是这个大厦的设计图。虚拟DOM的更新效率并不一定比直接命令式的更新真实DOM要快,没有任何框架可以比纯手动的优化 DOM 操作更快,它的最主要目的是:
- 把真实DOM抽象出来,通过新旧对比获取最小代价更新真实DOM的方法。
- 更高的可维护性,相对于JQ命令式更新DOM,Vue、React框架中运用虚拟DOM可以更好地维护应用。
- 可以渲染到 DOM 以外的平台,实现 SSR、同构渲染这些高级特性Weex 等框架应用的就是这一跨端特性。
Vue的组成与工作原理
Vue 的三个核心模块:
Reactivity Module 响应式模块
Compiler Module 编译器模块
Renderer Module 渲染模块
响应式模块允许我们创建 JavaScript 响应对象并可以观察其变化。当使用这些对象的代码运行时,它们会被跟踪,因此,它们可以在响应对象发生变化后运行。
编译器模块获取 HTML 模板并将它们编译成渲染函数(render function)。这可能在运行时在浏览器中发生,但在构建 Vue 项目时更常见。这样浏览器就可以只接收渲染函数。
渲染模块的代码包含在网页上渲染组件的三个不同阶段:
渲染阶段
挂载阶段
补丁阶段
在渲染阶段,将调用 render 函数,它返回一个虚拟 DOM 节点。
在挂载阶段,使用虚拟DOM节点并调用 DOM API 来创建网页。
在补丁阶段,渲染器将旧的虚拟节点和新的虚拟节点进行比较并只更新网页变化的部分。
现在让我们来看一个例子,一个简单组件的执行。它有一个模板,以及在模板内部使用的响应对象。首先,模板编译器将 HTML 转换为一个渲染函数。然后初始化响应对象,使用响应式模块。接下来,在渲染模块中,我们进入渲染阶段。这将调用 render 函数,它引用了响应对象。我们现在监听这个响应对象的变化,render 函数返回一个虚拟 DOM 节点。接下来,在挂载阶段,调用 mount 函数使用虚拟 DOM 节点创建 web 页面。最后,如果我们的响应对象发生任何变化,正在被监视,渲染器再次调用render函数,创建一个新的虚拟DOM节点。新的和旧的虚拟DOM节点,发送到补丁函数中,然后根据需要更新我们的网页。
参考
跟尤雨溪一起解读Vue3源码【中英字幕】- Vue Mastery
Vue 3 Deep Dive with Evan You
手写Mini Vue
<!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>Document</title>
</head>
<style>
.red {
color: red;
}
</style>
<body>
<div id="app"></div>
</body>
<script src="./reactivity.js"></script>
<script>
// 第一部分compiler 编译成 render function => 形成虚拟dom
// 由于模板编译过于复杂,这里直接使用其编译结果,即vdom
function h(tag, props, children) {
return {
tag,
props,
children
}
}
// 第二部分 挂载到真实dom中 ,即mount方法
function mount(vdom, root) {
const el = vdom.el = document.createElement(vdom.tag)
// props
const props = vdom.props
if (props) {
for (const key in props) {
if (Object.hasOwnProperty.call(props, key)) {
if (key.startsWith('on')) {
document.addEventListener(key.slice(2).toLowerCase(), props[key])
}
el.setAttribute(key, props[key])
}
}
}
// children
if (vdom.children) {
const child = vdom.children
if (typeof child === 'string' || typeof child === 'number') {
el.textContent = child
} else {
child.forEach(currentChild => {
mount(currentChild, el)
});
}
}
root.appendChild(el)
}
// 第三部分 实现响应式 在reactivity文件中
// 第四部分 实现patch方法
// 思路:diff Vdom = 检查两个vdom中有何不同 = 比较各个方面 => 有何方面 => 检查Vdom的结构
// Vdom结构: { tag, props, children } => 从 tag , props , children 检查
// 所以 diff 实际就是比较 tag,props,children
function patch(oldNode, newNode) {
const el = newNode.el = oldNode.el
// 检查tag
if (newNode.tag === oldNode.tag) {
// 比较props
const newProps = newNode.props
const oldProps = oldNode.props
// 1. 有新的属性添加
for (const key in newProps) {
const newPropsValue = newProps[key]
const oldPropsValue = oldProps[key]
if (Object.hasOwnProperty.call(newProps, key)) {
if (oldPropsValue !== newPropsValue) {
el.setAttribute(key, newPropsValue)
}
}
}
// 2.有旧的属性删除
for (const key in oldProps) {
if (Object.hasOwnProperty.call(oldProps, key)) {
if (!key in newProps) {
el.removeAttribute(key)
}
}
}
// 比较children
// 目前children有两种形式 string字符串 和 数组
const oldChild = oldNode.children
const newChild = newNode.children
// 1.如果新children是字符串
if (typeof newChild === 'string') {
if (typeof oldChild === 'string' && oldChild !== newChild) {
el.textContent = newChild
} else {
el.textContent = newChild
}
} else {
// 2.如果新children是数组
if (typeof oldChild === 'string') {
el.innerHTML = ''
newChild.forEach(child => {
mount(child, el)
});
} else {
// 2.1 如果两者都是数组如何比较
// Vue有两种比较方法,一种是提供key作为提示(稍稍复杂),一种是粗暴的比较
// 这里用粗暴的比较:比较同一index下的子项类型是否一致
const commonLength = Math.min(oldChild.length, newChild.length)
for (let i = 0; i < commonLength; i++) {
patch(oldChild[i], newChild[i])
}
if (newChild.length > commonLength) {
newChild.slice(commonLength).forEach(child => mount(child, el))
} else if (newChild.length < oldChild.length) {
oldChild.slice(commonLength).forEach(child => el.removeChild(child.el))
}
}
}
} else {
// replace
}
}
// 第五部分 将以上的连接起来
const App = {
data: reactive({
count: 0
}),
methods: {
},
render() {
return h('div', { class: 'red', onClick: () => this.data.count++ }, String(this.data.count))
}
}
function createApp(app, root) {
let isMounted = false
let prevDom
watchEffect(() => {
if (!isMounted) {
prevDom = app.render()
mount(prevDom, root)
isMounted = true
} else {
const newDom = app.render()
patch(prevDom, newDom)
prevDom = newDom
}
})
}
createApp(App, document.querySelector('#app'))
</script>
</html>
响应式系统
// reactivity.js
let activeEffect
function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}
class Dep {
subscriber = new Set()
track() {
activeEffect && this.subscriber.add(activeEffect)
}
trigger() {
this.subscriber.forEach(effect => effect())
}
}
const DepMap = new WeakMap()
function getDep(target, key) {
let targetMap = DepMap.get(target)
if (!targetMap) {
DepMap.set(target, targetMap = new Map())
}
let dep = targetMap.get(key)
if (!dep) {
targetMap.set(key, dep = new Dep())
}
return dep
}
const reactiveHandler = {
get(target, key, receiver) {
const dep = getDep(target, key)
dep.track()
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const dep = getDep(target, key)
const res = Reflect.set(target, key, value, receiver)
dep.trigger()
return res
}
}
function reactive(obj) {
return new Proxy(obj, reactiveHandler)
}
网友评论