浅谈vue原理(二)

  上篇说了一下vue中的数据劫持和数据代理,就是将data中的数据都添加set/get方法,这使得扩展性更好了,后续的会在这个set/get方法添加咱们须要的逻辑;html

  如今咱们说说怎么才可以使得data中的数据和html标签中的内容绑定呢?vue

1.编译模板node

  首先咱们要思考一下,若是是你,你会怎么让data和html标签中的{{user.name}}这种东西进行匹配啊?正则表达式

 

  千万别想花里胡哨的东西,最直接的办法就是遍历全部的html标签,根据正则匹配到有两个大括号的就行啊,而后就取出大括号中的数据user.name,而后切割一下,就成了[user,name],再以后遍历这个数组,拿到data[user][name]就好了,并且因为前面已经作好数据代理,咱们能够直接这样取值myVue[user][name],取到值以后,就把html标签中{{xxx}}进行覆盖,而后继续找下一个有{{}}占位符的;设计模式

  这就是大概的逻辑,基于这个就能够将html中的标签中的{{user.name}}变成实际的数据了;api

  可是还须要解决一个问题,怎么拿到全部的html标签呢?并且在遍历以后把数据渲染到页面上效率提升一点呢?数组

  因此咱们须要知道一个容器:document.createDocumentFragment,这是一个虚拟节点的容器树,咱们能够将当多个dom元素丢到DocumentFragment中,再统一将DocumentFragment添加到页面,会减小页面渲染dom的次数,效率会明显提高。有兴趣的能够自行了解一下这个微信

  ok,理论说完了,大概就是这么几步app

  (1)首先根据实例化myVue实例时候传进去的el属性 "#app",就能够找到dom节点dom

 let myVue = new MyVue({
      el: '#app',
      data: {
        message: { a: { b: 1 } }
      }
    })

  (2)根据document.createDocumentFragment建立虚拟节点的容器树,遍历全部的dom节点都丢到容器树中

  (3)再遍历虚拟容器树中每一个节点,使用正则匹配到有两个大括号的节点,将节点中的表达式取出来,例如user.name

    (4)  根据表达式取出data中的值,因为进行了数据劫持,data中的值能够直接用myVue[user][name]获取,取到了值以后,就将对应的容器树中的对应节点中{{user.name}}覆盖掉

  (5)将虚拟容器树渲染到页面中,咱们就能看出效果了;

下面用代码简单实现一下模板编译的代码:

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];
      })
      // 把值覆盖到虚拟节点的占位符{{xxx}}这里
      node.textContent = nodeText.replace(reg, val);
    }
    // 最开始第一个遍历的节点是<div id="app">这一行后面有个你看不到的换行,因此nodeType等于3,可是没有占位符{{}},因此会进行递归调用内部
    //的每个节点,直到找到是文本节点并且有占位符{{}}
    if (node.childNodes) {
      replace(node, vm);
    }
  })
}

  

2.js实现发布订阅模式

  咱们说说设计模式,什么叫作发布订阅模式呢?你想一想你微信订阅的公众号就知道了,假如一个微信的公众号Dep,而后有3个用户Watcher去订阅它,而后公众号Dep只要一发布什么信息,那么每个Watcher都会收到一份;

  而后咱们要思考,首先是Dep,这里面确定有一个容器,能够装订阅过当前公众号的全部用户,并且还须要对外提供两个api,一个是订阅当前公众号的方法,方便用户使用;另一个是发布信息的方法,是给公众号所属的那我的使用,只要他调用这个方法,就会遍历这个容器中的全部用户,向每一个用户发送信息;

  再就是用户端Watcher,这端须要啥呢?确定也须要一个对外的api,方便公众号那一端调用这个api把信息发送到用户手机这边来吧!而后每一个用户都是不同的,咱们须要有个属性做为标识;

// 能够看作是公众号端
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 (fn) {
  // 这个能够看做是用户的标志;注意,这个fn通常是一个回调函数
  this.fn = fn;
}

// 用户端提供的对外api,让公众号端使用
Watcher.prototype.update = function () {
  this.fn();
}


// ================================测试================================
// 新建三个用户
let watcher1 = new Watcher(() => {
  console.log('发送给用户1');
})
let watcher2 = new Watcher(() => {
  console.log('发送给用户2');
})
let watcher3 = new Watcher(() => {
  console.log('发送给用户3');
})

// 用户注册
let dep = new Dep();
dep.addSub(watcher1);
dep.addSub(watcher2);
dep.addSub(watcher3);

// 像每一个用户发送信息
dep.notify();

  使用node运行一下结果以下

 

  理解了这个模式以后,后面咱们须要将这个设计模式应用到咱们本身的vue中,使得当咱们呢修改data中的数据以后,页面上也会马上刷新;

  其实也很好理解,data只有一份嘛,就能够想象成上面说的公众号端,而后html中确定有多个占位符{{}}的吧,那就是用户端呗!只要data中的数据一修改,这确定就会触发数据劫持的那个set方法吧,而后咱们只须要再set方法中搞点事,调用一下Dep的notify方法通知一下全部的Watcher,在Watcher中就会触发那个fn回调函数去把页面上的数据覆盖局ok了;

  这个发布订阅的实现代码下一篇再写;

 

3.现阶段代码

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];
      })
      // 把值覆盖到虚拟节点的占位符{{}}这里
      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) {
  // 遍历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 () {
        return val;
      },
      set (newVal) {
        if (val === newVal) return;
        val = newVal;
        //当后续可能由于业务逻辑使得_data.message = {name: "小王"},设置对象类型的属性值,就须要递归的给对象中{name: "小王"}的每一个属性也添加get和set方法
        //不然name是没有get/set方法的
        observe(val);
      }
    })
  }
}
View Code

 

当前代码的效果是页面中能够显示数据了,下一篇咱们说说根据发布订阅原理,使得咱们改变data中的数据,页面上的数据也会跟着变化

相关文章
相关标签/搜索