天天看点

Vue学习 -- 双向数据绑定(二)初始化分析DocumentFragmentcompiledefineProperty订阅者Dep 及 Watcher最终代码

上一章双向数据绑定(一)主要讲解了Object.defineProperty() 的作用及用法。

github代码地址

现在咱们一起实现一个MyVue

MyVue封装

  • 初始化
    • html
    • myVue.js
  • 分析
  • DocumentFragment
    • 使用方式
    • 将子节点劫持到文档
  • compile
    • nodeType = 1 :元素
    • nodeType = 3 :文本
    • 示例
    • 效果图
  • defineProperty
  • 订阅者
  • Dep 及 Watcher
  • 最终代码

初始化

首先创建一个html、myVue.js ,然后按照vue的写法,看看会是什么样子。

html

<body>
  <div id="app">
    <input id="inputId" placeholder="请输入" v-model="text" />
    <div>
      这是内容text:
      <p id="txtId">{{text}}</p>
    </div>
  </div>
  <script src="./assets/myVue.js"></script>
  <script>
    var vm = new MyVue({
      el:'app',
      data:{
        text:'hello word'
      }
    })
  </script>
</body>
           

myVue.js

(function(win){

  function MyVue (opts){
    console.group('opts =======================')
    console.log(opts)
  }
  
  win.MyVue = MyVue;

})(window)
           
Vue学习 -- 双向数据绑定(二)初始化分析DocumentFragmentcompiledefineProperty订阅者Dep 及 Watcher最终代码

分析

控制台打印的值,是咱们出入的对象,这里没有问题。

但是页面展示的文本,却不是咱们想要的,text 我们希望替换成 hello word 才对。那么咱们首先就应该先替换dom节点。

DocumentFragment

documentFragment最大的特点是可以像document一样,但不是真实 dom 树的一部分,它的变化不会触发 dom树的重新渲染,且不会导致性能等问题。

最常用的方法是使用文档片段作为参数,这种情况下被添加(append)或被插入(inserted)的是片段的所有子节点, 而非片段本身。因为所有的节点会被一次插入到文档中,而这个操作仅发生一个重渲染的操作,而不是每个节点分别被插入到文档中,因为后者会发生多次重渲染的操作。

使用方式

  1. 创建documentFragment对象flag
  2. 取出ul中的所有子节点并保存到flag
  3. 更新flag中的所有节点(app的内容)
  4. 将flag插入到app

关于DocumentFragment具体可以戳这里~

将子节点劫持到文档

(function(win){

  function MyVue (opts){
    console.group('opts =======================')
    console.log(opts)

    var id = opts.el;
    var dom =nodeToFragment(document.getElementById(id),this);
    document.getElementById(id).appendChild(dom)
  }

  //文档片段
  function nodeToFragment (node,vm){
    var flag = document.createDocumentFragment();
    var child;

    // firstChild 属性返回被选节点的第一个子节点

    //如果 while (child = node.firstChild) 成立
    // appendChild 方法向节点添加最后一个子节点
    // appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除
    // 第二次循环时,node.firstChild 已经不再是之前的第一个子元素了
    // 直到  child = node.firstChild 不成立

    while (child = node.firstChild){

      //将子节点劫持到文档片段中
      flag.appendChild(child);
    }

    return flag
  }


  win.MyVue = MyVue;
})(window)
           

这部分只是对子节点进行了劫持,具体变化还需要数据绑定。

compile

compile的作用主要是数据初始化,确定数据绑定关系。使用node.nodeType 判断节点类型。

nodeType = 1 :元素

元素的话,需要判断两个方面:

  1. 是否还有子节点,有的话递归循环重复一下compile操作
  2. 当前节点是否有属性 v-model,有的话设置value值,并删除v-model属性

nodeType = 3 :文本

文本的话,需要判断其是否有 {{}} ,有的话从data中取值替换

示例

(function(win){

  function MyVue (opts){
    
    this.data = opts.data;

    var id = opts.el;

    var dom =nodeToFragment(document.getElementById(id),this);
    document.getElementById(id).appendChild(dom)
  }

  //文档片段
  function nodeToFragment (node,vm){
    var flag = document.createDocumentFragment();
    var child;

    // firstChild 属性返回被选节点的第一个子节点
    //如果 while (child = node.firstChild) 成立
    // appendChild 方法向节点添加最后一个子节点
    // appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除
    // 第二次循环时,node.firstChild 已经不再是之前的第一个子元素了
    // 直到  child = node.firstChild 不成立

    while (child = node.firstChild){
      compile(child,vm);
      //将子节点劫持到文档片段中
      flag.appendChild(child);
    }

    return flag
  }

  //数据初始化
  function compile (node,vm){
    var reg = /\{\{(.*)\}\}/;
  
    //节点类型:元素
    if(node.nodeType ===1){
      var attr = node.attributes;
      // 有属性
      if(attr.length){
        for(var i=0;i<attr.length;i++){
        
          if(attr[i].nodeName == 'v-model'){
    
            var name = attr[i].nodeValue;
  
            node.value = vm.data[name];
    
            node.removeAttribute('v-model');
    
          }
        }
      }
      

      var childs = node.childNodes;
      //有子节点
      if(childs.length){
        
        for(var i=0;i<childs.length;i++){
          compile(childs[i],vm)
        }
      }
      
    }  
    //节点类型:text
    else if(node.nodeType ===3){
      if(reg.test(node.nodeValue)){
        //指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串
        var name = RegExp.$1;
        name = name.trim();
        node.nodeValue = vm.data[name];
      }
    }
  }


  win.MyVue = MyVue;
})(window)
           

重点 :

1、MyVue 里面需要指定

this.data = opts.data;

2、正则匹配符合 /{{(.*)}}/ ,取其值,node.nodeValue = vm.data[name]

效果图

Vue学习 -- 双向数据绑定(二)初始化分析DocumentFragmentcompiledefineProperty订阅者Dep 及 Watcher最终代码

到这里,已经可以实现 {{}} 替换成定义的值了。但是改变input的值,文案还无法改变,因此需要加defineProperty拦截。不清楚戳这里~

defineProperty

// MyVue 添加observer 方法初始化
observer(this.data,this)



// v-model 判断处,添加input方法
node.addEventListener('input', function (e) {
  // 给相应的 data 属性赋值,进而触发该属性的 set 方法
  vm[name] = e.target.value;
});


// observer
function observer (obj, vm) {
    Object.keys(obj).forEach(function (key) {
      defineReactive(vm, key, obj[key]);
    })
  }
  
  function defineReactive (obj, key, val) {
    
    Object.defineProperty(obj, key, {
      get: function () {

        return val
      },
      set: function (newVal) {
        if (newVal === val) return
        console.log(newVal,'newVal')
        val = newVal;
      }
    });
  }


           

效果图

随着输入框值的改变,set 里的newVal 也会发生变化

Vue学习 -- 双向数据绑定(二)初始化分析DocumentFragmentcompiledefineProperty订阅者Dep 及 Watcher最终代码

订阅者

text属性变化了,set方法触发了,但是文本还没有改变,如何让文本同步,这里又有一个知识点:订阅发布模式。

订阅发布模式(又称观察者模式)定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有观察者对象。

发布者发出通知 => 主题对象收到通知并推送给订阅者 => 订阅者执行相应操作

var pub = {
      publish:function (){
        dep.notify()
      }
    }

    var sub1 = {
      update:function(){console.log(1)}
    }


    var sub2 = {
      update:function(){console.log(2)}
    }


    var sub3 = {
      update:function(){console.log(3)}
    }

    function Dep (){
      this.subs = [sub1,sub2,sub3];
    }

    Dep.prototype.notify = function(){
      this.subs.forEach(function(sub){
        sub.update()
      })
    };

    var dep = new Dep();

    pub.publish()
           

简单来说dep是一个数组集合,每次触发notify时,进行update更新

Dep 及 Watcher

//input监听
new Watcher(vm,node,name,'input')
//text 监听
new Watcher(vm,node,name,'text')

//Dep
function Dep () {
    this.subs = []
  }
  
  Dep.prototype = {
    addSub: function(sub) {
      console.log(sub,'addSub')
      this.subs.push(sub);
    },
  
    notify: function() {
      this.subs.forEach(function(sub) {
        sub.update();
      });
    }
  }

// Watcher
  function Watcher (vm, node, name, nodeType) {
    Dep.target = this;
    this.name = name;
    this.node = node;
    this.vm = vm;
    this.nodeType = nodeType;
    this.update();
    Dep.target = null;
  }
  
  Watcher.prototype = {
    update: function () {
      this.get();
      if (this.nodeType == 'text') {
        this.node.nodeValue = this.value;
      }
      if (this.nodeType == 'input') {
        this.node.value = this.value;
      }
    },
    // 获取 data 中的属性值
    get: function () {
      this.value = this.vm[this.name]; // 触发相应属性的 get
    }
  }
           

最终代码

(function(win){

  function MyVue (opts){
    this.data = opts.data;

    observer(this.data,this)

    var id = opts.el;
    var dom =nodeToFragment(document.getElementById(id),this);
    document.getElementById(id).appendChild(dom)
  }

  //文档片段
  function nodeToFragment (node,vm){
    var flag = document.createDocumentFragment();
    var child;

    // firstChild 属性返回被选节点的第一个子节点
    //如果 while (child = node.firstChild) 成立
    // appendChild 方法向节点添加最后一个子节点
    // appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除
    // 第二次循环时,node.firstChild 已经不再是之前的第一个子元素了
    // 直到  child = node.firstChild 不成立

    while (child = node.firstChild){
      compile(child,vm);
      //将子节点劫持到文档片段中
      flag.appendChild(child);
    }

    return flag
  }

  //数据初始化
  function compile (node,vm){
    var reg = /\{\{(.*)\}\}/;
  
    //节点类型:元素
    if(node.nodeType ===1){
      var attr = node.attributes;
      // 有属性
      if(attr.length){
        for(var i=0;i<attr.length;i++){
        
          if(attr[i].nodeName == 'v-model'){
    
            var name = attr[i].nodeValue;

            node.addEventListener('input', function (e) {
              // 给相应的 data 属性赋值,进而触发该属性的 set 方法
              vm[name] = e.target.value;
            });
  
            node.value = vm.data[name];
    
            node.removeAttribute('v-model');
    
          }
        }

        new Watcher(vm,node,name,'input')
      }
      

      var childs = node.childNodes;
      //有子节点
      if(childs.length){
        
        for(var i=0;i<childs.length;i++){
          compile(childs[i],vm)
        }
      }
      
    }  
    //节点类型:text
    else if(node.nodeType ===3){
      if(reg.test(node.nodeValue)){
        //指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串
        var name = RegExp.$1;
        name = name.trim();

        node.nodeValue = vm.data[name];

        new Watcher(vm,node,name,'text')
      }
    }
  }


  function observer (obj, vm) {
    Object.keys(obj).forEach(function (key) {
      defineReactive(vm, key, obj[key]);
    })
  }
  
  function defineReactive (obj, key, val) {
    var dep = new Dep();

    Object.defineProperty(obj, key, {
      get: function () {
        // 添加订阅者 watcher 到主题对象 Dep
        if (Dep.target) {
          dep.addSub(Dep.target)
        };

        return val
      },
      set: function (newVal) {
        if (newVal === val) return
        
        val = newVal;

        // 作为发布者发出通知
        dep.notify();
      }
    });
  }


  function Dep () {
    this.subs = []
  }
  
  Dep.prototype = {
    addSub: function(sub) {
      this.subs.push(sub);
    },
  
    notify: function() {
      this.subs.forEach(function(sub) {
        sub.update();
      });
    }
  }


  function Watcher (vm, node, name, nodeType) {
    Dep.target = this;
    this.name = name;
    this.node = node;
    this.vm = vm;
    this.nodeType = nodeType;
    this.update();
    Dep.target = null;
  }
  
  Watcher.prototype = {
    update: function () {
      this.get();
      if (this.nodeType == 'text') {
        this.node.nodeValue = this.value;
      }
      if (this.nodeType == 'input') {
        this.node.value = this.value;
      }
    },
    // 获取 data 中的属性值
    get: function () {
      this.value = this.vm[this.name]; // 触发相应属性的 get
    }
  }


  win.MyVue = MyVue;
})(window)