浅谈vue原理(三)

  上一篇已经实现了发布订阅模式了,如今咱们实现从model->view的数据绑定,也就是当data中的数据改变后,页面上的数据也要跟着变化;html

1.发布订阅代码的实际应用vue

  咱们思考一下,怎么把咱们上一篇实现到的发布订阅模式用到咱们的vue中去呢?node

  (1)Watcher应该什么时机建立呢?正则表达式

  以前说了一个{{xxx}}占位符就表明一个watcher,那么恰好在遍历虚拟节点树的时候会正则判断每一个出现{{xxx}}的所在节点,此时建立watcher实例正合适;api

 

 (2)Watcher构造函数中作了什么?app

  在上图中能够看到Watcher中传入了三个参数,其中根据第一个和第二个参数就能够取到data中对应的{{user.name}}的值myVue[user][name],因为全部的属性都绑定了有get方法,因此在获取myVue[user][name]的时候就会触发get方法,因此咱们能够在get方法中添加新的逻辑,就是将当前Watcher注册到Dep中一份;dom

(2.1)Watcher构造器(注意这里最后设置为null的这句,后面有用):mvvm

 

(2.2)get方法新增逻辑:ide

  

(3)那么当咱们修改myVue.user.name=”hello“的时候会发生什么?函数

  因为以前的数据劫持和数据代理,因此此时会触发name这个属性的set方法,咱们须要在这个set方法中添加逻辑,就是触发Dep中全部的Watcher的回调方法,就是(1)中回调函数;

 

(4)在(3)调用notify以后,每一个watcher将会执行的行为?

 

(5)大概捋一下思路:

  (5.1)首先启动项目,就会进行初始化,在编译模板的阶段会把html标签中的全部{{}}占位符都找到,咱们就在这个时候新建Watcher,并设置回调函数就是给每一个{{}},以便于后续调用直接覆盖这个占位符

  (5.2)在Watcher建立的时候,会因为从data中取值的缘由,会触发该属性的get方法,咱们就把这个Watcher实例的指针丢到这个get方法中,实现注册到Dep的逻辑

  (5.3)在每一个属性的set方法中加入了notify的机制,这是为了保证只要咱们手动的将data数据修改以后,就会调用全部注册到Dep中的Watcher的回调函数

  (5.4)启动完毕

  (5.5)当咱们修改data中的数据的时候,首先会触发set方法中的notify方法,直接调用全部的Watcher的update方法实现发布,在update方法中会获取data中修改的最新值(此时虽然也会触发这个属性的get方法,可是没有target属性,由于target属性只会在建立Watcher的时候才会赋值,建立完了以后就会设置为null,不会去重复注册同一个Watcher)

   (5.6)获取到了最新的值以后,传到(5.1)中的回调函数,就能够实现覆盖虚拟节点中的占位符{{}},实现页面的刷新,咱们就能看到效果

 

2.代码

html:

<body>
  <div id="app">
    <h1>呵呵:{{user.name}}</h1>
  </div>

  <script src="./mvvm.js"></script>
  <script>

    // 自定义的myVue实例
    let myVue = new MyVue({
      el: '#app',
      data: {
        user: { name: "小王" }
      }
    })
  </script>
</body>

 

js:

function MyVue (options = {}) {
  //第一步:首先就是将实例化的对象给拿到,获得data对象
  this.$options = options;
  this._data = this.$options.data;

  //第二步:数据劫持,将data对象中每个属性都设置get/set方法
  observe(this._data);

  //第三步:数据代理,这里就是将_data的对象属性放到myVue实例中一份,实际的数据仍是_data中的
  for (let key in this._data) {
    //这里的this表明当前myVue实例对象
    Object.defineProperty(this, key, {
      enumerable: true,
      get () {
        return this._data[key];
      },
      set (newVal) {
        this._data[key] = newVal;
      }
    })
  }

  //第四步:compile模板,须要将el属性和当前myVue实例
  compile(options.el, this)
}

function compile (el, vm) {
  return new Compile(el, vm);
}

function Compile (el, vm) {
  //将el表明的那个dom节点挂载到myVue实例中
  vm.$el = document.querySelector(el);

  //建立虚拟节点容器树
  let fragment = document.createDocumentFragment();

  //将el下全部的dom节点都放到容器树中,注意appendChild方法,这里是将将dom节点移动到容器树中啊,不是死循环!
  while (child = vm.$el.firstChild) {
    // console.log('count:' + vm.$el.childElementCount);
    fragment.appendChild(child)
  };

  //遍历虚拟节点中的全部节点,将真实数据填充覆盖这种占位符{{}}
  replace(fragment, vm);

  //将虚拟节点树中内容渲染到页面中
  vm.$el.appendChild(fragment);
}

function replace (n, vm) {
  //遍历容器树中全部的节点,解析出{{}}里面的内容,而后将数据覆盖到节点中去
  Array.from(n.childNodes).forEach(node => {
    console.log('nodeType:' + node.nodeType);

    let nodeText = node.textContent;
    let reg = /\{\{(.*)\}\}/;
    // 节点类型经常使用的有元素节点,属性节点和文本节点,值分别是1,2,3
    //必定要弄清楚这三种节点,好比<p id="123">hello</p>,这个p标签整个的就是元素节点,nodeType==1
    //id="123"能够看做是属性节点,nodeType==2
    //hello 表示文本节点,nodeType==3
    //由于占位符{{}}只在文本节点中,因此须要判断是否等于3
    if (node.nodeType === 3 && reg.test(nodeText)) {
      // RegExp.$1是RegExp的一个属性,指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串,以此类推,RegExp.$2。。。
      let arr = RegExp.$1.split(".");
      let val = vm;
      // 这个for循环就是取出这样的值:myVue[name][user]
      arr.forEach(i => {
        val = val[i];
      })


      // 建立Watcher,最主要的是传入的这个回调函数,会覆盖node节点中的占位符{{xxx}}
      new Watcher(vm, RegExp.$1, function (newVal) {
        node.textContent = nodeText.replace(reg, newVal);
      })
      // 把值覆盖到虚拟节点的占位符{{}}这里
      node.textContent = nodeText.replace(reg, val);
    }
    // 第一个遍历的节点是<div id="app">这一行后面的换行,nodeType等于3,可是没有占位符{{}},因此会进入到这里进行递归调用内部
    //的每个节点,直到找到文本节点并且占位符{{}}
    if (node.childNodes) {
      replace(node, vm);
    }
  })
}

//数据劫持操做
function observe (data) {
  // 若是data不是对象,就结束,否则递归调用会栈溢出的
  if (typeof data !== 'object') return;
  return new Observe(data);
}

function Observe (data) {
  let dep = new Dep();

  // 遍历data全部属性
  for (let key in data) {
    let val = data[key];
    //初始化的时候, data中就有复杂对象的时候,例如data: { message:{a:{b:1}}}  ,就须要递归的遍历这个对象中每个属性都添加get和set方法
    observe(val);
    Object.defineProperty(data, key, {
      enumerable: true,
      get () {
        // 订阅
        Dep.target && dep.addSub(Dep.target);
        return val;
      },
      set (newVal) {
        if (val === newVal) return;
        val = newVal;
        //当后续可能由于业务逻辑使得_data.message = {name: "小王"},设置对象类型的属性值,就须要递归的给对象中{name: "小王"}的每一个属性也添加get和set方法
        //不然name是没有get/set方法的
        observe(val);
        dep.notify();
      }
    })
  }
}


// ===============================发布订阅===============================
// 能够看作是公众号端
function Dep () {
  // 存放每一个用户的容器
  this.subs = [];
}

//对外提供的api之一,供用户订阅
Dep.prototype.addSub = function (sub) {
  this.subs.push(sub);
}

// 对外提供的api之二,遍历每一个用户,给每一个用法发信息
Dep.prototype.notify = function () {
  this.subs.forEach(sub => {
    sub.update();
  });
}


// 用户端
function Watcher (vm, exp, fn) {
  // 这个能够看做是用户的标志;注意,这个fn通常是一个回调函数
  this.vm = vm;
  this.exp = exp;
  this.fn = fn;

  Dep.target = this;
  let val = vm;
  let arr = exp.split(".");
  arr.forEach(item => {
    val = val[item];
  })
  Dep.target = null;
}

// 用户端提供的对外api,让公众号端使用
Watcher.prototype.update = function () {
  let val = this.vm;
  let arr = this.exp.split(".");
  arr.forEach(item => {
    val = val[item];
  })
  this.fn(val);
}
View Code

   现阶段,当咱们在控制台修改myVue.user.name="xxx"的时候,页面上也会跟着修改了

相关文章
相关标签/搜索