vue3 亮点
更小、更快、更优雅,更好的 ts 支持
为什么快?
diff优化
vue2中是全量对比,就是对比组件的每一个节点
vue3新增了静态标记,为可能变化的响应式节点添加 flag 属性,在对比的时候只比较标记的节点。
flag 的值对应不同的节点类型,比如取值为1代表动态文本,只对比文本变化,取值为2代表动态calss,只对比class变化等等。
静态提升
vue2 中无论元素是否参与更新,每次都会重新创建
vue3 中对不参与更新的元素会做静态提升,只会被创建一次。
静态提升就是把不参与更新的元素创建vdom的操作提到 render 外面,在 render 中不重复创建。
事件侦听器缓存
默认情况下事件视为动态绑定,每次都会追踪变化。但是因为是同一个函数,缓存复用即可。
创建 vdom 时候通过取消静态标记避免追踪提升性能
为什么优雅?
vue2 中一个功能的逻辑分散在多个区域,项目多次迭代后维护成本增加。
vue3 中使用组合api 可以将相同的逻辑写在一个函数中,而且可以将一个功能提到一个文件中,通过导入的方式使用,直观高效便于维护。
composition 解读
coposition 的实质
组合方式和配置方式可以混用。使用 setup() 暴露的变量会注入到对应的配置项中。
setup 执行时机
在 beforeCreated 和 created 之间。 因为 data 和 methods 未初始化完毕,setup 方法中不能data和methods。 为了避免错误使用,vue将this赋值为 undefine。
setup 可以是异步的么
只能是同步的
API
-
ref ,toRef,toRefs,reactive
这些容器的作用都是将原始数据包装成响应式数据。
区别在于:修改响应式数据是否影响原数据以及是否会自动更新视图
说明:包装类型是说传的参数是什么数据类型,对应关系中复制关系修改响应式数据不会影响原数据。
注意点:
reactive 用于数组和对象
如果传了其他对象,比如日期对象,不会自动更新,可以通过重新赋值的方式触发更新。 只有修改经过包装后的对象才会触发视图更新,而修改原对象,值会变化但是不触发更新。 因为 reactive 和 原始值是 引用关系,即存的是内存地址,修改原始值并未修改 reactive 的指向所以不会更新。
ref 用于基本数据类型
本质:ref(xx) => reactive({value: xx})
使用 ref 创建的响应式数据,在模板中不用通过 .value 获取值。 vue 会根据 __v_isRef 取判断,自动添加.value。可以通过 isRef / isReactive 方法来判断响应式对象是什么类型。
-
shallowRef,shallowReactive,triggerRef
- shadllowRef / shallowReactive
ref 和 reactive 都属于递归监听,数据量大时会有性能问题。
shallowRef 和 shallowReactive 可以实现非递归监听。
shallowRef 监听的是 value 的变化
-
triggerRef
根据传入的数据主动更新界面,只适用于 ref ,配合 shallowRef 使用
-
toRaw
用于从 ref 或者 reactive 中获得原始数据。
使用场景:在使用了ref或者reactive跟踪数据变化后,对于一些不需要追踪更新页面的操作,可以通过修改原始数据,防止不必要的更新操作,提升性能。
想获取 ref 的原始数据,要使用 toRaw(xx.value)
-
markRaw
让数据永远不被追踪,包装后的数据再去用响应式容器包装无效
vue2 和 vue3 数据响应式实现
- vue2 数据劫持 + 发布订阅
通过数据劫持将data转化为getter和setter并记录相应的依赖。
每个组件实例都对应一个watcher实例,而watcher实例依赖于setter。
当数据变化会触发setter,setter去通知对应的watcher,watcher去更新视图
class Dep {
constructor() {
this.subs = []
}
depend() {
Dep.target && this.subs.push(Dep.target)
}
notify() {
this.subs.forEach((sub) => { sub.update() })
}
}
class Watcher {
constructor(target, key, cb) {
Dep.target = this
this.target = target
this.key = key
this.cb = cb // 假设 cb 是渲染函数
this.val = target[key]
Dep.target = null
}
update() {
this.val = this.target[this.key]
this.cb(this.value)
}
}
class Observer {
constructor(data) {
this.data = data
if (typeof data !== 'object') {
return data
}
if (Array.isArray(data)) {
const dp = new Dep()
const methods = ['pop', 'push'] // 等等数组方法
const proto = Object.create(Array.prototype)
methods.forEach((method) => {
const originMethod = proto[method]
Object.defineProperty(methods, method, {
value: function () {
const res = originMethod.call(this, ...arguments)
// notify
dp.notify()
return res
}
})
})
return
}
Object.keys(data).forEach((key) => { defineReactive(data, key, data[key]) })
}
}
function defineReactive(target, key, val) {
new Observer(val)
const dp = new Dep()
Object.defineProperty(target, key, {
get() {
dp.depend()
console.log('get', val)
return val
},
set(newVal) {
new Observer(newVal)
if (val === newVal) { return }
val = newVal
console.log('set', newVal)
dp.notify()
},
})
}
class Vue {
constructor(options) {
this.data = options.data;
new Observer(this.data);
/* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
new Watcher(this.data, 'name',
function () {
console.log('模拟视图渲染')
});
}
}
let vm = new Vue({
data: { name: 'aaa' }
})
vm.data.name = 'ccc'
-
vue3 proxy重写
使用 proxy 重写拦截,track 中以 weakMap -> Map -> Set 的数据结构收集依赖,trigger 触发依赖。
const toProxy = new WeakMap() // WeakMap 结构是弱引用,用来解决内存泄露
const toRaw = new WeakMap()
function isOwnProperty(target, key) {
return target.hasOwnProperty(key)
}
function isObject(obj) {
return typeof obj === 'object' && obj !== null
}
const activeEffectStacks = []
const targetMap = new WeakMap()
function effect(fn) {
const effectFn = createReactiveEffect(fn)
effectFn()
}
function createReactiveEffect(fn) {
const effect = function () {
run(effect, fn)
}
return effect
}
function run(effect, fn) {
try {
activeEffectStacks.push(effect)
fn()
} finally {
// 防止只存不删
activeEffectStacks.pop(effect)
}
}
function track(target, key) {
let effect = activeEffectStacks[activeEffectStacks.length - 1]
if (!effect) { return }
// 构造 weakMap -> Map -> Set 形式的数据结构
if (!targetMap.get(target)) {
targetMap.set(target, depsMap = new Map())
}
if (!depsMap.get(key)) {
depsMap.set(target, deps = new Set())
}
if (!deps.has(effect)) {
deps.add(effect)
}
}
function trigger(target, type, key) {
// 取 effect 执行
if (
targetMap.get(target) &&
depsMap.get(key)
) {
depsMap.get(key).forEach((effect) => { effect() })
}
}
function reactive(target) {
if (!isObject(target)) {
return target
}
let proxy = toProxy.get(target)
if (proxy) {
return proxy
}
if (toRaw.has(target)) {
return target
}
const handler = {
get(target, key, receive) {
const res = Reflect.get(target, key, value, receive)
// 依赖收集
track(target, key)
return isObject(target[key]) ? reactive(target[key]) : res
},
set(target, key, value, receive) {
const oldVal = target[key]
const res = Reflect.set(target, key, value, receive)
// 如不区分,拦截数组时会重复触发
if (!isOwnProperty(target, key)) {
trigger(target, 'add', key)
console.log('添加新属性,渲染视图')
} else if (oldVal !== value) {
trigger(target, 'set', key)
console.log('修改原属性,渲染视图')
}
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
console.log('删除,渲染视图')
return res
},
}
const result = new Proxy(target, handler)
// 缓存结果,解决重复包装响应式数据的性能问题
toProxy.set(target, result)
toRaw.set(result, target)
return result
}
const data = { age: 18 }
const data2 = [1, 2]
const obj = reactive(data2)
p = reactive(data2)