自定义指令
自定义组件分为两种
- 自定义局部指令:组件中通过directives选项,只能在当前组件中使用。
- 自定义全局指令:app的directive方法,可以在任意组件中被使用。
使用v-focus自定义指令使得输入框自动获取焦点 局部指令
<template>
<input type="text" v-focus>
</template>
<script>
directives: {
focus: {
//生命周期函数,当挂载完成时执行
mounted(el, bindings, vnode, preVnode) {
console.log("focus")
el.focus();
}
}
},
全局指令 在main.js里写
const app = createApp(App);
app.directive("focus", {
mounted(el) {
el.focus();
}
})
指令的生命周期
-
- created: 在绑定元素的attribute或事件监听器被应用之前调用
- beforeMount
- mounted
- beforeUpdate:在更新组件的VNode之前调用;
- updated:在包含组件的VNode及其子组件的VNode更新后调用
- beforeUnmount: 在卸载绑定元素的父组件之前调用
- unmounted:当指令与元素解绑且父组件已卸载时 (可以使用v-if)
可以在bindings里modify获取修饰符
v-focus.aaa
//在bindings里可以打印出来
时间戳指令
在开发中,大多数从服务器获取到的都是时间戳,我们需要将时间戳转换成具体格式化的时间来展示,vue2我们可以通过过滤器完成。vue3我们可以通过计算属性,自定义方法或自定义的指令完成
import dayjs from 'dayjs'
app.directive("format-time", {
mounted(el, bindings) {
const textContent = el.textContent;
let formatString = bindings.value;
if(!formatString) {
formatString = "YYYY-MM-DD hh:mm:ss"
}
let timestamp = parseInt(textContent);
if(textContent.length === 10) {
timestamp = timestamp * 10
}
el.textContent = dayjs(timestamp).format(formatString);
}
})
teleport
在组件化开发中,我们封装一个组件a,在另一个组件b中使用,那么template中元素,会被挂载到组件b的dom树中。如果我们希望组件移动到body元素,或者其他的div#app之外的元素,可以使用teleport完成。
index.html中
<div id="app"></div>
<!-- built files will be auto injected -->
<div id="juju"></div>
app.vue中
<div class="app">
<teleport to="#juju">
<h2>你傻瓜</h2>
</teleport>
</div>
认识vue插件
use里是个对象,可以设置app的一些属性
app.use({
install(app) {
app.config.globalProperties.$name = 'juju'
}
})
可以在其他地方打印
import {getCurrentInstance } from 'vue'
setup() {
const instance = getCurrentInstance();
console.log(instance.appContext.config.globalProperties.$name);
//等同于methods里this.$name
vue3三大核心系统
- compiler模块:编译模板系统
- runtime模块:渲染模块
- reactivety模块:响应式系统
实现mini-vue
- 渲染系统模块:runtime > vnode > 真实dom
- 可响应式系统模块 reactive
- 应用程序入口
渲染系统实现
- h函数,用于返回一个VNode对象
- mount函数,用于将VNode挂载到DOM
- patch函数,用于两个VNode进行对比,决定如何处理新VNode
h函数实现
const h = (tag, props, children) => {
//vnode ->js对象
return {
tag,
props,
children
}
}
// 1.h函数来创建一个vnode
const vnode = h('div', {class: 'juju'}, [
h('h2', null, "哈哈"),
h('button', null, "+1"),
])
此时可以打印出一个vnode对象
mount函数实现
//2.mount函数,挂载到divapp上
mount(vnode, document.querySelector('#app'))
const mount = (vnode, container) => {
//vnode ->element
// 1.创建真实dom,并且在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)
}
el.setAttribute(key, value)
}
}
// 3.处理子节点
if(vnode.children) {
if(typeof vnode.children === "string") {
el.textContent = vnode.children;
}else {
vnode.children.forEach(item => {
mount(item, el); //递归调用
})
}
}
//将el 挂载到container上
container.appendChild(el);
}
patch函数实现
// diff算法
const patch = (n1, n2) => {
// 如果类型不同,直接替换
if(n1.tag != n2.tag) {
const n1ElParent = n1.el.parentElement; //获取n1父元素
n1ElParent.removeChild(n1.el); //移除n1
mount(n2, n1ElParent); //挂载n2
}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(oldValue !== newValue) {
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 in newProps)) {
if(key.startsWith("on")) {
// 删除属性
const value = oldProps[key];
el.removeEventListener(key.slice(2).toLowerCase(), value)
}else {
el.removeAttribute(key, value)
}
}
}
//3.处理children
const oldChildren = n1.children || [];
const newChildren = n2.children || [];
//如果新节点是一个字符串,直接替换原来的children
if(typeof newChildren === 'string') {
el.innerHTML = newChildren;
}else {//如果oldChildren是一个数组
if(typeof oldChildren === 'string') {
el.innerHTML = '';
newChildren.forEach(item => {
mount(item, el);
})
}else {
//oldChild: [v1, v2, v3]
//newChild: [v1, v5, v6, v8, v9]
// 针对的是无key情况
//1.上面有相同节点的原生进行patch操作
const commonLength = Math.min(oldChildren.length, newChildren.length);
for(let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i]);
}
//2.newChildren > oldChildren 将后面vnode的进行挂载
if(newChildren.length > oldChildren.length) {
newChildren.slice(oldChildren.length).forEach(item => {
mount(item, el);
})
}
//2.newChildren < oldChildren 将后面的进行删除
if(newChildren.length < oldChildren.length) {
oldChildren.slice(newChildren.length).forEach(item => {
el.removeChild(item.el);
})
}
}
}
}
}
发布订阅者模式
class Dep {
constructor() {
this.subscribers = new Set(); //集合,集合中不能有重复的元素
}
depend() {
// 检测到activeEffect有值时,添加依赖
if(activeEffect) {
this.subscribers.add(activeEffect);
}
}
notify() {
this.subscribers.forEach(effect => {
effect();
})
}
}
定义一个dep类,用于收集依赖以及发布通知
let activeEffect = null;
function watchEffect(effect) {
activeEffect = effect;
effect();
activeEffect = null;
}
定义了一个函数用于在外部添加订阅
// Map({key: value}): key是一个字符串
//WeakMap({key(对象)}): 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;
}
定义一个方法,用于创建不同对象属性的不同dep,这样可以实现不同属性访问实现不同的方法
//对数据进行劫持。当数据发生改变时,通知dep
function reactive(raw) {
//vue2方法
Object.keys(raw).forEach(key => {
const dep = getDep(raw, key);
let value = raw[key];
Object.defineProperty(raw, key, {
get() {
//当watchEffect执行时,触发get方法,收集dep依赖
dep.depend();
return value;
},
set(newValue) {
if(value !== newValue) {
value = newValue;
//值发生改变时,发布通知,执行副作用函数
dep.notify();
}
}
});
})
return raw;
}
数据劫持方法
const info = reactive({counter: 100, name: 'juju'});
const foo = reactive({height: 1.88});
//watchEffect1
watchEffect(function doubleCounter() {
console.log("effect1: ",info.counter * 2, info.name);
})
//watchEffec2
watchEffect(function powerCounter() {
console.log("watchEffect2:",info.counter * info.counter);
})
//watchEffec3
watchEffect(function powerCounter() {
console.log("watchEffect3:",info.counter+10, info.name);
})
//watchEffec4
watchEffect(function powerCounter() {
console.log("watchEffect4:",foo.height);
})
// info.counter++;
info.name = 'juju'; //修改name,只有1,3函数执行
此时修改counter,只有1,2,3执行
完整vue2订阅劫持写法
class Dep {
constructor() {
this.subscribers = new Set(); //集合,集合中不能有重复的元素
}
depend() {
// 检测到activeEffect有值时,添加依赖
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(对象)}): 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;
}
//对数据进行劫持。当数据发生改变时,通知dep
function reactive(raw) {
//vue2方法
Object.keys(raw).forEach(key => {
const dep = getDep(raw, key);
let value = raw[key];
Object.defineProperty(raw, key, {
get() {
//当watchEffect执行时,触发get方法,收集dep依赖
dep.depend();
return value;
},
set(newValue) {
if(value !== newValue) {
value = newValue;
//值发生改变时,发布通知,执行副作用函数
dep.notify();
}
}
});
})
return raw;
}
const info = reactive({counter: 100, name: 'juju'});
const foo = reactive({height: 1.88});
//watchEffect1
watchEffect(function doubleCounter() {
console.log("effect1: ",info.counter * 2, info.name);
})
//watchEffec2
watchEffect(function powerCounter() {
console.log("watchEffect2:",info.counter * info.counter);
})
//watchEffec3
watchEffect(function powerCounter() {
console.log("watchEffect3:",info.counter+10, info.name);
})
//watchEffec4
watchEffect(function powerCounter() {
console.log("watchEffect4:",foo.height);
})
// info.counter++;
info.name = 'juju'; //修改name,只有1,3函数执行
vue3proxy
proxy的好处
- vue2是对对象的每一个属性进行劫持。如何新加入一个属性,要使用$set方法。而vue3是对整个对象劫持
- proxy能必defineproperty观察更丰富。除了get set外还有delete等
vue3劫持过程
function reactive(raw) {
//vue3方法
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();
}
})
}