浅谈vue原理(四)

  经过了前面这么久,我们已经弄好了数据劫持,数据代理,还通过了发布订阅模式实现了data中数据变化,页面上也会跟着变化;

  现在还差点东西,就是当页面上的数据变化的时候,data中的数据也能跟着变化,进而使得页面的数据都刷新成最新数据,这就是view->model这条线;

1.html准备

  首先我们需要准备一个input标签

<body>
  <div id="app">
    <h1>呵呵:{{message}}</h1>
    <input type="text" v-model="message">
  </div>

  <script src="./mvvm.js"></script>
  <script>

    // 自定义的myVue实例
    let myVue = new MyVue({
      el: '#app',
      data: {
        message: "hello world"
      }
    })
  </script>
</body>

2.数据双向绑定的实现

  之前说过,html标签中每一个{{}}占位符都是对应着一个Watcher,而且{{}}占位符处于文本节点中,我们在初始化的时候使用node.nodeType === 3 && reg.test(nodeText)条件,使用正则就能匹配文本节点中的{{}}占位符;

  现在在初始化的过程,添加新逻辑:首先需要找到input标签,我们可以使用node.nodeType === 1先找到元素节点,再找有v-model属性的节点,然后取出其中的属性值,这个属性就对应着data中的数据;

  初始化完成之后,有两种可能:

  (1)当我们手动的在input框中改变值,data中对应的属性值也应该发生变化,这个就添加监听事件去处理

node.addEventListener("input", e => {
          // 获取input输入框中的值
          let inputValue = e.target.value;
          //input中修改的值同步到到data中去,这里又会触发该属性的set方法,set方法中又会触发发布订阅模式,将所有的Watcher都调用一遍
          vm[exp] = inputValue;
        })

(2)当手动的修改data中的数据,set方法中会触发所有的Watcher的回调函数,这里的input标签中的值也应该变化;

  而之前我们只是对{{xxx}}占位符的文本节点创建了Watcher,这种占位符节点的值肯定会刷新,但是input是元素节点,不会刷新!所以需要将这种带有v-model的元素节点也创建Watcher:

  这段逻辑完整的代码下图所示,初始化的过程中首先找到v-model所在标签的属性值,将data中对应的属性值覆盖input框中内容!而且还需要创建一个Watcher(Watcher注册逻辑之前说过)以及给input添加事件监听;

  事件监听用于初始化完成之后,用户手动修改input框中的值,触发data中的数据发生变化,进一步触发set方法的notify方法,调用所有的Watcher的回调函数,刷新页面数据;

全部的js代码:

function MyVue (options = {}) {
  //第一步:首先就是将实例化的对象给拿到,得到data对象
  this.$options = options;
  this._data = this.$options.data;

  //第二步:数据劫持,将data对象中每一个属性都设置get/set方法
  observe(this._data);

  //第三步:数据代理,这里就是将_data的对象属性放到myVue实例中一份,实际的数据还是_data中的
  for (let key in this._data) {
    //这里的this代表当前myVue实例对象
    Object.defineProperty(this, key, {
      enumerable: true,
      get () {
        return this._data[key];
      },
      set (newVal) {
        this._data[key] = newVal;
      }
    })
  }

  //第四步:compile模板,需要将el属性和当前myVue实例
  compile(options.el, this)
}

function compile (el, vm) {
  return new Compile(el, vm);
}

function Compile (el, vm) {
  //将el代表的那个dom节点挂载到myVue实例中
  vm.$el = document.querySelector(el);

  //创建虚拟节点容器树
  let fragment = document.createDocumentFragment();

  //将el下所有的dom节点都放到容器树中,注意appendChild方法,这里是将将dom节点移动到容器树中啊,不是死循环!
  while (child = vm.$el.firstChild) {
    // console.log('count:' + vm.$el.childElementCount);
    fragment.appendChild(child)
  };

  //遍历虚拟节点中的所有节点,将真实数据填充覆盖这种占位符{{}}
  replace(fragment, vm);

  //将虚拟节点树中内容渲染到页面中
  vm.$el.appendChild(fragment);
}

function replace (n, vm) {
  //遍历容器树中所有的节点,解析出{{}}里面的内容,然后将数据覆盖到节点中去
  Array.from(n.childNodes).forEach(node => {
    console.log('nodeType:' + node.nodeType);

    let nodeText = node.textContent;
    let reg = /\{\{(.*)\}\}/;
    // 节点类型常用的有元素节点,属性节点和文本节点,值分别是1,2,3
    //一定要弄清楚这三种节点,比如<p id="123">hello</p>,这个p标签整个的就是元素节点,nodeType==1
    //id="123"可以看作是属性节点,nodeType==2
    //hello 表示文本节点,nodeType==3
    //因为占位符{{}}只在文本节点中,所以需要判断是否等于3
    if (node.nodeType === 3 && reg.test(nodeText)) {
      // RegExp.$1是RegExp的一个属性,指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串,以此类推,RegExp.$2。。。
      let arr = RegExp.$1.split(".");
      let val = vm;
      // 这个for循环就是取出这样的值:myVue[name][user]
      arr.forEach(i => {
        val = val[i];
      })
      // 创建Watcher,最主要的是传入的这个回调函数,会覆盖node节点中的占位符{{xxx}}
      new Watcher(vm, RegExp.$1, function (newVal) {
        node.textContent = nodeText.replace(reg, newVal);
      })
      // 把值覆盖到虚拟节点的占位符{{}}这里
      node.textContent = nodeText.replace(reg, val);
    }

    //这里遍历到元素的节点,例如:<p id="xx">aaa</p></p>,<input type="text v-model=" username">
    //然后需要取到其中的vue相关的指令,例如v-model="xxx",一般是以v-开头的
    if (node.nodeType === 1) {
      // console.log(node);
      let nodeAttributes = node.attributes;
      Array.from(nodeAttributes).forEach(attr => {
        let name = attr.name;
        let exp = attr.value;
        // 找到v-model指令,data中的数据填充到input框中
        if (name.indexOf("v-model") == 0) {
          node.value = vm[exp];
        }
        // data中的数据变化,input中的数据也要跟着变化
        new Watcher(vm, exp, function (newVal) {
          node.value = newVal;
        })
        node.addEventListener("input", e => {
          // 获取input输入框中的值
          let inputValue = e.target.value;
          //input中修改的值同步到到data中去,这里又会触发该属性的set方法,set方法中又会触发发布订阅模式,将所有的Watcher都调用一遍
          vm[exp] = inputValue;
        })

      })
    }

    // 第一个遍历的节点是<div id="app">这一行后面的换行,nodeType等于3,但是没有占位符{{}},所以会进入到这里进行递归调用内部
    //的每一个节点,直到找到文本节点而且占位符{{}}
    if (node.childNodes) {
      replace(node, vm);
    }
  })
}

//数据劫持操作
function observe (data) {
  // 如果data不是对象,就结束,不然递归调用会栈溢出的
  if (typeof data !== 'object') return;
  return new Observe(data);
}

function Observe (data) {
  let dep = new Dep();

  // 遍历data所有属性
  for (let key in data) {
    let val = data[key];
    //初始化的时候, data中就有复杂对象的时候,例如data: { message:{a:{b:1}}}  ,就需要递归的遍历这个对象中每一个属性都添加get和set方法
    observe(val);
    Object.defineProperty(data, key, {
      enumerable: true,
      get () {
        // 订阅
        Dep.target && dep.addSub(Dep.target);
        return val;
      },
      set (newVal) {
        if (val === newVal) return;
        val = newVal;
        //当后续可能因为业务逻辑使得_data.message = {name: "小王"},设置对象类型的属性值,就需要递归的给对象中{name: "小王"}的每个属性也添加get和set方法
        //否则name是没有get/set方法的
        observe(val);
        dep.notify();
      }
    })
  }
}

// ===============================发布订阅===============================
// 可以看做是公众号端
function Dep () {
  // 存放每个用户的容器
  this.subs = [];
}

//对外提供的api之一,供用户订阅
Dep.prototype.addSub = function (sub) {
  this.subs.push(sub);
}

// 对外提供的api之二,遍历每个用户,给每个用法发信息
Dep.prototype.notify = function () {
  this.subs.forEach(sub => {
    sub.update();
  });
}

// 用户端
function Watcher (vm, exp, fn) {
  // 这个可以看作是用户的标志;注意,这个fn一般是一个回调函数
  this.vm = vm;
  this.exp = exp;
  this.fn = fn;

  Dep.target = this;
  let val = vm;
  let arr = exp.split(".");
  arr.forEach(item => {
    val = val[item];
  })
  Dep.target = null;
}

// 用户端提供的对外api,让公众号端使用
Watcher.prototype.update = function () {
  let val = this.vm;
  let arr = this.exp.split(".");
  arr.forEach(item => {
    val = val[item];
  })
  this.fn(val);
}

View Code

(0)

相关推荐

  • Vue2.x 响应式部分源码阅读记录

    之前也用了一段时间Vue,对其用法也较为熟练了,但是对各种用法和各种api使用都是只知其然而不知其所以然.最近利用空闲时间尝试的去看看Vue的源码,以便更了解其具体原理实现,跟着学习学习. Proxy ...

  • DOM - 元素节点属性

      元素节点属性  childNodes和firstChild和lastChild 1.childNodes  //获取当前元素节点的所有的子节点 举个小例子: 运行结果返回了一个节点列表: 节点列表 ...

  • 设计师会编程、程序员懂艺术之设计规范

    本公众号定期更新关于 设计师.程序员发挥创意 互相融合的指南.作品. 主要技术栈: nodejs.react native.electron 本号正在更新的系列有: 写给设计师的人工智能指南 移动Ap ...

  • 简述vue的双向绑定原理

    一.前言 在vue的视图层与modal层进行数据交互的时,视图层的数据传入到modal层,modal层通过defineProperty来劫持每个元素,并绑定监听事件进行监听,一旦监听到数据变化,就通过 ...

  • Vue的MVVM是如何实现的?本文项目详解原理

    相信只要你去面试vue,都会被问到vue的双向数据绑定,你要是就说个mvvm就是视图模型模型视图,只要数据改变视图也会同时更新!那你离被pass就不远了! 视频已录制,地址(www.bilibili. ...

  • 理解vue数据驱动

    vue是双向数据绑定的框架,数据驱动是他的灵魂,他的实现原理众所周知是Object.defineProperty方法实现的get.set重写,但是这样说太牵强外门了.本文将宏观介绍他的实现 使用vue ...

  • 石桥码农:20 vue计算属性和侦听器

    目录 计算属性 替代计算属性的计算方法 计算属性实现的原理 侦听属性 把侦听属性当作事件监听用 handler方法和immediate属性:监听属性时要立即执行函数怎么做? 想监听子属性.子子属性的变 ...

  • 手摸手带你理解Vue的Watch原理

    前言 watch 是由用户定义的数据监听,当监听的属性发生改变就会触发回调,这项配置在业务中是很常用.在面试时,也是必问知识点,一般会用作和 computed 进行比较. 那么本文就来带大家从源码理解 ...

  • 浅谈vue原理(三)

    上一篇已经实现了发布订阅模式了,现在我们实现从model->view的数据绑定,也就是当data中的数据改变后,页面上的数据也要跟着变化: 1.发布订阅代码的实际应用 我们思考一下,怎么把我们上 ...

  • 18 vue 实例及其双向绑定的实现原理

    目录 一个vue实例 生命周期钩子函数 set/get 访问器属性的实现 v-model属性与{{text}}在模板中是如何被解析的? 观察者模式 源码 一个vue实例 一个典型的vue实例: < ...

  • Vue响应式系统如何操作运用?本文详解

    前言 之前学习 vue 的时候,一直没刨根问底过.在看到网上这类文章比较多,参差不齐的质量有时候看的一头雾水.当然也有不错的文章,但是终究是别人的理解.于是写一篇关于自己的理解记录下来,亲身实践才能收 ...