设计思路

双向绑定实现的三大关键类 和 一个劫持属性
下面是双向绑定实现的一个最基础的架子:

  1. # html
  2. <input type="text" id="inputElm"/>
  3. <span id="textElm"></span>
  4. <script>
  5. const obj = {
  6. desc: 'hello, world'
  7. };
  8. Object.defineProperty(obj,'desc',{
  9. get(){
  10. console.log("啦啦啦,方法被调用了");
  11. },
  12. set(newVal){
  13. document.getElementById('inputElm').value = newVal;
  14. document.getElementById('textElm').innerHTML = newVal;
  15. }
  16. })
  17. # view => data
  18. // 接受用户的交互 view视图的更改
  19. document.addEventListener('keyup',function(e){
  20. obj.desc = e.target.value;
  21. })
  22. # data => view
  23. setTimeout(() => {
  24. obj.desc = 'async setting'
  25. }, 1000);
  26. </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

类设计

综上我们的类设计: vue-响应式base - 图2

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

代码原理

以上的分析用逻辑图是如下:

image.png

上述我们主要是在讲解data->view的过程,弱化了 view->data这一步
而view-data这一步主要是:

data->view:
这一步已经解释了,当读了这个属性就会触发getter,视为这个属性的观察者,被dep收入
此后data变化,view会自动变化,因为watcher.update()管理了这个dom的render函数
view->data:
当用户输入的时候,怎么实现data.name总是跟随用户输入的最新值的
TODO: compile

代码如下:

image.png
image.png
以上是首次渲染
这里是简化 将渲染函数(依赖了data的视图操作)直接放入了watcher,实际vue会从模板里解析后才能得到渲染函数 如

{{num}}

会处理包裹成渲染函数() => render(xx ) 载入watcher


接下来是 当data改变的时候 触发 依赖data的各个渲染函数执行 实现视图自动更新
(可以说 首次渲染帮 之后的 自动更新 铺好了路,经过首次渲染 每个属性对应的观察者 都被 属性各自对应的dep依赖收集器给纳入囊中了)

之后的data改变 视图自动更新 就是 后人乘凉的过程
大致是 data属性数据改变,触发监听器的setter响应,dep依赖管理器通知各个watcher更新的过程
image.png

P.S.
pushTarget\popTarget:维护了一个targetStack = [] 的栈,
因为渲染函数可以是嵌套运行的,Vue中每个组件都会有自己用来存放渲染函数的一个watcher,当遇到嵌套组件的时候 就会有 watcher栈的概念

  1. <template>
  2. <div>
  3. <Son组件 />
  4. </div>
  5. </template>

watcher的运行路径就是: 开始 -> ParentWatcher -> SonWatcher -> ParentWatcher -> 结束

完整代码可参考这里

扩展

computed是如何实现的、watch是如何实现的