彩蛋来了 写在前面,最近打算学习vue3.0 相关知识,本着学习一个东西,最好方法就是模仿写一个,所以自己动手写了一个简化版vue3.0,自己称作mini-vue3.0 感觉对vue3.0 或者 vue2.x核心原理的理解有很大帮助,所以分享出来。mini-vue3.0主要包括:模板编译、响应式、组件渲染过程等, 仓库地址mini-vue3.0,欢迎star
响应式原理
Proxy
众所周知,vue2.x响应式是基于Object.defineProperty的数据劫持来实现的,而在vue3.0 中则采用新的ES6 API Proxy来做数据劫持。
具体的Proxy用法本文就不做详述了,具体可以参考Proxy,这里简单介绍一下Proxy 优缺点。
优点:
- 可以劫持对象新加属性
const obj = { a: 1 } const proObj = new Proxy(obj, ...) proObj[b] = 2 // Object.defineProperty 是不能劫持的,而Proxy 可以劫持 复制代码
- 可以劫持数组的push、shift等相关操作
const ary = [1] const proObj = new Proxy(ary, ...) proObj[1] = 2 // Object.defineProperty 是不能劫持的,而Proxy 可以劫持 复制代码
缺点:
- 不能深度劫持对象属性
- 可能会触发多次的数据劫持调用
const ary = [1, 2, 3]
const proObj = new Proxy(ary, ...)
proObj.slice(1, 0, 4) // 插入一个数字4 ,会触发多次proObj 数据劫持更新
```
## vue3.0 响应式原理解析
在分析vue 响应式原理时,需要时刻牢记观察者模式(发布/订阅模式)。很简单理解,就是一个对象存储回调,然后在适当时机触发回调。
本质的思想还是比较简单。接下来,我们简单实现一个数据响应式功能。
### 数据劫持
这里主要是做数据拦截,当渲染模板时候,访问响应式数据时,会做依赖收集。简单实现如下,代码都有详细注释
```js
// 一些辅助工具函数
const isObject = (val) => val !== null && typeof val === 'object'
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (obj, key) => hasOwnProperty.call(obj, key)
const toRaw = new WeakMap() // raw -> proxy 对象映射
const toProxy = new WeakMap() // proxy -> raw 对象映射
const targetMap = new Map() // 回调收集Map
// 设置响应式
const reactive = (obj) => {
// 如果已经是代理过的对象,直接返回代理对象
if (toProxy.has(obj)) {
return toProxy.get(obj)
}
// 如果已经是代理对象,直接返回代理对象
if (toRaw.has(obj)) {
return obj
}
// 注意Proxy 只能代理到一层
const proxy = new Proxy(obj, {
get(target, key, receiver) {
track(target, key) // 这里是依赖收集,具体逻辑见下文
const value = Reflect.get(target, key, receiver)
return isObject(value) ? reactive(value) : value
},
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver)
value = toRaw.get(value) || value
const observed = Reflect.set(target, key, value, receiver)
// 解决数组多次触发问题
if (!hasOwn(target, key)) {
trigger(target, key)
} else if (value !== oldValue) {
trigger(target, key)
}
if (!targetMap.has(target)) {
// 设置对象回调函数Map
targetMap.set(target, new Map())
}
return observed
}
})
toRaw.set(proxy, obj)
toProxy.set(obj, proxy)
return proxy
}
复制代码
依赖收集
// 依赖收集
const track = (target, key) => {
// 获取对象回调函数Map
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 获取对象,对应属性的回调函数Set
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 这里的 activeEffect 其实就是渲染函数,你可以认为就是 render 函数
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
}
}
复制代码
触发执行
// 这里触发执行触发执行
const trigger = (target, key) => {
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
const effects = depsMap.get(key)
effects.forEach(effect => effect())
}
复制代码
简单举个例子说明一下:
// 例如将如下数据设置响应式
const data = {
a: 1
}
// 设置数据响应式
const proxyData = reactive(data);
// 此时
toProxy = {
data: proxyData
}
toRaw = {
proxyData: data
}
// 当我们访问数据属性时候
activeEffect = () => {
console.log(proxyData.a)
}
activeEffect()
// 会收集回调函数 activeEffect
targetMap = {
data: {
a: [activeEffect]
}
}
// 我们改变响应数据
proxyData.a = 2
// 则会触发执行,开始重新收集依赖
targetMap[data][a].forEach(cb => cb())
复制代码
以上只是简单说明vue3.0 响应式核心原理,vue 3.0数据源代码实现复杂的多,有兴趣同学可以自行了解。有了上面基础,想必会更加容易了
ref、computed实现原理
介绍一个数据响应式原理,这里再简单介绍一下ref、computed的原理
ref实现原理
为什么会需要ref函数?
因为reactive 和 ref满足两种代码风格
- reactive 风格
const reac = reactive({
a: 1,
b: 2
})
复制代码
- ref 风格
const a = ref(1)
const b = ref(2)
复制代码
一个典型实际应用例子,比如我们经常在页面设置各种loading,控制加载。
// 风格1
const loading = {
a: false,
b: false
}
// 风格2
let loadingA = false
let loadingB = false
复制代码
ref 源码分析
function ref(raw) {
// 判断是否已经经过ref 处理
if (isRef(raw)) {
return raw
}
// 如果值为对象,设置数据响应式
raw = reactive(raw)
const r = {
_isRef: true,
get value() {
// 依赖收集
track(r, TrackOpTypes.GET, 'value')
return raw
},
set value(newVal) {
raw = reactive(newVal)
// 触发响应式回调
trigger(
r,
TriggerOpTypes.SET,
'value',
)
}
}
return r
}
复制代码
其实ref实现原理比较简单,就是在原始数据外面再包一层代理,实现响应式
computed实现原理
以下为 computed 简单实现代码
function computed(getterOrOptions)
let getter
let setter
// 参数如果为函数的话,默认为getter 函数
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
setter = () => {
console.warn('Write operation failed: computed value is readonly')
}
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
let dirty = true
let value
// effect 等价于 vue2.x 中 watcher
const runner = effect(getter, {
lazy: true, // 不会立即执行,所以computed 可以起到缓存的作用
computed: true,
scheduler: () => {
dirty = true
}
})
return {
_isRef: true,
get value() {
// 为dirty时候才会重新求值
if (dirty) {
value = runner()
dirty = false
}
// 具体作用见下文分析
trackChildRun(runner)
return value
},
set value(newValue: T) {
setter(newValue)
}
}
}
复制代码
- 举例分析:
const b = reactive({ a: 1})
const c = computed(() => b.a)
复制代码
当改变b的值时候,如 b.a = 2。此时只会触发computed的scheduler,设置dirty =true
只有当访问 c.value 值时,才会触发computed的get代理,执行runner函数,重新计算求值
- trackChildRun作用:实现链式计算属性,父effect会记录computed的runner记录的dep回调函数,从而实现链式计算属性
这里举个例子说明,会更清晰一些
const obj = {a: 1}
const objProxy = reactive(obj)
const comp = computed(() => { console.log(objProxy.a)} )
// 当我们访问 comp
comp.value
// obj1 响应式依赖收集时,获得计算属性的 runner函数,作为回调
targetMap[obj].a = [runner]
// 渲染模板
//<div>{{comp.value}}</div>
// 调用render 函数后
targetMap[obj].a = [runner, render]
objProxy.a = 2
同时触发计算属性表达式重新求值、模板更新,达到链式调用
复制代码