Vue 响应式原理模拟

Vue 响应式模拟原理

<!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>Vue 基础结构</title>
</head>
<body>
  <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/vue.js"></script>
  <script>
    let vm = new Vue({
      el: '#app',
      data: {
        msg: 'Hello Vue',
        count: 20,
        items: ['a', 'b', 'c']
      }
    })
  </script>
</body>
</html>

数据驱动

响应式的核心原理

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

数据驱动

数据响应式、双向绑定、数据驱动
数据响应式
     数据模型不仅仅只是普通的JS对象,而当我们改变数据时,视图会进行更新,避免频繁的                    Dom操作,提高效率
双向绑定
    数据改变,视图改变;视图改变,数据也发生改变
    可以使用V-model在表单元素上创建双向数据绑定
数据驱动是Vue最独特的特性之一
     开发过程只需要关注数据本身,不需要关心数据是如何渲染到视图

数据响应式的核心原理

Vue2.0 Object.defineProperty
Object.defineProperty 是 ES5 中一个无法 shim 的特性,
这也就是 Vue 不支持 IE8以及更低版本浏览器的原因。

<!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>defineProperty</title>
</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 删除,可以通过 defineProperty 重新定义)
      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 = 'Hello World'
    console.log(vm.msg)
  </script>
</body>
</html>

多个属性支持数据劫持

<!DOCTYPE html>
<html lang="en">
<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>defineProperty 多个成员</title>
</head>
<body>
  <div id="app">
    hello
  </div>
  <script>
    // 模拟 Vue 中的 data 选项
    let data = {
      msg: 'hello',
      count: 10
    }

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

    proxyData(data)

    function proxyData(data) {
      // 遍历 data 对象的所有属性
      Object.keys(data).forEach(key => {
        // 把 data 中的属性,转换成 vm 的 setter/setter
        Object.defineProperty(vm, key, {
          enumerable: true,
          configurable: true,
          get () {
            console.log('get: ', key, data[key])
            return data[key]
          },
          set (newValue) {
            console.log('set: ', key, newValue)
            if (newValue === data[key]) {
              return
            }
            data[key] = newValue
            // 数据更改,更新 DOM 的值
            document.querySelector('#app').textContent = data[key]
          }
        })
      })
    }

    // 测试
    vm.msg = 'Hello World'
    console.log(vm.msg)
  </script>
</body>
</html>

Vue 3.0 数据劫持 proxy

proxy 直接监听对象,而非属性
ES 6新增,IE不支持,性能由浏览器优化

<!DOCTYPE html>
<html lang="en">
<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>Proxy</title>
</head>
<body>
  <div id="app">
    hello
  </div>
  <script>
    // 模拟 Vue 中的 data 选项
    let data = {
      msg: 'hello',
      count: 0
    }

    // 模拟 Vue 实例
    let vm = new Proxy(data, {
      // 执行代理行为的函数
      // 当访问 vm 的成员会执行
      get (target, key) {
        // target 代理的目标对象 key是访问的那个属性
        console.log('get, key: ', key, target[key])
        return target[key]
      },
      // 当设置 vm 的成员会执行
      set (target, key, newValue) {
        // target 代理对象 key 是属性 newValue 是设置的新的值
        console.log('set, key: ', key, newValue)
        if (target[key] === newValue) { // 前后值相等不需要修改
          return
        }
        target[key] = newValue
        document.querySelector('#app').textContent = target[key]
      }
    })

    // 测试
    vm.msg = 'Hello World'
    console.log(vm.msg)
  </script>
</body>
</html>

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

发布订阅模式
订阅者
发布者
信号中心 —数据存储处

Vue 中的自定义事件 都是基于发布订阅模式

let vm = new Vue();
vm.$on("datachange",()=>{
   console.log("datachange")
})
vm.$on("datachange",()=>{
   console.log("datachange1")
})
vm.$emit('datachange')

兄弟组件通信过程

// eventBus.js
let eventHub = new Vue()
// 发布者
addTODo:function(){
    // 发布事件
    eventHub.$emit("add-todo",{text:this.newTodoText})
    this.newTodoText = ""
}
// 订阅者
create:function(){
    // 订阅事件
    eventHub.$on('add-todo',this.addTodo)
}

模拟Vue 自定义事件的实现

    // 模拟事件中心
    // 发布者
    class EventEmitter{
        constructor(){
            // 订阅者 {eventType:[handler1,handler2]}
            // {'click':[fn1,fn2],"change":[fn]}
            this.subs = {}
        }
        // 订阅通知
        $on (eventType,handler){
            // 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')

观察者模式

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

// 发布者 - 目标
class Dep{
    constructor(){
        // 记录所有的订阅者
        this.subs = []
    }
    addSub(sub){
       // 将传递过来的订阅者存储到subs中
       // 存储之前需要先判断是否含有update方法
       if(sub&&sub.update){
           this.subs.push(sub)
       }
    }
    notify(){
       // 当事件发生时,调用notify方法,遍历所有的subs,调用对应的update方法
       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()

Vue响应式原理模拟

整体分析
vue基本结构
打印vue实例观察
整体结构
在这里插入图片描述

Vue 将data中的成员注入到Vue实例,并且把data中的成员转成getter/setter
Observer 能够对数据对象的所有属性进行监听,如有变动可以拿到最新值并通知Dep 需要模拟get set
注入到vue实例, d a t a , data, data,options,$el

模拟vue实现
Vue 功能

  • 负责接收初始化的参数(选项)
  • 负责把data中的属性注入到Vue实例,转换成Getter/Setter
  • 负责调用observer 监听data中所有属性的变化
  • 负责调用compiler 解析指令/插值表达式

结构

 <!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 Vue</title>
</head>
<body>
  <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',
        count: 100,
        person: { name: 'zs' }
      }
    })
    console.log(vm)

    console.log(vm.msg)
    // vm.msg = { test: 'Hello' }
    vm.test = 'abc'
  </script>
</body>
</html>

vue.js

class Vue{
    constructor(options){
        // 1. 通过属性保存选项的数据
        this.$options = options||{}
        this.$data = options.data || {}
        this.$el = typeof options.el ==='string' ? document.querySelector(options.el): el
        // 2. 把data中的成员转换成getter和setter,注入到vue实例中
        this._proxyData(this.$data)
        // 3. 调用observer对象,监听数据的变化
        // 4. 调用compiler对象,解析指令和插值表达式
    }

    _proxyData(data){
      // 遍历data中的所有属性
      Object.keys(data).forEach(key=>{
        // 将data的属性注入到vue实例中
        Object.defineProperty(this, key, {
            // configrable 描述属性是否配置,以及可否删除
            // enumerable 描述属性是否会出现在for in 或者 Object.keys()的遍历中
            enumerable: true,
            configurable: true,
            get(){
                return data[key]
            },
            set(newValue){
               if(newValue === data[key]){
                  return
               }
               data[key] = newValue
            }
        })
      })
    }
}

observer
功能

负责把data选项中的属性转换成响应式数据
data中的某个属性也是对象,把该属性转换成响应式数据
数据变化发生通知

结构

Observer
walk(data)defineReactive(data,key,value)
// 负责数据劫持,监听data中的变化作出处理
// 把$data中的成员 转换成getter/setter


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;
       // 如果val是对象,把val内部的属性转换成响应式数据,将data数据里的对象结构转换成响应式的
       this.walk(val)
       Object.defineProperty(obj,key,{
           enumerable:true,
           configurable:true,
           get(){
               // 不使用obj[key]是因为会发送死递归,所以传val
               return val
           },
           set(newValue){
             if(newValue === val){
                return
             }
             val = newValue
             // 将从新赋值为对象结构也转换为响应式结构
             that.walk(newValue)
             // 发送通知
           }
       })
    }
}

将对应data中的数据转换成get,set方式

Compiler
功能

负责模板编译
负责页面的首次渲染
当数据变化后重新渲染视图

结构

Compiler
elvm
compile(el)
compileElement(node)
compileText(node)
isDirective(attrName)
isTextNode(node)
isElementNode(node)
不使用虚拟dom,直接改变dom结构

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.compileElement(node)
           }

           // 判断node节点,是否有子节点,若有,递归调用compile
           if(node.childNodes&&node.childNodes.length){
               this.compile(node)
           }
       })
    }

    // 编译元素节点,处理指令
    compileElement(node){
       // 根据attributes 找到带有v-开头的属性节点
       // 遍历所有的属性节点
       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(node,this.vm[key])
    }

    // 处理v-text指令
    textUpdater(node,value){
       node.textContent = value
    }
    // 处理v-model指令

    modelUpdater(node,value){
        node.value = value
    }

    // 编译文本节点,处理插值表达式
    compileText(node){
       //    console.dir(node)
       // 处理插值{{msg}}
       let reg = /\{\{(.+?)\}\}/
       let value = node.textContent  // 获取文本节点内容
       if(reg.test(value)){
           let key=RegExp.$1.trim()
           node.textContent = value.replace(reg,this.vm[key])
       }

    }

    // 判断元素属性是否是指令
    isDirective(attrName){
       // 判断是不是以v-开头
       return attrName.startsWith('v-')
    }

    // 判断是否为文本节点
    isTextNode(node){
        // nodeType 3是文本
        return node.nodeType === 3
    }

    // 判断节点是否是元素节点
    isELementNode(node){
        // nodeType 1 是元素节点
        return node.nodeType === 1
    }
}

Dep
功能

收集依赖,添加观察者(watcher)
通知所有观察者

结构

Dep
Subs
addSub(sub)
notify()

class Dep{
    constructor(){
        // 存储所有的观察者
        this.subs = [];
    }

    // 添加观察者
    addSub(sub){
      if(sub && sub.update){
        this.subs.push(sub)
      }
    }

    // 发送通知
    notify(){
      this.subs.forEach(sub =>{
          sub.update()
      })
    }
}

Watcher
功能

当数据变化触发依赖,dep 通知所有的Watcher 实例更新视图
自身实例化的时候往dep 对象中添加自己

结构

Watcher
vmkeycboldValue
update()

 class Watcher{
     constructor(vm,key,callback){
         this.vm = vm
         // data中的属性名称
         this.key = key
         this.callback = callback

         // 将watcher对象记录到Dep对象中的静态属性target去
         // 触发get方法,在get方法中会调用addSub
         Dep.target = this
         this.oldValue = vm[key]
     }
     // 当数据发生变化的时候更新视图
     update(){
         let newValue = this.vm[this.key]
         if(this.oldValue === newValue){
            return
         }
         this.callback(newValue)
     }
 }

当数据改变时,Dep通知所有的观察者重新渲染视图

双向绑定机制

数据发生变化,更新视图
视图发生变化,更新数据
文本框发生变化,触发一个事件,从新赋值给data的msg
触发input事件,会将文本框的值取出,从新赋值给vm.msg,触发了响应式机制

modelUpdater(node,value,key){
        node.value = value
        new Watcher(this.vm,key,(newValue)=>{
            node.value = newValue
        })
        // 双向绑定
        node.addEventListener('input',()=>{
            this.vm[key] = node.value
        })
    }

首次渲染

首次渲染会将 所有内容渲染到界面

直到执行 new Vue,进入Vue的构造函数,执行_proxyData,将所有的值添加到vue实例中

执行Observer,将所有的值添加setget,执行到defineProperty,将所有的值添加到Dep的观察者对象

进入compiler解析对应模板,compile处理所有的文本节点,找到需要处理的v-节点,处理到_proxyData中的get()方法

此节点渲染完成之后,进入Watcher对象,进行监听,等待数据响应执行对应回调,

进入Watcher对象,执行到Dep.target,Dep.target执行的是对应的Watcher对象

this.oldValue,访问的就是Observer的get方法	,再给dep设为null	

数据改变

数据改变时,执行到observer的 set方法  -------- dep.notify方法
找到dep对应的sub观察者对应的update方法,
找到对应update方法,进行判断前后的值是否相同,执行到compiler的对应watcher中的newValue,
更新dom的值执行到对应绑定的回调中

总结

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值