vue中对象的双向数据绑定

1、变化侦测

     vue.js会自动通过状态生成DOM,并显示在页面上,这个过程叫渲染。vue.js渲染过程是声明式的,我们通过模板来描述状态与DOM之间的映射关系。通常,在运行时应用内部会不断发生变化,需要不停的渲染,此时需要变化侦测来确定状态中发生了什么变化。

     变化侦测一种是“推”,一种是“拉”。Angular和React中的变化侦测都属于“拉”,即当状态发生变化时,它不知道具体哪个状态变了,只知道有状态变化,然后会发送一个信号告诉框架,框架内部收到信号后,会进行一个暴力比对来找出哪些DOM节点需要重新渲染。这在Angular中是脏检查的流程,在React中使用的是虚拟DOM。而Vue.js的变化侦测属于“推”。一个状态绑定着很多依赖,每个依赖表示一个具体的DOM节点,当状态发生变化时,向这个状态的所有依赖发送通知,让它进行DOM更新操作。从VUE2.0开始,引入了虚拟DOM,一个状态所绑定的依赖不再是具体的DOM节点,而是一个组件。这样状态变化后,会通知到组件,组件内部在使用虚拟DOM进行比对,从而大大降低依赖的数量,降低依赖追踪所消耗的内存。

2、Object的变化侦测

    侦测对象的变化有两种方法:Object.defineProperty和ES6的proxy,这里只讨论使用Object.defineProperty的情况。

    2.1  如何追踪变化?

    我们首先使用一个函数defineReactive来对Object.defineProperty进行封装。其作用是定义一个响应式数据,即在函数中进行变化追踪,每当从data的key中读取数据时,get函数被触发;每当往data的key中设置数据时,set函数被触发。


function defineReactive(data, key, val){
    Object.defineProperty(data, key, {
        configurable: true,
        enumerable: true,
        get: function(){
            // 读取数据
            return val
        },
        set: function(newVal){
            // 修改数据
            if(val === newVal){
                return
            }
            val = newVal
        }
    })
}

  2.2  如何收集依赖?

    只是对Object.defineProperty进行封装,其实没有什么用处,真正有用的是收集依赖。我们之所以要观察数据,其目的就是当变量的属性发生变化时,可以通知那些使用到该属性的地方。

    总之: 在get中收集依赖,在set中触发依赖。

   例如:

<template>
    <h1>{{name}}</h1>
</template>

   该模板中使用了变量name,所以当它发生变化时,要向使用了它的地方发送通知。所以首先要收集依赖,即把用到变量name的地方收集起来,然后等属性发生变化时,把之前收集好的依赖循环触发一遍就好了。

 2.3  依赖收集在哪里?

    我们在getter中收集依赖,依赖收集在哪里?假设依赖是一个函数,保存在window.target上,我们可以新增一个dep数组,用来存储被收集的依赖。然后在set被触发时,循环dep以触发收集到的依赖。我们把依赖收集的代码封装成一个Dep类,专门管理依赖。使用这个类,可以收集依赖、删除依赖或者向依赖发送通知。

export default class Dep{
    constructor(){
        this.subs = []
    }
    addSub(sub){
        this.subs.push(sub)
    }
    removeSub(){
        remove(this.subs, sub)
    }
    depend(){
        if(window.target){
            this.addSub(window.target)
        }
    }
    notify(){
        const subs = this.subs.slice()
        for(let i=0,l=subs.length; i<l;i++){
            subs[i].update()
        }
    }
}
function remove(arr, item){
    if(arr.length){
        const index = arr.indexOf(item)
        if(index > -1){
            return arr.splice(index, 1)
        }
    }
}

    之后再改造下defineReactive:

function defineReactive(data, key, val){
    let dep = new Dep() // 修改
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function(){
            dep.depend()  // 修改
            return val
        },
        set: function(newVal){
            if(val === newVal){
                return
            }
            val = newVal
            dep.notify()
        }
    })
}

  2.4  依赖是谁? 

    在上面的代码中我们收集的依赖是window.target,它到底是什么?

     收集谁也就是当属性变化的时候通知谁?我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知只通知它一个。接着,他在负责通知其他地方,即watcher。

   2.5  什么是watcher?

    watcher是一个中介角色,数据发生变化时通知它,然后再通知其他地方。

    关于watcher,先看下例:

vm.$watch('a.b.c', function(newVal, oldVal){
    // 做点什么
})
// 当data.a.b.c的值发生变化时,触发第二个参数中的函数

 上面代码表示当data.a.b.c的值发生变化时,触发第二个参数中的函数,怎么实现呢?可以把这个watcher实例添加到data.a.b.c属性的Dep中就可以。然后,当data.a.b.c的值发生变化时,通知watcher。接着Watcher在执行参数中的这个回调函数。代码如下:

export default class Watcher{
    constructor(vm, expOrFn, cb){
        this.vm = vm
        // 执行this.getter()就可以读取data.a.b.c的内容
        this.getter = parsePath(expOrFn)
        this.cb = cb
        this.value = this.get()
    }
    get(){
        window.target = this
// 修改this指向,并将this.vm作为参数传入getter let value = this.getter.call(this.vm, this.vm) window.target = undefined return value } update(){ const oldValue = this.value this.value = this.get() this.cb.call(this.vm, this.value, oldValue) } } const bailRE = /[^\w.$]/ export function parsePath(path){ if(bailRE.test(path)){ return }
// 先按.来分割成数组,然后一层层遍历读取数据 const segments = path.split('.') return function(obj){ for(let i=0; i<segments.length; i++){ if(!obj) return obj = obj[segements[i]] } return obj } }

  这段代码可以将data.a.b.c主动添加到Dep中。首先在get方法中把window.target设置成this,也就是当前的watcher实例,然后再读取data.a.b.c的值,此时触发getter。触发getter就会触发收集依赖,会从window.target中读取一个依赖并添加到Dep中。依赖注入之后,每当data.a.b.c的值发生变化,就会让依赖列表中所有的依赖循环执行update方法,也就是Watcher中的update方法。update方法执行参数中的回调函数将value和oldValue传到参数中。

  2.6 递归侦测所有的key

      目前为止我们已经实现了变化侦测的功能,但是只能侦测数据中的某一个属性,我们希望把数据中的所有属性都侦测到,所以要封装一个Observer类。这个类的作用就是将一个数据内的所有属性(包括子属性)都侦测到,所以要封装一个Observer类。这个类的作用就是将一个数据内的所有属性(包括子属性)都转换成getter/setter形式,然后追踪他们的变化: 

export class Observer{
    constructor(value){
        this.value = value
        if(!Array.isArray(value)){
            this.walk(value)
        }
    }
    // walk会将每一个属性都转换成getter/setter的形式来侦测变化
    // 这个方法只有在数据类型为 Object时被调用
    walk(obj){
        const keys = Object.keys(obj)
        for(let i=0; i<keys.length; i++){
            defineReactive(obj, keys[i], obj[keys[i]])
        }
    }
}
function defineReactive(data, key, val){
    if(typeof val === 'object'){ // 修改
        new Observer(val)
    }
    let dep = new Dep()
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function(){
            dep.depend() 
            return val
        },
        set: function(newVal){
            if(val === newVal){
                return
            }
            val = newVal
            dep.notify()
        }
    })
}

 2.7 关于Object的问题

     Vue.js通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性,这是因为在ES6之前没有提供元编程的能力,无法侦测到一个新属性被添加到了对象中,也无法侦测到一个属性从对象中删除了。

   2.8 总结

   下图给出了Data、Observer、Dep和Watcher之间的关系。

    

    Data通过Observer转换成getter/setter的形式来追踪变化。

    当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。

    当数据发生变化时,会触发setter,循环遍历Dep中的依赖(Watcher),Watcher接收到通知后,会向外界发送通知,变化通知到外界后会触发视图更新。

 

    

 

  

 

   

posted @ 2021-06-08 17:29  leayun  阅读(621)  评论(0编辑  收藏  举报