实现一个最小版本的vue —— 原理分析及Vue、Observer实现(二)

234 阅读2分钟

一、原理分析

  1. Vue:初始化参数,并把data中的成员转换成getter/setter并注入到Vue实例中(可以在vue组件中使用this.xxx访问或者修改属性)vue中会调用Observer、Compiler。
  2. Observer:数据劫持,能够对data对象中的所有属性进行监听,如有变动可拿到最新值并通知Dep
  3. Compiler:解析每个元素中的指令/插值表达式,并替换成相应的数据
  4. Dep:发布者。添加观察者(watcher),当数据变化通知所有观察者
  5. watcher:观察者。有一个update方法复杂更新视图

以_(下划线)开始的是私有成员

二、html模板

 <div id="app">
    <h1>差值表达式</h1>
    <h3>{{ msg }}</h3>
    <h3>{{ count }}</h3>
    <h1>v-text</h1>
    <div v-text="msg"></div>
    <h1>v-model</h1>
    <input type="text" v-model="msg">
    <input type="text" v-model="count">
  </div>
  <!-- <script src="./js/dep.js"></script>
  <script src="./js/watcher.js"></script>
  <script src="./js/compiler.js"></script>
  <script src="./js/observer.js"></script> -->
  <script src="./js/vue.js"></script>
  <script>
    let vm = new Vue({
      el'#app',
      data: {
        msg'Hello Vue',
        count100,
        person: { name'zs' }
      }
    })
    console.log(vm.msg)
    // vm.msg = { test: 'Hello' }
    vm.test = 'abc'
  </script>

三、Vue实现

功能

  1. 负责接收初始化的参数(选项)optionsoptions,data,elel,data中的set是真正监视数据变化的地方。
  2. 调用_proxyData方法把 data 中的属性转换成 getter/setter并注入到 Vue 实例,好处:在组件中可以直接使用this.xxx访问data中的属性
  3. 负责调用 Observer 监听 data 中所有属性的变化
  4. 负责调用 Compiler 解析指令/插值表达式

代码

class Vue {
  constructor(options){
    // 1. 通属性保存选项的属性
    this.$options = options || {}
    this.$data = options.data || {}
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
    
    // 2. 把data中的成员转换成getter和setter并注入到vue实例中
    this._proxyData(this.$data)

    // 3. 调用Observer对象,监听数据变化
    new Observer(this.$data)
    // 4. 调用Compiler对象,解析指令和差值表达式
    new Compile(this)
  }
  _proxyData(data){
    // 遍历data中的所有属性 转化成getter setter,注入到Vue实例上
    Object.keys(data).forEach(key=>{
      Object.defineProperty(this,key,{ // 此处是this, 因为要往this上绑定属性
        enumerabletrue,
        configurabletrue,
        get(){
          return data[key]
        },
        set(newValue){
          if(newValue == data[key]){
            return
          }
          data[key] = newValue;
        }
      })
    })
  }
}

四、Observer

功能

  1. 负责把 $data 选项中的属性转换成响应式数据(defineReactive)
  2. data中的某个属性也是对象,把该对象中的属性也转换成响应式的数据(walk)
  3. 数据变化发送通知

代码

class Observer {
  constructor(data){
    this.walk(data);
  }
  // 如果是对象,遍历对象的所有属性
  walk(data){
    // 1. 判断data是否是对象
    if(!data || typeof data != 'object'){
      return
    }
    // 2. 遍历data对象的所有属性
    Object.keys(data).forEach(key=>{
      this.defineReactive(data,key,data[key])
    })
  }
  
  // 调用Object.defineProperty把对象转换成get/set
  defineReactive(obj,key,val){
    // 注意2:如果值是一个对象,把对象的属性也变成响应式
    this.walk(val)
    const _this = this;
    Object.defineProperty(obj,key,{ // 真正监听$data中的数据的变化
      enumerabletrue,
      configurabletrue,
      get(){ // 注意1:
        // return obj[key]
        // 收集依赖
        return val
      },
      set(newValue){
        if(newValue == val){
          return
        }
        val = newValue;
        _this.walk(newValue) // 注意3:对于新赋值的值(设置为对象)设置响应式
        // 发送通知
      }
    })
  }
}

区分两处的Object.defineProperty

Vue类中的方法_proxyData的的Object.defineProperty是为了给data中的属性添加getter和setter并绑定到this,当调用this.xxx时触发。

Observe类中的方法defineReactive的Object.defineProperty是给$data的属性添加getter和setter,当调用data.xxx时触发(即当vue类中的_proxyData getter中的调用的时候触发)。

注意1:避免死循环

此处要使用传入的val,不能使用obj[key],每次使用obj[key],相当于每次都获取key,每次都会触发当前Object中的get形成死递归循环。此处val使用了闭包。

注意2:复杂类型需要转换为响应式的

  • 此时只有person对象本身是响应式的,person中的属性不是,需要递归变成响应式的。

  • 此时的person是响应式,person中的属性也变成了响应式

注意3:属性重新赋值为对象后仍需要为响应式的

  • 对于修改旧值msg为一个对象{name: 'lyy'},此时只有msg本身是响应式的,msg中的属性不是响应式的。
  • 此时修改后的值为对象,对象中的属性也变成了响应式