vue响应式的原理 | Vue 核心知识点和实现原理

770 阅读3分钟

前言

本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。

本文章将带领大家理解vue数据驱动、响应式的核心原理、发布订阅模式和观察者模式的原理与基本实现。

一、数据驱动

数据驱动是 Vue 最独特的特性之一,开发过程中只需要关注数据本身,不需要关心数据是如何渲染到视图中,例如使用jQuery实现页面数据修改,则需要DOM操作获取需要更改数据的元素。而 vue 的数据模型仅仅是普通 js 对象,而当我们修改数据的时候,视图就会更新,避免了繁琐的DOM操作,提高了开发效率。

二、响应式的核心原理

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

2.0的数据响应式原理

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>test</title>
</head>
<body>
  <div id="app">
    hello
  </div>
  <script>
    //模拟 Vue 中的 data 选项
    let data = {
      msg: {},
      count: 10
    }

    // 模拟 Vue 的实例
    let vm = {}

    proxyData(data)

    function proxyData(data){
      // 把data中的属性转换为vm的setter/getter
      Object.keys(data).forEach(key =>{
        // 数据劫持,当访问或者设置vm中的成员的时候,做一些干预操作
        Object.defineProperty(vm,"msg",{
          // 设置可以枚举
          enumerable: true,
          // 可配置
          configurable: true,
          // 当获取值得时候执行
          get(){
            console.log('get:',data.msg);
            return data.msg
          },
          // 当设置值得时候执行
          set(newValue){
            console.log('set:', newValue);
            if(newValue === data.msg) return
            data.msg= newValue
            //数据更改,更新DOM的值
            document.querySelector('#app').textContent = data.msg
          }
        })
      })
    }
    vm.msg = 'hello1'
    console.log(vm.msg);
  </script>
</body>
</html>

打开页面的控制台可以看到

图片1.png

在执行 vm.msg = 'hello1' 的时候触发了set方法,打印出了set: hello1。在执行 console.log(vm.msg) 触发了get方法,打印出了 get: hello1,最后打印了vm.msg的值。

3.0的数据响应式原理

3.0的vue使用proxy直接监听对象,而非属性

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>test</title>
</head>
<body>
  <div id="app">
    hello
  </div>
  <script>
    //模拟 Vue 中的 data 选项
    let data = {
      msg: {},
      count: 10
    }


    // 模拟 vue 实例
    let vm = new Proxy(data,{
      get(target, key){
        console.log('get,key',key,target[key]);
        return target[key]
      },
      // 当设置值得时候执行
      set(target, key, newValue){
        console.log('set,key:', key,newValue);
        if(newValue === target[key]) return
        target[key]= newValue
        //数据更改,更新DOM的值
        document.querySelector('#app').textContent = target[key]
      }
    })
    vm.msg = 'hello1'
    console.log(vm.msg);
  </script>
</body>
</html>

同样打开页面的控制台可以看到,在执行 vm.msg = 'hello1' 的时候触发了set方法,打印出了set,key: msg hello1.在执行 console.log(vm.msg) 触发了get方法,打印出了 get,key msg hello1,最后打印了vm.msg的值.

三、发布订阅模式和观察者模式

发布订阅模式

我们可以假设,你在一个微信里面订阅了一个公众号一有新文章你就会去看,公众号作者在公众号里发布一个新文章的时候会通知你,这时候你就是订阅者,公众号作者就是发布者,公众号这个平台就是"信号中心" $on,$emit的实现原理

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>发布订阅模式</title>
</head>
<body>
  <script>
    class EventEmitter {
      constructor(){
        // {'type1': [fn1,fn2],'type2': [fn]}
        this.subs = Object.create(null)
      }

      $on (eventType, handler){
        this.subs[eventType] = this.subs[eventType] || []
        this.subs[eventType].push(handler)
      }

      $emit (eventType){
        if(this.subs[eventType]){
          this.subs[eventType].forEach(handler=>{
            handler()
          })
        }
      }
    }

    let em = new EventEmitter()
    em.$on('test',()=>{ console.log('test1'); })
    em.$on('test',()=>{ console.log('test2'); })
    em.$emit('test')
  </script>
</body>
</html>

观察者模式

  • 观察者(订阅者) -- Watcher
    • update(): 当事件发生时,具体要做的事情
  • 目标(发布者) -- Dep
    • subs数组: 存储所有的观察着
    • addSub(): 添加观察着
    • notify(): 当事件发生,调用所有观察着的update()方法

观察者模式实现原理

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>观察着模式</title>
</head>
<body>
  <script>
  // 发布者-目标
  class Dep {
    constructor(){
      this.subs =[]
    }
    addSub(watch){
      if(watch && watch.update){
        this.subs.push(watch)
      }
    }
    nofity(){
      this.subs.forEach(watch=>{
        watch.update()
      })
    }
  }
  // 订阅者-观察着
  class Watcher {
    update(){
      console.log('hello');
    }
  }

  // 测试
  let dep = new Dep()
  let wat = new Watcher()

  dep.addSub(wat)
  dep.nofity()

  </script>
</body>
</html>

观察着模式和发布/订阅模式的区别

  • 观察着模式是有具体目标调度,比如当事件触发,Dep就会调用观察着的方法,所以观察着模式的订阅者与发布者之间是存在依赖的

  • 发布/订阅模式是由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在 图片1.png

总结

本文只是简单介绍了vue实现响应式中的一些简单的原理,适合对源码没有了解的前端初学者,可以让初学者对源码的底层实现有基本的了解。
本人文笔水平有限,若有错误或不足,还望指正,谢谢大家。