「这是我参与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,搞模块化。
抽离render
我们在使用vue的时候,是可以不传render的,实际上这个render在大多数情况都是一样的,这种重复的逻辑就得是框架的事情。我们就直接把render挪到createApp内, 用空值赋值运算符合并用户的render。
后面的逻辑基本保持不变, 只是要让用户传入根容器。 这显然是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里面初始化。