阅读 119

【最简系列】手写一个vue的响应式原理 | 不贴源码,只求易懂

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

大家好,欢迎来到【最简系列】
该系列将会秉承用最简单的代码描述复杂原理理念,力求做到不贴源码,只求易懂,帮助大家理解一些热门框架/技术栈中的重点功能,希望大家会喜欢。

前言

从Vue面世以来,其响应式的特点就被人津津乐道,相应的源码解析文章层出不穷,但还是有些童鞋读过这些文章后依然困惑不已,究其原因大多是因为文章中倾向于整段/完整的复制源码,虽然这样可以将vue/vuex的响应式内容讲得更加完整,但对基础较差的童鞋却是一种煎熬,因此就有了【最简系列】

思路速读

  1. 发布-订阅模式
  2. 通过劫持对象上的属性,实现发布订阅
  3. vue2和vue3分别通过defineProperty和proxy实现劫持

几行代码

let data = {
    name: ''
}

data = new Proxy(data, {
    get(target, key) {
        return target[key]
    },

    set(target, key, newValue) {
        target[key] = newValue
        console.log('执行发布', target);
    }
})
// 这时候的data, 已经是被劫持后的data了(人家不纯洁了啦)

data.name = 'ssss'
复制代码

新建一个js文件,打上这几行代码,然后运行这个js文件,这时候你会发现,你的控制台出现了执行发布 { name: 'ssss' }

也就是说,我们只要修改data的属性,就会触发console.log('执行发布', target);,那如果我把这行代码换成别的呢? 例如修改div中的内容? 这是不是就实现了响应式?

PS: 因为proxy写起来更简洁,所以我就用了proxy。

揪斗麻袋哟,教练这也太简单了,我还能往下学!

进阶写法

哎呀,高难度的来啦。。。并不,就算你想看,我都懒得写代码,所以还是随便写几行,凑合看吧

来,首先创建个新的js文件,就叫它vue.js吧

class Vue {
    // 这里接收的option 其实就是在new Vue时传进去的对象
    constructor(option) {
        this.$el = document.querySelector(option.el);
        this.$data = _proxy(option.data);
    }
    
    _proxy(data) {
        return new Proxy(data, {
            get(target, key) {
                return target[key]
            },
        
            set(target, key, newValue) {
                target[key] = newValue
                console.log('执行发布', target);
            }
        })
    }
}

复制代码

是的,我把它改成了类的写法(什么,你不知道什么是类? 停!别往下看了,出门左转js的es6特性大全)

接着,再创建个html文件。名字嘛,叫page.html ?

<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <div id="app"></div>

        <script src="./vue.js"></script>

        <script>
            const app = new Vue({
                el: '#app',
                data: {
                    name: '',
                }
            })

            app.$data.name = 'sss';// 模拟修改data里的数据,假装我是用methond写的好不好
        </script>
    </body>
</html>
复制代码

把这个page.html在浏览器里打开,你会在控制台里看到执行发布 {name: "sss"},欸?难道我实现了个vue? 就当作是吧

我们再回到刚才的vue.js文件里,接下来再写几个类

class Vue {
    // 这里接收的option 其实就是在new Vue时传进去的对象
    constructor(option) {
        this.$el = document.querySelector(option.el);
        this.$data = _proxy(option.data);
    }
    
    _proxy(data) {
        return new Proxy(data, {
            get(target, key) {
                return target[key]
            },
        
            set(target, key, newValue) {
                target[key] = newValue
                console.log('执行发布', target);
            }
        })
    }
}


// 负责渲染模板(就是把你的data里的内容对应到div中)
class Compiler{
    compile() {
        //初始化渲染模板,并把每一个绑定数据的元素节点,例如v-model,添加watcher
    }
    update() {
        // 重新渲染有变动的地方
    }
}

// 管理所有的watcher
class Dep {
    notify() {
        //通知每个一个watcher
        Watcher.update()
    }
}

class Watcher {
    update() {
        // 进行新值、旧值比对(万一你往data属性里赋得值是一样呢?那肯定不用重新渲染呀)
        // 通知Compiler这个类,重新渲染模板
        Compiler.update()
    }
}

复制代码

没想到吧,每个类都这么空? 因为说好的不贴源码啊🤣

再列个图,清楚点

先是第一遍初始化

graph TD
初始化 --> 劫持data里的数据并添加进Watcher也就是收集依赖 --> 交由dep来管理 --> 通过Compiler编译template的内容并渲染,尤其是解析了各种v指令

然后后续数据有变动

graph TD
改动了data里的某个数据 --> dep通知各个watcher --> watcher会先进行比对看看自己管的这个数据有没有变动 --> 通知Compiler去把更新后的数据再次渲染出来

关于发布-订阅模式,有的人也会称它为观察者模式, 当然,两种其实也有一些细微的差别,不过,设计模式本身并不是僵硬的模板,尤其是在光怪陆离的各种场景中,层出不穷的变形写法往往难以准确界定两者的分界线

结语

以上就是实现响应式的核心思路啦,不过不要问我是vue2还是vue3哦,因为我可能混在一起啦🤣

另外其实学习vue的响应式,其目的并不是要求你徒手写个vue出来(太吓人了好吗?)或者复述响应式源码出来(说,你是不是在准备提桶跑路了?)

而是希望起到两个作用:

  1. 帮助你更好的进行vue的开发工作:例如在实际的vue项目开发中,你往一个对象中添加了新的属性,却发现并没有响应,不要着急休息一会儿啊不,脑子转一会儿,原来是vue2用defineProperty劫持对象上的属性,而你添加的新属性没被劫持到,当然不会有反应啦
  2. 学习设计思路,或许哪天你要编写一些东西时,会突然想到: 对哦,为什么不问问神奇的观察者呢?

对了,其实我在⚡qiankun微前端中的应用通信-不受框架限制的响应式数据管理 (juejin.cn)就利用了发布-订阅设计模式编写了一个工作中需要的响应式数据管理模块哦,要不要看看呢?(顺便给个点赞、收藏、关注?)

文章分类
前端
文章标签