Vue 数据绑定解析

vue 对对象的数据绑定

Vue.js之所有能实现数据的双向绑定,最核心也是最基础的是Object.defineProperty的使用。

那么 new Vue() 之后,option.data 的数据具体是如何绑定到 Vue,又如何进行数据双向绑定的呢?

new Vue() -> 
this._init() -> 
initState(vm) -> 
observe(vm._data = {}, true) -> 
new Observer(value) -> 
this.observeArray(value),this.walk(value) ->
defineReactive() -> 
Object.defineProperty

官网流程图:

解析流程图:

第一步:

this._init()

第二步:

initState(vm)

第三步:

observe(vm._data = {}, true)

第四步:

new Observer(value) 

第五步:

this.observeArray(value),this.walk(value)

第六步:

defineReactive(),Object.defineProperty

总结:

Vue首先会调用所有使用的数据,从而触发所有的 getter 函数,进而通过Dep对象收集所有响应式依赖,调用所有Watcher执行Render 操作,其中会进行虚拟Dom的存储和比较,进而渲染页面。

当有数据变更时会触发 setter 函数,触发dep.notify(),进而调用Watcher的update,推入Vue的异步观察队列中,最终调用Watch的getter或者cb方法进而调用vm._update(),再调用vmpatch方法进行虚拟DOM的diff,并最终渲染到页面。

vue对数组的操作

Object.definePropert只能把对象属性改为getter/setter,而对于数组的方法就无能为力了,其内部巧妙的使用了数组的属性来实现了数据的双向绑定。

总结

Vue的数据双向绑定有3大核心:Observer(观察响应式),Dep(Dependency[收集]),Watcher(订阅者)。

  • 首先来实现dep,dep主要负责依赖的收集,get时触发收集,set时通知watcher通信:
class Dep{
    constructor () {
        // 存放所有的监听watcher
        this.subs = []
    }

    //添加一个观察者对象
    addSub (Watcher) {
        this.subs.push(Watcher)
    }

    //依赖收集
    depend () {
        //Dep.target 作用只有需要的才会收集依赖
        if (Dep.target) {
          Dep.target.addDep(this)
        }
    }

    // 调用依赖收集的Watcher更新
    notify () {
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
    }
}
// 为Dep.target 赋值
function pushTarget (Watcher) {
  Dep.target = Watcher
}
  • 第二步实现Watcher,Watcher负责数据变更之后调用Vue的diff进行视图的更新:
class Watcher{
    constructor(vm,expOrFn,cb,options){
        //传进来的对象 例如Vue
        this.vm = vm
        //在Vue中cb是更新视图的核心,调用diff并更新视图的过程
        this.cb = cb
        //收集Deps,用于移除监听
        this.newDeps = []
        this.getter = expOrFn
        //设置Dep.target的值,依赖收集时的watcher对象
        this.value =this.get()
    }

    get(){
        //设置Dep.target值,用以依赖收集
        pushTarget(this)
        const vm = this.vm
        let value = this.getter.call(vm, vm)
        return value
    }

    //添加依赖
    addDep (dep) {
        // 这里简单处理,在Vue中做了重复筛选,即依赖只收集一次,不重复收集依赖
        this.newDeps.push(dep)
        dep.addSub(this)
    }

    //更新
    update () {
        this.run()
    }

    //更新视图
    run(){
        //这里只做简单的console.log 处理,在Vue中会调用diff过程从而更新视图
        console.log(`这里会去执行Vue的diff相关方法,进而更新数据`)
    }
}
  • 第三步实现Observer,Observer负责数据的双向绑定,并把对象属性改为getter/setter
//获得arrayMethods对象上所有属性的数组
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

class Observer{
    constructor (value) {
        this.value = value
        // 增加dep属性(处理数组时可以直接调用)
        this.dep = new Dep()
        //将Observer实例绑定到data的__ob__属性上面去,后期如果oberve时直接使用,不需要从新Observer,
        //处理数组是也可直接获取Observer对象
        def(value, '__ob__', this)
        if (Array.isArray(value)) {
            //处理数组
            const augment = value.__proto__ ? protoAugment : copyAugment  
            //此处的 arrayMethods 就是上面使用Object.defineProperty处理过
            augment(value, arrayMethods, arrayKeys)
            // 循环遍历数组children进行oberve
            this.observeArray(value)
        } else {
            //处理对象
            this.walk(value)
        }
    }

    walk (obj) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
            //此处我做了拦截处理,防止死循环,Vue中在oberve函数中进行的处理
            if(keys[i]=='__ob__') return;
            defineReactive(obj, keys[i], obj[keys[i]])
        }
    }

    observeArray (items) {
        for (let i = 0, l = items.length; i < l; i++) {
          observe(items[i])
        }
    }
}
//数据重复Observer
function observe(value){
    if(typeof(value) != 'object' ) return;
    let ob = new Observer(value)
    return ob;
}
// 把对象属性改为getter/setter,并收集依赖
function defineReactive (obj,key,val) {
    const dep = new Dep()
    //处理children
    let childOb = observe(val)
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            console.log(`调用get获取值,值为${val}`)
            const value = val
            if (Dep.target) {
                dep.depend()
                if (childOb) {
                    childOb.dep.depend()
                }
                //此处是对Array数据类型的依赖收集
                if (Array.isArray(value)) {
                    dependArray(value)
                }
            }
            return value
        },
        set: function reactiveSetter (newVal) {
            console.log(`调用了set,值为${newVal}`)
            const value = val
            val = newVal
            //对新值进行observe
            childOb = observe(newVal)
            //通知dep调用,循环调用手机的Watcher依赖,进行视图的更新
            dep.notify()
        }
  })
}

//辅助方法
function def (obj, key, val) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: true,
    writable: true,
    configurable: true
  })
}

//重新赋值Array的__proto__属性
function protoAugment (target,src) {
  target.__proto__ = src
}
//不支持__proto__的直接修改相关属性方法
function copyAugment (target, src, keys) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}
//收集数组的依赖
function dependArray (value) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
        //循环遍历chindren进行依赖收集
        dependArray(e)
    }
  }
}
原文链接:juejin.im

上一篇:微前端从理论到实践
下一篇:尝鲜vue3.0- 看完就开干(4)

相关推荐

  • 🚩Vue源码——组件是如何注册的

    最近参加了很多场面试,几乎每场面试中都会问到Vue源码方面的问题。在此开一个系列的专栏,来总结一下这方面的经验,如果觉得对您有帮助的,不妨点个赞支持一下呗。 前言 在上一篇 🚩Vue源码——组件...

    4 天前
  • 🚩Vue源码——组件如何渲染成最终的DOM

    最近参加了很多场面试,几乎每场面试中都会问到Vue源码方面的问题。在此开一个系列的专栏,来总结一下这方面的经验,如果觉得对您有帮助的,不妨点个赞支持一下呗。 前言 Vue有两个核心思想,一个是数据...

    9 天前
  • 🔥基于vue3.0.1 beta搭建仿京东淘宝的电商商城项目!

    前言 就在前段时间,vue官方发布了3.0.0-beta.1 版本,趁着五一假期有时间,就把之前的一个电商商城的项目,用最新的Composition API拿来改造一下! 👉GitHub地址请访问�...

    5 个月前
  • 🍊仿Element自定义Vue组件库

    前言 🍊 市面上目前已有各种各样的UI组件库,他们的强大毋庸置疑。但是有时候我们有必要开发一套属于自己团队的定制化组件库。还有时候原有的组件不能满足我们的各种需求,就需要在原有的组件上进行改造...

    3 个月前
  • 🌓vue页面换肤实践

    前言 最近要做一个换肤的功能,不过只是对一个页面换肤,换一下背景图呀,背景、边框、字体颜色呀之类的,并非整个项目换肤,相对比较简单,所以以下介绍的换肤方法仅适用于页面换肤而非整个项目换肤。

    23 天前
  • (记录)vue、element表格首行跑到最后一行去了

    问题 项目中,我们一般会对 Element 进行二次封装,因为这样更好的全局管理组件,一处改动即可全局改动。比如你有十个表格,因为表格需要新增一个小功能(比如:右上角新增一个控制列显示的按钮),这时...

    6 天前
  • (源码分析)为什么 Vue 中 template 有且只能一个 root ?

    引言 今年,疫情并没有影响到各种面经的正常出现,可谓是络绎不绝(学不动...)。然后,在前段时间也看到一个这样的关于 Vue 的问题,为什么每个组件 template 中有且只能一个 root? 可能...

    6 个月前
  • (小白篇)vue-cli3.0创建项目+引入element-ui

    vue-cli在2018年8月份发布了3.0版本,经过重构之后,可以说是一个船心版本! 在项目都落地之后,就想升级一下cli版本,尝一尝3.0带来的舒适,也是为后面项目的开展做一个准备。

    1 年前
  • (小小黑科技)vue+echarts实现半圆图表

    如何用echarts实现半圆图表?在echarts官方实例倒腾一波,发现官方并没有提供半圆图表的写法,那怎么办呢?官方没提供,但需求还是要实现的。 半圆图表其实就是饼图的一半,那么简单的思路如下:设置...

    1 年前
  • (原创)vue-router的Import() 异步加载模块问题的解决方案

    关注不迷路,如果解决了问题,留下个赞。 1、问题现象 2、出现问题的代码点 3、替代方案: 把import() 替换成如下: Promise.resolve().then(()=&...

    5 个月前

官方社区

扫码加入 JavaScript 社区