实现组件代理对象
实现 this 访问 setup 数据
- 其实我们要实现的功能是我们在模板里面要能够访问到 this,通过 this 拿到 setup 里面挂载的对象,
下一步而且可以通过this.$el可以获取当前组件的 div,我们还需要 兼容 vue2 通过this.$data.x获取属性 - 难道是我们把 render 的this绑定到 setup 对象上了吗?
- 其实是做了一层代理
- 我们创建 comopnent 时,创建一层代理对象,访问时如果 key 在 setupState 上,返回对应的 value
- 当我们调用我们的 render 函数时,this指向将我们创建好的 proxy 对象,这样在 render 函数里面,我们也能访问到 setup 里面的值
- 代码
js
// component.ts
export function createComponentInstance(vnode) {
const component = {
vnode,
type: vnode.type,
setupState: {} // ✅ 存放 setup 返回的状态
}
return component
}
function setupStatefulComponents(instance: any) {
const Component = instance.vnode.type
const { setup } = Component
instance.proxy = new Proxy({}, { // ✅ 做一层代理,拦截 get,我们通过 this 访问属性时,就会走这里
get(target, key) {
let { setupState } = instance // 这里的解构要注意在 get 里面,不然拿到的就是空对象
if(key in setupState) {
return setupState[key]
}
}
})
if(setup) {
const setupResult = setup()
handleSetupResult(instance, setupResult) // 将状态结果进心处理
}
}
// renerer.ts
function setupRenderEffect(instance, container) {
const { proxy } = instance
const subTree = instance.render.call(proxy) // ✅ 将render 的 this 指向 proxy,这样在 render 里面访问 this.msg 就能拿到值
// vnode -> patch
// vnode -> element -> mountElement
patch(subTree, container)
}
实现 this.$el
- 思路
-
我们在 mounteElement 这个函数里面创建了一个 el,我们可以挂在 虚拟dom上面,然后当在 this 中访问时,按照 proxy 拦截到 key 进行赋值
js// vnode.ts export function createVNode(type, props?, children?) { const vnode = { type, props, children, el: null // ✅ } return vnode } //renderer.ts function mountElement(vnode, container) { const { type, props, children } = vnode // 把 el 挂载在虚拟节点上 const el = (vnode.el = document.createElement(type)) // ✅ if (typeof children === 'string') { el.textContent = children } else if (Array.isArray(children)) { mountChildren(children, el) } for (let key in props) { let val = props[key] el.setAttribute(key, val) } container.append(el) }
- 测试
-
思路: 我们没有实现事件,所以想要通过点击事件获取
this.$el行不通,可以进行this赋值,然后通过window.self.$el直接在浏览器控制台来测试有么有成功jsimport {h} from '../lib/guide-mini-vue.esm.js' window.self = null // ✅ export const App = { // .vue // template // render render() { // render 必须写,不然无法渲染 // this 赋值 // ✅ window.self = this return h("div",{ id: 'root', class: ['red', 'hard'] }, // 实现 this 挂载变量,还可以 this.$el 可以获取当前组件的 div 'hello'+this.msg // 实现 h 函数 // [h("p",{class:"red"}, "hi"), h("p",{class:"blue"},"mini-vue")] ) }, setup() { // compsition api return { msg: "mini-vue-haha", } } } -
发现
window.self.$el是 null ,我们在if(key === '$el')上面 debugger,可以看到 vnode.type 是一个对象,然后children,props,都没有值,其实这是一个组件,所以我们赋值挂载 el 的虚拟节点,与我们proxy 拦截的$el不是一个虚拟节点,我们是在 mountElement 这个函数里面挂载的$el,所以我们得在element相关逻辑挂载完毕以后,再进行挂载,其实在 ---->
js
function setupRenderEffect(instance, vnode, container) {
const { proxy } = instance
const subTree = instance.render.call(proxy)
// vnode -> patch
// vnode -> element -> mountElement
patch(subTree, container)
// 这里 element 已经处理完毕 ✅,可以挂载 el 到 虚拟节点上 <------
vnode.el = subTree.el
}
- 总结一下,我们从 main.js 顺着入口进入,一开始总要先把根节点整上去吧,那此时vnode 还没渲染里面的子节点,也就是走 mounteElement 这个函数,所以el 还是 null,等到 patch 走完 mountElement 以后,el 就有值了,我们再把 el 挂载到 根节点的 vnode 上,这样我们在 proxy 里面访问
$el的时候,就能拿到值了
data props 等等也需要处理
- 思路:我们得把这块属性拦截的逻辑抽离出来,对 data props 做映射,后续只要在映射对象里面添加 key value ,就可以实现相应功能
- 代码实现
js
// component.ts
function setupStatefulComponents(instance: any) {
const Component = instance.vnode.type
const { setup } = Component
// 注意:这里的虚拟节点是属于 component 的
instance.proxy = new Proxy({_: instance}, PublicInstanceProxyHandlers) // get 逻辑抽离 // ✅
if(setup) {
const setupResult = setup()
handleSetupResult(instance, setupResult) // 将状态结果进心处理
}
}
// componentPublicInstance.ts // ✅
const publicPropertiesMap = {
$el:(i)=>i.vnode.el
}
export const PublicInstanceProxyHandlers = {
get({_: instance}, key) {
let { setupState } = instance
if(key in setupState) {
return setupState[key]
}
// key -> $el
// if(key === '$el') {
// return instance.vnode.el
// }
const publicGetter = publicPropertiesMap[key]
if(publicGetter) {
return publicGetter(instance)
}
}
}
- 易错问题
- elementMount 挂载完毕,对根节点 vnode.el 进行赋值,不要搞反了 vnode.el = subTree.el
- rollup.config.js 里面
import pkg from './package.json' with { type: 'json' };需要配置 type: 'json',不然会报错, 打包不动