【vue】Vue数据双向绑定原理及实现(持续更新。。。)

原理

使用Object.defineProperty() 进行数据劫持。主要为修改对象中数值的getter和setter函数

let person = {}
let name = "张二狗"
Object.defineProperty(person,"name",{
	set:function(newValue){
        //在此增加该数据被设置的时候的操作
        //例如:
        console.log("set")
        
        name = newValue
	},
    get:function(){
        //在此增加该数据被设置的时候的操作
        //例如:
        console.log("get")
        
        return name
    }
}) 
person.name = "张三"			//设置数值,触发set函数
console.log(person.name)	 //读取数值,触发get函数

最简单的双向绑定

/*
	以下代码功能仅有数据的双向绑定,没有其他功能。
*/
<!DOCTYPE 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>
    <input type="text" />
    <p></p>
  </body>
</html>
<script>
  let data = {}
  let content = ''

  let input = document.querySelector('input')
  let p = document.querySelector('p')
  
  //设置get和setter
  Object.defineProperty(data, 'content', {
    set: function (newValue) {
      //数据更新时,更新页面  
      input.value = newValue
      p.innerText = newValue
        
      content = newValue
    },
    get: function () {
      return content
    },
  })
	//页面更新时,更新数据
  input.addEventListener('input', (e) => {
    data.content = e.target.value
  })
    
</script>

问题

defineProperty get 死循环

let person = {name:"张二狗"}
Object.defineProperty(person,"name",{
	set:function(newValue){
        person.name = newValue
	},
    get:function(){
        return person.name
    }
}) 
//原因,调用get,返回person.name时也会调用get,死循环。
//解决方案1:使用临时变量,setter和getter里设置和返回时操作临时变量。但是数据量大之后,不好操作。
//解决方法2:Object.defineProperty()包裹一层函数,利用js函数传值的性质,自动生成临时变量
function defineProperty(obj,key,val){
    /*
    	因为js是按值传递
    	相当于自动创建临时变量
    	let key = key
    	let val = val
    	set中操作临时变量不会造成死循环
   	*/
    Object.defineProperty(obj,key,{
	set:function(newValue){
        val= newValue
	},
    get:function(){
        return val
    }
}) 
}

Vue双向绑定实现(简化)

基础功能:数据和视图的双向更新,页面渲染

额外功能:v-model,v-on:click,文本插值(双大括号)识别

不足:代码乱七八糟的。查阅资料之后发现可以将部分代码提炼成对象。

​ 仅支持含有单个文本差值的文字节点(像是“{{name}}{{name}}"这种就不支持)

0.1

<!DOCTYPE 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">
      <input v-model="name" type="text" />
      <p>{{name}}</p>
      <button v-on:click="clickEvent">点击</button>
    </div>
  </body>
</html>
<script>
  /*
      实现:v-model,v-on,插值渲染(仅支持只有单个插值的文本节点)

      逻辑:创建vue实例=》数据劫持=》dom树渲染,创建对应数据的watcher,添加到watcher数组
          =》页面更新=》元素绑定的时间触发对应数据的getter=》getter调用对应数据的更新函数,更新页面

  */

  function Watch(vm, value, callback) {
    //vue实例
    this.vm = vm
    //该watcher监听的数据
    this.value = value
    this.update = callback
    this.init()
  }
  Watch.prototype = {
    //watcher创建时,主动去触发对应属性值的getter,并加入vue实例中的watchers
    init: function () {
      this.vm.data[this.value]
      this.vm.watchers[this.value].push(this)
    },
  }

  function VueDemo(options) {
    this.el = document.querySelector(options.el)
    this.data = options.data
    this.methods = options.methods
    this.fragment = null
    this.watchers = []
    this.init()
  }
  VueDemo.prototype = {
    init: function () {
      if (!this.el) {
        console.log('根元素未找到')
        return
      }
      this.iterateData(this.data)
      //创建fragment
      this.fragment = this.createFragment(this.el)
      //渲染
      this.compile(this.fragment)
      this.el.appendChild(this.fragment)
    },
    //遍历data
    iterateData: function (data) {
      Object.keys(data).forEach((key) => {
        this.watchers[key] = []
        this.defineReactive(data, key, data[key])
      })
    },
    //劫持数据
    defineReactive: function (data, key, val) {
      let self = this
      Object.defineProperty(data, key, {
        get: function () {
          // console.log(key + ':get')
          return val
        },
        set: function (newValue) {
          // console.log(key + ':set')
          self.watchers[key].forEach((watcher) => {
            watcher.update(newValue)
          })
          val = newValue
        },
      })
    },
    //创建fragment
    createFragment: function (el) {
      let fragment = document.createDocumentFragment()
      while (el.firstChild) {
        //appedchild是移动而不是复制,才知道
        fragment.appendChild(el.firstChild)
      }
      return fragment
    },
    //解析dom树
    compile(el) {
      /*
        遍历节点
            判断节点类型
                元素类型
                    判断是否绑定了事件或者指令
                文本类型
                    判断是否含有文本差值
        */
      Array.prototype.slice.call(el.childNodes).forEach((node) => {
        let info = this.checkout(node)
        //如果是元素类型,判断是否有事件或者指令
        if (info.isElementNode && info.isDirective) {
          //事件指令
          if (info.isEventDirective) {
            info.events.forEach((event) => {
              this.compileEvent(node, event.name, event.value)
            })
            //v-model指令
          } else if (info.isModelDirective) {
            this.compileModel(node, info.modelName)
          }
          //如果是文本,判断是否有插值
        } else if (info.isTextNode && info.hasInterpolation) {
          //渲染插值
          this.compileText(node, info.interpolation.name)
        }
        if (info.hasChildNodes) {
          this.compile(node)
        }
      })
    },
    //渲染绑定事件的节点
    compileEvent: function (node, eventName, eventValue) {
      node.addEventListener(eventName, this.methods[eventValue])
    },
    //渲染带有v-model的节点
    compileModel: function (node, modelValue) {
      //绑定v-mdel
      let self = this
      new Watch(self, modelValue, function (value) {
        // console.log("model");
        self.updateModel(node, value)
      })
      node.addEventListener('input', (e) => {
        if (e.target.value == this.data[modelValue]) return
        console.log(e.target.value)
        this.data[modelValue] = e.target.value
      })
      this.updateModel(node, this.data[modelValue])
    },
    //带有v-model的节点的更新方法
    updateModel: function (node, value) {
      node.value = value
    },
    //渲染文本节点
    compileText: function (node, interpolation) {
      //正则不会,先单个替换
      this.updateText(node, this.data[interpolation])
      let self = this
      new Watch(self, interpolation, function (value) {
        // console.log("text");
        self.updateText(node, value)
      })
    },
    //文本节点更新方法
    updateText: function (node, value) {
      node.nodeValue = value
    },
    //处理dom节点的信息,返回信息数组
    checkout: function (node) {
      let info = {
        //节点类型
        isElementNode: false,
        isTextNode: false,
        //是否有指令
        isDirective: false,
        //指令数组
        directives: [],

        //是否有事件指令
        isEventDirective: false,
        //事件数组
        events: [],
        //是否有v-model指令
        isModelDirective: false,
        modelName: '',
        //是否有差值
        hasInterpolation: false,
        interpolation: { name: '' },
        //是否有子节点
        hasChildNodes: false,
      }
      if (node.nodeType == 1) {
        info.isElementNode = true
        Array.prototype.slice.call(node.attributes).forEach((attr) => {
          if (attr.name.indexOf('v-') != -1) {
            info.isDirective = true
            info.directives.push({ name: attr.name, value: attr.value })
          }
          if (attr.name.indexOf('v-on') != -1) {
            info.isEventDirective = true
            info.events.push({
              name: attr.name.split(':')[1],
              value: attr.value,
            })
          }
          if (attr.name.indexOf('v-model') != -1) {
            info.isModelDirective = true
            info.modelName = attr.value
          }
        })
      } else if (node.nodeType == 3) {
        info.isTextNode = true
        var reg = /\{\{(.*)\}\}/
        if (reg.test(node.nodeValue)) {
          info.hasInterpolation = true
          info.interpolation.name = reg.exec(node.nodeValue)[1]
        }
      }
      if (node.childNodes.length != 0) {
        info.hasChildNodes = true
      }
      return info
    },
  }

  let demo = new VueDemo({
    el: '#app',
    data: {
      name: '张二狗',
    },
    methods: {
      clickEvent: function () {
        alert('click')
      },
    },
  })
</script>

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值