前言

大家好,我是HoMeTown,明天聊一聊老成长谈的Vue之双向数据绑定

What?

首先咱们先看什么是双向数据绑定

对于不是很理解设计模式的敌人,你能够先了解一下单向数据绑定,就是把数据绑定到视图,每次触发操作批改了数据视图就会更新,数据 -> 视图,能够了解为MV数据驱动视图

举个:

通过点击按钮set name,触发点击事件,手动更新变量name的值为HoMeTown,然而当我扭转input输入框里的值,变量 name的值却不变,如下图:

那么双向数据绑定就是在单向的根底上,通过操作更新视图数据自动更新,那下面的来讲,就是我输出Input,变量name的值动静扭转。视图 -> 数据,能够了解为VM视图驱动数据

How?

Vue中的双向数据绑定由三个重要局部组成:

  • 数据层(Model):利用的数据及业务逻辑
  • 视图层(View):利用的展现成果,各类UI组件
  • 业务逻辑层(ViewModel):框架封装的外围,它负责将数据与视图关联起来

这个分层的架构计划,用专业术语来讲就是MVVM

ViewModel

ViewModel干了两件事儿:

  • 数据变动后更新视图
  • 视图变动后更新数据

它由两个重要局部组成:

  • 监听器(Ovserver):对所有数据的属性进行监听
  • 解析器(Compiler):对每个元素节点的指令进行扫描解析,依据指令模板替换数据,以及绑定相应的更新函数

入手实现

要做什么

Vue中,双向数据绑定的流程为:

  • new Vue()执行初始化,对data执行响应化解决,这个过程产生在Observe
  • 对模板执行编译,找到其中动静绑定的数据,从data中获取并初始化视图,这个过程产生在Compile
  • 定义一个更新函数和Watcher,未来对应数据变动时,Watcher调用更新函数
  • 因为data的某个属性在视图中可能呈现N次,所以每个属性都须要一个Dep来治理多个Watcher
  • data中的数据一旦发生变化,首先会找到对应的Dep,而后告诉这个Dep下所有的Watcher执行更新函数

参考下图:

Do it

首先定义一个Vue类, 做三件事

  • 数据劫持
  • 属性代理
  • 模板编译

    class Vue {  constructor(options) {      this.$data = options.data      this.$options = options      // 数据劫持      observe(this.$data)            // 属性代理      proxy(this.$data)            // 模板编译      compile(el, this)  }}

    接下来开始实现observe函数,做三件事

  • 递归data,劫持每一个
  • getter的时候收集
  • setter的时候告诉执行

    function observe(obj) {  // 递归终止条件  if(!obj || typeof obj !== 'object') return // 是空的 && 不是一个对象    Object.keys(obj).forEach( key => {      // 以后key对应的value      const value = obj[key]            // value能到这里,有可能是object,须要递归劫持      observe(value)            // 为以后的key所对应的属性增加getter & setter      Object.defineProrerty(obj, key, {          // 当且仅当该属性的 `enumerable` 键值为 `true` 时,该属性才会呈现在对象的枚举属性中。          enumerable: true,          // 当且仅当该属性的 `configurable` 键值为 `true` 时,该属性的描述符才可能被扭转,同时该属性也能从对应的对象上被删除。          configurable: true,          get() {              // 将new进去的Watcher实例进行收集              Dep.target ? dep.addSub(Dep.target) : null          },          set(newValue) {              if( val !== newValue ) dep.notify() // 告诉执行          }      })  })}

接下来实现Dep类,做两件事儿:

  • 依赖收集
  • 告诉执行

    //依赖收集的类class Dep {  constructor() {      // 所有Watcher实例存在这里      this.subs = []  }  // 增加Watcher实例  addSub(watcher) {      this.subs.push(watcher)  }  // 告诉Watcher实例执行更新函数  notify() {      this.subs.forEach( w => w.update())  }}

接下来实现订阅者Watcher类,做两件事:

  • 提供Dep.target
  • 提供更新数据的办法

    class Watcher {  // callback中,记录了以后watcher如何更新本人的文本内容  // 与此同时,须要拿到最新的数据,所以,在new Watcher的时候,须要传递vm进来  // 因为须要晓得在vm很多属性中,哪个数据,才是以后本人所须要的数据,所以,new Watcher的时候,须要指定key  constructor(vm, key, callback) {      this.vm = vm      this.key = key      this.callback = callback      // 把创立的watcher实例,在Dep.addSub时,存进Dep的subs里      Dep.target = this; // 自定义target属性      key.split(".").reduce((newobj, k) => newobj[k], vm);      Dep.target = null;  }  // 发布者告诉Watcher更新的办法  update() {      const value = this.key.split(".").reduce((newobj, k) => newobj[key], this.vm);            this.callback(value)  }}

最初实现compile,对HTML构造进行模板编译的办法:

function compile(el, vm) {    // 获取elDom元素    vm.$el = document.querySelector(el);    // 创立文档碎片,进步Dom操作性能    const fragment = document.createDocumentFragment();    // 取出来    while ((childNode = vm.$el.firstChild)) {      fragment.appendChild(childNode);    }        // 进行模板编译    replace(fragment)        // 放进去呀    vm.$el.appendChild(fragement)        function replace(node) {      // 定义匹配插值表达式的正则      const regMustache = /\{\{\s*(\S+)\s*\}\}/;      // \S匹配任何非空白字符      // \s匹配任何空白字符,包含空格、制表符、换页符等等。      // ()非空白字符提取进去,用一个小括号进行分组            // 以后的node节点是一个文本子节点,须要进行替换      if(node.nodeType == 3) {          const text = node.textContent // 文本子节点的字符串内容                    const execResult = regMustache.exec(text) //为一个数组,索引为0的为{{name}},为1的为name,exec() 办法用于检索字符串中的正则表达式的匹配。                    if(execResult) {              const value = execResult[1].split(".").reduce((newobj, k) => newobj[k], vm)                            node.textContent = text.replace(regMustache, value)                            // 此时,就能够创立Watcher实例,将这个办法存到watcher上,调用update就执行              new Watcher(vm, execResult[1], (newValue) => {                  node.textContent = text.replace(regMustache, newValue)              });              // good good          }                    // 递归完结          return      }             // 判断以后的node节点是否为input输入框      if(node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT' ){          // 首先要做v-model,就得先拿到属性节点          const attrs = Array.from(node.attributes);          const findResult = attrs.find((x) => x.name === "v-model");          if(findResult) {              // 以后有v-model,获取值              const expStr = findResult.value;              const value = expStr.split(".").reduce((newobj, k) => newobj[k], vm);              node.value = value;                            // 创立Watcher实例              new Watcher(vm, expStr, (newValue) => {                  node.value = newValue              })                            // 监听input事件,拿到文本框最新的值,而后更新到vm上              node.addEventListener("input", e => {                  const keys = expStr.split(".")                  const keysLen = keys.length                  const obj = keys.slice(0, keysLen - 1).reduce((newobj, k) => newobj[k], vm);                  obj[keys[keysLen - 1]] = e.target.value              })          }      }            // 走到这,证实不是文本节点,递归解决      node.childNodes.forEach( child => replace(child))    }}

测试

还是用最开始咱们的那个,批改如下:

HTML

<div id="app">  <p>name:<span id="nameBox">{{name}}</span></p>  <input v-model="name" id="ipt" type="text" />  <button id="set">Set name</button></div>

JS

  const vm = new Vue({    el: "#app",    data: {      name: "No name yet!",    },  });    const setBtn = document.getElementById("set");  setBtn.onclick = function () {    vm.name = "Is HoMeTown!!";  };

点击按钮,批改Vue实例vm的属性name = 'Is HoMeTown!!'

能够看到曾经胜利了!这是单向,而后咱们试一试,批改输入框的内容,上方name的值不会不跟着扭转:

SUCCESS!!!!!!!

总结

Vue中,双向数据绑定的原理总结的来说有几点:

  • observe 进行数据劫持,getter时增加Watcher,setter时告诉Watcher.update
  • Dep类实现 依赖收集与告诉执行
  • Watcher类实现 订阅者执行更新
  • compile 进行模板编译,解析v-model,给input增加事件

完结~