手写 Vue2 实例和数据绑定劫持,深度手写刨析 vue2 底层原理第一天

114 阅读3分钟

今天我们先手写 Vue 实例和数据绑定劫持

首先,我们先新建一个 index.html

<body>
  <div id="app">{{name}}</div>
  <script src="dist/vue.js"></script>
  <script>
        // viewModel 数据模型
        // 典型的 MVVM view vm model
        let vm = new Vue({
          el: '#app',
          data () {
            return {
              name: '小胡-函数'
            }
          }
        })
  </script>
</body>

第二步:封装 Vue 实例

/**
 *
 * @param {用户传入的选项} options
 */
function Vue(options) {
    // _命名是告诉外界是内部方法,不希望外部被调用
    this._init(options) // 初始化操作
}
initMixin(Vue)

第三步:封装 init 公共方法

因为公共方法要放在原型上,但是如果全部公共方法都用 Vue.prototype.xxx 就会造成代码很混乱,所以我们单独创一个 init.js 文件

    export function initMixin(Vue) {
        Vue.prototype._init = function(options) {
            const vm = this
            // 这样就可以像 vue 一样实现 vm.$options.data... 拿到里面的属性
            vm.$options = options
            
            // 对数据进行初始化 watch props computed data
            initState(vm)
        }
    }

第四步:封装 initState 初始化函数,新建一个 state.js 文件

    export function initState(vm) {
        const opts = vm.$options
        // 如果用户 data 里面有数据,我们才进行初始化
        if(opts.data) {
            // 初始化 data 数据
            initData(vm)
        }
    }

第五步:封装 initData 初始化 data 的函数

    function initData(vm) {
        let data = vm.$options.data
        // 因为在 vue 中 data 可以是函数也可以是对象,所以我们需要判断一下
        // 调用 data 函数的时候我们让它的 this 永远都指向 vue 实例
        data = isFunction ? data.call(vm) : data
    }

第六步:封装判断值是否为函数的方法,因为在很多地方都会用到,所以我们新建一个 utils.js 文件

    export function isFunction(val) {
        return typeof val === 'function'
    }

第七步:把 data 变成响应式,在 initData 初始化 data 函数里继续写

     function initData(vm) {
        // 省略之前的代码
        ......
        // 把 data 数据变成响应式
        observe(data)
    }

第八步:新建 observer/index.js 文件,在里面封装 响应式 的函数

    export function observe(data) {
        // 注意 vue 里面默认最外层的 data 必须是一个对象
        if(!isObject(data)) {
            return
        }
        // 如果 data 是一个对象,我们就把这个对象变成 响应式
        return new observer(data)
    }
    
    class observer {
        constructor(data) {
            this.walk(data)
        }
        walk(data) {
            // 既然要对 data 中所有属性进行数据劫持,必然要先循环这个 data 对象
            // 循环拿到 data 自身可枚举属性
            for(const key in data) {
                if(Object.prototype.hasOwnProperty.call(data,key)) {
                    defineReactive(data,key,data[key])
                }
            }
        }
    }
    
    function defineReactive(data,key,value) {
        // 如果 data 里面的属性是对象或者是嵌套对象,需要递归处理,性能差
        observe(value)
        // 对 data 里面的所有属性进行拦截,实现响应式
        Object.defineProperty(data,key,value) {
            get() {
                return value
            },
            set(newValue) {
                // 如果用户赋值的是一个对象,需要递归进行数据劫持
                observe(newValue)
                value = newValue
            }
        }
    }

做到这步我们发现还有一些问题,那就是看不到我们劫持的对象

手写 vue 底层第一天.png

所以我们需要在 vue 实例上挂载一个属性,方便我们看哪些属性被劫持了,打开 state.js 里面的 initData 函数

    function initData(vm) {
        ......
        // 在 vue 实例上绑定一个属性方便查看哪些属性被劫持了
        data = vm._data = isFunction ? data.call(vm) : data
    }

屏幕截图 2022-07-19 001503.png

这样我们就可以清楚的看到哪些属性是响应式的了!

接下来还有一个问题,那就是不能直接通过 vue 实例来点 data 里面的数据,我们接着来处理一下

打开封装的 initData 初始化函数

    function initData(vm) {
        ......
        for(let key in data) {
            // 监听 vue 实例
            proxys(vm,'_data',key)
        }
    }

封装 proxys 函数

    proxys(vm,source,key) {
        // 在 vue 实例上监听 data 里面的属性
        Object.defineProperty(vm,key,{
            get() {
                // 当我们用 vue.xxx 直接读取 data 里面属性的时候,我们就返回 vue._data.xxx 
                return vm[source][key]
            },
            set(newValue) {
                // 当我们直接用 vue.xxx = xxx 的时候,把数据保存在 vue._data.xxx 上
                vm[source][key] = newValue
            }
        })
    }