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文档的插图的意义就很明显了:

    免责声明

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

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

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

  • 相关阅读:
    HDU 5311
    HDU 1708
    HDU 1707
    计蒜之道 430
    Codeforces Round #292 (Div. 2)——C——Drazil and Factorial
    Codeforces Round #292 (Div. 2)——B——Drazil and His Happy Friends
    Codeforces Round #292 (Div. 2)——A——Drazil and Date
    Codeforces Round #293 (Div. 2)——C——Anya and Smartphone
    Codeforces Round #293 (Div. 2)——B——Tanya and Postcard
    Codeforces Round #293 (Div. 2)——A—— Vitaly and Strings
  • 原文地址:https://www.cnblogs.com/chedabang/p/10947680.html
Copyright © 2011-2022 走看看