vue2.x的实现原理

116 阅读4分钟

Vue的实现原理

三种双向数据绑定的方式

发布-订阅者模式(backbone.js)

  • 一般通过pub、sub的方式来实现数据和视图的绑定,但是使用起来比较麻烦

  • 发布-订阅者模式,也叫观察者模式

  • 它定义了一种一对多的依赖关系,即当一个对象的状态发生改变的时候,所有依赖于它的对象都会得到通知并自动更新,解决了主体对象与观察者之间功能的耦合

  • 例子:微信公众号

    • 订阅者:只需要订阅微信公众号
    • 发布者(公众号):发布新文章的时候,推送给所有订阅者
  • 优点:

    • 解耦合
    • 订阅者不用每次去查看公众号是否有新的文章
    • 发布者不用关心谁订阅了它,只要给所有订阅者推送即可

脏值检查(angular.js)

angular.js是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,类似于通过定时器轮询检测数据是否发生了改变。

数据劫持

vue.js则是采用数据劫持结合发布者-订阅者模式的方式。通过Object.defineProperty()来劫持各个属性的setter、getter,在数据变动时发布消息给订阅者,触发相应的监听回掉。

vuejs不兼容IE8以下版本

vue的实现思路

  1. 实现一个Compile模板解析器,能够对模板中的指令和插值表达式进行解析,并赋予不同的操作

    let fragment = document.createDocumentFragment();
    
    //创建了一个存在于内存中的虚拟DOM树
    
  2. 实现一个Observer数据监听器,能够对数据对象的所有属性进行监听

    Object.defineProperty()
    
    <div id="app">
        <p>您好,<sapn id="name"></sapn></p>
    </div>
    <script>
    	var obj={}
        Object.defineProperty(obj,"name",{
            get(){
                return document.querySelector("#name").innerHTML;
            },
            set(nick){
                document.querySelector("#name").innerHTML = nick
            }
        })
        obj.name = "jerry";
    </script>
    
  3. 实现一个Watcher观察者,将Compile的解析结果,与Observer所观察的对象连接起来,建立关系,在Observer观察到对象数据变化时,接收通知,同时更新DOM

  4. 创建一个公共的入口对象,接收初始化的配置并且协调上面三个模块,也就是vue

vue的核心实现源码

index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport"
    content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>mini-vue</title>
</head>

<body>
  <div id="app">
    <p>{{msg}}</p>
    <!-- 你好 -->
    <p v-text="msg"></p>
    <p v-html="msg"></p>
    <p>{{car.brand}}</p>
    <p v-text="car.color"></p>
    <input type="text" v-model="msg">
    <button v-on:click="clickFn">按钮</button>
  </div>
  <script src="./src/watcher.js"></script>
  <script src="./src/observe.js"></script>
  <script src="./src/compile.js"></script>
  <script src="./src/vue.js"></script>
  <script>
    let app = document.querySelector('#app')
    const vm = new Vue({
      //el:'#app',
      el: app,
      data:{
        msg:'hello vue',
        car:{
          brand:'大众',
          color:'blue'
        }
      },
      methods: {
        clickFn(){
          console.log(this.$data.msg);
        }
      }
    })
  </script>
</body>

</html>

vue.js

//vue实例类
class Vue {
  constructor(options = {}) {
    //绑定属性
    this.$el = options.el
    this.$data = options.data
    this.$methods = options.methods

    //监视data中的数据
    new Observer(this.$data)

    //把data中所有的数据代理到vm上
    this.proxy(this.$data)
    //把method中的数据代理到vm上
    this.proxy(this.$methods)


    if (this.$el) {
      //把app模板和Vue实例传给Compile进行解析
      new Compile(this.$el,this)
    }
  }


  proxy(data){
    Object.keys(data).forEach(key => {
      Object.defineProperty(this,key,{
        enumerable:true,
        configurable:true,
        get(){
          return data[key]
        },
        set(newVal){
          if (data[key] === newVal) {
            return;
          }
          data[key] = newVal
        }
      })
    })
  }


}

compile.js

/**
 * compile对html进行解析
 */
class Compile {
  /**
   * 构造函数
   * @param {app模板} el 
   * @param {vue实例} vm 
   */
  constructor(el, vm) {
    this.el = typeof el === "string" ? document.querySelector(el) : el
    this.vm = vm
    //编译模板
    if (this.el) {
      //第一步:把传进来的真是DOM树放到虚拟DOM树中并返回树节点
      // 1.在el中所有的子节点都放入到内存中,用虚拟DOM   franment
      let franment = this.node2franment(this.el)
      //第二步:把虚拟DOM树节点传给编译函数,进行文本节点,元素节点解析
      // 2.在内存中编译fragment
      this.compile(franment)
      // 3.把fragment一次性的添加到页面 
      this.el.appendChild(franment)
    }
  }



  /* 核心方法 */



  //把节点放到虚拟DOM中
  node2franment(node) {
    let franment = document.createDocumentFragment()
    let childNodes = node.childNodes
    Array.from(childNodes).forEach(node => {
      franment.appendChild(node)
    })
    return franment;
  }

  //解析虚拟DOM中的节点
  compile(franment) {
    let childNodes = franment.childNodes
    Array.from(childNodes).forEach(node => {
      //编译子节点
      if (this.isElementNode(node)) {
        // 如果是元素需要解析指令
        this.compileElement(node)
      }
      if (this.isTextNode(node)) {
        // 如果是文本节点,需要解析表达式
        this.compileText(node)
      }
      //判断当前节点还有子节点,需要递归的解析
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node)
      }

    })
  }

  //解析html标签
  compileElement(node) {
    let attribute = node.attributes
    //遍历属性,拿到属性名和属性值
    Array.from(attribute).forEach(attr => {
      //判断属性名是不是指令
      if (this.isDirective(attr.name)) {
        let type = attr.name.slice(2)
        //判断指令的类型
        if (this.isEventDirective(type)) {
          CompileUtil['eventHandler'](node, this.vm, type, attr.value)
        } else {
          CompileUtil[type] && CompileUtil[type](node, this.vm, attr.value)
        }

      }
    })
  }

  //解析文本
  compileText(node) {
    CompileUtil.mustache(node, this.vm)
  }




  /* 工具方法 */


  //是否是元素节点
  isElementNode(node) {
    return node.nodeType === 1;
  }
  //是否是文本节点
  isTextNode(node) {
    return node.nodeType === 3;
  }

  //判断是否是指令
  isDirective(attrName) {
    return attrName.startsWith('v-');
  }

  //判断是否是事件指令
  isEventDirective(type) {
    return type.split(':')[0] === 'on';
  }

}

let CompileUtil = {
  mustache(node, vm) {
    //获取文本
    let txt = node.textContent
    //用正则表达式匹配双花括号
    let reg = /\{\{(.+)\}\}/
    if (reg.test(txt)) {
      //替换花括号里的内容
      node.textContent = txt.replace(reg, this.getVmValue(vm, RegExp.$1))
      new Watcher(vm, RegExp.$1, newValue => {
        node.textContent = txt.replace(reg, newValue)
      })
    }
  },
  //解析v-text
  text(node, vm, expr) {
    node.textContent = this.getVmValue(vm, expr)
    new Watcher(vm, expr, newValue => {
      node.textContent = newValue
    })
  },
  //解析v-html
  html(node, vm, expr) {
    node.innerHTML = this.getVmValue(vm, expr)
    new Watcher(vm, expr, newValue => {
      node.innerHTML = newValue
    })
  },
  //解析v-model
  model(node, vm, expr) {
    let _this = this
    node.value = this.getVmValue(vm, expr)
    node.addEventListener('input', function() {
      _this.setVmValue(vm,expr,this.value)
    })
    new Watcher(vm, expr, newValue => {
      node.value = newValue
    })
  },
  //解析事件
  eventHandler(node, vm, type, expr) {
    let eventType = type.split(":")[1]
    let fn = vm.$methods && vm.$methods[expr]
    if (eventType && fn) {
      node.addEventListener(eventType, vm.$methods[expr].bind(vm))
    }
  },
  //获取vm中的数据
  getVmValue(vm, expr) {
    let data = vm.$data
    expr.split(".").forEach(key => {
      data = data[key]
    })
    return data
  },
  setVmValue(vm,expr,value){
    let data = vm.$data
    let arr = expr.split(".")
    arr.forEach((key,index)=>{
      if (index < arr.length -1 ) {
        data = data[key]
      }else{
        data[key] = value
      }
    })
  }
}

observe.js

/**
 * observer用于给data中所有的数据加上getter和setter,
 * 方便获取或设置data中数据的时候,实现我们的逻辑
 */
class Observer {

  constructor(data){
    this.data = data
    this.walk(this.data)
  }


  /* 核心方法 */


  //遍历data中所有的数据,都添加上getter和setter
  walk(data){
    if (!data || typeof data != 'object') {
      return;
    }
    Object.keys(data).forEach(key => {
      //给data对象的key添加getter和setter
      this.defineReactive(data,key,data[key])
      //递归遍历data里的数据
      this.walk(data[key])
    })
  }

  //定义响应式数据(数据劫持)

  // data中的每一个对象都应该维护一个dep对象
  //dep保存了所有的订阅了该数据的订阅者
  defineReactive(obj,key,value){
    let _this = this
    let dep = new Dep()
    Object.defineProperty(obj,key,{
      enumerable:true,
      configurable:true,
      get(){
        // 如果Dep.target中有watcher对象,存储到订阅者数组中
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      set(newValue){
        if (value === newValue) {
          return;
        }
        value = newValue
        //如果newValue也是一个对象,也应该对他进行劫持
        _this.walk(newValue)
        // 发布通知,让所有的订阅者更新内容
        dep.notify()
      }
    })
  }


}

watcher.js

/**
 * watcher模块负责把compile和observe关联起来
 */
class Watcher {
  /**
   * 
   * @param {当前的vue实例} vm 
   * @param {data中数据的名字} expr 
   * @param {数据变化的回调} cb 
   */
  constructor(vm,expr,cb){
    this.vm = vm;
    this.expr = expr;
    this.cb = cb

    // this表示的就是新创建的watcher对象

    // 存储到Dep的target属性上
    Dep.target = this

    this.oldValue = this.getVmValue(this.vm,this.expr)

    //清空Dep
    Dep.target = null
  }

  //对外暴露一个方法,更新页面数据
  update(){
    // 对比expr 是否发生了变化,如果发生了变化需要调用cb
    let oldValue = this.oldValue
    let newValue = this.getVmValue(this.vm,this.expr)
    if (oldValue != newValue) {
      this.cb(newValue,oldValue)
    }
  }

  //获取vm中的数据
  getVmValue(vm,expr){
    let data = vm.$data
    expr.split(".").forEach(key =>{
      data = data[key]
    })
    return data
  }
}




/**
 * dep类用于管理所有的订阅者和通知这些订阅者
 */
class Dep{
  constructor(){
    //用于管理订阅者
    this.subs = []
  }
  //添加订阅者
  addSub(watcher){
    this.subs.push(watcher)
  }
  //通知
  notify(){
    // 通知所有的订阅者,调用watcher的update方法
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}

vue的工作机制

  1. 初始化

    调用Vue原型上的_init()进行初始化,会初始化vue的生命周期,props,data,methods,computed,watch,最重要的是利用Object.definedPropty()对data对象里面的属性设置settergetter函数,也就是来实现响应式依赖收集

  2. 挂载组件

    调用$mount挂载组件

  3. 编译

    编译三部曲,parse(解析)、optimize(标记静态节点做优化)、generate(转成字符串) 3.1 parse:利用正则将模板转换成抽象语法树(AST); 3.2 optimize: 标记静态节点,以后update的时候,diff算法可以跳过静态节点 3.3 generate:将抽象语法树(AST)转成字符串,供render去渲染DOM

    经过以上步骤,就可以得到render funciton

  4. 响应式

    响应式是vue中我认为最核心的部分,利用Object.definedPropty 设置data所返回的对象后,在进行render function被渲染的时候,会对data对象进行数据读取,会触发getter函数,从而把data里面的属性进行依赖收集依赖收集的目的是将这些属性放到观察者(Watcher)的观察队列中,一旦我们对data里面的属性进行修改时,就会触发setter函数,setter告诉观察者数据变化,需要重新渲染视图,观察者调用update来更新视图

  5. 虚拟DOM

    render funtion 会被转换成虚拟DOM,虚拟DOM实际上就是一个js对象,从顶层DOM层层描述DOM,有tag, children, isStatic, isComment等等许多属性来做DOM描述

  6. 更新视图

    当数据发生变化时候,会经历setter => Watcher => update这些步骤,那么最终是怎么更新视图的呢? 在update的时候,会执行patch,将新旧VNode传进去,通过diff算法算出差异,局部更新视图,做到最优化。

vue工作机制.webp

依赖收集与追踪

编译compile

vue底层原理关系图.png转存失败,建议直接上传图片文件