• Vue源码探秘(二)—— Vue 响应式原理模拟


    前置知识

    数据驱动

    • 数据响应式——Vue 最标志性的功能就是其低侵入性的响应式系统。组件状态都是由响应式的 JavaScript 对象组成的。当更改它们时,视图会随即自动更新。
    • 双向绑定——数据改变,视图改变;视图改变,数据也随之改变
    • 数据驱动是Vue最独特的特性之一 —— 开发者只需关注数据本身,而无需关心数据如何渲染到视图

    数据响应式的核心原理

    Vue 2.x

    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>Documenttitle>
    head>
    <body>
      <div id="app">
        hello
      div>
      <script>
        // 模拟Vue中的data选项
        let data = {
          msg: 'hello'
        }
    
        // 模拟Vue的实例
        let vm = {}
    
        // 数据劫持,当访问或设置vm中的成员的时候,做一些操作
        Object.defineProperty(vm, 'msg', {
          // 是否可以枚举
          enumerable: true,
          // 是否可配置,即delete删除
          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.getElementById('app').textContent = data.msg
          }
        })
    
        // 测试
        vm.msg = 'Hello World'
        console.log(vm.msg)
      script>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    当data中有多个对象时,需要对其进行遍历,此时需要对上述代码进行一些改造。

    let data = {
      msg: 'hello',
      count: 10
    }
    
    let vm = {}
    
    proxyData(data)
    
    function proxyData(data) {
      Object.keys(data).forEach(key => {
        Object.defineProperty(vm, key, {
          enumerable: true,
          configurable: true,
          get() {
            return data[key]
          },
          set(newValue) {
            if (newValue === data[key]) return
            data[key] = newValue
            document.querySelector('#app').textContent = data[key]
          }
        })
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    Vue 3.x

    步入Vue3,尤小右使用Proxy对其进行了改造,不仅抛弃了如 $delete 之类的鸡肋API(因为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>Documenttitle>
    head>
    <body>
      <div id="app">hellodiv>
    
      <script>
        let data = {
          msg: 'hello',
          count: 0
        }
    
        let vm = new Proxy(data, {
          get(target, key) {
            return target[key]
          },
          set(target, key, newValue) {
            if (target[key] === newValue) return
            target[key] = newValue
            document.querySelector('#app').textContent = target[key]
          }
        })
      script>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    发布订阅和观察者模式

    发布/订阅模式

    注:为简便起见,代码实现并未加入对传参的考虑。

    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>Documenttitle>
    head>
    <body>
      <script>
        class EventEmitter {
          constructor() {
            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('click', () => {
          console.log('click1');
        })
        em.$on('click', () => {
          console.log('click2');
        })
        em.$emit('click')
      script>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    观察者模式

    注:为简便起见,代码实现并未加入对传参的考虑。

    DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>观察者模式title>
    head>
    <body>
      <script>
        // 发布者-目标
        class Dep {
          constructor() {
            this.subs = []
          }
    
          addSub(sub) {
            if (sub && sub.update) {
              this.subs.push(sub)
            }
          }
    
          notify() {
            this.subs.forEach(sub => {
              sub.update()
            })
          }
        }
    
        class Watcher {
          update() {
            console.log('update')
          }
        }
    
        // 测试
        let dep = new Dep()
        let watcher = new Watcher()
    
        dep.addSub(watcher)
    
        dep.notify()
      script>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    Vue响应式原理模拟实现

    在这里插入图片描述

    Vue

    • 功能
      • 接收初始化参数
      • 将data中的数据注入实例并转换成getter/setter
      • 调用observer监听data中属性的变化
      • 调用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 Compiler(this)
      }
    
      _proxyData(data) {
        Object.keys(data).forEach(key => {
          Object.defineProperty(this, key, {
            enumerable: true,
            configurable: true,
            get() {
              return data[key]
            },
            set(newValue) {
              if (newValue === data[key]) return
              data[key] = newValue
            }
          })
        })
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    Observer——对data中的属性进行监听

    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])
        })
      }
    
      defineReactive(obj, key, val) {
        let that = this
        // 负责收集依赖
        let dep = new Dep()
        // 如果val是对象,会将其内部的对象也变成响应式数据
        this.walk(val)
        Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get() {
            // 收集依赖
            Dep.target && dep.addSub(Dep.target)
            return val
          },
          set(newValue) {
            if (newValue === val) {
              return
            }
            val = newValue
            that.walk(newValue)
            // 发送通知
            dep.notify()
          }
        })
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    Compiler

    class Compiler {
      constructor(vm) {
        this.el = vm.$el
        this.vm = vm
        this.compile(this.el)
      }
    
      // 编译模板,处理文本节点和元素节点
      compile(el) {
        let childNodes = el.childNodes
        Array.from(childNodes).forEach(node => {
          // 处理文本节点
          if (this.isTextNode(node)) {
            this.compileText(node)
          } else if (this.isElementNode(node)) {
            // 处理元素节点
            this.comipleElement(node)
          }
    
          // 判断node节点是否有子节点
          if (node.childNodes && node.childNodes.length) {
            this.compile(node)
          }
        })
      }
      // 编译元素节点,处理指令
      comipleElement(node) {
        Array.from(node.attributes).forEach(attr => {
          let attrName = attr.name
          if (this.isDirective(attrName)) {
            // v-text => text
            attrName = attrName.substr(2)
            let key = attr.value
            this.update(node, key, attrName)
          }
        })
      }
      update(node, key, attrName) {
        let updateFn = this[attrName + 'Updater']
        updateFn && updateFn.call(this, node, this.vm[key], key)
      }
      // 处理 v-text 指令
      textUpdater(node, value, key) {
        node.textContent = value
        new Watcher(this.vm, key, (newValue) => {
          node.textContent = newValue
        })
      }
      // v-model
      modelUpdater(node, value, key) {
        node.value = value
        new Watcher(this.vm, key, (newValue) => {
          node.value = newValue
        })
        // 双向绑定
        node.addEventListener('input', () => {
          this.vm[key] = node.value
        })
      }
    
      // 编译文本节点,处理插值表达式
      compileText(node) {
        // console.dir(node);
        let reg = /\{\{(.+?)\}\}/
        let value = node.textContent
        if (reg.test(value)) {
          let key = RegExp.$1.trim()
          node.textContent = value.replace(reg, this.vm[key])
    
          // 创建watcher对象,当数据改变更新视图
          new Watcher(this.vm, key, (newValue) => {
            node.textContent = newValue
          })
        }
      }
      // 判断元素属性是否是指令
      isDirective(attrName) {
        return attrName.startsWith('v-')
      }
      // 判断节点是否是文本节点
      isTextNode(node) { 
        return node.nodeType === 3
      }
      // 判断节点是否是元素节点
      isElementNode(node) {
        return node.nodeType === 1
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88

    Watcher

    class Watcher {
      constructor(vm, key, cb) {
        this.vm = vm
        // data中的属性名称
        this.key = key
        // 回调函数负责更新视图
        this.cb = cb
    
        // 把watcher对象记录到Dep类的静态属性target
        Dep.target = this
        // 触发get方法,在get方法中会调用addSub
        this.oldValue = vm[key]
        Dep.target = null
      }
      // 当数据发生变化的时候更新视图
      update() {
        let newValue = this.vm[this.key]
        if (this.oldValue === newValue) {
          return
        }
        this.cb(newValue)
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    Dep

    class Dep {
      constructor() {
        this.subs = []
      }
      // 添加观察者
      addSub(sub) {
        if (sub && sub.update) {
          this.subs.push(sub)
        }
      }
      // 发送通知
      notify() {
        this.subs.forEach(sub => {
          sub.update()
        })
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    测试代码

    DOCTYPE html>
    <html lang="cn">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Mini Vuetitle>
    head>
    <body>
      <div id="app">
        <h1>差值表达式h1>
        <h3>{{ msg }}h3>
        <h3>{{ count }}h3>
        <h1>v-texth1>
        <div v-text="msg">div>
        <h1>v-modelh1>
        <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',
            count: 100
          }
        })
        vm.test = 'abc'
      script>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
  • 相关阅读:
    WavJourney:进入音频故事情节生成世界的旅程
    为什么选择测试/开发程序员这个职业?
    深度神经网络
    基础-SQL-DCL
    Docker知识总结 (五) Dockerfile
    网络安全(黑客)自学
    sp.coo_matrix(), sp.eye()
    LIN总线入门
    Blazor前后端框架Known-V1.2.5
    一文读懂Kotlin的数据流
  • 原文地址:https://blog.csdn.net/weixin_49172439/article/details/126705463