原本是想详细了解Vue的响应式原理,结果一不小心就写出了一个Mini_Vue
一、 主要工作流程
-
编译器将视图模板编译为渲染函数
-
数据响应模块将数据对象初始化为响应式数据对象
-
视图渲染
- RenderPhase : 渲染模块使用渲染函数根据初始化数据生成虚拟Dom
- MountPhase : 利用虚拟Dom创建视图页面Html
- PatchPhase:数据模型一旦变化渲染函数将再次被调用生成新的虚拟Dom,然后做Dom Diff更新视图Html
二、 模块各自分工
1. 数据响应式模块 ——reactive函数
提供创建一切数据变化都是可以被监听的响应式对象的方法
class Dep {
constructor() {
this.subscribers = new Set()
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
notify() {
this.subscribers.forEach(effect => {
effect()
})
}
}
//实现自动添加effect
let activeEffect = null
function watchEffect(effect) {
activeEffect = effect
//初次执行一次effect
effect()
activeEffect = null
}
//创建weakMap以raw对象为属性名保存数据,例如info、foo对象
//WeakMap :({key(对象): value}) key是一个对象,并且WeakMap是弱引用 ,可以在垃圾回收时被回收掉
//Map: ({key: value } key是字符串
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)//如果没有depsMap则创建一个depsMap
//2. 取出具体的dep对象
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Dep()
depsMap.set(key, dep)
}
return dep
}
//Vue2方式做数据劫持,实现自动进行依赖添加,以及通知依赖发生改变,并且执行effect函数
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) {
value = newValue
dep.notify()
}
})
})
return raw
}
//Vue3方式做数据劫持
function reactive(raw) {
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()
}
})
}
//测试代码
const info = reactive({ counter: 100, name: 'christina' })
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 + 100, info.name);
})
watchEffect(function () {
console.log('effect4:', foo.height);
})
// info.counter++;
// info.name = 'pp'
foo.height = '2.13'
2. 编译模块——挂载
将html模板编译为渲染函数
这个编译过程可以在一下两个时刻执行
- 浏览器运行时 (runtime)
- Vue项目打包编译时 (compile time)
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
}
})
}
}
}
3. 渲染函数
渲染函数通过以下三个周期将视图渲染到页面上
const h = (tag, props, children) => {
return {
tag,
props,
children
}
}
const mount = (vnode, container) => {
//1.创建真实的dom节点
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).toLocaleLowerCase(), 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挂载到containers上
container.appendChild(el)
}
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 || []
//情况一: 如果children类型是字符串
if (typeof newChildren === 'string') {
if (typeof oldChildren === 'string') {
el.textContent = newChildren
} else {
el.innerHTML = newChildren
}
} else {//情况二,有数组类型,并且新的children是数组,但是旧的是string类型
if (typeof oldChildren === 'string') {
el.innerHTML = ''
newChildren.forEach(item => {
mount(item, el)
})
} else {
//新旧children都是数组类型,前面有相同的节点直接进行patch操作
const commonLength = Math.min(oldChildren.length, newChildren.length)
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i])
}
//新的children长度大余旧的children的长度,或者小于旧的children的长度
if (newChildren.length > oldChildren.length) {
newChildren.slice(oldChildren).forEach(item => {
mount(item, el)
})
} else {
oldChildren.slice(newChildren.length).forEach(item => {
el.removeChild(item.el)
})
}
}
}
}
}