设计思路
双向绑定实现的三大关键类 和 一个劫持属性
下面是双向绑定实现的一个最基础的架子:
# html
<input type="text" id="inputElm"/>
<span id="textElm"></span>
<script>
const obj = {
desc: 'hello, world'
};
Object.defineProperty(obj,'desc',{
get(){
console.log("啦啦啦,方法被调用了");
},
set(newVal){
document.getElementById('inputElm').value = newVal;
document.getElementById('textElm').innerHTML = newVal;
}
})
# view => data
// 接受用户的交互 view视图的更改
document.addEventListener('keyup',function(e){
obj.desc = e.target.value;
})
# data => view
setTimeout(() => {
obj.desc = 'async setting'
}, 1000);
</script>
实现的功能:实现了input框与数据obj.desc的绑定
data=>view 数据改变使得视图自动渲染:Obj被代码设置改变后 input框数据也会改变,span标签实时显示出当前obj的值;
view =>data视图改变使数据更新:当input接受到用户的交互输入后 obj.desc也跟着改变,span标签实时显示出当前用户的输入;
核心就是:
1、数据监测器,监测到数据变化后,调用数据下游的事件发生
数据变化,通知依赖此数据的下游:v-model=data.xx的dom、watch()、computed计算属性
怎么确定当前数据对应的依赖者的? data的getter
2、响应用户交互,第一时间将用户最新的输入e.target.value 赋值给 响应值数据,使得响应式数据的settter触发,从而引起数据下游的事件发生;
而vue的响应式系统在上面的基础上引入了:
1、发布订阅模式(有观察者watcher、依赖收集dep):
reactive:对传入的普通数据对象进行检测
watcher:将视图的更新这个操作抽象给了watcher管理(watcher.update()),
dep:专门负责收集和通知观察者(dep.depend(), dep.notify())
2、专门负责dom解析的:
compile:模板解析
为什么要抽象单独的watcher,让wacher去通知update?
因为下游事件不一定:可能是dom渲染,可能是watch()函数的回调,所以不能再setter里直接写update函数
watcher:一个数据对应多个watcher
类设计
综上我们的类设计:
Dep类
这时候 vue在数据劫持里抽象了一层dep类 专门负责依赖管理
get:负责依赖收集(dep.add) ———— 收集依赖这个数据的观察者
set:负责通知依赖(dep.notify) ——— 通知观察者数据变化——watcher.update() ——compile
通知依赖做了什么呢? => 当消息发生改变后 引起下游的改变 具体而言就是 调用每个渲染函数
不是在dep类里维护一个队列存放渲染函数哦(dep.notify: this.deps.forEach(renderFunc => renderFunc())
而在 dep里 又抽象了 watcher 来管理 依赖的更新(watcher.update())
(dep.notify: this.deps.forEach(watcher => watcher.udpdate());
即依赖收集类的deps容器里收集的不是渲染函数,而是”包裹了渲染函数”的watcher
小总结下:
dep 和数据data的依赖关系: data的每个属性 都对应一个 dep, dep里记录着众多依赖者watcher
**
Watcher类
而watcher做了什么呢?
1、首先,渲染函数用watcher包裹 watcher(() => console.log(data.num));
渲染函数什么时候执行?constructor 立即触发,触发后由于console.log(data.num)读取了data的属性从而引发 data.get 机关,而reactive在get那埋伏了dep的依赖收集
只要视图wacher读取了data属性 那就是订阅了这个data,该视图watcher就是这个data的依赖者,该data的数据下游;
getter: Dep.add(); // Dep类里 将当前执行的watcher放入该data属性的dep容器里
2、提供watcher.update方法,给外界调用实现视图触发渲染更新(即dep通知消息变化时候)
Dep类与Watcher类的连接
那么,dep类里是怎么知道 当前读取data属性的 那个 watcher呢?
1、watcher类在首次渲染调用this.get()时就 将当前全局变量设为当前watcher本身:Dep.target = this;
2、this.get()后马上执行watcher里的渲染函数,渲染函数渲染了data某属性,触发了getter,dep.add();而Dep类
通过读Dep.target实现知道当前执行渲染函数对应的watcher
代码原理
上述我们主要是在讲解data->view的过程,弱化了 view->data这一步
而view-data这一步主要是:
data->view:
这一步已经解释了,当读了这个属性就会触发getter,视为这个属性的观察者,被dep收入
此后data变化,view会自动变化,因为watcher.update()管理了这个dom的render函数
view->data:
当用户输入的时候,怎么实现data.name总是跟随用户输入的最新值的
TODO: compile
代码如下:
以上是首次渲染
这里是简化 将渲染函数(依赖了data的视图操作)直接放入了watcher,实际vue会从模板里解析后才能得到渲染函数 如
{{num}}
会处理包裹成渲染函数() => render(xx ) 载入watcher接下来是 当data改变的时候 触发 依赖data的各个渲染函数执行 实现视图自动更新
(可以说 首次渲染帮 之后的 自动更新 铺好了路,经过首次渲染 每个属性对应的观察者 都被 属性各自对应的dep依赖收集器给纳入囊中了)
之后的data改变 视图自动更新 就是 后人乘凉的过程
大致是 data属性数据改变,触发监听器的setter响应,dep依赖管理器通知各个watcher更新的过程
P.S.
pushTarget\popTarget:维护了一个targetStack = [] 的栈,
因为渲染函数可以是嵌套运行的,Vue中每个组件都会有自己用来存放渲染函数的一个watcher,当遇到嵌套组件的时候 就会有 watcher栈的概念
<template>
<div>
<Son组件 />
</div>
</template>
watcher的运行路径就是: 开始 -> ParentWatcher -> SonWatcher -> ParentWatcher -> 结束
完整代码可参考这里