对标vue2看vue3(比较中学习,而非重建轮子)

132 阅读6分钟

尚硅谷的vue3笔记

Vue3 特征

对比Vue2,打包体积、占用内存变小;渲染速度变快; 语法-新特性:

  • 组合式api(setup配置);
  • ref与reactive;
  • watch与watchEffect;
  • provide与inject
  • 源码升级:响应式-proxy(ES6)代替Object.defineProperty
  • 工程构建:vue2使用webpack,vue3使用vite(新一代构建工具) vite优势:无需打包,冷启动;轻量快速,热重载;按需编译,少等待 --webpack是先编译,后分析;vite是动态引入和代码分析

创建vue3工程

脚手架创建(vue create prj_name/vue init vite-app prj_name); ​ 使用vite创建;

Vue3语法

入口配置
入口文件:index.html <% BASE_URL %>插值语法,用于动态匹配服务端路径配置
main.js 
    import {createApp} from 'vue'
    const app = createApp(App)
    app.config.globalProperties.$http = () => {}
    app.use(elementPlus).use(router).mount('#app')

Vue组件实例变精简

  • vue2的实例:this可访问到的都在实例上,另外还有其他全局挂载实例上的
  • vue3的实例:通过app.config.globalProperties.$http = () => {}设置全局属性 并使用 getCurrentInstance 方法获取当前实例
组合式composition api -- setup

功用:返回(模版)可用数据和方法,且不能是async(包裹promise),执行上下文不被阻塞

包含参数(props,context)

props获取子组件的传入参数

如不设置会在$attr中找到 props往往需要解构出来,取用参数(props本身具备响应式,但解构之后不具备)

一般使用toRefs使其具备响应性let {name, age} = toRefs(props)

对于可选prop有时需要手动设置以保证响应 const title = toRef(props, 'title')

context上下文参数

其中包含attrs-相当于vm.$attrs

emit触发自定义事件的-先在子组件中触发事件context.emit('testEvent')

slots用于获取父组件传入的插槽(一般写作v-slot: slotName)

其中可用方法需要手动导入 import {ref, reactive, toRefs, computed, watch} from 'vue'

声明响应式:ref和reactive

ref作用于值类型,构建RefImpl响应对象,在js中使用data1.value访问,设置对象时,交给reactive处理。

reactive(仅)作用于对象,构建Proxy(ES6)响应对象,深层嵌套的响应式(嵌套的每一层都被申明为Proxy包裹)

setup(){
    let name = ref('zhangsan')  //>>>变成响应式
    let age = ref('100')
    let persons = reactive({type: 'teacher', info: {name: 'gpf', age: 23}})//<<< 操作对象的响应式
    let common = 'test' //>>> 可以显示,但不具备响应式
​
    //ref必须设置数据项的value才能是响应式
    functionn changeObj(){
        name = 'lisi' >>> 不引起数据改变
        age = '22'
        name.value = 'changeName' //>>> 引起数据改变 (refImpl对obj.defineproperty的封装,原型中设置setter、getter,并把value暴露到实例属性中,用响应式操作《〈《 模板中自动解析value)
        age.value = 'changeAge'
        console.log(name) >>> refImpl(引用实现){name: 'lisi', age: '22'}
    }
    return {
        name, age
    }
}

注意点:操作对象(值类型,对象),操作方式(前者加value),响应式(defineproperty与proxy)

响应式-浅层次处理,优化性能

shallowReactive(shallow浅层次的)《〈《 用法类似,但只监听 第一层

shallowRef(处理基础类型,作为响应式,对象类型的不经过reactive的处理,默认ref则会) 只处理第一层:let value = shallowRef({y: 0}) 则在template中 value = {y: 100} //具备页面响应 value.y ++不具备页面响应 需要特别注意,只操作第一层的应用时机。

readonly(修改报提示,修改不成功)、shallowReadonly(处理响应式数据)

响应式原理

vue2(使用defineProperty,并用$set弥补缺陷,当然数组的push、pop等操作方法也是弥补)

let person = {}
Object.defineProperty(person, 'name', {
    configurable: true,
    get(){
        return person.name
    },
    set(val){
        // 进行页面更新渲染操作
        person.name = val
    }
}) // 缺陷新增的属性,不能响应, $set添加监听

$set的原理

重新设置依赖收集,并触发响应

function set(target: Array | Object, key: any, val: any): any { ... }

set 方法会对参数中的 target 进行类型判断

  1. 如果是 undefined 、null 、基本数据类型,直接报错;
  2. 如果为数组,取当前数组长度与 key 这两者的最大值作为数组的新长度,然后使用数组的 splice 方法将传入的索引 key 对应的 val 值添加进数组。target 在 observe 的时候,原型链被修改了, splice 方法也已经被重写了,触发之后会再次遍历数组,进行数据劫持,也就是说当使用 splice 方法向数组内添加元素时,该元素会自动被变成响应式的;
  3. 如果为对象,会先判断 key 值是否存在于对象中,如果在,则直接替换 value。如果不在,就判断 target 是不是响应式对象(其实就是判断它是否有 ob 属性),接着判断如果它是不是 Vue 实例,或者是 Vue 实例的根数据对象,如果是则抛出警告并退出程序。如果 target 不是响应式对象,就直接给 target 的 key 赋值,如果 target 是响应式对象,就调用 defineReactive 将新属性的值添加到 target 上,并进行依赖收集,更新视图更新;
  4. 最后

// 是响应式对象,进行依赖收集

defineReactive(ob.value, key, val) 

// ob.dep 这个里面放着订阅者模式里面的订阅者,通过notify来通知订阅者做处理

ob.dep.notify() 

vue3(使用proxy,与reflect一样为es6的元编程api)注意IE不支持proxy

let person = {name: 'gpf', age: 100}
const p = new Proxy(person, {
    get(obj, property){
        // 无明确指定属性
        returnn obj.property
    },
    // 修改或者新增属性
    set(obj, property, val){
        if (property === 'age') {
            if (!Number.isInteger(val)) {
                throw new TypeError('The age is not an integer');
            }
            if (val > 200) {
                throw new RangeError('The age seems invalid');
            }
        }
        // 属性值修改,进行页面更新渲染操作
        obj[property] = val
        console.log('修改了obj对象的property属性,修改值为val')
    },
    deleteProperty(target, propName){
        console.log('删除属性需要,自己去实现下,相当于封装了自己的元操作')
        return delete target[propName]
    }
})
console.log(p)     //====>Proxy{ [[Handler]], [[Target]]} 处理函数和处理数据对象
p.sex = '男' //<<<新增删除属性时,就不需要$set或者$delete
delete p.name

Reflect(ECMA正把Object上的方法和属性放到reflect上,解决哪些问题==>)

Object.defineProperty(Obj, 'name', {})
Object.defineProperty(Obj, 'name', {}). // 报错
const x1 = Reflect.defineProperty(Obj, 'name', {})
const x2 = Reflect.defineProperty(Obj, 'name', {})
//有返回结果(成功返回true,报错返回false),不抛出异常(对于框架封装来说,不至于总爆异常)
computed(()=>{}) 返回值为ref对象
//===> 作用与vue2一样,不同处在于可以作为返回值,挂在其他对象上
person.fullName = computed(()=>{
get(){
return person.name + ' ' + personn.lastName
}
set(val){
let. ret = person.fullName.split(' ')
person.firstName = ret[0]
person.lastName = ret[1]
}
})
watch(params: value | Array, function, options)
// 可以监视多个变量
watch(sum, (newVal, oldVal){
      console.log(newVal +'change: '+ oldVal)
}, {immediate: true, deep: true})
    
deep默认为true
现阶段版本watch的问题
    1.reactive定义的数据无法获得old的数据,也是new的;
    2.默认deep为true,设置为false也不行《〈 关注后期是否改变
这里又必须设置deep为true才能监听到person.job.salary,vue3还是有些问题的---监听对象的数据项一定要注意

// 且watch监听ref数据项就不要加value,因为.value把值给取出来了,不能监听值本身 <<< 待追加实例

// 但watch监听ref声明的reactive对象(ref会把数据包成RefImpl结构,需要.value取出proxy监听对象,或者手动加上深度监视)<<< 待追加实例

watchEffect 用谁,就监视谁,接着触发(默认深度触发),类似computed,但不用return
watchEffect(()=>{
    const x1 = sum.value
    log('change value')
})
生命周期

仅destroy变为unmounted,其他不变,且都需要加on 如onMounted:{}

  • 新增生命周期-renderTracked(模版/页面 有取值操作时触发) methods: { addCount() { this.count += 1; }, }, renderTracked({ type, key, target, effect }) { console.log('renderTracked ----', { type, key, target, effect }); }, 参数列表

    • type:操作类型,有get,has,iterate,也就是取值操作;
    • key:键,简单理解就是操作数据的key,e.g.上文使用的count;
    • target:响应式对象,如:data、ref、computed;
    • effect:数据类型为Function,effect方法的作用是重新render视图;
  • 新增-renderTriggered(数据改变触发) 在beforeUpdate(数据改变,DOM渲染前)前执行

  • 新增-errorCaptured 错误传播规则: 默认全局的config.errorHandler处理 app.config.errorHandler = (err, vm, info) => {} 链式传播:若组件实例的继承链或者父链,存在errorCaptured则逐级向上抛出和处理错误,最终到全局配置的错误处理。另外,若中继的errorCaptured抛出新的错误,则这些错误都会被抛出 链式传播过程中,中间过程返回false可以

数据操作方法

构造响应式数据项

toRef 解决问题 : return {name: toRef(person, 'name'); salary: toRef(person.job.type, 'salary');} 把对象属性扁平化出来,创造RefImpl对象(产生的结果关联原始数据,互相影响)<<< 可以是嵌套对象,一般抽离出对象中的常用数据项 分析: person.name 为值类型,不具有响应式,需要toRef变为RefImpl对象,具有响应式,且它与person产生关联

toRefs 批量处理const person=reactive({name: 'gpf', age: 100, job:{type: {salary: 1000}}}) 一般收集到返回结果中:return {…toRefs(person)} 把其中的内容都转为RefImpl《〈《name,age,job

强调不做响应式

toRaw不常用(获得原始对象)

markRaw(--第三方类库、大列表--标记、设置为不做响应式,一般作用于层级深的对象,提升性能) 类比于Object.freeze

customRef // 自定义ref实现响应式 <<<做一个防抖处理

function myRef(value, delay){
    let timer
    return customRef((track, trigger)=>{
        get(){
            console.log('所有获取渲染数据的地方都会获取这个数据')
            track()
            return value
        },
            set(newVal){
                console.log('修改该自定义数据项,引发')
                clearTimeout(timer)
                timer = setTimeout(()=>{
                    value = newVal
                    trigger()
                }, delay)
            }
    })
}

let person = myRef('hello', 1000)

状态判断(isRef、isReadonly)

provide/inject(跨层级调用)

传递响应式数据 const age = ref(18);

provide("provideData", {
  age,
  data: reactive({ name: "先早" }),
  getData: getData
});

取值 inject("provideData")

自定义hook(相当于mixin,抽离公共逻辑)
'./handler.js' 
function clickHandler(event){
    console.log('')
}
onMounted(){
    window.addEventLListener('click', clickHandler)
}
onUnmounted(){
    window.removeEvenntListener('click', clickHandler)
}

在实践中,mixin 有如下缺点:

  1. 引入了隐式依赖关系。
  2. 不同 mixins 之间可能会有先后顺序甚至代码冲突覆盖的问题
  3. mixin 代码会导致滚雪球式的复杂性
  4. 多个 mixin 导致合并项不明来源

为了避开这些问题,React 采用 HOC,但它依然存在缺陷:

  1. 一个组件的state影响许多组件的props
  2. 造成地狱嵌套

不过使用全新的 Hook 组件结构,可以实现平铺式调用组件的复用部分,解决了 mixin 的来源不明和 HOC 的地狱嵌套问题。

异步组件
自定义指令

指令生命周期 vue2(bind, inserted, update, componentUpdate, unbind) vue3(created, beforeMount, mounted, beforeUpdate, updated, beforeUnmount, Unmounted) 对标组件的生命周期(指令就是给组件用的,对标更合适)

全局注册指令

app.directive('pin', {
  mounted(el, binding) {
    el.style.position = 'fixed'
    // binding.arg 是我们传递给指令的参数
    const s = binding.arg || 'top'
    el.style[s] = binding.value + 'px'
  }
})

使用: <p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.

teleport(totast)组件

子组件将teleport包裹内容放到父组件之外的标记dom节点

<teleport to="#portal">
	<div v-if="isOpen" class="notification">
		This is rendering outside of this child component!
	</div>
</teleport>

to="#portal",即添加元素的位置