当前位置:   article > 正文

手写Vue响应式 【Object.defineProperty】_自己手写object.defineproperty把数据变为响应式

自己手写object.defineproperty把数据变为响应式

前言

使用Vue技术栈也有2年了,对里边的各种API,属性,内置组件封装可以说是非常熟练了,一直知道双向数据绑定的原理是通过数据劫持,结合发布-订阅的方式来实现的;可理论始终是理论,忍不住还是动动小手撸了一把 ; 请大家尽管吐槽吧 。

参考文章 简书:https://www.jianshu.com/p/23180880d3aa

Github 源码 - 来颗小星星我也不介意哦 biu ~ biu ~

我们先来看一张图,相信大家在不少的博客,贴吧,论坛等都有看到过这张图例 ,那么它到底是什么意思呢 ?下面听我细细道来 。
mvvm
这张图我先做个简要的描述:

首先创建一个实例对象,分别触发了 compile 解析指令 和 observer 监听器,

compile 解析指令则循环递归 解析 类似 v-model 这样的指令,初始化 data 绑定数据,同时为每个节点创建一个订阅者 watcher , 并添加至 Dep 订阅器

observer 监听器 则利用了 Object.defineProperty() 方法的描述属性里边的 set,get 方法,来监听数据变化,

get 方法是在创建实例对象,生成dom节点的时候都会触发,所以:在compile 解析指令编译的时候,依次给每一个节点添加了一个订阅者 watcher 到主题对象 Dep (Dep 订阅者集合,我们暂称为 订阅器)

set 方法则是数据发生改变了,通知Dep订阅器里的所有wachter,然后找到对应订阅者 watcher触发对应 update 更新视图

简单的说明就是这样了。好吧,废话不多说直接上代码:

1,实现一个解析器Compile
可以扫描和解析每个节点的相关指令(v-model,v-on等指令),如果节点存在v-model,v-on等指令,则解析器Compile初始化这类节点的模板数据,使之可以显示在视图上,同时初始化相应的订阅者(Watcher)

/*
  第一步
  1,创建文档碎片,劫持所有dom节点,重绘dom节点
  2,重绘dom节点,初始化文档碎片绑定数据 实现文档编译 compile
  3, 为每一个节点创建一个 watcher  
  */
  function getDocumentFragment(node, vm) {
    var flag = document.createDocumentFragment();
    var child;
    while (child = node.firstChild) {
      /*
       while (child = node.firstChild)
       相当于 
       child = node.firstChild
       while (child)
       */
      compile(child, vm);
      flag.appendChild(child);
    }
    node.appendChild(flag);
  }
  function compile(node, vm) {
    /*
   nodeType 返回数字,表示当前节点类型    
   1 Element    代表元素    Element, Text, 
   2    Attr    代表属性    Text, EntityReference
   3    Text    代表元素或属性中的文本内容。
   . . . 更多请查看文档
   */
    if (node.nodeType === 1) {
      // 获取当前元素的attr属性
      var attr = node.attributes;
      for (let i = 0; i < attr.length; i++) {
        // nodeName 是attr属性 key 即名称 , 匹配自定义 v-m
        if (attr[i].nodeName === 'v-m') {
          // 获取当前值 即 v-m = "test" 里边的 test 
          let name = attr[i].nodeValue;
          // 当前节点输入事件
          node.addEventListener('keyup', function (e) {
            vm[name] = e.target.value;
          });
          // 页面元素写值  vm.data[name] 即 vm.data['test'] 即 MVVM
          node.value = vm.data[name];
          //最后移除标签中的 v-m 属性
          node.removeAttribute('v-m');
          // 为每一个节点创建一个 watcher  
          new Watcher(vm, node, name, "input");
        }
      }
      /*
      继续递归调用 文档编译 实现 视图更新 ;
      */
      if (child = node.firstChild) {
        /*
        if (child = node.firstChild)
        相当于 
        child = node.firstChild
        id(child)
        */
        compile(child, vm);
      }
    }
    if (node.nodeType === 3) {
      let reg = /\{\{(.*)\}\}/;
      if (reg.test(node.nodeValue)) {
        let name = RegExp.$1.trim();
        node.nodeValue = vm.data[name];
        // 为每一个节点创建一个 watcher  
        new Watcher(vm, node, name, "text");
      }
    }
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72

2,实现一个监听器Observer
用来劫持并监听所有属性,如果有变动的,就通知订阅者

/* 
  第二步
  1,获取当前实例对象的  data 属性 key  
     observer(当前实例对象 data ,当前实例对象)
  2,使用 Object.defineProperty 方法 实现监听 
  */
  function observe(data, vm) {
    Object.keys(data).forEach(function (key) {
      defineReactive(vm, key, data[key]);
    });
  }
  function defineReactive(vm, key, val) {
    /*
    Object.defineProperty
    obj
    要在其上定义属性的对象。
    prop
    要定义或修改的属性的名称。
    descriptor
    将被定义或修改的属性描述符。 描述符有很多,就包括我们要市用 set , get 方法
    */
    var dep = new Dep();
    Object.defineProperty(vm, key, {
      get: function () {
        /* 
        if (Dep.target) dep.addSub(Dep.target);
        看到这段代码不要差异,生成每一个 dom节点,都会走 get 方法
        这里为每一个节点添加一个订阅者到主题对象 Dep
        */
        if (Dep.target) dep.addSub(Dep.target);
        console.log(val)
        return val;
      },
      set: function (newValue) {
        if (newValue === val) return;
        val = newValue;
        console.log(val + "=>" + newValue)
        // 通知所有订阅者
        dep.notify();
      }
    });
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

3,实现一个订阅者Watcher
每一个Watcher都绑定一个 update,watcher 可以收到属性的变化通知并执行相应的 update ,从而更新视图。

/*
  第三步
  1,实现一个 watcher 观察者/订阅者添加方法update 渲染视图
  2,定义一个消息订阅器
    很简单,维护一个数组,用来收集订阅者
    消息订阅器原型挂载两个方法 分别是  
    addSub 添加一个订阅者   
    notify 数据变动 通知 这个订阅者的 update 方法
  */
  function Watcher(vm, node, name, nodeType) {
    Dep.target = this;
    this.vm = vm;
    this.node = node;
    this.name = name;
    this.nodeType = nodeType;
    this.update();
    console.log(Dep.target)
    Dep.target = null;
  }
  Watcher.prototype = {
    update: function () {
      /*
      this.node 指向当前修改的 dom 元素
      this.vm 指向当前 dom 的实例对象
      根据 nodeType 类型 赋值渲染页面
      */
      if (this.nodeType === 'text') {
        this.node.nodeValue = this.vm[this.name]
      }
      if (this.nodeType === 'input') {
        this.node.value = this.vm[this.name]
      }
    }
  }
  function Dep() {
    this.subs = [];
  }
  Dep.prototype = {
    addSub: function (sub) {
      this.subs.push(sub);
    },
    notify: function () {
      this.subs.forEach(function (sub) {
        sub.update();
      });
    }
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

4,实现类似Vue的MVVM实例

/*
  创建一个构造函数,并生成实例化对象 vm
  */
  function Vue(o) {
    this.id = o.el;
    this.data = o.data;
    observe(this.data, this);
    getDocumentFragment(document.getElementById(this.id), this);
  }
  var vm = new Vue({
    el: 'app',
    data: {
      msg: 'HiSen',
      test: 'Hello,MVVM'
    }
  });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

也许看到最后大家也没有看出个所以然,曾几何时的我跟你们一样,看来看去,就是这么几段代码;其实我也是参考,揣摩,调试,最后才成功的 ;
建议:拿下我的源码,自己跑一跑,看一看,是骡子是马拉出来溜溜。去溜溜 ~

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号