Vue3 初见

104 阅读6分钟

Tips:斜体下划线 => 重点

Vue3 EventBus mitt 事件总线

下载

npm i mitt

导入:

// EventBus.jsimport 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 (实验性属性)

cn.vuejs.org/guide/built…

<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 (根本没用过)

parent拿到父组件parent 拿到父组件 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 里 均有所不同

选项式ApicompositionApi
beforeCreatesetup()
createdsetup()
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdated
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
activatedonActivated
deactivatedonDeactivated
 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:之前的值,仅在 beforeUpdateupdated 中可用。无论值是否更改,它都可用。
    • arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"
    • modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }
    • instance:使用该指令的组件实例。
    • dir:指令的定义对象。
  • vnode:代表绑定元素的底层 VNode。

  • prevNode:之前的渲染中代表指令所绑定元素的 VNode。仅在 beforeUpdateupdated 钩子中可用。

生命周期

// 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;
        }
      })
    }
  }
}