Vue2源码共读-响应式原理

68 阅读3分钟

数据变化影响视图

核心就是数据和页面渲染关联起来,属性和页面怎么做关联?结合上篇文章的内容,这个流程我的理解,本质上就是数据更新的时候,重新在生命周期混合函数里面调用patch方法,重新生成新的一个dom返回给实例上面的el

Vue.prototype._update=function(vnode){
    const vm=this
    vm.$el=patch(vm.$el,vnode)
}
export function patch(oldVnode,vnode){
    if(oldVnode.nodeType==1){
        const parentElm=oldVnode.parentNode;
        let elm=createElm(vnode)
        parentElm.insertBefore(elm,oldVnode.nextSibling)
        parentElm.removeChild(oldVnode)
        return elm
    }
}
let vm=new Vue({
    data(){
        return {
            name: 'gm'
        }
    }
})
vm.$mount('#app')

setTimeout(()=>{
    vm.name='hello'
    vm._update(vm._render())
},1000) // 重新调用render方法,产生虚拟dom。

我们的抽象语法树,其实结构不需要变,我们要做的不过是每次数据改变的时候调用_update方法更新视图,目前还没有diff,不过是生成了一个新的el,将老的给他替换掉。

这里就重新引入一种新的概念,叫做观察者模式。属性是被观察者,被观察者发生改变的时候就重新渲染页面。这里切记,Vue渲染的是组件。现在就是先写的刷新页面,后面我再去研究怎么刷新组件的

export function mountComponent(vm,el){
    let updateComponent=()=>{
        vm._update(vm._render())
    }
    
    new Watcher(vm,updateComponent,()=>{
        console.log('更新视图了')
    },true) // 最后一个参数true标识这是不是一个渲染的watcher。和后面的options里面的watch区分开来
}
// src/observer/watcher.js
let id=0 // 监听器的唯一标识
// 每个组件渲染的时候都会有一个watcher
class Watcher{
    //vm,updateComponent,()=>{console.log('视图更新了'),true}
    constructor(vm,exprOrFn,cb,options){
        this.vm=vm
        this.exprOrFn=exprOrFn
        this.cb=cb
        this.options=options
        // 默认初始化让exprOrFn执行,exprOfFn方法是做了什么?(去vm上取值)
        this.getter=exprOrFn
        this.get()
        this.depsId=new Set()
        this.deps=[] // 这里就存放所有的dep
    }
    get(){ // 稍后用户更新时,可以重新调用getter方法
        // defineProperty.get 每个属性都可以收集自己的watcher
        pushTarget(this) // 一个属性对应多个watcher 同时一个watcher可以对应多个属性
        this.getter() // render方法会去vm上取值. vm._update(vm._render)
        popTarget() // Dep.target=null 如果Dep.target有值说明这个变量在模板中使用了。
    }
    update(){
            this.get()
    }
    addDep(dep){
        let id=dep.id
        if(!this.depsId.has(id)){
            this.depsId.add(id)
            this.deps.push(dep)
            dep.addSub(this)
        }
    }
}

为了实现一个watcher可以收集自己关联的子watcher。我们将这部分的逻辑抽离出来,写一个observer/dep.js

let id=0 // 给每个dep也分配一个唯一ID值
class Dep{ // 每个属性分配一个dep,dep可以用来存放watcher,watcher中还要存放这个dep
    constructor(){
        this.id=id++
        this.subs=[] // 用来存放watcher的
    }
    depend(){
        // dep里面要存放这个watcher,多对多的关系
        if(Dep.target){
            Dep.target.addDep(this)
        }
    }
    addSub(watcher){
        this.subs.push(watcher)
    }
    notify(){
        this.subs.forEach(watcher=>watcher.update())
    }
}
Dep.target=null // 全局仅此一份
function pushTarget(watcher){
    Dep.target=watcher
}
function popTarget(){
    Dep.target=null
}
export default Dep

然后我们需要当把变量绑定到实例上面去的时候,我们需要做小小的改变

function defineReactive(data,key,value){
    observe(value)
    let dep=new Dep()
    Object.defineProperty(data,key,{
        get(){
            if(Dep.target){ // 此值是在模板中取的
                dep.depend() // 让dep记住watcher
            }
            return value
        }
        
        set(newV){
            observe(newV)
            value=newV
        }
    })
}

一个组件对应一个watcher,一个属性对应一个dep这样我们的关联关系就写完了。这里主要就是实现了,当每个属性改变的时候,通知watcher更新dep

额外情况分析

我们在vm方法外部改变实例上的name

<div id="app">{{name}}</div>
<script>
let vm=new Vue({
    el: '#app',
    data(){
        return {
            name: 'hello world'
        }
    }
})
vm.name='你好世界'

</script>
// src/observe/index.js
function defineReactive(data,key,value){
    observe(value)
    let dep=new Dep()
    Object.defineProperty(data,key,{
        get(){
            if(Dep.target){
                dep.depend()
            }
            return value
        },
        set(newV){
            if(newV!==value){
                observe(newV)
                value=newV
                dep.notify() // 告诉当前属性存放的watcher去执行。
            }
        }
    
    })
    
}

现在我们就可以实现当属性值改变的时候自动改变页面上面的值这个功能了。但是这种方法还是不好,因为如果我在一个方法里面反复修改一个变量的值N次。一次执行N遍。我最后JS会重复的通知Dep和watcherN编。这个性能就很差,所以需要优化。下篇文章我们来研究下怎么在这个地方给他做个防抖操作

我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!