Vue源码解析(2)-MVVM原理

361 阅读4分钟

MVVM

以下代码我已做了详细的注释以及思维导图图片版,对指令解析以及依赖收集、属性监听、更新视图等的核心部分进行手写,如需思维导图的可联系博主,不喜勿喷(前端小白)。

image.png

image.png

1.首先MVue的作用是利用Compile这个类去解析例如v-text v-model等等这种指令以及差值表达式让数据可以正常的显示在页面上 是利用了updater这个类去初始化视图的并new Watcher以便于后续的视图更新 当new Watcher的过程中会调用getOldVal的方法但是由于做了数据劫持所以这里必定会访问到get()方法 当我们访问到该方法后 就将订阅了观察者 随后就释放以免多个观察者进行观察同一个数据 2.利用Observer中的defineProperty去对数据进行劫持这样可以看到数据是否发生变化,当数据发生变化之后我们需要做以下两点 (1)创建每个数据对应的观察者并将其添加到Dep中(创建观察者添加订阅) (2)然后利用订阅去告诉对应的观察者什么数据发生了变化 (3)观察者收到通知之后去更新视图 即根据是否产生了新值去利用updater这个类更新视图

所以整个线路分为两步 1.编译指令和插值表达式并初始化视图 2.进行数据劫持 两者之间的关联 当初始化视图的时候我们要new Watcher也就是为了后续数据的更新绑定观察者,当数据更新的时候调用回调更新视图。但是由于做了数据劫持所以在new Watcher的时候会访问getOldVal这个函数 也就意味着会调用get方法 那么此时就会形成观察者与订阅者的关联 并且将观察者订阅起来。最后当数据变化的时候 就会调用set()方法然后通知订阅者就会通知观察者去更新视图

一.html代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <h2>{{person.name}} -- {{person.age}}</h2>
      <h3>{{person.fav}}</h3>
      <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
      </ul>
      <h3>{{msg}}</h3>
      <div v-text="x" class='a'></div>
      <div v-text="person.name" class='a'></div>
      <div v-html='str'></div>
      <div v-html="person.fav"></div>
      <input type="text" v-model="msg">
      <button @click='handleClick'>@</button>
      <button v-on:click="handleClick">on</button>
      <a href="x">百度</a>
      <a href="http://www.baidu.com">百度</a>
    </div>
    <script src="./Observer.js"></script>
    <script src="./MVue.js"></script>
    <script>
      let vm = new MVue({
        el:"#app",
        data:{
          str:'123',
          person:{
            name:'zcl',
            age:18,
            fav:'computer'
          },
          msg:"学习mvvm原理",
          x:'http://www.baidu.com'
        },
        methods:{
          handleClick(){
            console.log(this)
          }
        }
      })
    </script>
  </body>

</html>

二.Compile

const compileUtil = {
  // 工具类
  getVal(expr,vm){
    // 专门为了处理person.name这种形式的数据 获取真正的值
    return expr.split('.').reduce((data,currentVal)=>{
      return data[currentVal]
    },vm.$data)
  },
  getContentVal(expr,vm){
    return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
      return this.getVal(args[1],vm)
    })
  },
  text(node,expr,vm){  // expr -> "x" "person.name" {{person.name}}--{{person.age}} {{person.fav}}
    // 这是重点
    let value 
    if(expr.indexOf('{{')!==-1){
      // {{person.name}}--{{person.age}} {{person.fav}}
      value = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
        // replace() 方法的参数 replacement 可以是函数而不是字符串。在这种情况下,每个匹配都调用该函数,它返回的字符串将作为替换文本使用。该函数的第一个参数是匹配模式的字符串。
        // 接下来的参数是与模式中的子表达式匹配的字符串,可以有 0 个或多个这样的参数。
        // args[1] -> person.name
        console.log(args[1])
        new Watcher(vm,args[1],()=>{
          // {{person.name}} -- {{person.age}}  如果是replace这种情况 会创建两个Watcher对该插值表达式进行观察
          // 当你改变person.name的值的时候 只需要将前面{{person.name}}进行更新
          // 如果写成this.updater.textUpdater(node,newVal)会产生什么后果:
          // 首先我们传递进去的是两个expr 分别是person.name  person.age
          // 而我们回调回来的只可能是其中的一个 所以如果我们使用这种方法 当你修改person.name或person.age的时候都会将整个模板进行替换
          // 而我们这个只需要更新修改的值 所以我们在更新的时候继续去匹配{{}}
          this.updater.textUpdater(node,this.getContentVal(expr,vm))
        })
        return this.getVal(args[1],vm)
      })
    }else{
      // 不是插值表达式
      value = this.getVal(expr,vm)
      new Watcher(vm,expr,(newVal)=>{
        this.updater.textUpdater(node,newVal)
      })
    }
    this.updater.textUpdater(node,value)
  },
  html(node,expr,vm){  // expr -> "x" "person.name"
    const value = this.getVal(expr,vm)
    new Watcher(vm,expr,(newVal)=>{
      this.updater.htmlUpdater(node,newVal)
    })
    this.updater.htmlUpdater(node,value)
  },
  on(node,expr,vm,eventName){ // expr -> handleClick  eventName->click
    let fn = vm.$options.methods && vm.$options.methods[expr]
    node.addEventListener(eventName,fn.bind(vm),false)
  },
  setVal(expr,vm,inputVal){
    return expr.split('.').reduce((data,currentVal)=>{ 
        data[currentVal] = inputVal
    },vm.$data)
  },
  model(node,expr,vm){
    const value = this.getVal(expr,vm)
    // 数据驱动视图
    new Watcher(vm,expr,(newVal)=>{
      this.updater.modelUpdater(node,newVal)
    })
    // 视图驱动数据再驱动视图
    node.addEventListener('input',e=>{
      //设置值
      this.setVal(expr,vm,e.target.value)
    })

    this.updater.modelUpdater(node,value)
  },
  updater:{
    htmlUpdater(node,value){
      node.innerHTML = value
    },
    textUpdater(node,value){
      node.textContent = value
    },
    modelUpdater(node,value){
      node.value = value
    }
  }
}

class Compile{
  constructor(el,vm){
    this.el = this.isElementNode(el) ? el : document.querySelector(el)
    this.vm = vm 
    // 创建文档碎片对象 减少页面的回流和重绘
    const fragment = this.createFragment(this.el)
    // 编译模板
    this.compile(fragment)
    this.el.appendChild(fragment)
  }
  isElementNode(node){
    return node.nodeType === 1
  }
  createFragment(el){
    const f = document.createDocumentFragment()
    let firstChild
    while(firstChild=el.firstChild){
      f.appendChild(firstChild)  // appendChild的时候会将el种的元素添加到文档碎片对象中并移除!
    }
    return f
  }
  isDirective(attrName){
    return attrName.startsWith('v-')
  }
  isEventName(attrName){
    return attrName.startsWith('@')
  }
  compileElement(node){
    // <div v-text="x" class='a'></div>  里面含有v-text指令
    // <div v-html="person.fav"></div>  里面含有v-html指令
    //  <input type="text" v-model="msg"> 里面含有v-model指令
    // <button @click='handleClick'>@</button>  里面含有点击事件 又细分为@click与v-on:click
    // <button v-on:click="handleClick">on</button>
    const attributes = node.attributes;
    [...attributes].forEach(attr=>{
      const {name,value} = attr
      // name-> v-text v-html type v-model @click v-on:click
      if(this.isDirective(name)){
        // 判断是否是指令 对于普通属性不需要处理如id class style等因为这些会在模板编译的时候进行处理
        const [,dirctive] = name.split('-')
        // dirctive -> text html model on:click 注意这里@click并不在里面
        const [dirName,eventName] = dirctive.split(':')
        // dirName -> text html model on 
        // eventName -> click
        compileUtil[dirName](node,value,this.vm,eventName)
        node.removeAttribute('v-'+ dirctive)
      }else if(this.isEventName(name)){
          // 处理@click这种指令
          let [,eventName] = name.split('@')
          compileUtil['on'](node,value,this.vm,eventName)
      }
    })
  }
  compileText(node){
    // 这里主要是处理{{}}
    const content = node.textContent
    // content-> {{person.name}}---{{person.age}} {{person.fav}}
    if(/\{\{(.+?)\}\}/.test(content)){
      compileUtil['text'](node,content,this.vm)
    }
  }
  compile(fragment){
    const childNodes = fragment.childNodes;  // childNodes用来获取所有的子节点包括元素和文本节点
    [...childNodes].forEach(child=>{  // childNodes是类数组 需要将其转化为数组形式
      if(this.isElementNode(child)){
        // 解析元素节点
        this.compileElement(child)
      }else{
        // 解析文本节点
        this.compileText(child)
      }
      if(child.childNodes && child.childNodes.length){
        // 递归 将所有子节点暴露出来 类似<ul><li></li></ul> <div>{{name}}</div>
        this.compile(child)
      }
    })
  }
}


class MVue{
  constructor(options){
    this.$options = options
    this.$el = options.el
    this.$data = options.data
    if(this.$el){
      //1.实现数据观察
      new Observer(this.$data) 
      //2.指令解析
      new Compile(this.$el,this)
    }
  }
}

三.Observer

class Watcher{
  constructor(vm,expr,cb){
    this.vm = vm
    this.expr = expr
    this.cb = cb
    this.oldVal = this.getOldVal()
  }
  getOldVal(){
    Dep.target = this  // 让Dep与Watcher形成联系
    const oldVal = compileUtil.getVal(this.expr,this.vm)
    // 其实这里getVal 取值的方式是vm.$data.x 类似这种形式 所以这里会触发get方法
    // 我们在触发get方法的时候需要进行依赖的收集
    // 也就是说我们在这里需要对每个数据添加观察者 然后收集这些观察者
    Dep.target = null  // 收集完后就销毁 这时候subs里面已经有对应的观察者了
    return oldVal
  }
  update(){
    // 当设置新值的时候 就会触发set()方法 在set方法处 让Dep通知观察者更新
    const newValue = compileUtil.getVal(this.expr,this.vm)
    if(newValue!=this.oldVal){
      // 通过回调调用updater方法 更新数据再次更新视图
      this.cb(newValue)
    }
  }
}


class Dep{
  constructor(){
    this.subs = []  // 存储依赖 即观察者
  }
  addSub(watcher){
    // 依赖收集 即收集观察者
    this.subs.push(watcher)
  }
  notify(){
    // 当设置新值的时候 就是我们通知的时候
    // 通知对应的观察者进行视图更新
    this.subs.forEach(w=>w.update())
  }
}


class Observer{
  constructor(data){
    this.observe(data)
  }
  observe(data){
    if(data && typeof data === 'object'){
      Object.keys(data).forEach(key=>{
        this.defineReactive(data,key,data[key])
      })
    }
  }
  defineReactive(obj,key,value){
    this.observe(value)
    const dep = new Dep()
    Object.defineProperty(obj,key,{
      // 使用defineProperty进行劫持
      enumerable:true,
      configurable:false, // 不可删除
      get(){
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      set:(newValue)=>{ // 如果使用function(){}这里内部的this指向Object 但是我们希望内部指向为Observer所以用箭头函数忽略内部指向
        this.observe(newValue)
        if(newValue!=value){
          value = newValue
        }
        // 通知变化
        dep.notify()
      }
    })
  }
}