Vue3源码学习

第一次这样学习源码还是挺有难度的,希望后面可以慢慢提升,有自己阅读源码的能力叭

1. 真实的DOM渲染

传统的前端开发中,我们是编写自己的HTML,最终被渲染到浏览器上的,这个过程大概是:

解析 html 转化成 DOM 树,然后渲染到页面

image.png

2. 虚拟DOM

2.1 渲染过程

image.png

3. Vue 源码的三大核心系统

  • Complier模块:编译模板系统

  • Runtime模块:也可以称之为 Renderer模块,真正渲染的模块

  • Reactivity模块:响应式系统

image.png

  • 三大系统协同工作

image.png

4. 实现 Mini-Vue

mini-vue的实现也是,后面有时间得回来重新看看

包括三部分:

  • 渲染系统模块
  • 可响应式系统模块
  • 应用程序入口模块

4.1 渲染系统实现

包含三个功能:

  • h 函数,返回一个 VNode 对象
  • mount 函数,用于将 VNode 挂载到 DOM 上
  • patch 函数,用于对比两个 VNode,决定如何处理新的VNode(diff)

h函数生成 VNode

// 生成 VNode
// 直接返回一个 VNode对象即可
const h = (tag, props, children) => {
return {
tag,
props,
children
}
}

mount函数挂载 vnode

// mount函数, 挂载VNode
const mount = (vnode, container) => {
// 1.根据tag创建HTML元素,并且存储到vnode的el中
const el = vnode.el = document.createElement(vnode.tag)
// 2.处理props属性
// 2.1 如果以on开头,那么监听事件
// 2.2 普通属性直接通过 setAttribute 添加
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,对比两个 VNode
const patch = (n1, n2) => {
// 1. tag不同,直接加入新的节点
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") { // 3.1newChildren本身是一个string
// 边界情况 如果oldChildren也是一个字符串
if (typeof oldChildren === "string") {
if (newChildren !== oldChildren) {
el.textContent = newChildren
}
} else {
el.innerHTML = newChildren
}

} else { // 3.2 newChildren本身是一个数组
if (typeof oldChildren === "string") {
el.innerHTML = ""
newChildren.forEach(item => {
mount(item, el)
})
} else {
// oldChildren 也是数组
// 1. 前面有相同节点的
const commonLength = Math.min(oldChildren.length, newChildren.length)
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i])
}
// 2.如果新节点的length更长,那么剩余的新节点进行挂载操作
if (newChildren.length > oldChildren.length) {
newChildren.slice(oldChildren.length).forEach(item => {
mount(item, el)
})
}

// 3.如果旧节点的length更长,那么移除剩余的旧节点进行
if (newChildren.length < oldChildren.length) {
oldChildren.slice(newChildren.length).forEach(item => {
el.removeChild(item.el)
})
}
}
}
}
}

4.2 响应式系统

依赖收集系统 + vue2响应式系统

class Dep {
constructor() { // 只要new Dep,就会给你添加subscribes属性
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.根据对象(target)取出对应的Map对象
let depsMap = targetMap.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
}

// vue2对raw进行数据劫持
function reactive(raw) {
// 根据对象拿到所有key,组成数组
Object.keys(raw).forEach(key => {
// 获取key对应的依赖
const dep = getDep(raw, key)
let value = raw[key]

Object.defineProperty(raw, key, {
get() { // 用到了某个key,调用get,所以可以在这里收集依赖
dep.depend()
return value
},
set(newValue) { // 当属性被重新赋值的时候,会调用set,所以在这里执行这个属性依赖的函数
// raw[key] = newValue 不能这样设置,否则递归了(又对raw操作,又劫持,又来到set这里了)
if (value !== newValue) {
value = newValue
dep.notify()
}
}
})
})
return raw
}

// 测试代码
const info = reactive({
counter: 100,
name: 'hillyee'
})
// watchEffect1
watchEffect(function () {
console.log(info.counter * 2, info.name, 'w1');
})
// watchEffect2
watchEffect(function () {
console.log(info.counter * info.counter, 'w2');
})
// info.counter++ // 修改数据的时候,所有对这个数据有依赖的函数都应该被执行一次
// info.name = "hahahhahah"

响应式系统 vue3-proxy实现

// vue3对raw进行数据劫持
function reactive(raw) {
// Proxy(原对象,代理对象)
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()
}
})
}

为什么 Vue3 选择 Proxy 呢?

  • Object.definedProperty 是劫持对象的属性时,如果新增元素,

    那么Vue2需要再次 调用definedProperty,而 Proxy 劫持的是整个对象,不需要做特殊处理

  • 修改对象的不同: 使用 defineProperty 时,我们修改原来的 obj 对象就可以触发拦截;

    而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截;

4.3 框架外层 API 设计

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) {
// rootComponent.render() 返回根组件的vnode
oldVNode = rootComponent.render()
mount(oldVNode, container)
isMounted = true
} else {
const newVNode = rootComponent.render()
patch(oldVNode, newVNode)
oldVNode = newVNode
}
})
}
}
}

使用案例–计数

<body>
<div id="app"></div>
<script src="../mini_vue/renderer.js"></script>
<script src="../mini_vue/reactive.js"></script>
<script src="./index.js"></script>
<script>
// 1.创建根组件
const App = {
data: reactive({
counter: 0
}),
render() {
return h("div", null, [
h("h2", null, `当前计数:${this.data.counter}`),
h("button", {
onClick: () => {
this.data.counter++
console.log(this.data.counter);
}
}, "+1")
])
},
}
// 2.挂载根组件
const app = createApp(App)
app.mount('#app')
</script>
</body>

Vue3源码阅读

根据图的流程,加上源码,多看看叭,第一次看源码确实有点吃力了

先看熟悉流程,然后可以 debugger 在浏览器上简单过一下整个流程

createApp

image.png

源码阅读之挂载根组件

image.png

const app = {props: {message: String}
instance
// 1.处理props和attrs
instance.props
instance.attrs
// 2.处理slots
instance.slots
// 3.执行setup
const result = setup()
instance.setupState = proxyRefs(result);
// 4.编译template -> compile
<template> -> render函数
instance.render = Component.render = render函数
// 5.对vue2的options api进行知识
data/methods/computed/生命周期

组件化的初始化

image.png

Compile过程

对于不会改变的静态节点进行作用于提升

我都没找到这部分函数。。。

image.png

Block Tree 分析

vue3的一个优化:对于不会改变的静态节点进行作用域提升,仅对新的vnode进行创建

image.png

生命周期回调

image.png

template中数据的使用顺序

如果setup跟data中有同一个属性,首先选择setup的,内部做了一个判断吧

image.png