「Vue源码学习(一)」你不知道的-数据响应式原理

前言

当今的前端面试,越来越注重源码这一块了。而且就算没有面试,我想,每一个vue的使用者,在使用了一段时间的vue框架之后,也应该自觉去思考,这个框架是怎么实现的,他怎么就能这么方便呢?当然,这也不是必须的,其实看源码,可能就是一个兴趣爱好吧。。从今天开始,本菜鸟将稳定更新vue源码分析系列的文章,如果你们觉得我的文章不错,请给我一个小赞,嘻嘻。

代码

1.目录

2.new一个Vue的实例

想要使用vue,肯定要先new一个Vue实例,参数是一个对象,我们称之为options

// index.js
// 实例一个Vue对象
let vue = new Vue({
    props: {},
    data() {
        return {
            a: 1,
            b: [1],
            c: { d: 1 }
        }
    },
    watch: {},
    render: () => {}
})
3.对options对象的初始化

传进来的options对象,需要对数据进行初始化

// index.js
const { initMixin } = require('./init')

function Vue(options) {
    // 初始化传进来的options配置
    this._init(options)
}

// 配置Vue构造函数的_init方法
// 这么做有利于代码分割
initMixin(Vue)
// 实例一个Vue对象
let vue = new Vue({
    props: {},
    data() {
        return {
            a: 1,
            b: [1],
            c: { d: 1 }
        }
    },
    watch: {},
    render: () => {}
})
将初始化函数_init挂到Vue的原型上
// init.js
const { initState } = require('./state')

function initMixin(Vue) {
    // 在Vue的原型上挂载_init函数
    Vue.prototype._init = function (options) {
        // vm变量赋值为Vue实例
        const vm = this

        // 将传进来的options对象赋值给vm上的$options变量
        vm.$options = options

        // 执行初始化状态函数
        initState(vm)
    }
}

module.exports = {
    initMixin: initMixin
}

initState:总初始化函数,初始化propsdatawatchmethodsomputed
initData:初始化data的函数
proxy:代理函数,主要作用是this.data.xxx的读写可以直接this.xxx实现,少去中间的data
const { observe } = require('./observer/index')

function initState(vm) {

    // 获取vm上的$options对象,也就是options配置对象
    const opts = vm.$options
    if (opts.props) {
        initProps(vm)
    }
    if (opts.methods) {
        initMethods(vm)
    }
    if (opts.data) {
        // 如有有options里有data,则初始化data
        initData(vm)
    }
    if (opts.computed) {
        initComputed(vm)
    }
    if (opts.watch) {
        initWatch(vm)
    }
}
// 初始化data的函数
function initData(vm) {
    // 获取options对象里的data
    let data = vm.$options.data

    // 判断data是否为函数,是函数就执行(注意this指向vm),否则就直接赋值给vm上的_data
    // 这里建议data应为一个函数,return 一个 {},这样做的好处是防止组件的变量污染
    data = vm._data = typeof data === 'function' ? data.call(vm) : data || {}

    // 为data上的每个数据都进行代理
    // 这样做的好处就是,this.data.a可以直接this.a就可以访问了
    for (let key in data) {
        proxy(vm, '_data', key)
    }

    // 对data里的数据进行响应式处理
    // 重头戏
    observe(data)
}
// 数据代理
function proxy(object, sourceData, key) {
    Object.defineProperty(object, key, {
        // 比如本来需要this.data.a才能获取到a的数据
        // 这么做之后,this.a就可以获取到a的数据了
        get() {
            return object[sourceData][key]
        },
        // 比如本来需要this.data.a = 1才能修改a的数据
        // 这么做之后,this.a = 1就能修改a的数据了
        set(newVal) {
            object[sourceData][key] = newVal
        }
    })
}

module.exports = { initState: initState }
4.响应式处理

Observer:观察者对象,对对象或数组进行响应式处理的地方
defineReactive:拦截对象上每一个key的get与set函数的地方
observe:响应式处理的入口
流程大概是这样:observe -> Observer -> defineReactive -> observe -> Observer -> defineReactive 递归

const { arrayMethods } = require('./array')

// 观察者对象,使用es6的class来构建会比较方便
class Observer {
    constructor(value) {
        // 给传进来的value对象或者数组设置一个__ob__对象
        // 这个__ob__对象大有用处,如果value上有这个__ob__,则说明value已经做了响应式处理
        Object.defineProperty(value, '__ob__', {
            value: this, // 值为this,也就是new出来的Observer实例
            enumerable: false, // 不可被枚举
            writable: true, // 可用赋值运算符改写__ob__
            configurable: true // 可改写可删除
        })

        // 判断value是函数还是对象
        if(Array.isArray(value)) {
            // 如果是数组的话就修改数组的原型
            value.__proto__ = arrayMethods
            // 对数组进行响应式处理
            this.observeArray(value)
        } else {
            // 如果是对象,则执行walk函数对对象进行响应式处理
            this.walk(value)
        }
    }

    walk(data) {
        // 获取data对象的所有key
        let keys = Object.keys(data)
        // 遍历所有key,对每个key的值进行响应式处理
        for(let i = 0; i < keys.length; i++) {
            const key = keys[i]
            const value = data[key]
            // 传入data对象,key,以及value
            defineReactive(data, key, value)
        }
    }

    observeArray(items) {
        // 遍历传进来的数组,对数组的每一个元素进行响应式处理
        for(let i = 0; i < items.length; i++) {
            observe(items[i])
        }
    }
}

function defineReactive(data, key, value) {
    // 递归重要步骤
    // 因为对象里可能有对象或者数组,所以需要递归
    observe(value)

    // 核心
    // 拦截对象里每个key的get和set属性,进行读写监听
    // 从而实现了读写都能捕捉到,响应式的底层原理
    Object.defineProperty(data, key, {
        get() {
            console.log('获取值')
            return value
        },
        set(newVal) {
            if (newVal === value) return
            console.log('设置值')
            value = newVal
        }
    })
}

function observe(value) {
    // 如果传进来的是对象或者数组,则进行响应式处理
    if (Object.prototype.toString.call(value) === '[object Object]' || Array.isArray(value)) {
        return new Observer(value)
    }
}

module.exports = {
    observe: observe
}
5.为什么对象和数组要分开处理呢:

对象的属性通常比较少,对每一个属性都劫持setget,并不会消耗很多性能
数组有可能有成千上万个元素,如果每一个元素都劫持setget,无疑消耗太多性能了
所以对象通过defineProperty进行正常的劫持setget
数组则通过修改数组原型上的部分方法,来实现修改数组触发响应式

6.遗留下的问题:

对象新增属性时没有劫持到set函数,所以新增属性无法触发响应式
数组修改只能通过改写的方法,无法直接arr[index] = xxx 进行修改,也无法通过length属性进行修改

7.Vue官方提供的解决方案

Vue官方提供了$set的方法解决了以上问题,使用方法是this.$set(obj, key, value)

8.流程图

结语
本文仅实现数据修改响应的功能,后续会讲视图更新,请关注我哦。

转载于:https://juejin.cn/post/6968732684247892005#heading-9

扒皮猴社区版权所有,转载请标注出处!
扒皮猴 » 「Vue源码学习(一)」你不知道的-数据响应式原理

发表评论