Skip to content

vue3 源码

接上Vue3源码的文章,没看过的点击 跳转

Reflect

我们都知道proxy可以对对象进行代理,fn不也是对象么,同理,也可以对方法进行代理:

js
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 只能够拦截对一个对象的基本操作。比如下文就无能为力:

js
obj.fn()

这个例子的流程是:

  1. 第一个基本语义是 get,即先通过 get 操作得到 obj.fn 属性。
  2. 第二个基本语义是函数调用,即通过 get 得到 obj.fn 的值后再调用它

你可能已经注意到了,Reflect 下的方法与 Proxy 的拦截器方法名字相同,其实这不是偶然。任何在 Proxy 的拦截器中能够找到的方法,都能够在 Reflect 中找到同名函数。拿 Reflect.get 函数来说,它的功能就是提供了访问一个对象属性的默认行为。那他的价值体现在哪?

看例子:

js
const obj = { foo: 1 }
console.log(Reflect.get(obj, 'foo', { foo: 2 })) // 输出的是 2 而不是 1

Reflect提供了第三个参数有点像this

继续看例子:

js
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代理以后的对象。

那我们来如下改造一下:

js
new Proxy(state, {
    get(target, key, receiver) {
        track(target, key)
        // 使用Reflect读取值
        return Reflect.get(target, key, receiver);
    },
    ...
    ...
    ...
})

这样就行了,this的指向也正确了,map的key也对了

继续

js
const obj = reactive({ foo: { bar: 1 } })

effect(() => {
    // for...in 循环
    console.log(obj.foo.bar)
});

obj.foo.bar = 3;

监听不到,因为obj虽然是响应式,但是obj.foo不是。所以解决方案是:

js
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中去写其他的数组方法也是需要挨个修正的,在此不做赘述
js
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)只能构建一个对象去存储了。

js
 // 封装一个 ref 函数
 function ref(val) {
    // 在 ref 函数内部创建包裹对象
    const wrapper = {
         value: val
    }  
    // 将包裹对象变成响应式数据
    return reactive(wrapper)
 }

但是我们还需要区别 ref 和 reactive,因为ref在某些时候可以脱衣(结构value),所以:

js
 function ref(val) {
    const wrapper = {
         value: val
    }
    // 使用 Object.defineProperty 在 wrapper 对象上定义一个不可枚举的属性 __v_isRef,并且值为 true
    Object.defineProperty(wrapper, '__v_isRef', {
         value: true 
    })

    return reactive(wrapper)
 }

响应式丢失

js
 export default {
    setup() {
        // 响应式数据
        const obj = reactive({ foo: 1, bar: 2 })

        // 1s 后修改响应式数据的值,不会触发重新渲染
        setTimeout(() => {
            obj.foo = 100
        }, 1000)

        return {
          ...obj
        }
    }
 }

这里结构以后返回的是一个普通对象,自然就丢失了响应式,但是也是有办法解决的:

js
 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也得修改为:

js
const wrapper = {
    get value() {
         return obj[key]
    }
    // 允许设置值,如果不加的话ref出来的值只是可读的
    set value(val) {
        obj[key] = val
    }
}

脱衣服

由于 toRefs 会把响应式数据的第一层属性值转换为 ref,因此必须通过 value 属性访问值,但是会增加负担,上代码!

JS
 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 属性来访问

穿衣服

只能脱不能穿确实不太礼貌。

js
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方法中,实现双向绑定:

js
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++

以上只是实现了渲染,但是一个渲染器是抽象概念,应该赋予更多能力(比如跨平台),所以:

js
 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赋予跨平台的能力:

js
 // 在创建 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)
    }
 })

鄂ICP备2024055897号