vue-响应式原理

1. 自己动手实现响应式

1.1. 原理

1.1.1. 两个问题

首先给你如下的一段代码,要实现响应式,你需要考虑什么问题?

<div>
    {{ message }} <!--这个人是张三-->
    {{ message }} <!--这个人是李四-->
    {{ message }} <!--这个人时候王五-->
    {{ name }}
</div>
<script>
    const app = new Vue({
        data: {
            message: 'hello',
            name: 'yu wan'
        }
    })
</script>
  • app.message 修改后,vue 内部如何监听 message 发生改变?
  • vue 监听到它发生改变,怎么通知页面中的使用者更新?

答案:Object.defineProperty() -> 监听对象属性的改变;通过发布-订阅模式 -> 让对应的使用者更新

1.1.2. Object.defineProperty()

下面看一下,Object.defineProperty 大致是怎么实现数据劫持的

    let data = { // 假设这是 Vue.data 对象 
    message: 'hello',
    name: 'yu wan'
}
Object.keys(data).forEach(key => {
    let value = data[key]
    Object.defineProperty(data, key, {
        set(newVal) {
            // 这里的逻辑应该是什么呢?既然外部通过 data.message = 'xxx' 来修改了它,那我就应该通知它的使用者更新数据塞
            // 如上面的 message,是张三、李四、王五在用,就要通知他们仨,这就需要发布-订阅模式了
            value = newVal
        },
        get() {
            // 这里的逻辑又应该是什么呢?既然页面能够显示 message 的值,说明这里就要记录哪些人用的是 message,哪些人用的是 name 塞
            return value
        }
    })
})

// 只要调用 data.name -> 就会触发 get()
// 只要调用 data.name = 'xxx' -> 就会触发 set()

1.1.3. 发布-订阅模式

那就先说说,发布-订阅模式这里面的一些属于吧(指的是 vue 源码中用到的名称)

  • 发布者(Dep):它就像个公众号,有新的文章发出来了,就会推送给所有的用户
  • 订阅者(Watcher):它就好比用户,都去关注某个公众号,有新的文章,用户就会去读这个文章
class Dep { // dependency-发布者
    constructor() {
        this.subs = [] // subscribes-存放它所有订阅者
    }

    addSub(watcher) {
        this.subs.push(watcher) // 用户添加订阅
    }

    notify() { // 给所有订阅者推送消息
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update() // 让所有的 message 用户修改它们的值
        }
    }
}

class Watcher { // 订阅者
    constructor(name) {
        this.name = name // 只是一个示例,区分不同订阅者
    }

    update() {
        console.log(this.name + '更新了!');
    }
}

// 相当于执行了上面 Object.keys(obj).forEach() 里的 get() 方法,就会让那几个人来订阅
const dep = new Dep()
const w1 = new Watcher('张三')
dep.addSub(w1)
const w2 = new Watcher('李四')
dep.addSub(w2)
const w3 = new Watcher('王五')
dep.addSub(w3)

// 一旦修改 data.message ,就在 set() 那里通知所有人更新
dep.notify() // 说明每个字段都有一个 dep,如 message 有一个 dep 记录着,name 有一个 dep 记录着

1.1.4. el

el 需要干啥呢?

  • 正如第一段代码,我们传给了 Vue() 一个 el 属性,肯定要把 #app 里面的 {{}} 用法都解析出来,然后再展示里面的数据
  • 还有一个问题,我们怎么知道 message 就刚好三个人用呢?,因此在解析的时候还需要根据{{}} 来生成对应的 Watcher,并添加到相应的 Dep 中

1.1.5. 原理图

这个时候,把原理图拿出来,应该都能够明白了吧 🥰

  • 把 data 交给 Observer,它来劫持数据,然后为每个字段(message、name)生成相应的 Dep 对象
  • el 读取到 #app 的内容,把里面的字段({{name}}、{{message}})都生成 Watcher,然后分别订阅 Dep(即加入到相应的 Dep 对象中)
  • Observer 里 set() 那里监听到变化,就通知对应的 dep,然后 dep.notify() 就会让所有使用 message 的人更新数据

在这里插入图片描述

1.2. 实践

说了这么多,下面就来写一个最简单的双向绑定,先准备好我们的 HTML🤗

<div id="app">
    <!-- https://developer.mozilla.org/zh-CN/docs/Web/API/Node ,这里因为换行了,所以 app 有三个子节点 Text、input、{{username}}   -->
    <input type="text" v-model="username"/>{{ username }}
</div>

1.2.1. 所有的类写出来

先写出这五个必须用到的类:VueObserverDepWatcherCompiler,其他的逻辑下一步再写

<script>
    class Vue {
        constructor(options) {

        }
    }

    class Observer {
        constructor() {
        }
    }

    class Dep {
        constructor() {
        }
    }

    class Watcher {
        constructor() {

        }
    }

    class Compiler {
        constructor() {
        }
    }
</script>

1.2.2. 基础逻辑

根据上面的原理图,我们知道 new Vue({}) 的时候,是分两步走的,一部分是去交给了 Observer,另一部分是把 el 拿去解析了,因此可以添加下面的一些代码


<script>
    class Vue {
        constructor(options) {
            // 保存数据
            this.$options = options
            this.$el = options.el
            this.$data = options.data

            new Observer(this.$data) // 1.
            new Compiler(this.$el, this) // 2.可想而知,我们需要在 $el 获取到页面元素后,也能够拿到 vm(this).data 的值才能够显示,因此需要传过去
        }
    }

    class Observer {
        constructor(data) { // 3.Observer 是用来进行数据劫持的,因此我们应该添加下面的代码
            this.data = data

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

        defineReactive(data, key, val) { // 4.封装了一下
            const dep = new Dep() // 5.每个字段都会有自己的 Dep 嘛,所以这里 new 一个
            Object.defineProperty(data, key, {
                enumerable: true,
                configurable: true,
                get() {
                    if(Dep.target){ // 13.这里要统计哪些人使用了 message/name 分别加入到他们的 Dep 中 
                        dep.addSub(Dep.target) // 可以统计人数了
                    }
                    return val
                },
                set(newVal) {
                    if (val === newVal) return;
                    val = newVal // 这必须要噢,不然值修改不了,可以测试上面小节,如果不写是无法更新的
                    dep.notify() // 这里就应该是通知所有使用者更新了
                }
            })
        }
    }

    const regex = /\{\{(.*)\}\}/

    class Compiler {
        constructor(el, vm) { // 7.
            this.el = document.querySelector(el)
            this.vm = vm

            this.frag = this._createFragment() // 8.什么叫文档片段呢?我查了一下,就像个占位符,子节点会填充它;然后它存在于内存中,子节点填充它时不会引起页面回流(也叫重排,就是计算这部分元素的位置、尺寸等信息),因此性能更好
            this.el.appendChild(this.frag) // 14.相当于把 #app 的内容创建一份文档片段,然后填充给它
        }

        _createFragment() { // 创建文档片段
            const frag = document.createDocumentFragment()
            let child;
            while (child = this.el.firstChild) {
                this._compile(child)
                frag.appendChild(child) // 9.分别把 input Text 添加至 frag 里,最后 this.el.childNodes 就会为空,因此就退出循环啦
                // https://segmentfault.com/a/1190000009912513 这里说明了 frag 添加一个现有节点,原节点会被删除(这也就是为什么打包后的文件会替换app内容的一种方式吧)
            }
            return frag
        }

        _compile(node) { // 解析每个节点
            if (node.nodeType === 1) { // 元素节点
                const attrs = node.attributes
                if (attrs.hasOwnProperty('v-model')) {
                    const name = attrs['v-model'].nodeValue // 获取 v-model 的值 username
                    // 输入框绑定初值
                    node.value = this.vm[name] // 10.这个输入框类型是 HTMLInputElement,只能通过 value 设置它的值,无法通过 nodeValue 设置它的值 https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLInputElement
                    node.addEventListener('input', e => {
                        this.vm[name] = e.target.value // 捕获事件,绑定值到 vm 属性上
                    })
                }
            }
            if (node.nodeType === 3) { // 文本节点
                if (regex.test(node.nodeValue)) {
                    const name = RegExp.$1.trim() // {{ username }} ,$1就是正则表达式 (a)(b) 第一组括号的值,然后去掉两边的空格,就得到 username
                    new Watcher(node, name, this.vm) // 11.然后就可以把这个 {{username}} 生成一个 Watcher 了
                }
            }
        }
    }

    class Dep {
        constructor() {
            this.subs = []
        }

        addSub(watcher) {
            this.subs.push(watcher)
        }

        notify() {
            const subs = this.subs.slice() // slice 会返回一个新数组
            for (let i = 0, l = subs.length; i < l; i++) {
                subs[i].update()
            }
        }
    }

    class Watcher {
        constructor(node, name, vm) {
            // 因为它需要某个设置某个节点的值为 vm[name] ,所以要这三参数
            this.node = node
            this.name = name 
            this.vm = vm
            Dep.target = this
            this.update() // 给 {{message}} 绑定初值
            Dep.target = null // 因为当 input 改变,它就会触发 get() 这个时候不置为空就会把最后这个watcher 又给添加到它对应的 dep 里了,这显然是不对的呀
        }
        update(){
            this.node.nodeValue = this.vm[this.name] // 12.this.vm[this.name] 就相当于调用 data.username 因此会触发 get() 方法,所以就可以在那里统计人数了
        }
    }
</script>

1.2.3. 代理

现在上面的代码,执行会发现如下结果,其实原因很简单 绑定初值:node.value = this.vm[name],我们有给 app.message 赋值吗,或者说有给它添加这个属性吗?当然没有
所以这里就需要一个代理,来通过 app.message 绑定到 app.data.message

在这里插入图片描述

    class Vue {
        constructor(options) {
            // ...
            // 代理所有的 data 属性
            Object.keys(this.$data).forEach(key=>{
                this._proxy(key)
            })
            // ...
        }
        _proxy(key){
            Object.defineProperty(this, key, { // 定义 this.username
                enumerable: true,
                configurable: true,
                get() {
                    return this.$data[key]
                },
                set(v) {
                    this.$data[key] = v
                }
            })
        }
    }

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

1.2.4. 恭喜

经过以上的操作,相信你已经掌握了基本的原理了,接下来去学习更多吧!😁

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值