Tips:斜体下划线 => 重点
Vue3 EventBus mitt 事件总线
下载
npm i mitt
导入:
// EventBus.js
import mitt from "mitt"
// mitt Vue3 官方推荐的事件总线处理程序
const emitter = mitt()
export default emitter()
使用:
绑定
import emitter from "EventBus.js"
emitter.emit("why",{name:"wusu",age:"19"})
触发
且可以正常触发methods里的方法
import emitter from "eventBus.js"
export default {
created() {
// 监听 why
emitter.on("why",this.Dialog);
emitter.on("kobe", (info) => {
console.log("kobe:", info);
});
// 监听全部 每次触发其他时都会触发
emitter.on("*", (type, info) => {
console.log("* listener:", type, info + '1');
})
},
methods:{
Dialog(e){
console.log(e) // {name: 'why', age: 18}
}
}
}
取消监听
取消全部
emitter.all.clear()
取消单个
// 绑定
emitter.on("why",this.Dialog);
// 取消
emitter.off('why',this.Dialog)
Slot
使用:
基本使用:
匿名插槽
// my-slot-cpn
<template>
<div>
<h5>Start</h5>
<slot>
默认
</slot>
<h5>End</h5>
</div>
</template>
//引用
//传什么都可行 甚至组件
<my-slot-cpn>
<button>我是按钮</button>
</my-slot-cpn>
具名插槽
<template>
<div>
<h5>Start</h5>
<slot name="come">
默认
</slot>
<h5>End</h5>
</div>
</template>
//------------------------
<my-slot-cpn >
<template #come> // #come 为 v-slot:come的简写
<button>我是按钮</button>
<template>
</my-slot-cpn>
//--------------------------
<my-slot-cpn :name="name">
<template #[name]>
<i>why内容</i>
</template>
</my-slot-cpn>
<template>
<div>
<slot :name="name">
默认
</slot>
</div>
</template>
作用域插槽
可以在绑定插槽的时候使用template 并且绑定属性(v-slot)="slotProps"
slotProps的数据为 子组件slot上所绑定的属性值
// App.vue
<template>
<div>
// v-solt = #default
<show-names :names="names">
<template #default="slotProps">
// slotProps => { "item": "why", "index": 0 }
<button>{{slotProps.item}} + {{slotProps.index}}</button>
</template>
</show-names>
<!-- 若插槽有名字 v-slot:left => #left -->
<show-names :names="names">
<template #left="slotProps">
<button>{{slotProps.item}} + {{slotProps.index + 1}}</button>
</template>
</show-names>
<!-- 如果插槽为默认 则可以下方这种写法 ==> 独占默认插槽 -->
<show-names :names="names" v-slot="slotProps">
<button>{{slotProps.item}} + {{slotProps.index}}</button>
</show-names>
</div>
</template>
<script>
import ShowNames from './ShowNames.vue';
export default {
components: {
ShowNames
},
data() {
return {
names: ["why", "kobe", "james", "curry"]
}
}
}
</script>
//ShowNames.vue
<template>
<div>
<template v-for="(item,index) in names" :key="item">
<!-- 可以取不同的名字访问 但是不可以访问同样的值两次 -->
<!-- name="default" 等同于不写 -->
<slot :item="item" :index="index"></slot>
<slot name="left" :item="item" :index="index"></slot>
</template>
</div>
</template>
<script>
export default {
props: {
names:{
type:Array,
default:()=>[]
}
}
}
</script>
组件
动态组件
keep-alive/component/生命周期
<!-- include/exclude 包括=> string | array | RegExp -->
<!-- 匹配时 是根据子组件的 name属性确定的 -->
<keep-alive exclude="about" include="home,about">
<component :is="currentTab"
name="coderwhy"
:age="18"
@pageClick="pageClick">
</component>
</keep-alive>
currentTab // tabs: ["home", "about", "category"]中其一
keep-alive // 缓存组件 会保留状态 并且加入两个新的生命周期函数
activated 组件进入时触发
deactivated 组件离开时触发
若组件被缓存了
离开时不会触发unmouted函数
只有首次进入才会触发 created属性
created() {
console.log("about created");
},
unmounted() {
console.log("about unmounted");
},
activated() {
console.log("about activated");
},
deactivated() {
console.log("about deactivated");
}
异步组件
默认的打包过程:
默认情况下,在构建整个组件树的过程中,因为组件和组件之间是通过模块化直接依赖的,那么webpack在打包时就会将组
件模块打包到一起(比如一个app.js文件中);
这个时候随着项目的不断庞大,app.js文件的内容过大,会造成首屏的渲染速度变慢;
打包时,代码的分包:
所以,对于一些不需要立即使用的组件,我们可以单独对它们进行拆分,拆分成一些小的代码块chunk.js;
这些chunk.js会在需要时从服务器加载下来,并且运行代码,显示对应的内容;
个人(Wusu)理解 :可以理解为常用的路由懒记载类比的东西 分包之后使用时进行请求 ;
() => import("组件.vue")
或者
(resolve) => require(['zujian.vue'],resolve)
Vue的异步组件
异步组件的目的是可以对其进行分包 上线后可单独加载 速度加快
// Vue3异步组件
import { defineAsyncComponent } from 'vue';
const AsyncCategory = defineAsyncComponent(() => import("./AsyncCategory.vue"))
vue Suspense (实验性属性)
<suspense>
<template #default>
<async-category></async-category>
</template>
<template #fallback>
<loading></loading>
</template>
</suspense>
// 效果不知道 目前来看 首先显示fallback里的东西 然后显示 default的东西 速度很快会闪屏 算是个加载效果(?
Refs(vue3)
<nav-bar ref="navBar"></nav-bar>
console.log(this.$refs.navBar) // proxy
拿组件属性方法之类与从前一致 拿到DOM节点要$el
vue3 移除了 $children (根本没用过)
root 拿到根元素 (=> 不推荐使用 一般还是props emits 传值 )
组件的v-model
Vue2的写法
两种写法只是绑定值的时候有所不同
<all :filetype.sync="filetype"></all>
以下为Vue3的写法
组件上使用 v-model Vue做了什么事情
绑定一个值并且改变
// 父组件
<!-- 组件上使用v-model -->
<hy-input v-model="message"></hy-input>
||
<hy-input :modelValue="message" @update:model-value="message = $event"></hy-input>
// 子组件
<input type="text" :value="modelValue" @input="changeValue">
props: {
modelValue: {
type: [String, Array, Number],
},
},
//methods
changeValue(e){
this.$emit("update:model-value",e.target.value)
}
或者 计算属性来改变外部的值 (=> 目前能想到的 ele组件的 输入框)
<input type="text" v-model="value">
computed:{
value:{
set(val){
this.$emit("update:model-value",val)
},
get(){
return this.modelValue;
}
}
},
v-model 和 :value 绑定在组件上的区别
<!-- v-model 双向数据绑定 : 单向数据绑定 ==> 简单来说 v-model的东西变了 :value 跟着变 但是 :value的数据是独立的 与外面的无瓜 -->
<input type="text" v-model:value="message">
<input type="text" :value="message">
//改变多个值
p:两个输入框 上下分别可以对应改变message 和 title的值
父组件
<hy-inputa v-model="message" v-model:title="title"></hy-inputa>
{{message}}
{{title}}
hy-inputa
<input type="text" v-model="value">
<input type="text" v-model="titleTit">
props: {
modelValue: {
type: [String, Array, Number],
},
title:String,
},
emits:["update:model-value","update:title"],
computed:{
value:{
set(val){
this.$emit("update:model-value",val)
},
get(){
return this.modelValue;
}
},
titleTit:{
set(val){
this.$emit("update:title",val)
},
get(){
return this.title;
}
}
},
动画
基本使用
注意事项
<!-- type="transition" // transition 或者 animation ==> 一般没啥用,过渡和动画同时存在时根据某一个来结束过渡 不然会抽搐闪烁 -->
mode
<!-- in-out ==> 当前元素后离开 要加入的元素先加载 -->
<!-- out-in ==> 当前元素先离开 然后之后的在进入 -->
<!-- appear 进入界面时 直接有动画效果 -->
<transition name="why" mode="out-in" type="animation" appear>
<component :is="isShow ? 'home': 'about'"></component>
</transition>
Transition 各阶段
xxx-enter-from => xxx-enter-active => xxx-enter-to
leave 同理
直接拿animation的动画会很<(^-^)>
添加的类名同 直接设置css动画名同理
<transition
enter-active-class="animate__animated animate__fadeInDown"
leave-active-class="animate__animated animate__fadeOut"
>
<h2>我是BBB</h2>
</transition>
gsap动画库
效果为列表动画 非常丝滑
<template>
<div>
<input v-model="keyword" />
<transition-group tag="ul" name="susu" :css="false" @before-enter="beforeEnter"
@enter="enter"
@leave="leave">
<li
v-for="(item,index) in showNames"
:key="item"
:data-index="index"
>
{{ item }}
</li>
</transition-group>
</div>
</template>
<script>
import gsap from "gsap";
export default {
data() {
return {
names: ["abc", "cba", "nba", "why", "lilei", "hmm", "kobe", "james"],
keyword: "",
};
},
computed: {
showNames() {
return this.names.filter((item) => item.indexOf(this.keyword) !== -1);
},
},
methods: {
beforeEnter(el) {
el.style.opacity = 0;
el.style.height = 0;
},
enter(el, done) {
gsap.to(el, {
opacity: 1,
height:"1.5em",
delay:el.dataset.index * 0.1,
onComplete: done,
});
},
leave(el, done) {
gsap.to(el, {
opacity: 0,
height:0,
delay:el.dataset.index * 0.1,
onComplete: done,
});
},
},
mounted() {
},
};
</script>
Mixins 代码复用 (共享)
// demoMixin
export const demoMixin = {
data() {
return {
message: "Hello DemoMixin"
}
},
methods: {
foo() {
console.log(this)
console.log(this.title);
}
},
created() {
console.log("执行了demo mixin created");
}
}
App.vue 可以直接掉 等于是写在了外面的方法之类
<button @click="foo">按钮</button>
import { demoMixin } from './mixins/demoMixin';
有冲突 生命周期都执行 其他的 本体大 引入的小
全局混入
app.mixin({
created() {
console.log("@222")
},
})
extend(了解)
<template>
<div>
Home Page
<h2>{{title}}</h2>
<button @click="bar">按钮</button>
</div>
</template>
<script>
// 引入其他组件 可以使用其他的方法/数据
import BasePage from './BasePage.vue';
export default {
extends: [BasePage],
data() {
return {
content: "Hello Home"
}
}
}
</script>
composition API
setup (不是响应式)
<template>
<div>
Home Page
<h2>{{message}}</h2>
<h2>{{title}}</h2>
<h2>当前计数: {{counter}}</h2>
<button @click="increment">+1</button>
</div>
</template>
<script>
export default {
props: {
message: {
type: String,
required: true
}
},
data() {
return {
counter: 100
}
},
/**
* 参数一: props, 父组件传递过来属性
*/
// setup函数有哪些参数?
// setup函数有什么样的返回值
// setup(props, context) {
setup(props, {attrs, slots, emit}) {
// console.log(props) // props里的东西
// console.log(attrs) // 绑定的值 除去props里的东西
// console.log(slots)
// console.log(emit)
return {
title: "Hello Home",
counter: 100
}
},
methods: {
btnClick() {
this.$emit("")
}
}
}
</script>
响应式API
setup函数没有this:
表达的含义是this并没有指向当前组件实例;
并且在setup被调用之前,data、computed、methods等都没有被解析;
所以无法在setup中获取this;
reactiveAPI
reactive API对传入的类型是有限制的,它要求我们必须传入的是一个对象或者数组类型
let state = reactive({
counter:100,
})
return{
state.counter
}
ref API
灵活
toRefs toRef
let counter = ref(100)
script操作 counter.value++
template模板里则直接可以 counter
return {
counter,
}
const info = reactive({name: "why", age: 18});
//1.toRefs: 将reactive对象中的所有属性都转成ref, 建立链接
let { name, age } = toRefs(info); // 可修改
// 2.toRef: 对其中一个属性进行转换ref, 建立链接
let { name } = info; // 只读
let age = toRef(info, "age"); //可修改
readonly API
只读api
let su = readonly(counter)
传入后 su 为只读不可修改 但是可以通过改变原对象的值进行改变
一般用来 传给子组件 让子组件只读不能修改 ==> 修改直接报错
API补充
isProxy
检查对象是否是由 reactive 或 readonly创建的 proxy。
isReactive
检查对象是否是由 reactive创建的响应式代理:如果该代理是 readonly 建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true;
isReadonly
检查对象是否是由 readonly 创建的只读代理。
toRaw
返回 reactive 或 readonly 代理的原始对象(不建议保留对原始对象的持久引用。请谨慎使用)。
shallowReactive
创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (深层还是原生对象)。
shallowReadonly
创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(深层还是可读、可写的)。
Computed
import { ref, computed } from 'vue';
export default {
setup() {
const firstName = ref("Kobe");
const lastName = ref("Bryant");
// 1.用法一: 传入一个getter函数
// computed的返回值是一个ref对象 不可变
let fullName = computed(() => firstName.value + " " + lastName.value);
// 2.用法二: 传入一个对象, 对象包含getter/setter 可以通过设置来改变
// const fullName = computed({
// get: () => firstName.value + " " + lastName.value,
// set(newValue) {
// const names = newValue.split(" ");
// firstName.value = names[0];
// lastName.value = names[1];
// }
// });
const changeName = () => {
// firstName.value = "James"
// fullName.value = "coder why"; // 计算值为只读 ==> 方法一定义
// firstName.value = "Code";
// lastName.value = "Why";
}
return {
fullName,
changeName
}
}
}
watchEffect
// watchEffect: 自动收集响应式的依赖
const name = ref("why");
const age = ref(18);
const changeName = () => name.value = "kobe"
const changeAge = () => age.value++
const stop = watchEffect(() => {
console.log("name:", name.value, "age:", age.value);
// name or age 改变值触发 会立即触发一次
});
return {
name,
age,
changeName,
changeAge
}
停止监听
// watchEffect() 函数返回一个stop函数 调用会停止监听 比如在 age.value 大于某个值的时候 去除监听
const stop = watchEffect(() => {
console.log("name:", name.value, "age:", age.value);
});
const changeName = () => name.value = "kobe"
const changeAge = () => {
age.value++;
if (age.value > 25) {
stop();
}
}
清楚副作用
setup() {
// watchEffect: 自动收集响应式的依赖
const name = ref("why");
const age = ref(18);
const stop = watchEffect((onInvalidate) => {
const timer = setTimeout(() => {
console.log("网络请求成功~");
}, 2000)
// 根据name和age两个变量发送网络请求
onInvalidate(() => {
// 在这个函数中清除额外的副作用
// request.cancel()
clearTimeout(timer);
console.log("onInvalidate");
})
// "name:", name.value,
console.log("age:", age.value);
});
const changeName = () => name.value = "kobe"
const changeAge = () => {
age.value++;
if (age.value > 25) {
stop();
}
}
return {
name,
age,
changeName,
changeAge
}
}
ref执行时机
<template>
<div>
<h2 ref="title">哈哈哈</h2>
</div>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
setup() {
const title = ref(null); // 拿到原生ref节点
console.log(title)
watchEffect(() => {
console.log(title.value);
},{
flush:"post" // 若是pre watchEffect会立即执行 打印null
// 默认值是pre 即在挂载或者更新前就执行
// post将监听移动到组件挂载完毕后执行
})
return {
title
}
}
}
</script>
watch
监听单个
const info = reactive({name: "why", age: 18});
// 1.侦听watch时,传入一个getter函数
watch(() => info.name, (newValue, oldValue) => {
console.log("newValue:", newValue, "oldValue:", oldValue);
})
// 2.传入一个可响应式对象: reactive对象/ref对象
// 情况一: reactive对象获取到的newValue和oldValue本身都是reactive对象
// watch(info, (newValue, oldValue) => {
// console.log("newValue:", newValue, "oldValue:", oldValue);
// })
// 如果希望newValue和oldValue是一个普通的对象 {...info} 解构
watch(() => {
return {...info}
}, (newValue, oldValue) => {
console.log("newValue:", newValue, "oldValue:", oldValue);
})
// 情况二: ref对象获取newValue和oldValue是value值的本身
// const time = ref("why");
// watch(time, (newValue, oldValue) => {
// console.log("newValue:", newValue, "oldValue:", oldValue);
// })
const changeData = () => {
info.name = "kobe";
info.age++
// time.value = "Wusu"
}
return {
changeData,
info,
// time
}
监听多个
// 1.定义可响应式的对象
const info = reactive({name: "why", age: 18});
const name = ref("why");
const title = ref(null)
// 2.侦听器watch
// watch([() => ({...info}), name,title], ([newInfo, newName,newTit],[oldInfo, oldName,oldTit]) => {
// // console.log(a,b) a => newV ; b => oldV
// console.log(newInfo, newName, oldInfo, oldName);
// console.log(newTit,oldTit)
// },{
// // deep:true,
// // immediate:true,
// flush:"post"
// })
watch([() => ({...info}), name,title], (a,b) => {
// console.log(a,b) a => newV ; b => oldV
console.log(a,b)
},{
// deep:true,
// immediate:true,
flush:"post"
})
const changeData = () => {
info.name = "kobe";
}
return {
changeData,
info,
title
}
生命周期
// 在选项式api里卸载时与vue2有所改变
// compositionApi 里 均有所不同
| 选项式Api | compositionApi |
|---|---|
| beforeCreate | setup() |
| created | setup() |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdated |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
| activated | onActivated |
| deactivated | onDeactivated |
setup() {
const counter = ref(0);
const increment = () => counter.value++;
// 都会触发
onMounted(() => {
console.log("App Mounted1");
});
onMounted(() => {
console.log("App Mounted2");
});
// ---------
onUpdated(() => {
console.log("App onUpdated");
});
onUnmounted(() => {
console.log("App onUnmounted");
});
return {
counter,
increment,
};
},
mounted(){
// 也会触发
console.log("@2333") // @2333
}
provide/inject
都可以改变provide的数据 但是不建议这样子更改 一般会传递 readonly()的数据 保证单向数据流
通过共享provide方法来改变数据
<template>
<div>
子组件
<home/>
--> 父组件
<h2>App Counter: {{counter}}</h2>
<button @click="increment">App中的+1</button>
</div>
</template>
<script>
import { provide, ref, readonly } from 'vue';
import Home from './Home.vue';
export default {
components: {
Home
},
setup() {
const name = ref("coderwhy");
let counter = ref(100);
// 共享属性
provide("name", readonly(name));
provide("counter", readonly(counter));
// 共享方法
provide("changeName",(val)=>{
name.value = val;
})
const increment = () => counter.value++;
return {
increment,
counter
}
}
}
</script>
Hooks
代码逻辑拆分
App.vue
import { ref, computed } from "vue";
import {
useCounter,
useLocalStorage,
useMousePosition,
useScrollPosition,
useTitle,
} from "./hooks";
export default {
setup() {
const { num, doubbleNum, obj, addNum, delNum } = useCounter();
const titleCon = useTitle("变身tit");
setTimeout(() => {
titleCon.value = "WuSu";
}, 2000);
const { scrollX, scrollY } = useScrollPosition();
const { mouseX, mouseY } = useMousePosition();
const { data } = useLocalStorage("name", { name: "abd", age: "19" });
const changeData = () => (data.value = "csssss");
return {
num, doubbleNum,obj,addNum,delNum,
titleCon,
scrollX,scrollY,
mouseX, mouseY,
data,changeData,
};
},
};
hooks.js
import useCounter from './useCounter';
import useTitle from './useTitle';
import useScrollPosition from './useScrollPosition';
import useMousePosition from './useMousePosition';
import useLocalStorage from './useLocalStorage';
export {
useCounter,
useTitle,
useScrollPosition,
useMousePosition,
useLocalStorage
}
useCounter.js
import { ref, computed } from "vue"
export default function () {
const num = ref(12);
const doubbleNum = computed(() => num.value * 2)
const addNum = () => {
num.value += 1
}
const delNum = () => {
num.value += -1
}
const obj = {
a:'1',
b:'2',
}
return {
num,
doubbleNum,
obj,
addNum,
delNum
}
}
render h函数
基本使用
import { h } from 'vue';
export default {
render() {
return h("h2",{class:"123321 aaa"},"XIXIHAHA")
}
}
JSX
render() {
const increment = () => this.counter++;
const decrement = () => this.counter--;
return (
<div>
<h2>当前计数: {this.counter}</h2>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<HelloWorld></HelloWorld>
</div>
)
}
可以在return里的东西 比较灵活 组件库用的比较多 直接div里写js
自定义指令(Vue3)
局部注册
directives:{
focus:{
mounted(el,binding,vnode,prenode) {
el.focus()
},
},
}
指令的钩子会传递以下几种参数:
-
el:指令绑定到的元素。这可以用于直接操作 DOM。 -
binding:一个对象,包含以下属性。value:传递给指令的值。例如在v-my-directive="1 + 1"中,值是2。oldValue:之前的值,仅在beforeUpdate和updated中可用。无论值是否更改,它都可用。arg:传递给指令的参数 (如果有的话)。例如在v-my-directive:foo中,参数是"foo"。modifiers:一个包含修饰符的对象 (如果有的话)。例如在v-my-directive.foo.bar中,修饰符对象是{ foo: true, bar: true }。instance:使用该指令的组件实例。dir:指令的定义对象。
-
vnode:代表绑定元素的底层 VNode。 -
prevNode:之前的渲染中代表指令所绑定元素的 VNode。仅在beforeUpdate和updated钩子中可用。
生命周期
// Data加载完成后
created() {
},
// Dom挂载前后
beforeMount() {
console.log("why beforeMount");
},
mounted() {
console.log("why mounted");
},
// 状态更新
beforeUpdate() {
console.log("why beforeUpdate");
},
updated() {
console.log("why updated");
},
// 消失解除事件绑定
beforeUnmount() {
console.log("why beforeUnmount");
},
unmounted() {
console.log("why unmounted");
}
案例:
// format-time
// 外部时间库
import dayjs from 'dayjs';
export default function(app) {
app.directive("format-time", {
created(el, bindings) {
bindings.formatString = "YYYY-MM-DD HH:mm:ss";
if (bindings.value) {
bindings.formatString = bindings.value;
}
},
mounted(el, bindings) {
const textContent = el.textContent;
let timestamp = parseInt(textContent);
if (textContent.length === 10) {
timestamp = timestamp * 1000
}
el.textContent = dayjs(timestamp).format(bindings.formatString);
}
})
}
// directives.js
import registerFormatTime from './format-time';
export default function registerDirectives(app) {
registerFormatTime(app);
}
// main.js
import registerDirectives from './directives'
registerDirectives(app);
teleport
<template>
<div class="app">
<teleport to="#why"> // 将下列元素加入到 id为why的div中去 ==> 必须得有id为why的Div
<h2>当前计数</h2>
<button>+1</button>
<hello-world></hello-world>
</teleport>
<teleport to="body"> // 将该元素加到body末尾
<span>呵呵呵呵</span>
</teleport>
</div>
</template>
plugins
插件
// main.js
import pluginObject from './plugins/plugins_object'
const app = createApp(App);
app.use(pluginObject);
export default {
install(app) {
app.config.globalProperties.$name = "coderwhy"
}
}
// 如何拿到config里的值
import { getCurrentInstance } from "vue";
setup() {
const instance = getCurrentInstance();
console.log(instance.appContext.config.globalProperties.$name)// coderwhy
},
mini-vue
render.js 渲染器
const h = (tag, props, children) => {
return {
tag,
props,
children,
}
}
const mount = (vnode, container) => {
// vnode -> dom
const el = vnode.el = document.createElement(vnode.tag);
// 添加 属性 or 方法
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key];
// console.log(key, value)
if (key.startsWith('on')) {
el.addEventListener(key.slice(2).toLocaleLowerCase(), value)
} else {
el.setAttribute(key, value)
}
}
}
// vnode的子节点添加
if (vnode.children) {
if (typeof vnode.children === "string") {
el.textContent = vnode.children
} else {
vnode.children.forEach(item => {
mount(item, el)
});
}
}
// 添加el到父节点
container.appendChild(el);
}
// diff算法函数
/*
n1 oldVnode
n2 newVnode
*/
const patch = (n1, n2) => {
// 新增
if (n1.tag !== n2.tag) {
const parElement = n1.el.parentElement;
parElement.removeChild(n1.el)
mount(n2, parElement)
} else {
const el = n2.el = n1.el;
const oldProps = n1.props || {};
const newProps = n2.props || {};
// 改变或者新增props
for (const key in newProps) {
const newValue = newProps[key];
const oldValue = oldProps[key];
if (newValue !== oldValue) {
if (key.startsWith('on')) {
el.addEventListener(key.slice(2).toLocaleLowerCase(), newValue)
} else {
el.setAttribute(key, newValue)
}
}
}
// 删除props的
for (const key in oldProps) {
if (key.startsWith("on")) {
const value = oldProps[key];
el.removeEventListener(key.slice(2).toLocaleLowerCase(), value)
}
if (!(key in newProps)) {
el.removeAttribute(key)
}
}
// 处理children
const oldChildren = n1.children || {};
const newChildren = n2.children || {};
// 新child为字符串
if (typeof newChildren === 'string') {
// 旧的也是字符串
if (typeof oldChildren === 'string') {
el.textContent = newChildren;
} else {
el.innerHTML = newChildren;
}
} else {
if (typeof oldChildren === 'string') {
el.innerHTML = '';
newChildren.forEach(item => {
mount(item, el)
})
} else {
// 按位对比
// 给大聪明自己以后看的:patch这步过后会直接比较完共有的的 多的或者少的走后面的判断;
// vue在没有key的情况的笨办法 硬判断 拿完一样的后新的多就加 旧的多就删掉
const minLength = Math.min(oldChildren.length, newChildren.length);
for (let i = 0; i < minLength; i++) {
patch(oldChildren[i], newChildren[i])
}
// new节点数据多 多遍历
if (newChildren.length > oldChildren.length) {
newChildren.slice(oldChildren.length).forEach(item => {
mount(item, el)
})
}
// 循环完成后 如果是old节点数据多 则去除多出来的
if (oldChildren.length > newChildren.length) {
oldChildren.slice(newChildren.length).forEach(item => {
el.removeChild(item.el)
})
}
}
}
}
}
reactive.js 响应式
class Dep {
constructor() {
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;
}
// Map({key: value}): key是一个字符串
// WeakMap({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);
}
// 2.取出具体的dep对象
let dep = depsMap.get(key);
if (!dep) {
dep = new Dep();
depsMap.set(key, dep);
}
return dep;
}
// vue3对raw进行数据劫持
function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
// console.log(target,key) // { counter: 0, num: 20 },'counter'
const dep = getDep(target, key);
dep.depend();
return target[key];
},
set(target, key, newValue) {
// console.log(target, key, newValue) //{ counter: 2, num: 20 } 'counter' 3
const dep = getDep(target, key);
target[key] = newValue;
dep.notify();
}
})
}
// vue2对raw进行数据劫持
function reactive(raw) {
// 遍历传入的对象的key
Object.keys(raw).forEach(key => {
// dep对象
const dep = getDep(raw, key);
// value
let value = raw[key];
Object.defineProperty(raw, key, {
get() {
dep.depend();
return value;
},
set(newValue) {
if (value !== newValue) {
value = newValue;
dep.notify();
}
}
})
})
return raw;
}
index.js 挂载器
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;
}
})
}
}
}