vue3 源码
接上Vue3源码的文章,没看过的点击 跳转
Reflect
我们都知道proxy
可以对对象进行代理,fn不也是对象么,同理,也可以对方法进行代理:
const fn = (name) => {
console.log('我是:', name)
}
const p2 = new Proxy(fn, {
// 使用 apply 拦截函数调用
apply(target, thisArg, argArray) {
target.call(thisArg, ...argArray)
}
})
p2('hcy') // 输出:'我是:hcy
上文中揭示了Proxy 来拦截函数的调用操作,值得注意的是Proxy 只能够拦截对一个对象的基本操作。比如下文就无能为力:
obj.fn()
这个例子的流程是:
- 第一个基本语义是 get,即先通过 get 操作得到 obj.fn 属性。
- 第二个基本语义是函数调用,即通过 get 得到 obj.fn 的值后再调用它
你可能已经注意到了,Reflect 下的方法与 Proxy 的拦截器方法名字相同,其实这不是偶然。任何在 Proxy 的拦截器中能够找到的方法,都能够在 Reflect 中找到同名函数。拿 Reflect.get 函数来说,它的功能就是提供了访问一个对象属性的默认行为。那他的价值体现在哪?
看例子:
const obj = { foo: 1 }
console.log(Reflect.get(obj, 'foo', { foo: 2 })) // 输出的是 2 而不是 1
Reflect提供了第三个参数有点像this
。
继续看例子:
const state = reactive({
name: "111",
get bar() {
return this.name
}
})
effect(() => {
console.log(state.bar);
});
state.name = 1
你会发现输出一次111
就没了,聪明的你也许会说因为bucket里面的map里面存储的key是bar
,确实如此。但是这只是结果,明明bar方法中读取name了,为什么还是不行呢?答案是this
指向的是{name: "111",get bar() {return this.name}}
,而并非proxy代理以后的对象。
那我们来如下改造一下:
new Proxy(state, {
get(target, key, receiver) {
track(target, key)
// 使用Reflect读取值
return Reflect.get(target, key, receiver);
},
...
...
...
})
这样就行了,this的指向也正确了,map的key也对了
继续
const obj = reactive({ foo: { bar: 1 } })
effect(() => {
// for...in 循环
console.log(obj.foo.bar)
});
obj.foo.bar = 3;
监听不到,因为obj虽然是响应式,但是obj.foo不是。所以解决方案是:
get(target, key, receiver) {
track(target, key)
const res = Reflect.get(target, key, receiver);
if (typeof res === 'object' && res !== null) {
// 调用 reactive 将结果包装成响应式数据并返回
return reactive(res)
}
return res
},
数组
- 对数组来说只需要监听数组的length就好了,当修改的key也就是index大于数组长度我们就触发add,小于就触发set(index大于length更新length的长度是规范)
- 当然,对于数组的方法去修改数组也是在proxy中做一个拦截,这样就可以正确触发响应式了
- 无论是使用 for...of 循环,还是调用values 等方法,它们都会读取数组的 Symbol.iterator 属性。该属性是一个 symbol 值,为了避免发生意外的错误,以及性能上的考虑,我们不应该在副作用函数与 Symbol.iterator 这类 symbol 值之间建立响应联系,因此需要修改 get 拦截函数
- 当然在effect中去写其他的数组方法也是需要挨个修正的,在此不做赘述
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
// 省略其他拦截函数
ownKeys(target) {
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
},
get(target, key, receiver) {
console.log('get: ', key)
if (key === 'raw') {
return target
}
// 添加判断,如果 key 的类型是 symbol,则不进行追踪
if (!isReadonly && typeof key !== 'symbol') {
track(target, key)
}
const res = Reflect.get(target, key, receiver)
if (isShallow) {
return res
}
if (typeof res === 'object' && res !== null) {
return isReadonly ? readonly(res) : reactive(res)
}
}
})
}
ref
经过前面的学习可以制造proxy只可以代理对象,那么对于原始的变量(是 Boolean、Number、 BigInt、String、Symbol、undefined 和 null)只能构建一个对象去存储了。
// 封装一个 ref 函数
function ref(val) {
// 在 ref 函数内部创建包裹对象
const wrapper = {
value: val
}
// 将包裹对象变成响应式数据
return reactive(wrapper)
}
但是我们还需要区别 ref 和 reactive,因为ref在某些时候可以脱衣(结构value),所以:
function ref(val) {
const wrapper = {
value: val
}
// 使用 Object.defineProperty 在 wrapper 对象上定义一个不可枚举的属性 __v_isRef,并且值为 true
Object.defineProperty(wrapper, '__v_isRef', {
value: true
})
return reactive(wrapper)
}
响应式丢失
export default {
setup() {
// 响应式数据
const obj = reactive({ foo: 1, bar: 2 })
// 1s 后修改响应式数据的值,不会触发重新渲染
setTimeout(() => {
obj.foo = 100
}, 1000)
return {
...obj
}
}
}
这里结构以后返回的是一个普通对象,自然就丢失了响应式,但是也是有办法解决的:
function toRef(obj, key) {
const wrapper = {
get value() {
return obj[key]
}
}
return wrapper
}
function toRefs(obj) {
const ret = {}
// 使用 for...in 循环遍历对象
for (const key in obj) {
// 逐个调用 toRef 完成转换
ret[key] = toRef(obj, key)
}
return ret
}
const newObj = { ...toRefs(obj) }
那么为了统一,ref也得修改为:
const wrapper = {
get value() {
return obj[key]
}
// 允许设置值,如果不加的话ref出来的值只是可读的
set value(val) {
obj[key] = val
}
}
脱衣服
由于 toRefs 会把响应式数据的第一层属性值转换为 ref,因此必须通过 value 属性访问值,但是会增加负担,上代码!
function proxyRefs(target) {
return new Proxy(target, {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver)
// 自动脱 ref 实现:如果读取的值是 ref,则返回它的 value 属性值
return value.__v_isRef ? value.value : value
}
})
}
// 调用 proxyRefs 函数创建代理
const newObj = proxyRefs({ ...toRefs(obj) })
这样一来,我们可以在模板直接访问一个 ref 的值,而无须通过 value 属性来访问
穿衣服
只能脱不能穿确实不太礼貌。
function proxyRefs(target) {
return new Proxy(target, {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver)
return value.__v_isRef ? value.value : value
},
set(target, key, newValue, receiver) {
// 通过 target 读取真实值
const value = target[key]
// 如果值是 Ref,则设置其对应的 value 属性值
if (value.__v_isRef) {
value.value = newValue
return true
}
return Reflect.set(target, key, newValue, receiver)
}
})
}
渲染器
将template代码通过编译器转义到effect方法中,实现双向绑定:
const { effect, ref } = VueReactivity
function renderer(domString, container) {
container.innerHTML = domString
}
const count = ref(1)
effect(() => {
renderer(`<h1>${count.value}</h1>`, document.getElementById('app'))
})
count.value++
以上只是实现了渲染,但是一个渲染器是抽象概念,应该赋予更多能力(比如跨平台),所以:
function createRenderer() {
function render(vnode, container) {
if (vnode) {
// 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行打补丁
patch(container._vnode, vnode, container)
} else {
if (container._vnode) {
// 旧 vnode 存在,且新 vnode 不存在,说明是卸载(unmount)操作
// 只需要将 container 内的 DOM 清空即可
container.innerHTML = ''
}
}
// 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode
container._vnode = vnode
}
return {
render
}
}
const renderer = createRenderer()
// 首次渲染
renderer.render(vnode1, document.querySelector('#app'))
// 第二次渲染
renderer.render(vnode2, document.querySelector('#app'))
// 第三次渲染
renderer.render(null, document.querySelector('#app'))
- 在首次渲染时,渲染器会将 vnode1 渲染为真实 DOM
- 在第二次渲染时,旧 vnode 存在,此时渲染器会把 vnode2 作为新 vnode,并将新旧 vnode 一同传递给 patch 函数进行打补丁
- 在第三次渲染时,新 vnode 的值为 null,即什么都不渲染。但此时容器中渲染的是 vnode2 所描述的内容,所以渲染器需要清空容器
在架构中,将各种方法封装,给vue3赋予跨平台的能力:
// 在创建 renderer 时传入配置项
const renderer = createRenderer({
// 用于创建元素
createElement(tag) {
return document.createElement(tag)
},
// 用于设置元素的文本节点
setElementText(el, text) {
el.textContent = text
},
// 用于在给定的 parent 下添加指定元素
insert(el, parent, anchor = null) {
parent.insertBefore(el, anchor)
}
})