Vue的数据双向

90 阅读2分钟

Vue2

Object.defineProperty

传送门

传入data,遍历所有key。使用Object.defineProperty。get() set()监听读写数据。 通过订阅与发布原理,收集所有订阅者。

当数据发生改变 set()里面调用notify()通知订阅者更改数据和对应的视图

html模板解析Compile()

使用createDocumentFragment()收集页面dom代码片段

判断节点内容(nodeType===3)通过正则匹配插值语法返回文本渲染页面

创建订阅者

判断节点类型监听输入框的事件和v-moel关键词语法糖

创建订阅者

class Vue {
  constructor(obj_instance) {
    this.$data = obj_instance.data;
    Observer(this.$data)//监听data
    Compile(obj_instance.el,this)
  }
}

// 数据劫持
function Observer (data_instance){  
  // 递归出口
  if(!data_instance || typeof data_instance !=='object') return
  const dependency = new Dependency();
  //这里遍历data中的每一个key
  Object.keys(data_instance).forEach(key=>{
    let value = data_instance[key]
    // 递归
    arguments.callee(value)//Observer(value)
    Object.defineProperty(data_instance,key,{
      enumerable:true,//枚举是指对象中的属性是否可以被遍历。
      configurable:true,//该属性是否可以被改变
      // writable: false, //是否课写,默认只读
      get() {//访问时,调用
        // console.log(' 访问',key + '--->' + value )
        Dependency.temp && dependency.addSub(Dependency.temp)
        // if(Dependency.temp){
        //   console.log(Dependency.temp)
        // }
        return value
      },
      set(newValue) {//写属性调用
        // console.log(`属性${key}---${value}---》修改为${newValue}`)
        value =newValue
        Observer(newValue)
        dependency.notify()
      }
    })
  })
}

// html模板解析 dom解析函数
function Compile(element,vm){
  vm.$el =document.querySelector(element)
  //创建一个新的空白的文档片段
  const fragment = document.createDocumentFragment();
  let child
  while (child = vm.$el.firstChild) {
    fragment.append(child)
  }
  fragment_compile(fragment)
  // 处理文本节点
  function fragment_compile(node){
    const pattern = /\{\{\s*(\S+)\s*\}\}/
    if(node.nodeType===3){
      const xoldVlue =node.nodeValue
      //匹配到的放在数组
      const result_regex = pattern.exec(node.nodeValue)
      if(result_regex){
        const arr = result_regex[1].split('.')
        //  处理data中的对象
        const value = arr.reduce((prev,curr)=>prev[curr],vm.$data)
        // 替换文本
        node.nodeValue = xoldVlue.replace(pattern,value)
        // 创建订阅者
        new Watcher(vm,result_regex[1],newValue=>{
          node.nodeValue = xoldVlue.replace(pattern,newValue)
        })
      }
      return //文本节点
    }
    if(node.nodeType ===1 && node.nodeName === 'INPUT'){
      const attrsArr = [...node.attributes]
      attrsArr.forEach(i =>{
        if(i.nodeName==='v-model'){
          // model里面的值
          const value = i.nodeValue.split('.').reduce((prev,curr)=>prev[curr],vm.$data)
          node.value =value
           // 创建订阅者
          new Watcher(vm,i.nodeValue,newValue=>{
            node.value =newValue
          })
          node.addEventListener('input',e=>{
            // 分割后数组 ['','']
            const arr1 = i.nodeValue.split('.')
          console.log(' arr1', arr1)
          // ['']
          const arr2 = arr1.slice(0,arr1.length - 1 )
          console.log(' arr2', arr2)
          const final = arr2.reduce((prev,curr)=>prev[curr],vm.$data)
          console.log(' final', final,arr1[arr1.length-1])
            final[arr1[arr1.length-1]] = e.target.value
            //   const name = i.nodeValue
          // console.log('name',name )
          //   vm.$data[name] = e.target.value
          })
        }
      })
      // attr.includes('v-model')&&console.log('attr ',attr )

    }
    node.childNodes.forEach(child=>arguments.callee(child))
  }
  // 将处理后的文档碎片,重新渲染到页面上
  vm.$el.appendChild(fragment)
}

// 收集所有订阅者
class Dependency{
  // 生成类实例的初始化属性
  constructor(){
    this.subscribe = []
  }
  // 添加订阅者
  addSub(sub){
    this.subscribe.push(sub)
  }
  // 通知订阅者
  notify(){
    this.subscribe.forEach(sub=>sub.update())
  }
}
// 订阅者
class Watcher{
  constructor(vm,key,callback){
    this.vm = vm 
    this.key = key 
    this.callback =callback
    // 设置临时属性-变量 触发 getter
    Dependency.temp = this
    // console.log(`${key}创建订阅者`)
    key.split('.').reduce((prev,curr)=>prev[curr],vm.$data)
    Dependency.temp =null
  }
  // 发布者通知订阅
  update(){
    const value = this.key.split('.').reduce((prev,curr)=>prev[curr],this.vm.$data)
    this.callback(value)
  }
}

Vue3

对比Vue3双向绑定文章

proxy

总结:

Object.defineProperty只能遍历对象属性进行劫持

Proxy直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的

Proxy可以直接监听数组的变化(pushshiftsplice

Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等,这是Object.defineProperty不具备的

正因为Object.defineProperty自身的缺陷,导致Vue2在实现响应式过程需要实现其他的方法辅助(如重写数组方法、增加额外set、delete方法)嵌套太深影响性能,有时会导致响应式丢失的情况。

Proxy 不兼容IE,也没有 polyfilldefineProperty 能支持到IE9

props

父传子可以直接修改的数据类型

1.传入基本数据类型会报错

2.传入引用数据类型可以直接在子组件中修改。

注意父组件定义的引用数据类型传入某个属性也是无法修改的比如传入 arr[0].value 子组件接收到的是基本数据类型也是会报错的

修改前: props-1.png

修改后: props-2.png

如何修改传入的基本数据类型呢?

.sync

通过父组件传入props.sync语法糖 子组件$emit('update:props', 修改值)能直接修改基本数据类型

父组件

    <propsData :data-value="data" :temp.sync="total" :computed-value.sync="data.levelName" :arr-value="options"></propsData>

子组件

<template>
  <div class="info-card">
    <p>子组件</p>
    {{ dataValue }}<br>
    修改对象<el-input v-model=" dataValue.detailsId"></el-input><br>
    修改数组<el-input v-model=" arrValue[0].label"></el-input><br>
    修改data基本类型<el-input v-model="value"></el-input><br>
    修改计算属性基本类型<el-input v-model="ccalue"></el-input><br>

    <el-button type="primary" @click="$emit('update:computedValue', ccalue)">updata:temp</el-button><br>
    temp:{{ temp }}<br>
    value:{{ value }}<br>
  </div>
</template>
<script>
  props: {
    dataValue: {
      type: Object,
      default: () => { }
    },
    temp: {
      type: String,
      default: ''
    },
    computedValue: {
      type: String,
      default: ''
    },
    arrValue: {
      type: Array,
      default: () => []
    }
  },
  data () {
    return {
      value: this.temp,
      ccalue: this.computedValue,
    }
  },

传送门

父组件

<template> <div class="app"> App.vue 我现在有 {{total}} <hr> <Child :money="total" v-on:update:money="total = $event" /> //$event获得$emit参数 </div> </template>

<script> 
import Child from "./Child.vue" 
export default { data() { return {total: 10000} },
components: {
Child: Child} 
}
</script>

Child.vue:

<template>
    <div class="child"> {{money}} <button @click="$emit('update:money', money-100)"> //儿子不能直接修改外部prop的值,使用$emit触发事件并传参 
    <span>花钱</span> </button> 
    </div>
</template> 

<script> 
export default { props: ["momey"] } 
</script>
<Child :money="total" v-on:update:money="total = $event"/>

可以简写成

<Child :money.sync="total">