复制一份minivue(二)

105 阅读4分钟

「这是我参与2022首次更文挑战的第23天,活动详情查看:2022首次更文挑战

前面,已经实现了依赖收集和触发,实际上用的发布订阅模式,我们的依赖的集合就是一个中间层。接下来接着实现reactive的功能。

用proxy实现依赖收集和触发

reactive就是用Proxy来进行数据劫持的, 不过用代理就要解决一个问题,我们的依赖是和具体对象的具体属性相关联的。然后就是注意严格模式下, set捕获器必须返回一个逻辑值,表示修改是否成功。所以这里就按统一规范采用Reflect

function reactive(obj ={}) {
    return new Proxy(obj,  get(target, key){
            /* 如何获取 dep ,如何存储, dep是和具体对象具体属性关联的 */
              let dep  =  getDep(target, key)
            dep.depend()
            return  Reflect.get(target,key)
        },
        set (target, key,val){
            /* 这里的dep只用于收集和触发依赖了,并不代理数据,数据劫持和依赖收集分开了 */
            let dep  =  getDep(target, key)
            dep.notice()
            return  res ;
        })

dep的存储

所以,重点来了,我们要在get捕获器里 收集对应属性的依赖,在get捕获器里触发。

这里使用映射 Map来存储依赖,两层映射,第一层是从对象实例到 这个对象的全部属性的依赖的映射的映射。

也就是第一层的key 是对象(这也是用map存储的原因),值是另一个map。 第二层是属性到依赖的映射。 依赖收集和触发这两件事还是由依赖完成 从映射里取单独拿出来作为一个函数

function getDep(target, key){
    let depMaps =targetMap.get(target)
    if(!depMaps){
        depMaps = new Map() 
        targetMap.set(target, depMaps)
    }
    let dep = depMaps.get(key) 
    if(!dep){
        dep = new Dep(target[key]) 
        depMaps.set(key, dep)
    }
    return dep 
}

这样我们就完成了reative函数,并且让对应的依赖自我收集和触发。

写完之后,还是觉得比较有意思的就是, 数据的劫持代理是由Proxy完成的,但是收集依赖和触发依赖还是用Dep完成,没有用到Dep的数据劫持。

模拟视图更新

前面的依赖收集和触发弄好了之后,更新视图就简单了。只要在触发依赖的时候更新即可。也就是要把渲染的逻辑放在回调里,使用我们之前定义的watchEffect,然后模仿一下vue组件的写法,来个setup,setup定义所需数据(依赖),render渲染视图。

const App = {
        render(context){
            watchEffect(()=> {
                 let root =  document.querySelector('#app')
                 root.textContent = '' // 清空容器
                 const element = document.createElement('div')
                 const text  = document.createTextNode('你好')
                 const text2  = document.createTextNode(context.obj.count) ;
                 element.append(text)
                 element.append(text2)
                 app.append(element)        
                 })
        },
        setup(){
            const obj = reactive({count: 10})
            globalThis.obj = obj
            return  {obj}
        },
    }
 App.render( App.setup() )

模拟createApp

但是vue里面不是这样写的,render就是mount,但是mount里面不用传组件数据而是容器,这里我们先不管容器。整个实例的数据是用户外部传入的,就比如setup,mount是我们要实现的,把我们的 App.render( App.setup() )写进mount即可,然后仿照最新的api ,用creatApp来创建组件实例,我们这里暂且只要挂载一个mount方法即可。

export * from './reactivity.js'
 /* 模拟 一个app 实例 至少有一个mount方法 */
export function createApp(rootComponent){
    return {
        mount(){
            const setupResult = rootComponent.setup();
            rootComponent.render(setupResult)
        }
    }
}

然后,我们的初始化就可以改成 const App = createApp(AppComponent) ; App.mount() ,这个AppComponent就是用户穿进去的。 我们再把组件的逻辑单独一个模块App.js,搞模块化。

image.png

image.png

抽离render

我们在使用vue的时候,是可以不传render的,实际上这个render在大多数情况都是一样的,这种重复的逻辑就得是框架的事情。我们就直接把render挪到createApp内, 用空值赋值运算符合并用户的render。

image.png

后面的逻辑基本保持不变, 只是要让用户传入根容器。 这显然是mount的职责。最后返回组件实例,vue3返回的是组件实例的代理,我们暂时这么处理。

  return {
        mount(el){
            this.data = setupResult ;
            const setupResult = rootComponent.setup();
            let rootContainer = el ;
            watchEffect(()=> {
            if(typeof el.nodeType !== 'number' ){
                rootContainer = document.querySelector(el)
            }
             rootComponent.render(setupResult, rootContainer)
        })

            return this
        }
    }

这样改完之后,看我们的使用部分是不是和写vue有一点相似之处了。单独声明一个组件App.js,mian.js里面初始化。

image.png