Vue3源码学习之Mini-Vue的实现
实现Mini-Vue
这里我们实现一个简洁版的Mini-Vue框架,该Vue包括三个模块:
- 渲染系统模块;
- 可响应式系统模块;
- 应用程序入口模块;
一、渲染系统实现
渲染系统,该模块主要包含三个功能:
- 功能一:h函数,用于返回一个VNode对象;
- 功能二:mount函数,用于将VNode挂载到DOM上;
- 功能三:patch函数,用于对两个VNode进行对比,决定如何处理新的VNode;
h函数 – 生成VNode
h函数的实现:直接返回一个VNode对象即可
Mount函数 – 挂载VNode
mount函数的实现:
第一步:根据tag,创建HTML元素,并且存储到vnode的el中;
第二步:处理props属性
- 如果以on开头,那么监听事件;
- 普通属性直接通过 setAttribute 添加即可;
第三步:处理子节点
- 如果是字符串节点,那么直接设置textContent;
- 如果是数组节点,那么遍历调用 mount 函数;
Patch函数 – 对比两个VNode
patch函数的实现,分为两种情况:
n1和n2是不同类型的节点:
- 找到n1的el父节点,删除原来的n1节点的el;
- 挂载n2节点到n1的el父节点上;
n1和n2节点是相同的节点:
处理props的情况:
- 先将新节点的props全部挂载到el上;
- 判断旧节点的props是否不需要在新节点上,如果不需要,那么删除对应的属性;
处理children的情况:
- 如果新节点是一个字符串类型,那么直接调用 el.textContent = newChildren;
- 如果新节点不同一个字符串类型:
- 旧节点是一个字符串类型
- l将el的textContent设置为空字符串;
- 旧节点是一个字符串类型,那么直接遍历新节点,挂载到el上;
- 旧节点也是一个数组类型
- 取出数组的最小长度;
- 遍历所有的节点,新节点和旧节点进行path操作;
- 如果新节点的length更长,那么剩余的新节点进行挂载操作;
- 如果旧节点的length更长,那么剩余的旧节点进行卸载操作;
- 旧节点是一个字符串类型
const h = (tag, props, children) => {
// vNode-->javascript对象-->{}
return {
tag,
props,
children
}
}
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)
}
const patch = (n1, n2) => {
// 1.如果n1和n2节点的type类型不相同
if (n1.tag !== n2.tag) {
const n1ElParent = n1.el.parentElement
n1ElParent.removeChild(n1.el)
mount(n2, n1ElParent)
} else {
// 2.n1和n2节点类型相同
// 2.1取出element对象,并且在n2中保存
const el = n2.el = n1.el
// 2.2处理props
const newProps = n2.props || {}
const oldProps = n1.props || {}
// 2.2.1处理newProps
for (const key in newProps) {
// 对于newProps,就是添加属性
const newValue = newProps[key]
const oldValue = oldProps[key]
if (newValue !== oldValue) {
if (key.startsWith('on')) {
el.addEventListener(key.slice(2).toLowerCase(), newValue)
} else {
el.setAttribute(key, newValue)
}
}
}
// 2.2.2处理oldProps
for (const key in oldProps) {
// 对于oldProps,只需删除旧的属性
if (key.startsWith('on')) {
el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key])
}
if (!(key in newProps)) {
el.removeAttribute(key)
}
}
// 3.处理children
const newChildren = n2.children
const oldChildren = n1.children
// 3.1newChildren本身是一个字符串
if (typeof newChildren === 'string') {
// 边界判断
if (typeof oldChildren === 'string') {
if (newChildren !== oldChildren) {
el.textContent = newChildren
}
} else {
el.innerHTML = newChildren
}
} else {
// 3.2newChildren本身是一个数组
// 3.2.1oldChildren是一个字符串
if (typeof oldChildren === 'string') {
el.innerHTML = ''
newChildren.forEach(item => {
mount(item, el)
})
} else {
// 3.2.2oldChildren也是一个数组
// oldChildren: [v1, v2, v3, v8, v9]
// newChildren: [v1, v5, v6]
// 1.取出oldChildren和newChildren数组的最小长度,进行patch操作
// 这种对应的diff算法中没有key的操作,效率低
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)
})
}
}
}
}
}
二、响应式系统模块
依赖收集系统
响应式系统Vue2实现
响应式系统Vue3实现
class Dep{
constructor(){
this.subscribes=new Set()
}
depend(){
if(activeEffect){
this.subscribes.add(activeEffect)
}
}
notify(){
this.subscribes.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响应式对数据劫持
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){
if(value!==newValue){
value=newValue
dep.notify()
}
}
})
})
return raw
}
// 测试代码
const info=reactive({counter:100,name:'coder'})
const foo=reactive({height:1.88})
// watchEffect1
watchEffect(function(){
console.log('effect1:',info.counter*2,info.name)
})
// watchEffect2
watchEffect(function(){
console.log('effect2:',info.counter*info.counter)
})
// watchEffect3
watchEffect(function(){
console.log('effect3:',info.counter+10,info.name)
})
// watchEffect4
watchEffect(function(){
console.log('effect4:',foo.height)
})
info.counter++
// info.name='kobe'
// foo.height=2
为什么Vue3选择Proxy呢?
Object.definedProperty 是劫持对象的属性时,如果新增元素:
- 那么Vue2需要再次 调用definedProperty,而 Proxy 劫持的是整个对象,不需要做特殊处理;
修改对象的不同:
- 使用 defineProperty 时,我们修改原来的 obj 对象就可以触发拦截;
- 而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截;
Proxy 能观察的类型比 defineProperty 更丰富
- has:in操作符的捕获器;
- deleteProperty:delete 操作符的捕捉器;
- 等等其他操作;
Proxy 作为新标准将受到浏览器厂商重点持续的性能优化;
缺点:Proxy 不兼容IE,也没有 polyfill, defineProperty 能支持到IE9
三、框架外层API设计
这样我们就知道了,从框架的层面来说,我们需要有两部分内容:
- createApp用于创建一个app对象;
- 该app对象有一个mount方法,可以将根组件挂载到某一个dom元素上;