从0实现Vue响应式原理

277 阅读1分钟

响应式原理也就是vue管理的数据变化,会通知相应的视图更新。

我们都知道vue的响应式是基于Object.definePreperty实现的,但他究竟怎么观测data,并响应视图的变化。我们先看一个例子:

    <div id="app"></div>
    <script>
        class Vue {
            constructor(options) {
                this.$el = options.el
                this.data = options.data
                this.template = options.template
                this.init()
            }
            init() {
                this.defineReactive()
                this.notify()
            }
            defineReactive() {
                for (const key of Object.keys(this.data)) {
                    //通过this[key]访问到data上的数据
                    Object.defineProperty(this,key, {
                        get(){
                            return this.data[key]
                        },
                        set(value){
                            this.data[key] = value
                            this.notify()
                        }
                    })
                }
            }
            render(){
                let dom = document.querySelector("#app")
                //这里大致模拟了一下template中获取data中的动态数据
                let tmp = this.template.replace(/{{this.(\w+)}}/g,(match,value)=>{
                    return this[value]
                })
                dom.innerHTML = tmp
            }
        }
        let app = new Vue({
            el:"#app",
            template:`<div>{{msg}}</div>`,
            data:{
                msg:"hello!!"
            }
        })
        let dom = document.querySelector("#app")
        dom.onclick = function(){
            app.msg = "world!!"
        }
    </script>

虽然实现了,但是乍一看,怎么这么辣眼睛呢,漏洞百出,没关系,我们一步一步完善它。

首先想一个问题,如果我们data中有a和b两个属性,但是模板中值引用了a,那么我改变b的属性页面会渲染吗?不会,闭着眼睛就知道。但是我们这个最初版的响应式还不支持啊,赶紧修复了,不然要挨产品大哥的揍。

修复Bug不能急急忙忙,考虑清楚了,不然返工就很蛋疼了。想了很久......

我们最终的目的,只有模板中动态数据变化的时候才触发更新。所以我们给每个属性,派遣了个小弟,他叫Dep,他用于跑腿的,也就是通知页面更新。但是我们现在页面更新的逻辑写在vue类里面了,感情他两不是一路人,赶紧拆散他们,于是更新页面的逻辑自立门派叫做Watcher

画个图总结一下,我们接下来要做的改动,年纪大了容易忘事。




引入了两个新的伙伴Dep和Watcher


        //收集依赖
        class Dep {
            constructor() {
                this.watchers = []
            }
            addWatcher(watcher){
                //相同watcher不能重复添加
                if(!this.watchers.include(watcher)){
                    this.watchers.push(watcher)
                }
            }
            notify(){
                this.watchers.forEach(watcher=>{
                    watcher.render()
                })
            }
        }
        //派发更新
        class Watcher {
            constructor(vm) {
                this.vm = vm
            }
            render() {
                const vm = this.vm
                let dom = document.querySelector(vm.$el)
                let tmp = vm.template.replace(/{{(\w+)}}/g, (match, value) => {
                    return vm[value]
                })
                dom.innerHTML = tmp
            }
        }
        class Vue {
            constructor(options) {
                this.$el = options.el
                this.data = options.data
                this.template = options.template
                this.init()
            }
            init() {
                this.defineReactive()
            }
            mount(){
                this.watcher = new Watcher(this)
                this.watcher.render()
            }
            defineReactive() {
                for (const key of Object.keys(this.data)) {
                    const dep = new Dep()
                    Object.defineProperty(this, key, {
                        get() {
                            dep.addWatcher(this.watcher)
                            return this.data[key]
                        },
                        set(value) {
                            this.data[key] = value
                            dep.notify()
                        }
                    })
                }
            }
            
        }
        let app = new Vue({
            el: "#app",
            template: `<div>{{msg}}=={{value}}</div>`,
            data: {
                msg: "hello!!",
                value:"111"
            }
        })
        //在vue中渲染一个组件,共两个步骤
        //1.new Components:初始化的一些操作
        //2.mount:把template元素挂载到页面上
        app.mount()
        let dom = document.querySelector("#app")
        dom.onclick = function () {
            app.msg = "world!!"
            app.value = "222"
        }

到这基本的响应流程算是走通了,开心了一分钟,又发现了一个新问题,笑容渐渐消失。

如果第一次渲染a,b两个属性的dep收集了watcher,然后再次一次更新操作后b属性不在template模板中了,但是b属性变化还会触发watcher的更新。我们岂能容忍我们的代码里有这样的问题出现,趁别人没发现,赶紧悄悄的改了,于是又想了很长一段时间......

我们只有在render结束后,才能知道这次渲染依赖的哪些动态数据,所以dep收集的过程要在render之后,上面代码的逻辑是在访问到的时候就收集。

先想一下我们将要改动的思路,只有在render结束之后才能知道依赖的动态数据,所以我们可以把本次依赖的数据替换掉上次依赖的动态数据就行了,不可以,因为一个动态数据可能收集了多个watcher,例如vuex中的数据可以在多个组件中使用,就收集了多个watcher。所以我们遍历一遍上次依赖的数据如果在本次渲染中没有依赖就把它的依赖清楚掉。

知道思路,那就开干把,一顿操作

        //收集依赖
        class Dep {
            constructor() {
                this.watchers = []
            }
            depend(watcher){
                watcher.addDep(this)
            }
            addWatcher(watcher){
                //相同watcher不能重复添加
                if(!this.watchers.includes(watcher)){
                    this.watchers.push(watcher)
                }
            }
            removeWatcher(watcher){
                this.watchers.splice(this.watchers.indexOf(watcher),1)
            }
            notify(){
                this.watchers.forEach(watcher=>{
                    watcher.render()
                })
            }
        }
        //派发更新
        class Watcher {
            constructor(vm) {
                this.vm = vm
                this.oldDeps = []
                this.newDeps = []
            }
            addDep(dep){
                //相同dep不能重复添加
                if(!this.newDeps.includes(dep)){
                    this.newDeps.push(dep)
                }
            }
            depend(){
                //清楚不再依赖的动态数据
                while(this.oldDeps.length){
                    let dep = this.oldDeps.pop()
                    if(!this.newDeps.includes(dep)){
                        dep.removeWatcher(this)
                    }
                }
                //通知dep收集
                this.newDeps.forEach(dep=>{
                    dep.addWatcher(this)
                })
                this.oldDeps = this.newDeps
                this.newDeps = []
            }
            render() {
                const vm = this.vm
                let dom = document.querySelector(vm.$el)
                let tmp = vm.template.replace(/{{(\w+)}}/g, (match, value) => {
                    return vm[value]
                })
                dom.innerHTML = tmp
                this.depend()
            }
            
        }
        class Vue {
            constructor(options) {
                this.$el = options.el
                this.data = options.data
                this.template = options.template
                this.init()
            }
            init() {
                this.defineReactive()
            }
            mount(){
                this.watcher = new Watcher(this)
                this.watcher.render()
            }
            defineReactive() {
                for (const key of Object.keys(this.data)) {
                    const dep = new Dep()
                    Object.defineProperty(this, key, {
                        get() {
                            //通知watcher把需要收集的dep临时存起来
                            dep.depend(this.watcher)
                            return this.data[key]
                        },
                        set(value) {
                            this.data[key] = value
                            dep.notify()
                        }
                    })
                }
            }
        }

代码能跑了吗,这是个玄学问题,那我们测试一下吧

       let app = new Vue({
            el: "#app",
            template: `<div>{{msg}}=={{value}}</div>`,
            data: {
                msg: "hello!!",
                value:"111"
            }
        })
        app.mount()
        let dom = document.querySelector("#app")
        dom.onclick = function () {
            //代表更新之后不再依赖value,然后更新
            app.template = `<div>{{msg}}</div>`
            app.msg = "word!!"
            
            //再次更新看value的dep中是否有收集watcher
            setTimeout(()=>{
                app.value = "222"
            },1000)
        }


发现watchers为空了,上一次收集的watcher,更新之后没有再收集了。

不同的功能分成不同的模块用webpack简单做下打包:github github.com/13866368297…

打完收工,代码可能看起来像纸糊的一样,主要带大家过一遍响应式的流程,学习里面的思想。

有问题望指正,谢谢大家的观看!!!