Vue3源码
三大核心系统
虚拟 DOM 优势
- 首先是可以对真实的元素节点进行抽象,抽象成 VNode(虚拟节点),这样方便后续对其进行各种操作
- 对于直接操作 DOM 来说是有很多限制的,比如:diff、clone 等,但是使用 JavaScript 来操作就变得非常简单
- 我们可以使用 JavaScript 来表达非常多的逻辑,对于 DOM 本身来说是非常不方便的
- 其次是方便实现跨平台,包括你可以将 VNode 节点渲染成任意你想要的节点
- 如渲染在 canvas、WebGL、SSR、Native(IOS、Android)上
- 并且 Vue 允许你开发属于自己的渲染器(renderer),在其他的平台上渲染
虚拟 DOM 优势渲染过程
三大核心系统
- Compiler 模块:编译模板系统
- Runtime 模块:也可以称之为 Renderer 模块,真正渲染的模块
- Reactivity 模块:响应式系统
三个系统之间如何协调工作?
Mini-Vue
Vue 包括三个模块:
- 渲染系统模块
- 响应式系统模块
- 应用程序入口模块
渲染系统实现
- 功能一:h 函数,用于返回一个 VNode 对象
- 功能二:mount 函数,用于将 VNode 挂载到 DOM 上
- 功能三:patch 函数,用于对两个 VNode 进行对比,决定如何处理新的 VNode
h 函数
- 直接返回一个 VNode 对象即可
const h = (tag, props, children) => {
// vnode -> javascript 对象
return {
tag,
props,
children
}
}
mount 函数(挂载 VNode)
- 第一步:根据 tag,创建 HTML 元素,并且存储到 vnode 的 el中
- 第二步:处理 props 属性
- 如果以 on 开头,那么监听事件
- 普通属性直接通过 setAttribute 添加即可
- 第三步:处理子节点
- 如果是字符串节点,那么直接设置 textContent
- 如果是数组节点,那么遍历调用 mount 函数
const mount = (vnode, container) => {
// vnode -> element
// 1.创建出真实的原生, 并且在 vnode 上保留 el
const el = (vnode.el = document.createElement(vnode.tag))
// 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).toLowerCase(), value)
} else {
el.setAttribute(key, value)
}
}
}
// 3.处理 children
if (vnode.children) {
if (typeof vnode.children === 'string') {
el.textContent = vnode.children
} else {
vnode.children.forEach(item => {
mount(item, el)
})
}
}
// 4.将 el 挂载到 container 上
container.appendChild(el)
}
patch 函数(对比两个 VNode)
patch 函数的实现,分为两种情况
- n1 和 n2 是不同类型的节点
- 找到 n1 的 el 父节点,删除原来的 n1 节点的 el
- 挂载 n2 节点到 n1 的 el 父节点上
- n1 和 n2 节点是相同节点
- 处理 props 的情况
- 先将新节点的 props 全部挂载到 el 上
- 判断旧节点的 props 是否不需要在新节点上,如果不需要,那么删除对应的属性
- 处理 children 的情况
- 如果新节点是一个字符串类型,那么直接调用
el.textContent = newChildren
- 如果新节点不是一个字符串类型
- 旧节点是一个字符串类型
- 将 el 的 textContent 设置为空字符串
- 旧节点是一个字符串类型,那么直接遍历新节点,挂载到 el 上
- 旧节点也是一个数组类型
- 取出数组的最小长度
- 遍历所有的节点,新节点和旧节点进行 patch 操作
- 如果新节点的 length 更长,那么剩余的新节点进行挂载操作
- 旧节点是一个字符串类型
- 如果新节点是一个字符串类型,那么直接调用
- 处理 props 的情况
const patch = (n1, n2) => {
if (n1.tag !== n2.tag) {
const n1ElParent = n1.el.parentElement
n1ElParent.removeChild(n1.el)
mount(n2, n1ElParent)
} else {
// 1.取出 element 对象,并在 n2 中进行保存
const el = (n2.el = n1.el)
// 2.处理 props
const oldProps = n1.props || {}
const newProps = n2.props || {}
// 2.1.获取所有的 newProps 添加到 el
for (const 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)
}
}
}
// 2.2.删除旧的 props
for (const key in oldProps) {
if (key.startsWith('on')) {
const value = oldProps[key]
el.removeEventListener(key.slice(2).toLowerCase(), value)
}
if (!key in newProps) {
el.removeAttribute(key)
}
}
// 3.处理children
const oldChildren = n1.children || []
const newChildren = n2.children || []
if (typeof newChildren === 'string') {
// 情况一: newChildren 本身是一个 string
// 边界情况 (edge case)
if (typeof oldChildren === 'string') {
if (newChildren !== oldChildren) {
el.textContent = newChildren
}
} else {
el.innerHTML = newChildren
}
} else {
// 情况二: newChildren 本身是一个数组
if (typeof oldChildren === 'string') {
el.innerHTML = ''
newChildren.forEach(item => {
mount(item, el)
})
} else {
// oldChildren: [v1, v2, v3, v8, v9]
// newChildren: [v1, v5, v6]
// 1.前面有相同节点的元素进行 patch 操作
const commonLength = Math.min(oldChildren.length, newChildren.length)
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i])
}
// 2.newChildren.length > oldChildren.length
if (newChildren.length > oldChildren.length) {
newChildren.slice(oldChildren.length).forEach(item => {
mount(item, el)
})
}
// 3.newChildren.length < oldChildren.length
if (newChildren.length < oldChildren.length) {
oldChildren.slice(newChildren.length).forEach(item => {
el.removeChild(item.el)
})
}
}
}
}
}
响应式系统模块
Dep
class Dep {
constructor() {
this.subscribers = new Set()
}
addEffect(effect) {
this.subscribers.add(effect)
}
notify() {
this.subscribers.forEach(effect => {
effect()
})
}
}
const info = { counter: 100 }
const dep = new Dep()
function doubleCounter() {
console.log(info.counter * 2)
}
function powerCounter() {
console.log(info.counter * info.counter)
}
dep.addEffect(doubleCounter)
dep.addEffect(powerCounter)
info.counter++
dep.notify()
添加 Effect 方法重构
class Dep {
constructor() {
this.subscribers = new Set()
}
/* addEffect(effect) {
this.subscribers.add(effect)
} */
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
notify() {
this.subscribers.forEach(effect => {
effect()
})
}
}
let activeEffect = null
function watchEffect(effect) {
activeEffect = effect
dep.depend()
effect()
activeEffect = null
}
const info = { counter: 100 }
const dep = new Dep()
watchEffect(function () {
console.log(info.counter * 2)
})
watchEffect(function () {
console.log(info.counter * info.counter)
})
info.counter++
dep.notify()
reactive 和 getDep 的实现
Map({ key: value })
:key 可以是任意类型
WeakMap({ key(对象): value })
:key只能将对象作为键名
对 key 的引用是弱引用,不影响垃圾回收器的工作
只要对应 key 置为 null,其 value 就不存在了,规避内存泄露的风险
jslet button = document.getElementById('button') let wm = new WeakMap() wm.set(button, { count: 0 }) function buttonClick() { let data = wm.get(button) data.count += 1 console.log(data.count) } button.addEventListener('click', buttonClick) button.removeEventListener('click', buttonClick)
正因为 weackMap 对键名引用的对象是弱引用关系,因此 weakMap 内部成员是会取决于垃圾回收机制有没有执行,运行前后成员个数可能是不一样的,而垃圾回收机制的执行又是不可预测的,因此不可遍历
可以做私有成员
jslet Stack = (function () { let vm = new WeakMap() return class { constructor() { vm.set(this, []) } push(el) { vm.get(this).push(el) } toString(el) { console.log(vm.get(this)) } } })() let stack = new Stack() stack.push(1) stack.push(2) console.log(stack) // {} stack.toString() // [1, 2]
class Dep {
constructor() {
this.subscribers = new Set()
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
notify() {
this.subscribers.forEach(effect => {
effect()
})
}
}
let activeEffect = null
function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}
const targetMap = new WeakMap()
function getDep(target, key) {
// 1.根据对象取出对应的Map对象
let depsMap = target.get(target)
if (depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 2.取出具体的dep对象
let dep = depsMap.get(key)
if (!dep) {
dep = new Dep()
depsMap.set(key, dep)
}
return dep
}
function reactive(raw) {
Object.keys(raw).forEach(key => {
const dep = new Dep()
let value = raw[key]
Object.defineProperty(raw, key, {
get() {
dep.depend()
return value
},
set(newValue) {
if (value !== newValue) {
value = newValue
dep.notify()
}
}
})
})
return raw
}
const info = reactive({ counter: 100, name: 'cat' })
const foo = reactive({ height: 1.88 })
watchEffect(function () {
console.log('effect1', info.counter * 2, info.name)
})
watchEffect(function () {
console.log('effect2', info.counter * info.counter)
})
watchEffect(function () {
console.log('effect3', info.counter * 2, info.name)
})
watchEffect(function () {
console.log('effect4', foo.height)
})
// info.counter++
info.name = 'bird'
/*
effect1 200 cat
effect2 10000
effect3 200 cat
effect4 1.88
effect1 200 bird
effect3 200 bird
*/
proxy
vue2 使用 Object.defineProperty
劫持 data 属性中的 getter 和 setter,在数据变动时发布消息给订阅者
Object.defineProperty
劫持对象的属性时,如果新增元素:
- Vue2 需要再次调用 defineProperty,而 Proxy 劫持的是整个对象,不需要做特殊处理
修改对象的不同:
- 使用 defineProperty 时,我们修改原来的 obj 对象就可以触发拦截
- 而使用 Proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截
Proxy 能观察的类型比 defineProperty 更丰富
- has:in 操作符的捕获器
- deleteProperty:delete 操作符的捕捉器
Proxy 作为新标准将受到浏览器厂商重点持续的性能优化
- 缺点:不兼容 IE,也没有 polyfill
- deleteProperty 能支持到 IE9
function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
const dep = getDep(target, key)
dep.depend()
return Reflect.get(target, key)
},
set(target, key, newValue) {
const dep = getDep(target,key)
const result = Reflect.set(target, key)
dep.notify()
return result
}
})
}
应用程序入口模块
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
}
})
}
}
}
源码阅读
createApp 入口
packages/runtime-dom/src/index.ts
- 先拿到渲染器(对象),里面有一个属性:createApp
packages/runtime-core/src/renderer.ts
- 渲染器对象,最终会返回一个对象,里面带有:createApp
packages/runtime-core/src/apiCreateApp.ts
- 里面定义了 app 对象,有 mixin、component、mount 等方法
调用 mount 时,首先会使用 createVNode 来创建 vnode 对象,之后调用 render 渲染 vnode
根据传入的 App 创建一个 VNode
组件 -> VNode -> 真实 DOM
渲染 VNode
render(vnode, container)
mount 流程
packages/runtime-core/src/renderer.ts
patch 函数
- 判断类型,因为第一次传的是一个根组件(
<App/>
),会走processComponent
来处理组件函数
processComponent
会判断旧的 vnode 是否挂载过,第一次是没有挂载的,会调用mountComponent
来处理组件函数
组件的 VNode 和 instance 区别:
- 组件的 VNode:虚拟 DOM 里的虚拟节点
- 组件的 instance:保存组件的各种状态
mountComponent
会创建组件实例,并且初始化 props、data 等数据,接下来会调用 setupRenderEffect
setupRenderEffect
这里会调用 effet 函数(当数据发生变化时就会重新执行),函数里面判断组件是否挂载,没有则挂载组件,有则更新组件。第一次没有挂载会走挂载操作
挂载操作会拿到组件 subTree,在调用 patch
方法
patch
方法会判断节点的类别,如果有根会直接调用 processElement
挂载元素,如果没有根会当成 Fragment 来处理
有根会调用 mountElement
挂载节点
mountElement
首先会调用 hostCreateElement
创建元素
如果里面有 children 会调用 mountChildren
挂载,最终会把 el 挂在到 container 上
组件的初始化
packages/runtime-core/src/component.ts
处理 props 和 attrs
instance.props
、instance.attrs
处理 slots
instance.slots
执行 setup
instance.setupState = proxyRefs(result)
编译 template
template -> render
对 vue2 的 options api 进行支持
处理完 props、slots 会设置有状态的组件,调用 setup 函数,执行 handleSetupResult
,
setup 会把结果放到 setupState 里
调用 compiler 函数
对 vue2.x 的 options api 进行支持
applyOptions
里处理生命周期、methods、computed...
Compile 过程
Scope Hoisting、Block Tree -> Vue3 性能优化
- 在 Vue 中更新是组件级别的,在下一次更新时会重新执行 render 函数,如果在 render 函数里面写 createVNode,每次都会执行会浪费性能
- 所以 Vue3 对此进行优化,对于不会改变的静态节点进行了作用域的提升
- render 函数在 return 时进行了 openBlock 操作,它会创建一个数组,会将可能修改的元素放到这个数组中
[divVNode, buttonVNode] -> dynamicChildren
,之后 diff 时只 diff 可能会修改的元素即可
生命周期回调
v2 v3 对比
template 中数据的使用顺序
vue3
packages/runtime-core/src/component.ts
这里解构 ctx、setupState 等变量
那这里的 ctx 是什么呢?
packages/runtime-core/src/componentOptions.ts
- methods、data、computed 等都放在 ctx 中
setupState 是 setup 的返回结果
最后通过一个 if 进行判断,取值优先级顺序如下:
- setup
- data
- props
- context
Vue3 可以进行代码测试:
<div id="app"></div>
<script src="https://unpkg.com/vue@3"></script>
<script>
const Son = {
template: `<div>{{message}}</div>`,
props: {
message: String
},
data() {
return {
message: 'data'
}
},
computed: {
message() {
return 'computed'
}
},
setup() {
const message = Vue.ref('setup')
return { message }
}
}
const app = Vue.createApp({
components: { Son },
template: `<Son message="props"></Son>`,
}).mount('#app')
</script>
vue2
src/core/instance/state.ts
初始化顺序:props
、setup
、methods
、data
、computed
、watch
v-for v-if 优先级
vue2
src/compiler/codegen/index.ts
v-for
优先级比 v-if
高
vue3
v-if
优先级比 v-for
高