持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情
实现一个简洁版的Mini-Vue框架,包括三个模块:
- 渲染系统模块;
- 响应式系统模块;
- 应用程序入口模块;
Ps:理解mini-vue的实现流程后,便于理解Vuejs框架的设计,也便于源码的阅读。本次demo中几乎每行都带有注释非常方便理解,案例实现源码附于文章最后,欢迎大家采摘。
渲染系统
该模块主要包含三个功能:
- 功能一:h函数,用于返回一个VNode对象;
- 功能二:mount函数,用于将VNode挂载到DOM上;
- 功能三:patch函数,用于对两个VNode进行对比,决定如何处理新的VNode;
h函数的实现:
这里非常的简单,其实就是返回一个vnode
const h=(tag,props,children)=>{
// vnode -->javascript ->{}
return {
tag,
props,
children
}
}
mount函数的实现:
-
第一步:根据tag,创建HTML元素,并且存储到vnode的el中;
-
第二步:处理props属性
- 如果以on开头,那么监听事件;
- 普通属性直接通过 setAttribute 添加即可;
-
第三步:处理子节点
-
如果是字符串节点,那么直接设置textContent;
-
如果是数组节点,那么遍历调用 mount 函数;
-
const mount = (vnode,container)=>{
// 创建真实的 dom
// 1. vnode --》 el ,并且在vnode中保留一份
const el = vnode.el = document.createElement(vnode.tag);
// 2.处理 props
if(vnode.props){
for(const key in vnode.props){
const value = vnode[key];
// edge processing
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函数的实现:
Ps: patch是一个非常重要的函数,在Vue源码里面将近2k行代码,mini-vue中我们先简单处理下业务场景,方便以后我们阅读源码。
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更长,那么剩余的新节点进行挂载操作;
- 如果旧节点的length更长,那么剩余的旧节点进行卸载操作;
-
const patch = (n1,n2)=>{
if(n1.tag !== n2.tag){
//直接移除
const n1ElParent = n1.el.parentElement;
n1ElParent.removeChild(n1.el);
mount(n2,n1ElParent)
}else{
// 1.取出 el对象并且在n2 中保存一份
const el = n2.el = n1.el
// 2.处理 props
const oldProps = n1.props || {}
const newProps = n2.props || {}
// 2.1 将所有newPros 添加到 n1 中
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 ,如果新的node没有 旧中的属性,旧中就移除
for(const key in oldProps){
if(!(key in newProps)){
if(key.startsWith('on')){
const value = oldProps[key]
el.removeEventListener(key.slice(2).toLowerCase(),value)
}else{
el.removeAttribute(key)
}
}
}
// 3处理 children
const oldChidren = n1.children || [];
const newChidren = n2.children || [];
if(typeof newChidren === 'string'){ // first case: newChildren is string
// TODO: edge case 边界情况的处理
// el.innerHTML = newChidren
if(typeof oldChidren === 'string'){
if(newChidren !== oldChidren){
el.textContent = newChidren
}
}else{
el.innerHTML = newChidren
}
}else{ // second case: newChildren is array
if(typeof oldChidren === 'string'){
el.innerHTML = '';
newChidren.forEach(item =>{
mount(item,el)
})
}else{
// oldChildren:[v1,v2,v3]
// newChildren:[v1,v4,v5,v6,v7]
// 1.前面有相同节点的情况处理
const commonLength = Math.min(oldChidren.length,newChidren.length)
for(let i=0;i<commonLength;i++){
patch(oldChidren[i],newChidren[i])
}
// 2.newChildren > oldChildren
if(newChidren.length > oldChidren.length){
newChidren.slice(oldChidren.length).forEach(item=>{
mount(item,el)
})
}
// 3.newChildren < oldChildren
if(newChidren.length < oldChidren.length){
// unmount
oldChidren.slice(newChidren.length).forEach(item=>{
el.removeChild(item.el);
})
}
}
}
}
}
响应系统
有多个地方都依赖Data数据的时候,将相关依赖的函数进行收集,放入到deps中。可以是Array、Map、set等
当Data数据发生变化的时候,遍历deps中依赖,执行相关函数。
依赖收集
依赖我们可以理解为,数据之间的联系。
// 依赖
class Dep {
constructor() {
// 添加订阅者,并删除重复依赖
this.subsribers = new Set();
}
// 添加effect 的重构
depend() {
if (activeEffect) {
this.subsribers.add(activeEffect);
}
}
// notify
notify() {
this.subsribers.forEach((effect) => {
effect();
});
}
}
// 执行dep 的方法时,不需要依赖effect。依然可以添加到subscribers中
let activeEffect = null;
function watchEffect(effect) {
activeEffect = effect;
// deep:true
effect();
activeEffect = null;
}
数据劫持,响应式
// 通过数据结构来管理 dep
const targetMap = new WeakMap();
function getDep(target, key) {
// 根据target对象取出 对应的Map对象
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 取出对应的dep
let dep = depsMap.get(key);
if (!dep) {
dep = new Dep();
depsMap.set(key, dep);
}
return dep;
}
Vue2 数据劫持
Vue3-数据劫持
Ps:WeakMap key可以存储对象类型,方便内存的回收。
defineProperty 与Proxy 的区别
1、 Object.definedProperty 是劫持对象的属性时,如果新增元素:那么Vue2需要再次 调用definedProperty,而 Proxy 劫持的是整个对象,不需要做特殊处理;
2、 修改对象的不同:
-
使用 defineProperty 时,我们修改原来的 obj 对象就可以触发拦截;
-
而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截;
3、Proxy 能观察的类型比 defineProperty 更丰富
-
has:in操作符的捕获器;
-
deleteProperty:delete 操作符的捕捉器,以及其他操作;
4、 Proxy 作为新标准将受到浏览器厂商重点持续的性能优化;
5、缺点:Proxy 不兼容IE,也没有 polyfill, defineProperty 能支持到IE9
应用程序入口
//非常熟悉的入口
vue.createApp().mount
这样我们就知道了,从框架的层面来说,我们需要提供createApp、mount方法
有两部分内容:
-
createApp用于创建一个app对象;
-
该app对象有一个mount方法,可以将根组件挂载到某一个dom元素上;