注:
- 学习版本vue:3.2.23
- 最新版使用了pnpm包管理工具
- 启用dev后会生成packages/vue/dist/vue.global.js文件,为vue的SourceMap映射,可以参考examples文件夹中的实例使用,然后就可以调试了
项目结构
vue分为3个模块
- Reactivity Module(响应式系统)
- Compiler Module (编译阶段,将template转换成可供renderer调用的函数)
- Renderer Module
- render phase(将render function转换成vartuil DOM)
- mount phase(将vartuil DOM绘制成真实的DOM)
- patch phase(将old vartuil DOM与new vartuil DOM对比,绘制不同部分)
reactivty
reactive
该方法主要判断接收值的类型,是否只读,是否已经被打上标记,而后利用Proxy代理
baseHandle
为Proxy代理的基础。
- get、has、ownKeys中调用track触发订阅,记录需要订阅的方法。其中get还处理了更多的边界情况以及是否是浅层代理
- set、deleteProperty中调用trigger,用于触发effect副作用,同时判断事件类型(新增、删除、修改等)
effect
创建ReactiveEffect对象用于管理自身需要监听的值,以及调用副作用方法时的一些上下文。如果不是lazy类型,则立马执行副作用,副作用中如果有被Proxy值的get,则触发了track方法
- track:为每个对象建立在全局WeakMap上的弱引用Map,为对象上的每个键创建Dep(发布订阅),调用trackEffects方法,判断是否要将activtyEffect(全局定义,用于存储当前执行的effect)加入至Dep中,同时在ReactiveEffect上引用当前dep方便ReactiveEffect主动解除绑定
- trigger:接受事件类型后判断要通知哪些Dep,调用triggerEffects,执行所有effect(effect中包含scheduler和run方法,如果有则会执行scheduler,scheduler可以是queueJob)
ref
该api内部实现相比reactive简单得多,判断如果是深度拷贝并且isObject,则value = toReactive(value),否则直接使用。然后编写set、get逻辑
get value() {
trackRefValue(this) // 内部创建了Dep,调用了trackEffects方法
return this._value
}
set value(newVal) {
newVal = this._shallow ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
}
}
computed
如果传入的是Function,则禁用set,如果是ref,则复用ref的set和get。内部由ReactiveEffect实现,并重写scheduler
内部包含变_dirty,只有当computed创建对象被别人使用时scheduler才生效。并通过改值的true和false交换,使得一次更新只计算一次
nextTick(异步执行核心)
该方法在runtime-core包中
- 全局定义两个Promise,resolvedPromise、currentFlushPromise(会在resolvedPromise结束后有则执行)
- nextTick将执行函数fn设置为在currentFlushPromise|resolvedPromise结束后执行
- queueJob,在forceUpdata、reload、renderer中会调用,状态值设置后最终也会执行,表示要开始执行队列中的任务了。内部去重排序任务,然后开启Promise(只会开启一次直到执行结束)。开启的同时设置currentFlushPromise
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
- flushJobs中消耗任务,内部再次对任务进行排序执行,并使用全局变量flushIndex应对当前job中新生成的job插入位置。(执行job时可能会产生新的副作用,而新的副作用可能是之前执行过的job,需要再次执行。因此新添加job只需以flushIndex以后的对比去重)
<div id='app'>
<div v-if='state.bool'>
<div id='text'>
{{state.num}}
</div>
</div>
<button @click='click'>点击</button>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const { createApp, reactive, nextTick } = Vue;
const vue = Vue.createApp({
setup() {
const state = reactive({
bool: false,
num: 1
})
const click = () => {
nextTick(() => { // fn1
console.log('before', document.querySelector("#text")) // null
})
state.bool = !state.bool // set
state.num++
nextTick(() => { // fn2
console.log('after', document.querySelector("#text"))
})
}
return {
state,
click
}
}
})
vue.mount("#app")
</script>
从以上代码中可以看到第一个nextTick将fn加入到resolvedPromise中,第二个nextTick将fn加入到currentFlushPromise中,所以微任务添加顺序是fn1加到resolvedPromise,flushJobs加到resolvedPromise,设置currentFlushPromise为resolvedPromise.then()的执行结果,fn2加到currentFlushPromise。所以点击第一下fn1检查不到元素,点击第二下fn2检查不到元素。
流程
mount -> 收集effect -> get -> track绑定至Dep -> ... -> set -> queueJob -> flushJobs消耗effect -> patch -> ...
核心代码实现
关于mount的实现
递归遍历h对象
function h(tag, props, children) {
return {
tag,
props,
children
}
}
function mount(vnode, container) {
// 记录vnode.el方便patch阶段获取dom节点
let element = vnode.el = document.createElement(vnode.tag)
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key]
element.setAttribute(key, value)
}
}
if (vnode.children) {
if (typeof vnode.children === 'string') {
element.innerText = vnode.children
} else {
vnode.children.forEach(item => {
if (typeof item === 'string') {
element.appendChild(document.createTextNode(item))
} else {
mount(item, element)
}
})
}
}
container.appendChild(element)
}
const vdom = h('div', { class: 'red' }, [
h('span', null, 'hello') // children暂不支持[text]
])
mount(vdom, document.querySelector("#app"))
关于patch实现
利用递归,先对比tag和key,然后对比props,最后递归children。我们将看到patch里面递归效率极低,vue Compiler将优化节点,为响应式节点或props打上标记,递归时智能识别有修改的节点
function patch(oldNode, newNode) {
if (oldNode.tag === newNode.tag) {
const el = newNode.el = oldNode.el
// 处理props
const oldProps = oldNode.props || {}
const newProps = newNode.props || {}
for (const key in newProps) {
const newValue = newProps[key]
const oldValue = oldProps[key]
if (newValue !== oldValue) {
if (key.startsWith('on')) {
el.removeEventListener(key.substring(2).toLocaleLowerCase(), oldValue)
el.addEventListener(key.substring(2).toLocaleLowerCase(), newValue)
} else {
el.setAttribute(key, newValue)
}
}
}
for (const key in oldProps) {
const newValue = newProps[key]
const oldValue = oldProps[key]
if (!newValue) {
if (key.startsWith('on')) {
el.removeEventListener(key.substring(2).toLocaleLowerCase(), oldValue)
} else {
el.removeAttribute(key)
}
}
}
// 处理children
const oldChildren = oldNode.children
const newChildren = newNode.children
if (typeof newChildren === 'string') {
if (newChildren !== oldChildren) {
el.innerText = newChildren
}
} else if (!newChildren) {
el.innerHTML = ""
} else if (typeof oldChildren === 'string') {
el.innerHTML = ""
newChildren.forEach(item => {
mount(item, el)
})
} else {
const minLength = Math.min(oldChildren.length, newChildren.length)
for (let i = 0; i < minLength; i++) {
patch(oldChildren[i], newChildren[i])
}
if (oldChildren.length > minLength) {
oldChildren.slice(minLength).forEach(item => {
el.removeChild(item.el)
})
}
if (newChildren.length > minLength) {
newChildren.slice(minLength).forEach(item => {
mount(item, el)
})
}
}
} else {
// 此处处理移除后重新创建逻辑
}
}
响应式原理
利用发布订阅模式,第一次调用watchEffect便订阅,设置值后通知
let currentEffect
class Dep {
list = new Set()
add() {
if (currentEffect) {
this.list.add(currentEffect)
}
}
notify() {
this.list.forEach(fn => {
fn()
})
}
}
const weakMap = new WeakMap()
function getDep(obj, key) {
let map = weakMap.get(obj)
if (!map) {
map = new Map()
weakMap.set(obj, map)
}
let dep = map.get(key)
if (!dep) {
dep = new Dep()
map.set(key, dep)
}
return dep
}
const handler = {
get(obj, key, rec) {
const dep = getDep(obj, key)
dep.add()
return Reflect.get(obj, key, rec)
},
set(obj, key, value, rec) {
const dep = getDep(obj, key)
const result = Reflect.set(obj, key, value, rec)
dep.notify()
return result
}
}
function reactive(state) {
return new Proxy(state, handler)
}
function watchEffect(effect) {
currentEffect = effect
effect()
currentEffect = undefined
// 为保证每次执行时收集effect中最新的依赖,可以使用闭包封装
// function fn() {
// currentEffect = fn
// effect()
// currentEffect = undefined
// }
// fn()
// 应对如下写法
// watchEffect(() => {
// if (state.bool) {
// console.log(state.count);
// }
// })
}
const state = reactive({
count: 0,
})
watchEffect(() => {
console.log(state.count);
})
state.count++
vue-minni,点击数字数字加一
const App = {
data: reactive({ number: 1 }),
render() {
return h('div', {
onClick: () => {this.data.number++}
}, String(this.data.number))
}
}
function createApp(app, container) {
let isMount = true
let preDom
watchEffect(() => {
if (isMount) {
preDom = app.render()
mount(preDom, container)
isMount = false
} else {
const nowDom = app.render()
patch(preDom, nowDom)
preDom = nowDom
}
})
}
createApp(App, document.querySelector('#app'))
结束语
本文是我花了几天时间对vue源码的一个大体理解,如果有有问题的地方,望指正。后续我将继续对vue源码进行更深入的学习,同时修补该篇文章的问题。