vue双绑原理浅析

双向数据绑定的实现思路
要想弄懂双向数据绑定,我们需要了解三个概念:
1,对象的访问器属性
2,闭包
3,发布订阅模式
对象的访问器属性
用来实现数据劫持

var obj = {name: 'apple'};
Object.defineProperty(obj, name, {
  get(){ // 1
    return obj.name;
  },
  set(newVal){ // 2
    obj.name = newVal;
  }
})

get/set及obj对象的访问器属性, 当我们执行obj.name来读取name属性的值时,实际执行1, 同理,执行obj.name='banana’来设置name属性,实际执行set函数,即2。
闭包
函数内部能访问外层的变量,
但外层不能访问函数内部的变量。
当函数通过某种方式,让外层函数可以访问到内部变量时,
我们称这个函数形成了闭包。
闭包是很危险的,因为有外层的引用,
这个变量不会被内存释放,
换句话说,
不会被js的自动垃圾回收机制回收。
那么,怎样可以形成一个闭包呢?
常见的是返回函数的形式,如下:

function foo(){
  var name;
  return function(){
    console.log(name);
  };
}

foo()(); // 在外层,成功打印了foo内部变量name的值

vue中,是通过上面介绍的访问器属性形成了闭包

defineReactive($data, key, value){
  Object.defineProperty($data, key, {
    get(){
      return value;
    },
    set(newVal){
      if(newVal != value){
        value = newVal;
      }
    }
  })
}

value在函数内部相当于私有变量, 外层通过$data.key读取或设置数据,实际都是操作的内部变量value。

我们平时写vue项目,常用的this.xxx,实际都被代理到了$data.xxx。

发布订阅模式
是一种设计模式,想象一种场景:
下雨了:
建筑工:休息一天~
农民工:小苗可以快快长高了
卖伞的:天助我也哈哈哈~
程序员:关我毛事~

天晴了:
建筑工:开工~
农民工:小苗长高高~
卖伞的:哎卖不动了
程序员:关我毛事~

一般会将主动变化因素(天气)视为发布者, 被动变化因素(4位小哥)视为订阅者。

发布者收集所有与之相关的订阅者, 并在发生变化时统一通知他的订阅者们更新状态。

订阅者可以注册到一个发布者, 并实现状态变更。

用代码表示:
1-发布者:

class Dep {
  constructor(){
    this.deps = [];
  }

  // 收集
  addDep( watcher ){
    this.deps.push(watcher);
  }

  // 通知更新
  notify(){
    this.deps.forEach( dep => {
      dep.update();
    })
  }
}
2-订阅者:
class Watcher {
  constructor(){},

  update(){} // 状态变更
}

vue中,每个绑定了数据的节点(包括文本节点/元素节点等)都对应一个watcher, 每个data中的数据属性都对应一个Dep(dependency依赖)。

如:

data: {
  name: 'apple'
}
`name属性对应一个Dep, 这个Dep可能对应多个watcher,

如:``

{{name}}

```

这就对应了3个watcher。

双向数据绑定的大体流程是这样的:
input标签输入内容,触发input事件,
inputHandler中修改data的属性(this.name=value),
访问器属性劫持到修改,通知watchers去更新(修改dom内容)

好了,概念讲的差不多了,下面我们来上具体代码~
简易版,功能不全,情况考虑也不周全,只是为了体现大体流程,想看具体实现可以去github上拉源码。相信看了这个简易版,源码读起来会更加易懂!

当new一个Vue实例时,vue主要做了这两件事:
1,数据劫持
2,编译dom,解析指令 (compile)
下面来一步步实现:
1:数据劫持
code

// KVue.js
class KVue {
  constructor( options ){
    this.vm = this;
    this.$options = options;
    this.$data = options.data;
    this.observe( this.$data );
  }

  observe( obj ){
    // 递归结束条件
    if(!obj || typeof obj != 'object') return;

    Object.keys(obj).forEach( key => {
      // 注意:普通非箭头函数 this不指向KVue实例
      this.defineReactive(obj, key, obj[key]);
    });
  }

  defineReactive(obj, key, value){
    // 递归 劫持 深层数据
    this.observe(value);

    Object.defineProperty(obj, key, {
      get(){ // 劫持到读
        return value;
      },
      set(newVal){ // 劫持到写
        if(value != newVal){
          vale = newVal;
        }
      }
    })
  }
}

2:添加发布订阅模式代码(收集依赖)
code

// KVue.js
class KVue {
  constructor( options ){
    this.vm = this;
    this.$options = options;
    this.$data = options.data;
    this.observe( this.$data );
  }

  observe( obj ){
    if(!obj || typeof obj != 'object') return;

    Object.keys(obj).forEach( key => {
      this.defineReactive(obj, key, obj[key]);
    });
  }

  defineReactive(obj, key, value){
    this.observe(value);

    // 每个属性对应一个Dep实例
    var dep = new Dep();

    Object.defineProperty(obj, key, {
      get(){
        // 配合实现watcher实例添加
        if(Dep.target){
          dep.addDep(Dep.target);
        };

        return value;
      },
      set(newVal){
        if(value != newVal){
          value = newVal;
          // 通知更新所有订阅者
          dep.notify();
        }
      }
    })
  }
}

// 发布者
class Dep {
  constructor(){
    this.deps = [];
  }

  addDep( watcher ){
    this.deps.push( watcher );
  }

  notify(){
    this.deps.forEach(dep => {
      dep.update();
    })
  }
}
// 订阅者
class Watcher {
  constructor(vm, key, cb){
    this.cb = cb;

    // 注意:这3步 结合 get劫持 实现
    // 将该Watcher实例 添加到 vm[key]对应的Dep实例
    Dep.target = this; // 通过Dep静态属性
    vm[key];
    Dep.target = null; // 重置
  }

  update(){
    this.cb && this.cb(); // 这里更新dom,由compile传过来
  }
}

3:添加代理,代理this.name到this.$options.data.name
code

// KVue.js
class KVue {
  observe(obj){
    if(!obj || typeof obj != 'object') return;

    Object.keys(obj).forEach(key => {
      this.defineReactive(obj, key, obj[key]);
      // 这里添加
      this.proxy(obj, key, obj[key]);
    });
  }

  proxy(obj, key, value){
    Object.defineProperty(this, key, {
      get(){
        return obj[key];
      },
      set(newVal){
        obj[key] = newVal;
      }
    })
  }  
}

4:添加编译过程(解析指令)
编译是vue1.0版本,没有虚拟dom这一块儿
code

// KVue.js
class KVue {
  constructor( options ){
    this.vm = this;
    this.$options = options;
    this.$data = options.data;
    this.observe( this.$data );
    // 添加编译
    new Compile(this, this.$options.el);
  }
}


// compile.js 循环遍历dom,解析指令
class Compile {
  constructor(vm, selector){
    this.vm = vm;
    this.dom = document.querySelector(selector);

    // 转移到fragment,比直接操作dom更高效
    this.createFragment();
    this.compile(this.$fragment);
    this.dom.appendChild(this.$fragment);
  }

  createFragment(){
    this.$fragment = document.createDocumentFragment();
    while(this.dom.firstChild){
      this.$fragment.appendChild(this.dom.firstChild);
    }
  }

  compile(frag){
    Array.from(frag.childNodes).forEach(node => {
      if(node.nodeType == 1){// 元素        
        this.compileElement(node);

        // 递归结束条件
        if(node.childNodes && node.childNodes.length > 0){
          this.compile(node);
        };
      }else if(node.nodeType == 3){// 文本        
        this.compileText(node);
      };
    });
  }

  compileElement(ndoe){
    Array.from(ndoe.attributes).forEach(attr => {
      if(attr.name.indexOf('k-') == 0){
        var dir = attr.name.slice(2);
        this[dir](ndoe, attr.value);
      }else if(attr.name.indexOf('@') == 0){
        var event = attr.name.slice(1);
        this.event(ndoe, event, attr.value);
      }
    })
  }

  text(node, key){
    node.textContent = this.vm[key];

    new Watcher(this.vm, key, function(){
      node.textContent = this.vm[key];
    });
  }

  html(node, key){
    node.innerHTML = this.vm[key];

    new Watcher(this.vm, key, function(){
      node.innerHTML = this.vm[key];
    })
  }

  model(node, key){
    node.value = this.vm[key];

    new Watcher(this.vm, key, function(){
      node.value = this.vm[key];
    });

    node.addEventListener('input', () => {
      this.vm[key] = node.value;
    })
  }

  compileText(node){
    /**
     * 这里用到正则知识点:
     * rexExp.test()后,
     * 所有捕获到的内容,可以通过RegExp.$1...$9访问到
    */
    if(/\{\{(.*)\}\}/.test(node.textContent)){
      this.text(node, RegExp.$1);
    }
  }

  event(node, eventName, eventHandler){
    if(eventHandler && this.vm.$options.methods[eventHandler]){
      node.addEventListener(
        eventName, 
        this.vm.$options.methods[eventHandler].bind(this.vm)
      )
    }
  }
}

(文章错误、不周之处,还请斧正,我们一起探讨共同进步 ^ ^)

原文链接:https://juejin.im/post/5eba043c6fb9a043710eadec
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值