vue双向数据绑定原理简单实现

vue双向数据绑定原理实现

准备工作

​ 新建一个index.js文件, 一个index.html文件

​ index.js文件中, 定义Vue类, 并将Vue并称全局变量 window.Vue = Vue

​ index.html中引入index.js

index.js

class Vue({})

window.Vue = Vue

index.html

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

​ 然后就可以在index.html中 new Vue() 了

初始化data

​ 在index.html中,先 new 一个 Vue实例

<div id="app">
        <input type="text" v-model="inputValue">
        <div>{{inputValue}}</div>
        <input type="text" v-model="obj.input">
        <div>{{obj.input}}</div>
</div>
<script src="./tt.js"></script>
<script>
    const vm = new Vue({
        el: '#app',
        data() {
            return {
                inputValue: '12345',
                obj: {
                    input: '输入狂里面的内容是:'
                },
                message: '通常'
            }
        }
    })
</script>

​ 在index.js中, 初始化数据

​ 获取用户选项,并初始化数据

class Vue {
    constructor(options) {
        this.$options = options
        const vm = this
        if(this.$options.data) {
            this.initData(vm)
        }
        if(this.$options.el) {
          compile(this.$options.el, vm)
        }
    }
    initData(vm) {
        let data = vm.$options.data
        data = typeof data === 'function' ? data.call(vm) : data
        vm._data = data
        observe(data)
        // 这个是为了实现 vm.name可以直接访问的代理方法
        for(let key in data) {
          proxy(vm, key, data[key])
        }
    }
}

​ 给数据添加setter和getter

function observe(data) {
    // 不是对象, 直接返回
    if(data === null || typeof data !== 'object') {
        return
    }
    return new Observer(data)
}

// 这里值考虑了对象和对象嵌套, 没有考虑数组的情况
class Observer {
    constructor(data) {
        this.walk(data)
    }
    walk(data) {
        // 遍历对象的每一项, 添加响应式方法
        Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
    }
}

function defineReactive(target, key, value) {
   // 如果值是对象的话, 也需要重复添加
  observe(value)
  Object.defineProperty(target, key, {
    get() {
      return value
    },
    set(newValue) {
      value = newValue
        // 对设置的新值也需要observe
      observe(newValue)
    }
  })
}

​ 访问代理的方法

function proxy(target, key, value) {
  Object.defineProperty(target, key, {
    get() {
      return target['_data'][key]
    },
    set(newValue) {
      target['_data'][key] = newValue
    }
  })
}

依赖收集

​ 添加一个Dep类, 用户收集属性的watcher

class Dep {
  constructor() {
    // 里面装的是属性收集的watcher
    this.subs = []
  }
    // 添加watcher的方法
  addSub(watcher) {
    this.subs.push(watcher)
  }
  // 更新的时候,找到属性里面的所有watcher, 触发watcher的update方法实现更新
  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
}
// 添加一个静态属性. 相当于全局变量, 指向当前的watcher, 初始值是null
Dep.target = null

​ 添加观察者watcher, 在取值的时候让Dep.target指向当前的watcher, 取值结束之后,让Dep.target为null, 这样就可以通过属性的get方法里面将当前的watcher添加到属性里面的dep中, dep也需要在定义响应式的时候添加

// 订阅者
class Watcher {
  constructor(vm, key, callback) {
      // vm: 实例, key: 需要更新的属性. callback: 更新时执行的回调
    this.vm = vm
    this.key = key
    this.callback = callback
    // 让Dep.target属性指向当前watcher
    Dep.target = this
    // 通过获取操作, 触发属性里面的get方法
    key.split('.').reduce((total, current) => total[current], vm._data)
    Dep.target = null
  }
  update() {
    const value = this.key.split('.').reduce((total, current) => total[current], this.vm._data)
    this.callback(value)
  }
}

​ 给属性收集依赖

function defineReactive(target, key, value) {
  observe(value)
  // 给属性添加dep实例, 因为是闭包,这个空间不会被销毁
  let dep = new Dep()
  Object.defineProperty(target, key, {
    get() {
        // Dep.target指向当前watcher, 如果当前watcher有值, 当前属性收集这个watcher
      Dep.target && dep.addSub(Dep.target)
      return value
    },
    set(newValue) {
      value = newValue
      observe(newValue)
      // 赋值时,触发该方法更新视图
      dep.notify()
    }
  })
}

解析模板,更新模板

​ 添加解析模板方法

if(this.$options.el) {
    compile(this.$options.el, vm)
}

​ 使用documentFragment创建模板, 注意fragment.append 时会移除页面上的元素, while循环结束后, 页面就没了,

​ 然后对模板里面的每一项进行解析, 先实例 node.nodeType === 3 的元素, 表示文本节点, 看文本节点里面有没有匹配到{{name}}模板表达式的, 如果有, 如vm._data里面去除对应的值, 替换文本的值, 最后vm.$el.appendChild(fragment)就可以将替换后的结果显示在页面上

​ 对nodeType === 1 的元素, 即标签解析, 这里我们处理的是input, 获取节点的所有属性, 一个伪数组, 变成真数组, 里面有个nodeName = v-model 和 nodeValue = name 的, 同样获取vm._data里面name的值, 然后让节点的 node.value = 这个值, 就能显示在输入框里面了, 这就是数据改变视图.接下来是视图改变数据, 添加input方法, 为node 添加 addEventListener方法, input, 然后让vm._data里面对应属性的值等于e.target.value, 这样就实现了视图改变数据

​ 重点: 上面的两种情况, nodeType == 3 的时候更新方法是 node.nodeValue = newValue, nodeType == 1 的时候更新方法是 node.value = value, 需要将这两个方法封装到 watcher中, 在更新之后 new 一个 Watcher, 并将对应的参数传入, 后面在获取值的时候就会自动收集依赖, set值的时候就会触发更新, ojbk

function compile(el, vm) {
   vm.$el = el = document.querySelector(el)
  
  const fragment = document.createDocumentFragment()
  let child
  while(child = el.firstChild) {
    fragment.append(child)
  }

  fragment_compile(fragment)

  function fragment_compile(node) {
    const parttern = /\{\{\s*(\S+)\s*\}\}/
    // 文本节点
    if(node.nodeType === 3) {
      // 匹配{{}}, 第一项为匹配的内容, 第二项为匹配的变量名称
      const match = parttern.exec(node.nodeValue)
      if(match) {
        const needChangeValue = node.nodeValue
        // 获取到匹配的内容, 可能是 msg,   也可能是 mmm.msg,
        // 注意通过 vm[mmm.msg]是拿不到数据的, 要 vm[mmm][msg]
        // 获取真实的值, 替换掉模板里面的 {{name}}, 真实的值从vm.$options.data里面取
        let arr = match[1].split('.')
        let value = arr.reduce(
          (total, current) => total[current], vm._data
        )
        // 将真实的值替换掉模板字符串, 这个就是更新模板的方法, 将这个方法封装到watcher里面
        node.nodeValue = needChangeValue.replace(parttern, value)
        const updateFn = value => {
          node.nodeValue = needChangeValue.replace(parttern, value)
        }
        // 有个问题, node.nodeValue在执行过一次之后, 值就变了, 不是 {{name}}, 而是 12345, 要救{{name}}里面的name暂存起来
        new Watcher(vm, match[1], updateFn)
      }
      return 

    }
    // 元素节点
    if(node.nodeType === 1 && node.nodeName === 'INPUT') {
      // 伪数组
      const attrs = node.attributes
      let attr = Array.prototype.slice.call(attrs)
      // 里面有个nodeName -< v-model,  有个nodeValue 对应 name
      attr.forEach(item => {  
        if(item.nodeName === 'v-model') {
          let value = getVmValue(item.nodeValue, vm)
          // input标签是修改node.value 
          node.value = value
          // 也需要添加watcher
          new Watcher(vm, item.nodeValue, newValue => node.value = newValue)
          // 添加input事件
          node.addEventListener('input', e => {
            const name = item.nodeValue
            // 给vm上的属性赋值
            // 不能直接 vm._data[name] = e.target.value , 因为那么可能是 a.b
            // 也不能直接获取b的值, 然后赋新值, 因为这个值是一个值类型, 需要先获取前面的引用类型
            // 如: let tem = vm._data.a   然后 tem[b] = 新值,  这样就可以
            const arr1 = name.split('.')
            const arr2 = arr1.slice(0, arr1.length - 1)
            const head = arr2.reduce((total, current) => total[current], vm._data)
            head[arr1[arr1.length - 1]] = e.target.value
          })
        }
      })
    }
    node.childNodes.forEach(child => fragment_compile(child))
  }

  vm.$el.appendChild(fragment)
}

完整代码

​ index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>实现简单的双向数据绑定</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="inputValue">
        <div>{{inputValue}}</div>
        <input type="text" v-model="obj.input">
        <div>{{obj.input}}</div>
    </div>
    <script src="./index.js"></script>
    <script>
    const vm = new Vue({
      el: '#app',
      data() {
        return {
          inputValue: '12345',
          obj: {
            input: '输入狂里面的内容是:'
          },
          message: '通常'
        }
      }
    })
   
    </script>
</body>
</html>

​ index.js


class Vue {
    constructor(options) {
        this.$options = options
        const vm = this
        if(this.$options.data) {
            this.initData(vm)
        }
        if(this.$options.el) {
          compile(this.$options.el, vm)
        }
    }
    initData(vm) {
        let data = vm.$options.data
        data = typeof data === 'function' ? data.call(vm) : data
        vm._data = data
        observe(data)
        for(let key in data) {
          proxy(vm, key, data[key])
        }
    }
}

function proxy(target, key, value) {
  Object.defineProperty(target, key, {
    get() {
      return target['_data'][key]
    },
    set(newValue) {
      target['_data'][key] = newValue
    }
  })
}

function observe(data) {
    if(data === null || typeof data !== 'object') {
        return
    }
    return new Observer(data)
}

class Observer {
    constructor(data) {
        this.walk(data)
    }
    walk(data) {
        Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
    }
}

function defineReactive(target, key, value) {
  observe(value)

  let dep = new Dep()
  Object.defineProperty(target, key, {
    get() {
      Dep.target && dep.addSub(Dep.target)
      return value
    },
    set(newValue) {
      value = newValue
      observe(newValue)
      // debugger
      dep.notify()
    }
  })
}




function compile(el, vm) {
   vm.$el = el = document.querySelector(el)
  
  const fragment = document.createDocumentFragment()
  let child
  while(child = el.firstChild) {
    fragment.append(child)
  }

  fragment_compile(fragment)

  function fragment_compile(node) {
    const parttern = /\{\{\s*(\S+)\s*\}\}/
    // 文本节点
    if(node.nodeType === 3) {
      // 匹配{{}}, 第一项为匹配的内容, 第二项为匹配的变量名称
      const match = parttern.exec(node.nodeValue)
      if(match) {
        const needChangeValue = node.nodeValue
        // 获取到匹配的内容, 可能是 msg,   也可能是 mmm.msg,
        // 注意通过 vm[mmm.msg]是拿不到数据的, 要 vm[mmm][msg]
        // 获取真实的值, 替换掉模板里面的 {{name}}, 真实的值从vm.$options.data里面取
        let arr = match[1].split('.')
        let value = arr.reduce(
          (total, current) => total[current], vm._data
        )
        // 将真实的值替换掉模板字符串, 这个就是更新模板的方法, 将这个方法封装到watcher里面
        node.nodeValue = needChangeValue.replace(parttern, value)
        const updateFn = value => {
          node.nodeValue = needChangeValue.replace(parttern, value)
        }
        // 有个问题, node.nodeValue在执行过一次之后, 值就变了, 不是 {{name}}, 而是 12345, 要救{{name}}里面的name暂存起来
        new Watcher(vm, match[1], updateFn)
      }
      return 

    }
    // 元素节点
    if(node.nodeType === 1 && node.nodeName === 'INPUT') {
      // 伪数组
      const attrs = node.attributes
      let attr = Array.prototype.slice.call(attrs)
      // 里面有个nodeName -< v-model,  有个nodeValue 对应 name
      attr.forEach(item => {  
        if(item.nodeName === 'v-model') {
          let value = getVmValue(item.nodeValue, vm)
          // input标签是修改node.value 
          node.value = value
          // 也需要添加watcher
          new Watcher(vm, item.nodeValue, newValue => node.value = newValue)
          // 添加input事件
          node.addEventListener('input', e => {
            const name = item.nodeValue
            // 给vm上的属性赋值
            // 不能直接 vm._data[name] = e.target.value , 因为那么可能是 a.b
            // 也不能直接获取b的值, 然后赋新值, 因为这个值是一个值类型, 需要先获取前面的引用类型
            // 如: let tem = vm._data.a   然后 tem[b] = 新值,  这样就可以
            const arr1 = name.split('.')
            const arr2 = arr1.slice(0, arr1.length - 1)
            const head = arr2.reduce((total, current) => total[current], vm._data)
            head[arr1[arr1.length - 1]] = e.target.value
          })
        }
      })
    }
    node.childNodes.forEach(child => fragment_compile(child))
  }

  vm.$el.appendChild(fragment)
}

// 依赖收集
class Dep {
  constructor() {
    // 里面装的是属性收集的watcher
    this.subs = []
  }
  addSub(watcher) {
    this.subs.push(watcher)
  }
  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
}

// 订阅者
class Watcher {
  constructor(vm, key, callback) {
    this.vm = vm
    this.key = key
    this.callback = callback
    // 让Dep.target属性指向当前watcher
    Dep.target = this
    // 通过获取操作, 触发属性里面的get方法
    key.split('.').reduce((total, current) => total[current], vm._data)
    Dep.target = null
  }
  update() {
    const value = this.key.split('.').reduce((total, current) => total[current], this.vm._data)
    this.callback(value)
  }
}

function getVmValue(key, vm) {
  return key.split('.').reduce((total, current) => total[current], vm._data)
}

function setVmValue(key, vm) {
  let tem = key.split('.')
  let fin = tem.reduce((total, current) => total[current], vm._data)
  return fin
}

window.Vue = Vue
posted @   littlelittleship  阅读(163)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
点击右上角即可分享
微信分享提示