zoukankan      html  css  js  c++  java
  • [切图仔救赎]炒冷饭--在线手撸vue2响应式原理

    --图片来源vue2.6正式版本(代号:超时空要塞)发布时,尤雨溪推送配图。

    前言

    其实这个冷饭我并不想炒,毕竟vue3马上都要出来。我还在这里炒冷饭,那明显就是搞事情。

    起因:

    作为切图仔搬砖汪,长期切图jq一把梭。重复繁琐的切图,让自己陷入了一个无限的围城。想出去切图这个围城看一看,但是又害怕因为切图时间久了,自己会的也只有切图了。

    为了后面能够继续搬砖恰饭,帮助自己跳出切图仔的围城。也去看了vue相关文档,当时记忆深刻觉得还行。可是G胖这个时候发动小紫本和打折魔咒,不知不觉又沉迷于DOTA小本子上面了。关于vue响应式原理很快忘得一塌糊涂,只记得一个属性Object.defindProperty,然后就没有然后了......

    为了避免自己后面再次忘记,所以这里炒一个冷饭加深记忆。

    炒vue2冷饭

    响应式vue

    在讲解vue响应式的原理之前,让我们来一段Vue代码作为示例:

    <div id="app">
      <div>主食: {{ food }}</div>
      <div>饮料: {{ drink }}</div>
      <div>菜单: {{ menu }}</div>
    </div>
    <script>
      let vue = new Vue({
        el: '#app',
        data: {
          food: '煎饼果子',
          drink: '热豆浆'
        },
        computed: {
          menu() {
            return  this.food + this.drink
          }
        }
      })
    </script>
    
    

    fooddrink发生变化后,Vue会做两件事:

    • 在页面上更新fooddrink的值。

    • 再次调用menu, 重新计算food + drink的值, 并在页面上面更新。

    更新值+计算值做的事情其实很简单,几行代码的事情。问题是当food或者drink变化时,Vue是怎么知道谁变化,然后马上响应其行为,去执行那"简单的几行代码"?

    所以,当看到Vue案例时,词穷的我当时第一反应就是牛皮

    牛批

    之所以发出感叹,是因为通常的JavaScript代码是实现不了这样的功能的。话不多说,让我们直接上代码来说明:

        let food = "煎饼果子"
        let drink = "热豆浆"
        let menu = null
        menu = food + drink
        food = '炸鸡汉堡'
        drink = '快乐水'
        console.log(menu) 
    

    最终控制台打印结果:

    煎饼果子热豆浆
    

    如果是在Vue当中,fooddrink发生了变化,那么Vue会跟着做出响应动作,从而在控制台输出我们想要的结果:

    炸鸡汉堡快乐水
    

    菜单响应

    这里就出现第一个问题,当food或者drink 发生变化之后,menu并不会响应其变化。这个时候就需要我们来解决这个问题,满足menu响应。

    借鉴Vue一样,我们先把menu的计算方法。也写成一个函数,取名为target。然后每次food或者drink变化的时候调用target函数

        let food = "煎饼果子"
        let drink = "热豆浆"
        let menu = null
        let target = () => {
            menu = food + drink
        }
        target() // 初始化菜单menu
        food = '炸鸡汉堡'
        drink = '快乐水'
        target()
        console.log(menu) 
    

    控制台输出:

    炸鸡汉堡快乐水
    

    浴室沉思

    前面一把梭直接调用的满足menu响应的问题,但是也间接留下一个新的疑惑点。这里针对一个菜单,就写了一个target。假设有多个菜单需要响应呢?

    例如:

    • 单人早餐 = 煎饼果子 + 热豆浆
    • 豪华套餐: 煎饼果子加两鸡蛋 + 热豆浆 + 油条一根午餐
    • ......

    如果这个时候切换成:

    • 单人午餐 = 炸鸡汉堡 + 快乐水
    • 豪华套餐: 双层炸鸡汉堡 + 快乐水 + 快乐薯条一包
    • ......

    按照前面的逻辑, 估计得写N个target。这个时候响应式又是一个麻烦事情,可是有句话说的好。梭哈一时爽,一直梭哈一直爽。既然前面直接采用target一把梭完成,所以针对N个target方法,我也可以直接来个for循环一把梭能完成响应式问题。

    for循环一把梭

    • 定义一个数组,每定义了一个target函数。就存储到数组当中。
    let storge = [] // 用来存储target
    function record (){  // 
      storge.push(target)
    }
    
    • 定义循环函数,每次data有变更。就调用这个函数,进行一把for循环.
    function replay (){
      storge.forEach(run => run())
    }
    
    • 合并成完整的代码:
        let food = "煎饼果子"
        let drink = "热豆浆"
        let menu = null
        food = '炸鸡汉堡'
        drink = '快乐水'
        let target = () => {
            menu = food + drink
        }
        let storge = []; //用来存储更多的target
        function record(target) {
            storge.push(target)
        }    
        function replay() {
            storge.forEach(run => run())
        }
        record(target)
        replay()
        food = '炸鸡汉堡'
        drink = '快乐水'
        replay()
        console.log(menu)
    

    最后控制台成功输出:

    炸鸡汉堡快乐水
    

    Dep依赖类

    通过一把梭实现功能,那么接下来就开始思考优化部分了。继续记录target这类的代码,这样有点怪怪的。为了后面方便管理,我们把代码进行简单的优化,封装成一个类:

        class Dep {
            constructor() {
                this.subs = []
            }
            // 收集依赖
            depend(sub) {
                if (sub && !this.subs.includes(sub)) {  // 做一个判断
                    this.subs.push(sub)
                }
            }
    
            notify() {
                console.log("暗号:下雨啦,收衣服啦!")
                this.subs.forEach(sub => sub()) // 运行我们的target
            }
        }
    

    就这样target函数存储在类的subs中,record也变成了depend,使用notify来代替replay

    封装成类之后,每次当data数据更新的时候,就会发出一个暗号下雨啦,收衣服啦! 然后就开始遍历运行相应的target依赖了。

    新的调用代码就更加清晰明了:

        let dep = new Dep()
        let food = "煎饼果子"
        let drink = "热豆浆"
        let menu = null
        let target = () => {
            menu = food + drink
        }
        dep.depend(target)
        target() // 完成menu第一次初始化
        console.log(menu)
        food = '炸鸡汉堡'
        drink = '快乐水'
        dep.notify()
        console.log(menu)
    

    控制台输出:

    煎饼果子热豆浆
    暗号:'下雨啦,收衣服啦!'
    炸鸡汉堡快乐水
    

    观察者亮相

    当前的代码,是确定一个依赖事件,就定义target,然后调用依赖类dep.depend将其存储起来。

    let target = () => { menu = food + drink }
    dep.depend(target)
    target()
    

    这个时候又新来一个target事件又该如何做:

    新添加一个target事件?

    let target2 = () => { 新的依赖事件 }
    dep.depend(target2)
    target2()
    

    要是有几百个依赖,那还不得上天。我估计要是这样写代码,估计你的同事要说你写代码像CXK

    观察者函数

    借鉴观察者模式,封装一个watcher函数. 帮你观察记录相关target事件,避免多次声明变量。

        function watcher(myFun) {
            target = myFun
            dep.depend(target)
            target()
            target = null
        }
        watcher(() => {
            menu = food + drink
        })
    

    正如你所看到的,watcher函数接受myFunc参数,将其赋给全局的target上,调用dep.depend()将其添加到数组里,之后调用并重置target

    既然又封装一个新的函数,那么验证又将是必不可少的了。这里我们修改一下drink来试试:

    drink = "快乐水"
    console.log(menu)
    dep.notify()
    console.log(menu)
    

    控制台输出结果:

    煎饼果子热豆浆
    暗号:下雨啦,收衣服啦!
    煎饼果子快乐水
    

    Object.defineProperty()

    基本用法

    铺垫了这么久,一个关键性角色这个时候也登场了。

    该方法允许精确添加或修改对象的属性。通过赋值操作添加的普通属性是可枚举的,能够在属性枚举期间呈现出来(for...in 或 Object.keys 方法), 这些属性的值可以被改变,也可以被删除。这个方法允许修改默认的额外选项(或配置)。默认情况下,使用 Object.defineProperty() 添加的属性值是不可修改的。
    --《MDN文档》

    不明觉厉? 那就先热身一下,进入快乐的举例子环节:

        let data = {
             food: '煎饼果子',
             drink: '热豆浆'
        }
        Object.defineProperty(data, 'food', {
            get() {
                console.log(`触发get方法`)
        },
            set(newVal) {
                console.log(`设置food为${newVal}`)
            }
        })
    data.food
    data.food = 炸鸡汉堡 
    

    控制台输出:

    触发get方法
    设置food为炸鸡汉堡
    

    简单封装

    但是仅仅凭借object.defineProperty是无法完成当一个数据更新了,完成数据响应。而且代码这里也是只是对food做了一个处理, 还有drink没有处理,所以为了完成data所以属性都做相应的处理。接下来就是对于Object.defineProperty()进行简单的封装处理了:

        Object.keys(data).forEach(key => {
            let value = data[key]
            Object.defineProperty(data, key, {
                get() {
                    return value
                },
                set(newVal) {
                    value = newVal
                }
            })
        })
    

    遍历了data每个属性,然后对每个属性进行侦听。这样data的属性一旦改变,就会自动发出通知.

    代码整合

    前面零零散散分别讲了 Depwatcherobject.defineProperty, 那么接下来就让我们把这个几个部分整合到一起,完整查看整个代码:

        let data = {
            food: '煎饼果子',
            drink: '热豆浆'
        }
        class Dep {
            constructor() {
                this.subs = []
            }
            // 收集依赖
            depend(sub) {
                if (sub && !this.subs.includes(sub)) { // 做一个判断
                    this.subs.push(sub)
                }
            }
    
            notify() {
                console.log("暗号:下雨啦,收衣服啦!")
                this.subs.forEach(sub => sub()) // 运行我们的target
            }
        }
        Object.keys(data).forEach(key => {
            let value = data[key]
            let dep = new Dep()
    
            Object.defineProperty(data, key, {
                get() {
                    dep.depend(target)
                    return value
                },
                set(newVal) {
                    value = newVal
                    dep.notify()
                }
            })
        })
    
        function watcher(myFun) {
            target = myFun
            // dep.depend(target)  这里修改,移动到Object.defineProperty当中去
            target()
            target = null
        }
        watcher(() => {
            data.menu = data.food + data.drink
        })
        console.log(data.menu)
        data.food = "炸鸡汉堡"
        data.drink = "快乐水"
        console.log(data.menu)
    

    控制台输出:

    煎饼果子热豆浆
    暗号:下雨啦,收衣服啦!
    炸鸡汉堡快乐
    

    这里完全实现了文章开头所提出的需求,每当fooddrink更新时,我们的menu也会跟着响应并更新。

    这时候Vue文档的插图的意义就很明显了:

    免责声明

    以上就是我的炒冷饭内容,怕忘记重写总结一下,有说错的地方多担待。(特拿前端劝退师骚声明一份,窥伺好久了。)

    意思就是写得略粗糙,别喷我。。。

    我是车大棒,我为我自己插眼。

  • 相关阅读:
    Leetcode Spiral Matrix
    Leetcode Sqrt(x)
    Leetcode Pow(x,n)
    Leetcode Rotate Image
    Leetcode Multiply Strings
    Leetcode Length of Last Word
    Topcoder SRM 626 DIV2 SumOfPower
    Topcoder SRM 626 DIV2 FixedDiceGameDiv2
    Leetcode Largest Rectangle in Histogram
    Leetcode Set Matrix Zeroes
  • 原文地址:https://www.cnblogs.com/chedabang/p/10947680.html
Copyright © 2011-2022 走看看