[切图仔救赎]炒冷饭--在线手撸vue2响应式原理
来源:车大棒     阅读:454
源码超市
发布于 2019-06-11 06:11
查看主页
image

--图片来源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会做两件事:

升级值+计算值做的事情其实很简单,几行代码的事情。问题是当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) 

控制台输出:

炸鸡汉堡快乐水

浴室沉思

image

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

例如:

假如这个时候切换成:

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

for循环一把梭

let storge = [] // 用来存储targetfunction record (){  //   storge.push(target)}
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)

最后控制台成功输出:

炸鸡汉堡快乐水
image

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

image

观察者函数

借鉴观察者模式,封装一个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文档》

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

image
    let data = {         food: '煎饼果子',         drink: '热豆浆'    }    Object.defineProperty(data, 'food', {        get() {            console.log(`触发get方法`)    },        set(newVal) {            console.log(`设置food为${newVal}`)        }    })data.fooddata.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)

控制台输出:

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

这里完全实现了文章开头所提出的需求,每当food或者drink升级时,我们的menu也会跟着响应并升级。

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

image

免责公告

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

image

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

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

image
免责声明:本文为用户发表,不代表网站立场,仅供参考,不构成引导等用途。 系统环境 服务器应用
相关推荐
Android:事件分发机制源码解读与滑动冲突处理方案
APICloud开发者进阶之路|APICloud扩展板块
程序员的工资高,究竟程序员的工资有多高?你不理解的程序员!
前台自学路线图之Node.js与Ajax自学
在大数据里读懂京东,你东哥为啥要裁员降薪?
首页
搜索
订单
购物车
我的