vue3学习18、19vue3高级语法补充及vue3源码学习

55 阅读2分钟

自定义指令

自定义组件分为两种

  • 自定义局部指令:组件中通过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或事件监听器被应用之前调用
  1. beforeMount
  2. mounted
  3. beforeUpdate:在更新组件的VNode之前调用;
  4. updated:在包含组件的VNode及其子组件的VNode更新后调用
  5. beforeUnmount: 在卸载绑定元素的父组件之前调用
  6. 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模块:响应式系统

598c97da8cae8f588346342a4ba67b9.jpg

实现mini-vue

  • 渲染系统模块:runtime > vnode > 真实dom
  • 可响应式系统模块 reactive
  • 应用程序入口

渲染系统实现

  1. h函数,用于返回一个VNode对象
  2. mount函数,用于将VNode挂载到DOM
  3. 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();
    }
  })
}