【前端进阶】Vue 高级

vue相关原理进阶

- 1 - 整体目标

  • 了解 Object.defineProperty 实现响应式

  • 了解指令编译的基础原理

  • 清楚Observe/watcher/deo 具体指什么

  • 了解发布订阅模式以及其解决的问题

- 2 - 数据响应式

- 2.1 响应式是什么

一旦数据发生变化,我们可以立刻知道,并且做一些你想完成的事

  • 发送一个网络请求
  • 打印一段文字
  • 操作一个DOM
- 2.2 如何实现数据响应式

在javascript里实现数据响应式 一般有两种方案,分别对应着vue2.x,和vue3.x 使用的方式,他们分别是:

  1. 对象属性拦截 (vue2.x)

    Objcet.defineProperty

  2. 对象整体代理 (vue3.x)

    Proxy

- 2.3 实现对象属性拦截
    // 1. 字面量定义
    let data = {
        name: " giao桑 "
    }
    data.name = '要洗~' // 普通字面量定义是无法检测到name属性发生了变化

    // 2. Object.defineProperty
    let data1 = {}
    Object.defineProperty(data1, 'name', {
        // 当我们设置 name 属性的时候自动调用的方法
        // 并且属性 最新的值 会被当成实参传进来
        set(...r) {
            // 设置属性 data.name = 'new name' data['name'] = 'new name '
            console.log('name set', r)
            // 只要修改了name属性 就会执行该函数
            // 如果想要在name变化的时候 完成一些自己的事情
            // 都可以在这儿执行!
            // 1. ajax ()
            // 2 操作 一块 DOM 区域
        },
        // 访问name属性 的时候 会自动调用的方法
        // 并且 get的返回值 就是你拿到的值
        get(...r) {
            // 访问属性 data.name data['name']
            console.log('name get', r)
        }
    })
    data1.name = 'new name~'
    let a = data1.name
- 2.4 优化1 - get 和 set 联动
let data = {}
Object.defineProperty(data,'name',{
    get(){
        console.log('data get',data)
    },
    set(newValue){
        console.log('data set',newValue)
    }
})

出现问题

  • data 设置name属性之后 再次访问name属性 直接返回了一个固定的值
  • 并且 set函数中拿到新值之后没有做任何操作

解决方案:

  • 通过声明一个中间变量,让get函数中return 出去这个变量

  • 并且在set函数中把最新的值设置到这个中间变量身上,起到一个set和get数据的效果

let data = {}
// 中间变量
let _name = 'giaogiaogiao '
Object.defineProperty(data,'name',{
    get(){
        console.log('data get',data)
        return _name
    },
    set(newValue){
        console.log('data set',newValue)
        _name = newValue
    }
})
- 2.5 优化2 - 更加通用的劫持方案

开发 vue 项目的时候 都是提前把响应式数据放到 data 选项中

/*
*   data(){
*       return { name:"giao" }
*   }
* */

// 所以 一般情况下,响应式数据 都是提前写好的,并且对象的属性很多
let data = {
    name: ' wo giao ',
    info: " xi huan  gou dan ",
    age: '20'
}

如何把这个提前生命的对象 巴黎变得所有属性都变成响应式的?

// 1. 遍历对象的每一个属性
Object.keys(data).forEach((key) => {
    console.log(key, data[key])
    defineReactive(data, key, data[key])
})

// 2. 将每一个属性转为响应式数据
function defineReactive(data, key, value) {
    Object.defineProperty(data, key, {
        get() {
            console.log('get',key)
            return value
        },
        set(newValue) {
            console.log('set',key,newValue)
            value = newValue
        }
    })
}

总结:

  • 尽量不要再一个函数中书写大量逻辑,而应该是 按照功能差分成多个小函数 ,然后再组合取来 便于维护
  • 对象遍历的时候 每遍历一次 调用一次defineReactive 函数 形成了多个独立的函数作用域 在每一个独立的函数作用域中 set 和 get 操作的都是独立的value 值 互不影响(闭包特性 1.创建私有作用域 2.延长变量生命周期 )
  • 函数定义形参相当于在函数内部声明了和形参名字相对应的变量 (局部) 并且初始值为undefined
  • 函数调用传入实参的时候,相当于给内部声明好的变量 做了赋值操作
  • 函数调用完毕 本来应该内部所有的变量都会被回收,但是如果内部有其他函数 使用了当前变量 则形成了闭包 不会被回收
  • 内部由于有其他方法 引用了value 属性 所以defineReactive 函数 的执行并不会导致value 变量的销毁 会一直常驻内存中
  • 由于闭包特性 每一个传入下来的value都会常驻内存 相当于 中间变量, 是为了set get 联动 从而实现数据的响应式
- 2.6 响应式总结
  1. 所谓的响应式 其实就是拦截数据的访问和设置 ,插入一些我们自己想要做的事情
  2. 在js 中 能实现响应式拦截的方法有两种 Object.defineProperty 和 Proxy 对象代理
  3. 回归到vue 2.x 中的data 配置项 只要放到了data 里的数据 不管层级多深,都会进行递归响应式处理,所以要求我们 如非必要,尽量不要添加太多冗余数据在data中。(消耗性能)
  4. 需要了解vue3.x 中解决了 2 中 对于数据响应式处理的无端性能消耗,使用的手段是proxy 劫持对象整体 + 惰性代理 ( 用到了才进行响应式处理)

- 3 - 数据的变化反应到视图

想要把数据反应到视图中 本质上还是操作DOM

- 3.1 命令式操作视图
/* 命令式 m -> v  */
// 1.准备数据
let data =(function (){
    return {
        name:"giao 桑",
        age:99
    }
})()

// 2. 将每一个属性转为响应式数据
Object.keys(data).forEach((key) => {
    console.log(key, data[key])
    defineReactive(data, key, data[key])
})
function defineReactive(data, key, value) {
    Object.defineProperty(data, key, {
        get() {
            console.log('get',key)
            return value
        },
        set(newValue) {
            // set 函数的执行 不会自动判断 两次修改的值是否相等
            // 因为DOM 操作 是非常消耗内存的, 所以当新值和旧值相等的时候 不应该进行set 逻辑
            if(newValue === value) return
            console.log('set',key,newValue)
            value = newValue
            // 将最新的值 反应都视图中去 关键code
            // 操作DOM
            document.querySelector('#app p').innerText = newValue
        }
    })
}
 document.querySelector('#app p').innerText = newValue
- 3.2 声明式操作视图
/*
*   v-text 指令式
* */
// 1 先通过标识 查找把数据放到对应的DOM元素
// 2 数据变化之后再次执行将最新的值放到对应的DOM上
let data = {
    name: "giao sang",
    age:"888"
}

function compile() {
    let app = document.getElementById('app');
    // 拿到所有节点
    const nodes = app.childNodes// 拿到所有的节点 包括文本节点
    // 筛选出元素节点 nodeType === 3 文本节点 nodeType === 1 元素节点
    nodes.forEach(node => {
        if (node.nodeType === 1) {
            // 筛选 v-text 属性 p
            const attrs = node.attributes
            Array.from(attrs).forEach(attr => {
                const nodeName = attr.nodeName
                const nodeValue = attr.nodeValue
                if (nodeName === 'v-text') {
                    node.innerText = data[nodeValue]
                }
            })
        }
    })
}

Object.keys(data).forEach((key) => {
    defineReactive(data, key, data[key])
})

function defineReactive(data, key, value) {
    Object.defineProperty(data, key, {
        get() {
            console.log('get', key)
            return value
        },
        set(newValue) {
            if (newValue === value) return
            console.log('set', key, newValue)
            value = newValue
            compile()
        }
    })
}

compile()
- 3.3 总结
  • 先通过标识 查找把数据放到对应的DOM元素
  • 数据变化之后再次执行将最新的值放到对应的DOM上

- 4 - 视图的变化反应到数据

目标:将data中的message 属性对应的值渲染到input上面 ,同时input值发生改变之后,可以反向修改message的值。

/*
*   v-model 双向绑定的实现
*   M - V
*   V - M
* */
let data = {
    name: "giao sang",
    age:"888"
}

function compile() {
    let app = document.getElementById('app');
    // 拿到所有节点
    const nodes = app.childNodes// 拿到所有的节点 包括文本节点
    // 筛选出元素节点 nodeType === 3 文本节点 nodeType === 1 元素节点
    nodes.forEach(node => {
        // 筛选元素节点
        if (node.nodeType === 1) {
            // 元素节点的属性
            const attrs = node.attributes
            Array.from(attrs).forEach(attr => {
                const nodeName = attr.nodeName
                const nodeValue = attr.nodeValue
                if (nodeName === 'v-text') {
                    node.innerText = data[nodeValue]
                }
                // 实现 v-model
                if(nodeName === 'v-model'){
                    // 调用dom操作 给 input 标签绑定数据
                    node.value  = data[nodeValue]
                    // 监听 input事件 在事件回调中 拿到最新的输入值 赋值给绑定的属性
                    node.addEventListener('input',(e)=>{
                        let val = e.target.value
                        data[nodeValue] = val
                    })
                }
            })
        }
    })
}

Object.keys(data).forEach((key) => {
    defineReactive(data, key, data[key])
})

function defineReactive(data, key, value) {
    Object.defineProperty(data, key, {
        get() {
            console.log('get', key)
            return value
        },
        set(newValue) {
            if (newValue === value) return
            console.log('set', key, newValue)
            value = newValue
            compile()
        }
    })
}

compile()

- 5 - 现存架构的问题

面临的问题:

  • 不管你修改了哪个属性,其他属性也会跟着一起更新
  • 只修改了name 的时候 应该只有name 相关的更新操作执行 而不是所有的属性一起更新

期望:

  • 哪个属性进行了实质性的修改,哪个属性的对应的编译部分得到更新 - “精准更新”

- 6 - 发布订阅模式优化

- 6.1 优化思路思考
*
         发布订阅模式 (自定义事件)
    * 

    *     优化思路:
    *     1. 数据变化之后,实际上执行那部分代码就可以了
    *     node.innerText = data[nodeValue]
    
    *     2. 要完成以上代码 我们需要关注两个问题
    *     node:当前要操作哪个dom元素
    *     nodeValue 我们要去data中查找的对象属性名是谁 'name' 'age'
    
    *     3. 正常逻辑下 函数的执行之后 内部所有的变量都会被销毁
    *        为了保证 node nodevalue 属性名 不被销毁
    *        我们需要引入闭包机制 强制让他们存储与内存
    *        之所以不能被销毁 是因为 应用跑起来之后 偶们一直在操作数据 ,一直在操作数据对应的dom
    *        ()=>{
    *           node.innerText = data[nodeValue]
    *       }
    *       这样可以借助函数引用外层函数的node 变量和nodeValue 变量从而使他们一直在内存中 不被销毁
    *       所以才可以一直对他们操作
    
    *     4. 由于我们响应式数据 绑定到的dom节点可能有多个 所以 node 节点可能存在多个
    *        一旦响应式属性 name 发生变化 与 name 属性相关的所有dom节点都需要进行一轮更新
    *        所以属性和更新函数之间是一个一对多的关系
*
- 6.2 理解发布订阅模式
    let btn = document.getElementById('btn')
    // btn.onclick = function (){
    //     console.log('click !!!!!')
    // }
    // btn.onclick = function (){
    //     console.log('so difficult!')
    // }
    //  以上实现 并不能完成俩回调函数的同时绑定
    //  它是一个 一对一 的实现 一个时间 -> 回调函数
    //  优化 从一对一 一对多

    btn.addEventListener('click', () => {
        console.log('click')
    })
    btn.addEventListener('click', () => {
        console.log('click me!!')
    })
    // 这种模式下 可以实现 同一事件 对应多个回调函数 实现了关键的 一对多
    // 这种优化 使用的就是发布订阅模式

    /*
    *   1. 浏览器实现了一个方法  叫做addEventListener
    *   2. 这个方法 接收两个参数 参数1 代表 事件类型 参数2 代表 回调函数
    *   3. {
    *           click:['回调函数1','回调函数2']
    *       }
    *   4. 当鼠标点击的时候 通过事件类型click 去数据结构中找到存放了所有相关回调函数的数组 然后遍历 都执行一边 从而实现一对多
    *   */

    /*
    *   实现一个自己的自定义事件 收集和 触发架构
    *   1.定义一个方法 接收两个参数 参数1 为 事件名称 参数2 为 回调函数
    *     只要方法一执行 就收集回调函数到对应的位置上去
    *   2.
    * */
    // 模拟鼠标点击  主动通过程序去触发收集起来的事件
    // 需要通过事件名称 找到对应的回调函数数组 然后遍历执行即可


    // 优化 把所有和事件相关的数据结构 以及方法 收敛到对象中

    const Dep = {
        map: {},
        Collect(eventName, fn) {
            if (!this.map[eventName]) {
                this.map[eventName] = []
            }
            this.map[eventName].push(fn)
        },
        trigger(eventName) {
            this.map[eventName].forEach(fn => fn())
        }
    }
    Dep.Collect('han',()=>{
        console.log('啦啦啦')
    })
    Dep.Collect('lao',()=>{
        console.log('lululu')
    })    let btn = document.getElementById('btn')
    // btn.onclick = function (){
    //     console.log('click !!!!!')
    // }
    // btn.onclick = function (){
    //     console.log('so difficult!')
    // }
    //  以上实现 并不能完成俩回调函数的同时绑定
    //  它是一个 一对一 的实现 一个时间 -> 回调函数
    //  优化 从一对一 一对多

    btn.addEventListener('click', () => {
        console.log('click')
    })
    btn.addEventListener('click', () => {
        console.log('click me!!')
    })
    // 这种模式下 可以实现 同一事件 对应多个回调函数 实现了关键的 一对多
    // 这种优化 使用的就是发布订阅模式

    /*
    *   1. 浏览器实现了一个方法  叫做addEventListener
    *   2. 这个方法 接收两个参数 参数1 代表 事件类型 参数2 代表 回调函数
    *   3. {
    *           click:['回调函数1','回调函数2']
    *       }
    *   4. 当鼠标点击的时候 通过事件类型click 去数据结构中找到存放了所有相关回调函数的数组 然后遍历 都执行一边 从而实现一对多
    *   */

    /*
    *   实现一个自己的自定义事件 收集和 触发架构
    *   1.定义一个方法 接收两个参数 参数1 为 事件名称 参数2 为 回调函数
    *     只要方法一执行 就收集回调函数到对应的位置上去
    *   2.
    * */
    // 模拟鼠标点击  主动通过程序去触发收集起来的事件
    // 需要通过事件名称 找到对应的回调函数数组 然后遍历执行即可


    // 优化 把所有和事件相关的数据结构 以及方法 收敛到对象中

    const Dep = {
        map: {},
        Collect(eventName, fn) {
            if (!this.map[eventName]) {
                this.map[eventName] = []
            }
            this.map[eventName].push(fn)
        },
        trigger(eventName) {
            this.map[eventName].forEach(fn => fn())
        }
    }
    Dep.Collect('han',()=>{
        console.log('啦啦啦')
    })
    Dep.Collect('lao',()=>{
        console.log('lululu')
    })
- 6.3 收集更新函数
function Collect(eventName, fn) {
            if (!this.map[eventName]) {
                this.map[eventName] = []
            }
            this.map[eventName].push(fn)
        },
- 6.4 触发更新函数
// 引入发布订阅模式

/*
*   发布订阅模式思路
*   1.  针对每一个响应式属性 收集与之相关的更新函数
*   2.  响应式属性更新之后 通过属性名 找到与之绑定在一起的所有更新函数 进行触发执行
* */
const Dep = {
    map: {},
    Collect(eventName, fn) {
        if (!this.map[eventName]) {
            this.map[eventName] = []
        }
        this.map[eventName].push(fn)
    },
    trigger(eventName) {
        this.map[eventName].forEach(fn => fn())
    }
}
let data = {
    name: "giao sang",
    age:"888"
}

function compile() {
    let app = document.getElementById('app');
    // 拿到所有节点
    const nodes = app.childNodes// 拿到所有的节点 包括文本节点
    // 筛选出元素节点 nodeType === 3 文本节点 nodeType === 1 元素节点
    nodes.forEach(node => {
        // 筛选元素节点
        if (node.nodeType === 1) {
            // 元素节点的属性
            const attrs = node.attributes
            Array.from(attrs).forEach(attr => {
                const nodeName = attr.nodeName
                const nodeValue = attr.nodeValue
                if (nodeName === 'v-text') {
                    node.innerText = data[nodeValue]
                    // 收集更新函数 订阅操作 当对象对应属性修改的时候 执行回调
                    Dep.Collect(nodeValue,()=>{
                        console.log(`当前您修改了 ${nodeValue}`)
                        node.innerText = data[nodeValue]
                    })
                }
                // 实现 v-model
                if(nodeName === 'v-model'){
                    // 调用dom操作 给 input 标签绑定数据
                    node.value  = data[nodeValue]
                    // 收集更新函数
                    Dep.Collect(nodeValue,()=>{
                        node.value  = data[nodeValue]
                    })
                    // 监听 input事件 在事件回调中 拿到最新的输入值 赋值给绑定的属性
                    node.addEventListener('input',(e)=>{
                        let val = e.target.value
                        data[nodeValue] = val
                    })
                }
            })
        }
    })
}

Object.keys(data).forEach((key) => {
    defineReactive(data, key, data[key])
})

function defineReactive(data, key, value) {
    Object.defineProperty(data, key, {
        get() {
            return value
        },
        set(newValue) {
            if (newValue === value) return
            value = newValue
            // compile()
            // 在这里 进行精准更新 -> 通过data 中的属性名 找到对应的更新函数依次执行
            Dep.trigger(key)
        }
    })
}

compile()
console.log(Dep)

- 7 - 整体总结

1. Object.defineProperty
      ES 6 提供原生的方法
      Object.defineProperty(data,'name',()=>{
          // 访问 data.name 属性 会自动调用 返回值 为 data.name的值
          get(){
          },
          // 设置一个对象属性的时候 自动调用的函数 会把设置的最新的值 当成实参传入set函数
          // data.name = 'hahaha'
          set(newValue){
          // 只要数据发生变化 我们可以做我们想做的任何事情
          // ajax console.log() 操作dom
          }
      })
     核心API
 2. 数据反应到视图
 数据的变化可以引起视图的变化 (通过操作dom把数据放到对应的位置上去 如果数据变化之后 就用数据最新的值 在重新放一次)

 方案一 : 命令式操作
 1. document.querySelector('#app p').innerText = data.name
 2. set 函数中重新执行一下 document.querySelector('#app p').innerText

 方案二 : 声明式操作
 v-text 指令的实现
 <p v-text='name'></p>
 核心逻辑 : 通过 “ 模板编译” 找到标记了v-text 的元素 然后把 对应的数据通过操作dom api放上去
 <div id='app'>
    <p v-text='name'></p>
 </div>
 1. 通过app 根元素找到所有的子节点(元素节点 文本节点) -> dom.nodeChilds
 2. 通过节点类型筛选出元素节点 p -> nodeType 1 元素节点 3 文本节点
 3. 通过v-text 找到需要设置的具体的节点 <p v-text></p>
 4. 找到 绑定了 v-text 标记的元素 拿到他身上所有的属性 id class  v-text
 5. 通过 v-text = 'name' 拿到指令类型 'v-text' 拿到需要绑定的数据属性名 ‘name’
 6. 判断当前是v-text 指令 让后通过操作dom api 把name 属性对应的值放上去 node.innerText = data[name]
  以上整个过程称之为 模板编译

 3.视图的变化反应到数据
 imput 元素 v-model 双向绑定
 M-V
 V-M
 1. M-V
     1. 通过app 根元素找到所有的子节点(元素节点 文本节点) -> dom.nodeChilds
     2. 通过节点类型筛选出元素节点 p -> nodeType 1 元素节点 3 文本节点
     3. 通过v-text 找到需要设置的具体的节点 <p v-text></p>
     4. 找到 绑定了 v-text 标记的元素 拿到他身上所有的属性 id class  v-text
     5. 通过 v-model = 'name' 拿到指令类型 'v-model' 拿到需要绑定的数据属性名 ‘name’
     6. 判断当前是v-model 指令 让后通过操作dom api 把name 属性对应的值放上去 node.value = data[name]
     v-model 和 v-text 除了指令类型不一致 使用的dom api 不一致 其他的步骤都是完全一致的

2. V-M
  本质: 事件监听 在回调函数中 拿到 input 中 输入的最新的值 然后赋值给绑定的属性
  node.addEventListener ('input',(e)={
      data[name] = e.target.value
  })

  以上 总结
  1. 数据的响应式
  2. 数据变化影响视图
  3.视图变化影像数据
  4.指令是如何是实现的

  优化工作:
  1. 通用的数据响应式处理
  data(){
      return{
          name:“cp”,
          age:28
          }
  }
  基于现成的数据 然后都处理成响应式数据
  Object.keys(data).forEach(key=>{
      // key 属性名
      // data[key] 属性值
      // data 源对象
      // 将所有的key都转成get和set的形式
      defineReactive(data,key,data[key])
  })

  function defineReactive(data,key,value){
      Object.defineProperty(data,key,{
          get(){
              return value
          }
          set(newValue){
              value = newValue
          }
      })
  }
  2. 发布订阅
  问题:
  <div>
      <p v-text='name'> </p>
      <p v-text='name'> </p>
      <input v-model='name'> </input>
  </div>
  name 发生改变之后 我需要做的事情是 更新两个p标签 而现在不管更新了哪个数据 所有的标签都会被重新
  操作赋值 无法做到精准更新

  解决的问题
  1. 数据发生变化之后 最关键的代码是什么?
  node.innerText = data[name]

  2. 设计一个存储结构
  每一个响应式数据可能被多个标签绑定 是一个 一对多的关系
  {
      name :[()=>{node(p1).innerText = data[name],}]
  }

  发布订阅(自定义事件) 解决的问题就是 ‘1 对多的问题’

  实现简单的发布订阅模式
  浏览器的事件模型
  dom.addEventListener('click',()=>{})
  只要调用click 事件 所有绑定的回调函数都会执行 显然是一个一对多的关系

  const Dep = {
      map:{},
      collect(eventName,fn){
          if(!data[eventName]){
              this.map[eventName] = []
          }
            this.map[eventName].push(fn)
      },
      trigger(eventName){
          this.map[eventName].forEach(fn=>fn())
      }
  }

  使用发布订阅模式 进行优化
      先前的写法 不管是哪个数据发生变化 都是粗暴地执行 compile 函数

      使用发布订阅模式之后 ,compile 函数初次执行的时候 完成更新函数的收集
      然后在数据变化的时候 通过数据的key 找到相对应的更新函数 依次执行 达到精准更新的效果
  node.innerText = data[name]

  2. 设计一个存储结构
  每一个响应式数据可能被多个标签绑定 是一个 一对多的关系
  {
      name :[()=>{node(p1).innerText = data[name],}]
  }

  发布订阅(自定义事件) 解决的问题就是 ‘1 对多的问题’

  实现简单的发布订阅模式
  浏览器的事件模型
  dom.addEventListener('click',()=>{})
  只要调用click 事件 所有绑定的回调函数都会执行 显然是一个一对多的关系

  const Dep = {
      map:{},
      collect(eventName,fn){
          if(!data[eventName]){
              this.map[eventName] = []
          }
            this.map[eventName].push(fn)
      },
      trigger(eventName){
          this.map[eventName].forEach(fn=>fn())
      }
  }

  使用发布订阅模式 进行优化
      先前的写法 不管是哪个数据发生变化 都是粗暴地执行 compile 函数

      使用发布订阅模式之后 ,compile 函数初次执行的时候 完成更新函数的收集
      然后在数据变化的时候 通过数据的key 找到相对应的更新函数 依次执行 达到精准更新的效果
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

貂蝉的腿毛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值