美文网首页
vue双向绑定原理及其简单实现

vue双向绑定原理及其简单实现

作者: Y_d4ea | 来源:发表于2019-10-29 16:54 被阅读0次

    也许很多人写了很久的vue但是实际对于其双向绑定和其渲染还是存在一个模糊的概念,仅知道是通过defineProperty来达到,但是具体详细怎么实现,及其分多少个模块,并不是非常清除。

    模块划分

    大体分三个模块:
    observer观察者(dep容器)
    compile解释器
    watcher订阅者

    原理.png
    下面用简单实现一个v-model及{{name}}的简单示例来描述此三个模块。
    1.先来看一下初始化
    <body>
      <div id="app">
        <div>{{name}}</div>
        <div>{{xiaxia}}</div>
        <div>
          <input type="text" v-model="name">
          <input type="text" v-model="xiaxia">
        </div>
      </div>
      <script src="js/compile.js"></script>
      <script src="js/watcher.js"></script>
      <script src="js/observer.js"></script>
      <script src="js/index.js"></script>
      <script>
        const app=new Vue({
          el: '#app',
          data: {
            name: 'ABC',
            xiaxia:'xiaxia'
          }
        })
      </script>
    </body>
    

    入口文件(index.js)

    class Vue {
      constructor(options) {
        this.$el = document.querySelector(options.el);
        this.$data = options.data;
        Object.keys(this.$data).forEach(key => {
          this.proxyData(key);
        });
        this.init(this.$data);
      }
      init() {
        // 加入观察者
        observer(this.$data);
        // 编译器
        new Compile(this);
      }
      // 数据劫持 重写get set 让数据直接读取写入
      proxyData(key) {
        Object.defineProperty(this, key, {
          get(){
            return this.$data[key]
          },
          set(value){
            this.$data[key] = value;
          }
        });
      }
    }
    

    定义一个构造函数
    在constructor传入了所挂载的节点以及其data对象(此处为对象,实则vue中data为一个方法,暂不做深入分析,往后会提到)
    遍历data对象中的元素修改其get和set以此来达到this.name可直接访问及修改其属性的效果。
    并在初始化中引入observer观察者及compile编译器

    observer观察者

    function observer(data) {
      if (!data || typeof data !== "object") {
        return;
      }
      for (let key in data) {
        defineReactive(data, key, data[key]);
      }
    }
    
    function defineReactive(data, key, value) {
      //递归调用,监听所有属性
      observer(value);
      const dep = new Dep();
      Object.defineProperty(data, key, {
        get(){
          if (Dep.target) {
            dep.addSub(Dep.target);
          }
          return value;
        },
        set(newVal){
          if (value !== newVal) {
            value = newVal;
            dep.notify(); //通知订阅器
          }
        }
      });
    }
    

    监听每一个属性(此处存在一定的性能问题在vue3.0已优化,目前哪怕没有用到的属性也会去监听浪费性能,暂不展开)
    get中addSub为把watcher加入到容器中管理
    set则是通过对不比其值来通知watcher订阅器

    // 容器
    class Dep {
      constructor() {
        this.subs = [];
      }
      addSub(sub) {
        this.subs.push(sub);
      }
      notify() {
        this.subs.forEach(sub => {
          sub.update();
        })
      }
    }
    // es6 class只能以此添加公共方法属性
    Dep.prototype.target=null
    

    其中update为watcher中方法

    watcher订阅者

    class Watcher{
      constructor(vm, prop, callback){
        this.vm = vm;
        this.prop = prop;
        this.callback = callback;
        this.value = this.get();
      }
      update(){
        const value = this.vm.$data[this.prop];
        const oldVal = this.value;
        if (value !== oldVal) {
          this.value = value;
          this.callback(value);
        }
      }
      get(){
        Dep.target = this; //储存订阅器
        const value = this.vm.$data[this.prop]; //因为属性被监听,这一步会执行监听器里的 get方法
        Dep.target = null;
        return value;
      }
    }
    

    watcher中get方法之所以设置Dep.target = this后对this.vm也就是data赋值是为了触发observer中的get属性,故赋值成功后则应当置空。
    update方法则含一个回掉函数,watcher在compile中创建

    compile解释器

    class Compile {
      constructor(vm) {
        this.vm = vm;
        this.el = vm.$el;
        this.init()
      }
      init() {
        this.fragment = this.nodeFragment(this.el);
        this.compileNode(this.fragment);
        this.el.appendChild(this.fragment);
      }
      nodeFragment(el) {
        // 创建内存中的DOM
        const fragment = document.createDocumentFragment();
        let child = el.firstChild;
        //将子节点,全部移动文档片段里
        while (child) {
          fragment.appendChild(child);
          child = el.firstChild;
        }
        return fragment;
      }
      // 编译节点
      compileNode(fragment) {
        const childNodes = fragment.childNodes;
        [...childNodes].forEach(node => {
          // 如果节点是元素节点,则 nodeType 属性将返回 1。
          // 如果节点是属性节点,则 nodeType 属性将返回 2。
          // ......
          // 参照https://www.w3school.com.cn/jsref/prop_node_nodetype.asp
          if (this.isElementNode(node)) {
            this.compile(node);
          }
    
          const reg = /\{\{(.*)\}\}/;
          const text = node.textContent;
          if (reg.test(text)) {
            const prop = reg.exec(text)[1];
            this.compileText(node, prop); //替换模板
          }
    
          if (node.childNodes && node.childNodes.length) {
            this.compileNode(node);
          }
        })
      }
      compile(node) {
        // 编译vue的指令
        let nodeAttrs = node.attributes;
        [...nodeAttrs].forEach(attr => {
          let { name, value } = attr;
          if (name === "v-model") {
            this.compileModel(node, value);
          }
        });
      }
      compileModel(node, prop) {
        let val = this.vm.$data[prop];
        // 初始化值
        this.updateModel(node, val);
        // 添加观察者
        new Watcher(this.vm, prop, (value) => {
          // 回调函数
          this.updateModel(node, value);
        });
    
        node.addEventListener('input', e => {
          let newValue = e.target.value;
          if (val === newValue) {
            return;
          }
          this.vm.$data[prop] = newValue;
        });
      }
      compileText(node,prop){
        let text = this.vm.$data[prop];
        this.updateView(node, text);
        new Watcher(this.vm, prop, (value) => {
          this.updateView(node, value);
        });
      }
      updateModel(node,value){
        node.value = typeof value === 'undefined' ? '' : value;
      }
      updateView(node,value){
        node.textContent = typeof value === 'undefined' ? '' : value;
      }
      isElementNode(node) {
        return node.nodeType === 1;
      }
    }
    

    init为初始化方法,也就是vue第一次默认渲染的数据。
    从onst fragment = document.createDocumentFragment();中可以看到其创建了一个文档片段,其实也就是所谓的VirtualDOM虚拟DOM
    compileNode 遍历其子节点通过正则找到{{name}}来替换其内容以及通过nodeType来定位到元素找到v-model来添加watcher观察者和给input添加监听来修改其值(之前定义好的get,set)
    通过Watcher的get来把watcher添加到dep及通过dep的notify来调用watcher的更新方法产生的值来更新数据(有点绕。。。。。。)

    Finally

    主要还是得读代码,三者存在循环调用密不可分。
    附上代码https://github.com/Xyifeng/vue-core-demo

    相关文章

      网友评论

          本文标题:vue双向绑定原理及其简单实现

          本文链接:https://www.haomeiwen.com/subject/znlgvctx.html