这一章就着重讲两个点:

  • 响应式零碎如何收集依赖
  • 响应式零碎如何更新视图 咱们晓得通过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.idif (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框数据关联,说白了也就是雷同的数据源,那咱们的数据源在哪呢?

//数据源dataconst 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的另一个外围概念组件零碎