关于vue.js:Vue响应式系统原理并实现一个双向绑定

23次阅读

共计 6244 个字符,预计需要花费 16 分钟才能阅读完成。

这一章就着重讲两个点:

  • 响应式零碎如何收集依赖
  • 响应式零碎如何更新视图 咱们晓得通过Object.defineProperty 做了数据劫持,当数据扭转的时候,get办法收集依赖,进而 set 办法调用 dep.notify 办法去告诉 Watcher 调用自身 update 办法去更新视图。那么咱们抛开其余问题, 就探讨 getnotifyupdate 等办法,间接上代码:

get()

  get: function reactiveGetter () {const value = getter ? getter.call(obj) : val
      if (Dep.target) {dep.depend()
        if (childOb) {childOb.dep.depend()
          if (Array.isArray(value)) {dependArray(value)
          }
        }
      }
      return value
    }

咱们晓得 Dep.target 在创立 Watcher 的时候是 null,并且它只是起到一个标记的作用,当咱们创立Watcher 实例的时候,咱们的 Dep.target 就会被赋值到 Watcher 实例,进而放入 target 栈中,咱们这里调用的是 pushTarget 函数:

// 将 watcher 实例赋值给 Dep.target,用于依赖收集。同时将该实例存入 target 栈中
export function pushTarget (_target: ?Watcher) {if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

那咱们继续执行到 if (Dep.target) 语句的时候就会调用 Dep.depend 函数:

 // 将本身退出到全局的 watcher 中
  depend () {if (Dep.target) {Dep.target.addDep(this)
    }
  }

那上面的 childOb 是啥货色呢?

  let childOb = !shallow && observe(val)

咱们通过这个变量判断以后属性上面是否还有 ob 属性,如果有的话持续调用 Dep.depend 函数,没有的话则不解决。
咱们还须要解决以后传入的 value 类型,是数组属性的话则会调用 dependArray 收集数组依赖

// 收集数组依赖
function dependArray (value: Array<any>) {for (let e, i = 0, l = value.length; i < l; i++) {e = value[i]
  e && e.__ob__ && e.__ob__.dep.depend()
  if (Array.isArray(e)) {dependArray(e)
  }
}
}

那么 收集依赖局部 到这里就完了当初进行下一步 触发更新

set()

   set: function reactiveSetter (newVal) {const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      // 判断 NaN 的状况
      if (newVal === value || (newVal !== newVal && value !== value)) {return}
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {customSetter()
      }
      if (setter) {setter.call(obj, newVal)
      } else {val = newVal}
      childOb = !shallow && observe(newVal)
      dep.notify()}

咱们看到了上面的 set函数触发了 dep.notify() 办法

notify()

  // 告诉所有订阅者
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()}
  }
}

notify 外面咱们就做了一件事件,遍历 subs 数组外面的所有 Watcher,逐个调用update 办法, 也就是咱们说的告诉所有的订阅者 Watcher 调用本身 update 办法 update()

  update () {if (this.lazy) {
      // 计算属性会进来这段代码块
      // 这里将 dirty 赋值为 true
      // 也不会马上去读取值
      // 当 render-watcher 的 update 被触发时
      // 从新渲染页面,计算属性会从新读值
      this.dirty = true
    } else if (this.sync) {this.run()
    } else {queueWatcher(this)
    }
  }

那么 update 办法实现了什么呢?lazydirtysync又是啥?

   if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } else {this.deep = this.user = this.lazy = this.sync = false}
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
     // 这里将 lazy 的值赋值给了 dirty
    // 就是说实例化的时候 dirty = lazy = true
    this.dirty = this.lazy // for lazy watchers

那是管制计算属性的,当 render—watcher 的办法 update 被调用的时候,this.dirty会变为 true 会从新计算 computed 值,渲染视图,咱们这里不叙述。
那么咱们间接看 queueWatcher() 函数:

export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {has[id] = true
 if (!flushing) {queue.push(watcher)
 } else {
   // if already flushing, splice the watcher based on its id
   // if already past its id, it will be run next immediately.
   let i = queue.length - 1
   while (i > index && queue[i].id > watcher.id) {i--}
   queue.splice(i + 1, 0, watcher)
 }
 // queue the flush
 if (!waiting) {
   waiting = true
   nextTick(flushSchedulerQueue)
 }
}
}

咱们能够看到一个更新队列,更新队列指向:

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted) {callHook(vm, 'updated')
    }
  }
}

参考 vue 实战视频解说:进入学习

咱们的 callback 调用 updated 钩子
讲到这里就有点 超纲 了,咱们 初始化渲染 会调用一个 initRender 函数创立 dom,还有下面所述的nextTick,前期都会讲, 那么理解了 更新机制 ,下一章咱们就来实现一个 让面试官都惊呆了 双向绑定

咱们对 Vue响应式零碎 有肯定的理解,并且晓得它是如何实现 数据更新视图 视图扭转数据的 , 那么有这样的根底,咱们来手写一个MVVM,以便面试的时候,吊打面试官(此为 笑谈 ,有余论,嘿嘿)。
那么先抛出一张在座的各位再也相熟不过的图:

1、当咱们 new MVVM 之后有两步操作,Observer,Compile,咱们晓得 Observer 是做数据劫持,Compile是解析指令,那么问题来了:

  • Observer为什么要做数据劫持?
  • Compile为什么要做解析指令?
    带着这两个问题,咱们回顾一下往期内容:
  • 什么是 数据响应式
  • 数据响应式原理 是什么?
  • 数据响应式是如何 实现 的?

数据响应式 就是数据双向绑定,就是把 Model 绑定到 View,当咱们用JavaScript 代码更新 Model 时,View就会自动更新;如果用户更新了 View,那么Model 数据也被自动更新了,这种状况就是双向绑定。

数据响应式原理

  • Vue实现数据响应式原理就是通过 Object.defineProperty() 这个办法从新定义了对象获取属性值 get 设置属性值 set 的操作来实现的
  • Vue3.0中是通过 ECMAScript6 中的 proxy 对象代理来实现的。
    那么本章节就是来实现 数据响应式 的。

那么答复后面的两个问题,为什么要 劫持数据 ?为什么要 解析指令

  • 只有劫持到数据,能力对数据做到监听,以便于数据更改可能及时做到更新视图。
  • Vue中自定义了 N 多指令,只有解析它,咱们 JavaScript 能力意识它,并运行它。
    诸如此类问题咱们不再复述,上面开始实现数据响应式。

写一个 demo 之前,咱们该当整顿好思路:

1. 首先实现整体的一个架构(包含 MVVM 类或者 VUE 类、Watcher 类),   / 这里用到一个订阅发布者设计模式。2. 而后实现 MVVM 中的由 M 到 V,把模型外面的数据绑定到视图。3. 最初实现 V -M, 当文本框输出文本的时候,由文本事件触发更新模型中的数据
4. 同时也更新绝对应的视图。
//html 代码
<div id="app">
      <h1>MVVM 双向绑定 </h1>
      <div>
        <div v-text="myText"></div>
        <div v-text="myBox"></div>
        <input type="text" v-model="myText" />
        <input type="text" v-model="myBox" />
      </div>
</div>

咱们创立了两个 divinput实现 input 框数据关联, 说白了也就是雷同的数据源,那咱们的数据源在哪呢?

// 数据源 data
const app = new Vue({
        el: "#app",
        data: {
          myText: "大吉大利!今晚吃鸡!",
          myBox: "我是一个盒子!",
        },
});

可见咱们须要一个 Vue 类, 也就是一个发布者,那么间接上代码:

//Vue 类(发布者)
class Vue{}

发布者有了,咱们还须要有订阅者:

//Watcher 类(订阅者)
class Watcher{}

可见两者都有了,那么咱们该怎么实现呢?

  • 获取 data 数据
  • 获取元素对象
  • 结构一个寄存订阅者的对象
 class Vue {constructor(optios) {
          this.$data = optios.data; // 获取数据
          this.$el = document.querySelector(optios.el); // 获取元素对象
          this._directive = {}; // 寄存订阅者}
 }       

那么咱们说了,咱们须要 劫持数据 解析指令,那么咱们得结构两个办法。

   class Vue {constructor(optios) {
         this.$data = optios.data; // 获取数据
         this.$el = document.querySelector(optios.el); // 获取元素对象
         this._directive = {}; // 寄存订阅者
         this.Observer(this.$data);
         this.Compile(this.$el);
       }
       // 劫持数据
       Observer(data) {
           Object.defineProperty(this.$data, key, {get: function(){},
             set: function(){}
             },
           });
       }
       // 解析指令   // 视图 --- > 对象 -- > 指令
       Compile(el) {}}

一个是 劫持数据 ,一个是 解析元素指令,劫持到的属性要依据属性调配容器,当以后容器不存在该属性的时候,咱们便须要把他增加到订阅器对象外面,期待告诉更新。

  for (let key in data) {this._directive[key] = [];
          let val =data[key];
          let watch = this._directive[key];
  }

那么解析指令,首先必须要 递归 以后节点,是否还有子节点,是否有 v-text 指令,v-model指令。

       let nodes = el.children;
          for (let i = 0; i < nodes.length; i++) {let node = nodes[i];
            // 递归 查问所有以后对象子类是否再含有子类
            if (node.children.length) {this.Compile(nodes[i]);
            }
            // 判断是否含有 V -text 指令
            if (node.hasAttribute("v-text")) {let attrVal = node.getAttribute("v-text");

              this._directive[attrVal].push(new Watcher(node, this, "innerHTML", attrVal)
              );
            }

            // 判断是否含有 V -model 指令
            if (node.hasAttribute("v-model")) {let attrVal = node.getAttribute("v-model");

              this._directive[attrVal].push(new Watcher(node, this, "value", attrVal)
              );
              node.addEventListener("input", () => {
                // 赋值到模型
                this.$data[attrVal] = node.value;
                // console.log(this.$data);
              });
            }
          }

那么咱们触发更新时候须要收集依赖,咱们间接吧收集到的依赖 return 进来

 Object.defineProperty(this.$data, key, {get: function(){return val;}
 }

那么咱们订阅者长什么样呢?咱们订阅者,接管以后元素信息,MVVM 对象,标识,属性。并且须要结构一个更新办法update

    class Watcher {constructor(el, vm, exp, attr) {
          this.el = el;
          this.vm = vm;
          this.exp = exp;
          this.attr = attr;
          this.update();}
        // 更新视图
        update() {this.el[this.exp] = this.vm.$data[this.attr];
          //div.innerHTML/value = this.Vue.$data["myText/myBox"]
        }
    }

到这里曾经快实现了,那么咱们收集了依赖就要去,告诉 watcher 去更新视图啊,那么来了:

    Object.defineProperty(this.$data, key, {get: function(){return val;},
              set: function(newVal){if(newVal !== val){
                    val = newVal;
                    watch.forEach(element => {element.update();  
                    });
                  }
              },
    });

做到这里,你就能够实现一个数据响应式了。

咱们曾经把握了响应式原理,那咱们开始着手 Vue 的另一个外围概念 组件零碎

正文完
 0