vue 2.0 通过model 指令来学习双向绑定

331 阅读1分钟

这里通过讲解model 指令的实现来学习双向绑定的实现过程

首先将整个实现的思路顺下。

  • 在初始化中,首先 new一个vue 实例,通过调用的过程我们可以知道,
  1. 先将参数options 对象存为$options
  2. 要想实现双向绑定,需要将传过来的data 参数进行 observe
  3. 然后将data 中的值绑定到当前实例上
  4. 然后根据传入的el 参数,编译模板
  • 接着探讨编译模板的过程,我们这里做一个简单版的,用fragment来实现,实际上使用的是ATS
  1. 调用函数 让节点变为 fragment
  2. 编译模板
  3. 将编译好的模板挂载到dom
  • 第二步骤中的1和3.思路比较简单,这里重点讲第二个编译模板。这里也只是简单实现,不使用递归。
  1. 通过根节点,获取子节点,然后遍历子节点。遍历过程中根据节点的nodeType 不同,来处理元素节点,和文本节点
  2. 元素节点的处理:获取元素节点的attributes 值, 然后遍历它。遍历过程找到v-model 这个指令
  3. 文本节点的处理:通过正则获取{{}}里面的值。
  • 找到v-model 的指令后,获取绑定的值 比如v-model="a" 中的a,对这个找到的值做处理
  1. 对找到的a添加 watcher
  2. 获取a这个属性的值,并给绑定的元素赋值
  3. 给input 添加监听事件,来实时更新最新的输入值
  • 文本节点的处理,找到{{}}中的值后,通过添加watcher 来添加处理

下面是实现过程中的代码粘贴,有一部分代码参考之前讲过的 数据响应式原理,juejin.cn/post/699458…

代码结构如下图

微信截图_20210825225558.png index.html

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    你好{{a}}
    <input v-model="a" type="text">
    <ul>
      <li>a</li>
      <li>b</li>
      <li>c</li>
    </ul>
    <button onclick="add()">点我加1</button>
  </div>
  <script src='/xuni/bundle.js'></script>
  <script>
    var vm = new Vue({
      el: 'app',
      data :{
        a:'33'
      }
    })
    function add () {
      vm._data.a++
    }
    console.log(vm)
  </script>
</body>
</html>

index.js

import Compile from './Compile'
import { observe } from './initdata/data/observer'
import Watcher from './initdata/data/watcher.js'
export default class Vue {
  constructor(options) {
    // 把参数options 对象存为$options
    this.$options = options || {}
    this._data = options.data || undefined;
    observe(this._data)
    this._initData()
    new Compile(options.el, this)
  }
  _initData() {
    let self = this;
    Object.keys(this._data).forEach(key => {
      Object.defineProperty(self, key,{
        get() {
          return self._data[key]
        },
        set(newVal) {
          self._data[key] = newVal
        }
      })
    })
  }
}

window.Vue = Vue

compile.js

import Watcher from './initdata/data/watcher.js'
export default class Compile {
  constructor (el, vue) {
    this.$vue = vue
    this.$el = document.querySelector('#'+el)
    if (this.$el) {
      // 如果传入了挂载点 调用函数 让节点变为 fragment  实际上用的AST 这里就是轻量级的
      let fragment = this.node2Fragment(this.$el)
      // 编译模板
      this.compile(fragment)
      // 将编译完的模板上树
      this.$el.appendChild(fragment)
    }

  }
  node2Fragment (el) {
    console.log(el)
    var fragment = document.createDocumentFragment();

    var child 
    // 让所有的dom 都进入 fragment
    while (child = el.firstChild) {
      fragment.appendChild(child)
    }
    return fragment
  }
  compile (el) {
    var childNodes = el.childNodes;
    var self = this;
    let reg = /\{\{(.*)\}\}/

    childNodes.forEach(node => {
      var text = node.textContent;
      if (node.nodeType==1) {
        self.compileElement(node)
      } else if (node.nodeType==3 && reg.test(text)) {
        let name = text.match(reg)[1]
        self.compileText(node, name)
      }
    })
  }
  compileElement (node) {
    let self = this;
    // 这里的方便之处是在 操作真正的属性列表  结构比较像虚拟节点
    var nodeAttrs = node.attributes;
    [].slice.call(nodeAttrs).forEach(attr => {
      // 这里分析指令  不用之前的虚拟dom 是因为这样比较简单, 思量逻辑是一致的
      var attrName = attr.name;
      var value = attr.value;
      // 指令是从 v-开头的
      var dir = attrName.substring(2)
      // 看看是不是指令
      if (attrName.indexOf('v-') == 0) {
        // 这里去判断是哪个指令
        if (dir=='model') {
          console.log('发现是指令' + dir)
          
          // 对指令添加watch
          new Watcher(self.$vue, value, newVal => {
            node.value = newVal
          })
          // 获取该值,赋值
          var val = this.getVueVal(this.$vue, value)
          node.value = val
          // 添加input 事件来触发
          node.addEventListener('input',(e) => {
            let newValue = e.target.value
            console.log(newValue)
            self.setVueVal(self.$vue, value, newValue)
          })
        }else if (dir =='if') {
          console.log('发现是指令' + dir)
        }
      }

    })
  }
  compileText(node, name) {
    node.textContent = this.getVueVal(this.$vue, name)
    new Watcher(this.$vue, name, value => {
      node.textContent = value
    })
  }
  getVueVal(vue, name) {
    console.log(vue,name)
    let val = vue._data
    let keys = name.split('.')
    keys.forEach(key => {
      val = val[key]
    })
    return val
  }
  setVueVal(vue, name, value) {
    
    let val = vue
    let keys = name.split('.')
    keys.forEach((key,index) => {
      if (index==keys.length-1) {
        val[key] = value
      }else {
        val = val[key]
      }
    })
  }
}

引用的响应式代码就不粘贴了,可以参考 juejin.cn/post/699458…