vue2.0源码之数据双向绑定原理和实现

这里什么说是vue2.0的双向绑定原理是因为和vue3.0的实现方式是有区别的。

一.双向绑定的原理介绍

  • vue的双向绑定是数据和视图的同步变化,即当数据发生变化的时候,相关的视图会发生改变;视图发生改变的时候,数据也会随之变化。它是通过 数据劫持 结合 发布订阅模式的方式来实现的。

下图是一个vue数据双向绑定的过程:

  1. 将vue中的data里的所有属性通过实现Observer来完成数据劫持
  2. Dep是一个容器来存放所有的订阅者Watcher(订阅者Watcher可能有很多),Dep用来解析页面的模板 {{ name }} ,执行相应方法将数据解析到页面上。
  3. 实现一个监听者Oberver来劫持并监听所有的属性,一旦有属性发生变化就通知订阅者Watcher,
  4. 订阅者watcher来接受属性变化的通知(notify)并执行相应的方法,从而更新视图
  5. 实现一个解析器compile,可以扫描和解析每个节点的相关指令(v-xxx),通过指令(v-xxx)去对DOM进行封装。当数据发生变化,指令修改对应的DOM,数据驱动DOM的变化。反向,Vue也会监听操作,修改视图时,Vue监听到变化后,改变数据。数据的双向变化形成

在这里插入图片描述
下面来简单实现一下这个流程(不代表源码实现)。

二.简单实现双向绑定

下面通过类来定义上图中的Observer,Dep,Watcher,Compile等来抽象出数据的双向绑定。

//定义一个容器类 来存放所有的订阅者
class Dep {
  constructor() {
    this.subs = [];
  }
  //订阅
  addSub(watcher) {
    this.subs.push(watcher);
  }
  //发布
  notify() {
    this.subs.forEach((watcher) => watcher.update());
  }
}

//观察者:将数据劫持和页面联系起来
class Watcher {
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr;
    this.cb = cb;
    //默认存放一个老值
    this.oldValue = this.get();
  }
  get() {
    Dep.target = this; //先把自己放在全局上
    //取值,把观察者和数据联系起来
    let value = CompileUtil.getVal(this.vm, this.expr);
    //不取消任何取值都会添加watcher
    Dep.target = null;
    return value;
  }
  update() {
    //更新操作,数据变化后会调用观察者update方法
    let newVal = CompileUtil.getVal(this.vm, this.expr);
    if (newVal !== this.oldValue) {
      this.cb(newVal);
    }
  }
}

//将data里的所有属性包括对象里的属性劫持
class Observer {
  constructor(data) {
    this.observer(data);
  }
  observer(data) {
    if (data && typeof data == "object") {
      for (let key in data) {
        this.defineReactive(data, key, data[key]);
      }
    }
  }
  defineReactive(obj, key, value) {
    //value还是对象的话要继续,才会给全部都赋予get和set方法
    this.observer(value);
    let dep = new Dep(); //给每个属性都加上一个发布订阅功能
    Object.defineProperty(obj, key, {
      get() {
        //创建watcher时候,会取到对应内容,并且把watcher放到全局上
        Dep.target && dep.addSub(Dep.target);
        return value;
      },
      set(newVal) {
        //若赋值的是一个对象,还需要继续监控
        if (newVal != value) {
          this.observer(newVal);
          value = newVal;
          dep.notify();
        }
      },
    });
  }
}

class Compiler {
  constructor(el, vm) {
    //判断el属性
    this.el = this.isElementNode(el) ? el : document.querySelector(el);
    this.vm = vm;
    //把当前节点中的元素获取到,并放到内存中
    let fragment = this.node2fragment(this.el);
    //把节点中内容进行替换

    //编译模板,用数据编译
    this.compile(fragment);
    //把内容塞回页面
    this.el.appendChild(fragment);
  }
  //判断是不是指令
  isDirective(attrName) {
    return attrName.startsWith("v-"); //开头
  }
  //编译元素的方法
  compileElement(node) {
    let attributes = node.attributes; //类数组
    [...attributes].forEach((attr) => {
      let { name, value: expr } = attr;
      if (this.isDirective(name)) {
        //v-model v-html v-bind
        let [, directive] = name.split("-"); //v-on:click
        let [directiveName, eventName] = directive.split(":");
        //调用不同指令来处理
        CompileUtil[directiveName](node, expr, this.vm, eventName);
      }
    });
  }
  //编译文本的方法
  compileText(node) {
    //判断文本节点中是否包含{{}}
    let content = node.textContent;
    //(.+?)匹配一个大括号内的,一个及以上,到第一个大括号结束时候结束
    if (/\{\{(.+?)\}\}/.test(content)) {
      CompileUtil["text"](node, content, this.vm);
    }
  }
  //编译的核心方法
  compile(node) {
    let childNodes = node.childNodes;
    [...childNodes].forEach((child) => {
      if (this.isElementNode(child)) {
        this.compileElement(child);
        this.compile(child); //递归,获得内层
      } else {
        this.compileText(child);
      }
    });
  }
  node2fragment(node) {
    //创建一个文本碎片
    let fragment = document.createDocumentFragment();
    let firstChild;
    while ((firstChild = node.firstChild)) {
      //appendChild具有移动性
      fragment.appendChild(firstChild);
    }
    return fragment;
  }

  isElementNode(node) {
    //判断是否为元素节点
    return node.nodeType === 1;
  }
}

//绑定处理事件的各种方法
CompileUtil = {
  //取得对应的数据
  getVal(vm, expr) {
    //vm.$data 'school.name'
    //返回name
    return expr.split(".").reduce((data, current) => {
      return data[current]; //继续取值,取到name
    }, vm.$data);
  },
  setValue(vm, expr, value) {
    expr.split(".").reduce((data, current, index, arr) => {
      if (index == arr.length - 1) {
        return (data[current] = value);
      }
      return data[current];
    }, vm.$data);
  },
  model(node, expr, vm) {
    //node节点,expr是表达式,vm是当前实例
    let fn = this.updater["modeUpdater"];
    //给输入框加一个观察者,稍后数据更新就会触发此方法,将新值给输入框赋予值
    new Watcher(vm, expr, (newVal) => {
      fn(node, newVal);
    });
    node.addEventListener("input", (e) => {
      let value = e.target.value; //获取用户输入的内容
      this.setValue(vm, expr, value);
    });
    let value = this.getVal(vm, expr);
    fn(node, value);
  },
  html(node, expr, vm) {
    let fn = this.updater["htmlUpdater"];
    new Watcher(vm, expr, (newVal) => {
      fn(node, newVal);
    });
    let value = this.getVal(vm, expr);
    fn(node, value);
  },
  getContentValue(vm, expr) {
    //遍历一个表达式,将内容重新替换成一个完整的内容,返还回去
    return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
      return this.getVal(vm, args[1]);
    });
  },
  on(node, expr, vm, eventName) {
    //  v-on:click="change"  expr就是change
    node.addEventListener(eventName, (e) => {
      vm[expr].call(vm, e); //this.change
    });
  },
  text(node, expr, vm) {
    let fn = this.updater["textUpdater"];
    let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
      //给表达式每个{{}}都加个观察者
      new Watcher(vm, args[1], (newVal) => {
        fn(node, this.getContentValue(vm, expr)); //返回一个全的字符串
      });
      return this.getVal(vm, args[1]);
    });
    fn(node, content);
  },

  //更新视图
  updater: {
    //把数据插入节点当中
    modeUpdater(node, value) {
      node.value = value;
    },
    textUpdater(node, value) {
      node.textContent = value;
    },
    htmlUpdater(node, value) {
      //xss攻击
      node.innerHTML = value;
    },
  },
};

// 基类,调度
class Vue {
  constructor(options) {
    this.$el = options.el;
    this.$data = options.data;
    let computed = options.computed;
    let methods = options.methods;
    if (this.$el) {
      //把数据全部转换成用Object.defineProperty来定义,数据劫持
      new Observer(this.$data);
      //{{getNewName}} reduce 取值是vm.$data.getNewName
      for (let key in computed) {
        Object.defineProperty(this.$data, key, {
          get: () => {
            //注意this指向el实例
            return computed[key].call(this);
          },
        });
      }
      for (let key in methods) {
        Object.defineProperty(this, key, {
          get: () => {
            //注意this指向el实例
            return methods[key];
          },
        });
      }
      //把数据获取操作vm上的取值操作都代理到vm.$data上
      this.proxyVm(this.$data);
      new Compiler(this.$el, this);
    }
  }
  proxyVm(data) {
    for (let key in data) {
      Object.defineProperty(this, key, {
        get() {
          //相当于在$data上取值,进行转换操作,不需要深层代理
          return data[key];
        },
        set(newVal) {
          //设置代理方法
          data[key] = newVal;
        },
      });
    }
  }
}

三.体验双向绑定

尝试将刚才写的代码引入到页面来使用,接下来就和Vue的基本使用一样了。

<!DOCTYPE html>
<html lang="en">
	<head>
	    <meta charset="UTF-8">
	    <meta name="viewport" content="width=device-width, initial-scale=1.0">
	    <title>Vue</title>
	</head>
	<body>
	    <div id="app">
	     <!-- 此时改变输入框的值,页面的值也会相应改变 -->
	        <input v-model="person.name"/>
	        <div>{{person.name}}</div>
	        <div>{{person.age}}</div>
	       
	    </div>
	    <script src="vue.js"></script>
	    <script>
	        new Vue({
	            el: '#app',
	            data: {
	                info: {
	                    name: 'goudan',
	                    age: 20
	                },
	            },   
	        })
	    </script>
	</body>
</html>

自己写一个简单版本的vue可以更好的理解它的原理, 提高自己的思维方式。我们用多了框架过后可能对原生的JS就有所遗忘,要知道框架都是原生的来构造的,所以阅读并理解源码对我们深入学习和技能提升有很大的帮助。

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值