vue2.0|思路篇|数据劫持

979 阅读4分钟

前言

  • 文章结构采用【指出阶段目标,然后以需解决问题为入口,以解决思路为手段】达到本文目标,若使诸君稍有启发,不枉此文心力^-^
  • 文分【思路篇】和【实现篇】,本文是思路篇,建议看两个窗口同步阅读-》vue2.0|具体实现篇|数据劫持

目标

  • 代码层面实现vue数据观测

第一阶段:实现rollup 环境安装

搭建vue编译环境,vue采用rollup进行打包

  1. 依赖安装

    Rollup

    npm install @babel/preset-env @babel/core rollup rollup-plugin-babel rollup-plugin-serve cross-env -D
    

    babel

    npm i @babel/core @babel/preset-env rollup-plugin-serve -D
    
  2. rollup.config.js文件编写

    import babel from "rollup-plugin-babel";
    import serve from 'rollup-plugin-serve';
    
    export default {
        input: './src/index.js',
        output: {
            format: 'umd',  // 采用umd规范导出入口文件导出变量
            name: 'Vue',
            file: 'dist/umd/vue.js',
            sourcemap: true
        },
        plugins: [
            babel({
                exclude: 'node_modules/**'
            }),
            serve({
                port: 3000,
                contentBase: '',
                openPage:'/index.html'
            })
        ]
    }
    

第二阶段:搭建vue基本结构

src
  ├── index.js
  ├── init.js
  ├── observer
  │   └── index.js
  └── state.js
src/index.js 打包入口文件导出函数Vue
  • 执行initMixin方法在Vue原型上挂载方法
  • 执行初始化方法
index.js
import {initMixin} from './init';

function Vue(options){
   console.log(options);
   this._init(options);
}
initMixin(Vue);

export default Vue;
Init.js
  • 挂载_init方法,进行初始化状态,即对data、watch、computed等进行对应处理
import { initState } from "./state";

export function initMixin(Vue) {
    Vue.prototype._init = function (options) {
        const vm = this;
        vm.$options = options;
        // 初始化数据
        initState(vm)
    }
}
state.js
export function initState(vm){
    let opts = vm.$options;
    let {methods,data,props,computed,watch} = opts;
    if(methods){
        initMethod(vm)
    }
    if(data){
        initData(vm)
    }
    if(props){
        initProps(vm)
    }
    if(computed){
        initComputed(vm)
    }
    if(watch){
        initWatch(vm)
    }

}
function initMethod(vm) {
    
}
function initData(vm) {

}
function initProps(vm) {
    
}
function initComputed(vm) {
    
}
function initWatch(vm) {
    
}

第三阶段:实现初步数据劫持,监听用户对对象属性的赋值取值操作,触发同时执行自定义逻辑

先看使用
let vm = new Vue({
            el: '#app',
            data(){
                return {
                    b: 1,
                  	a: {
                        a: '111'
                    }
                }
            }
        })
// _data是data的一层代理,在vue|具体实现篇|数据劫持 中可以明显看出
// 用户取值操作
 console.log(vm._data.b); 
// 某一属性赋值一个对象时,新对象中的属性被改变
vm._data.b = {x:1}
vm._data.b.x = 2

目标:打印出1同时执行vue定义的逻辑,此处我们定义为console.log('取值操作') 即要实现打印【1】同时打印【取值操作】

核心问题
  • 如何监听取值赋值操作
  • 多层(比如a.a)如何监听
  • 当我们给某一属性赋值一个对象时,如何能让新对象中的属性也被监听到(如x改变)
解决思路
  • 如何监听取值赋值操作:Object.defineProperty
  • 多层(比如a.a)如何监听:深度递归监听
  • 当我们给某一属性赋值一个对象时,如何能让新对象中的属性也被监听到(如x改变):在set中对新值也进行监听操作
扩展:解释了vue监听两大缺陷之一,对象属性的新增或删除无法被观测到

第四阶段 :数组数据劫持

先看使用
let vm = new Vue({
            el: '#app',
            data(){
                return {
                    a: [1,2,3,{
                      b:11
                    }]
                }
            }
        })
// 数组方法的劫持
vm._data.a.shift();
// 数组内对象的劫持
vm._data.a[2].b = 22;
核心问题
  • 如果不对数组单独处理,在处理逻辑中会遍历数组,如果数组元素很多,则会导致性能很差
  • 如果方法有新增的功能,需要对新增元素进行观测
  • 如何实现数组中对象的修改要被监听
解决思路

不对数组的属性进行遍历,而是采用函数劫持的方式进行数组的劫持,即重写数组原型上会改写本身的七个方法,且遍历数组中是对象的元素进行深度观测,从而实现数组监听;

  • 数组方法的劫持

    • 获取数组原型,重写七个方法,将此对象导出
    • 在Observer构造中判断,如果数据是数组特殊处理(observerArray),将数据的原型执行自定义对象
    • 对有新增功能的方法进行处理,新增值进行观测处理
  • 数组内对象的劫持

    • 遍历数组中是对象的元素调用观测方法

扩展:解释了vue监听两大缺陷之一,通过数组索引改变数组无法被观测到

第五阶段 :小优化,代理实现用户直接从vm上处理数据

先看使用
      let vm = new Vue({
            el: '#app',
            data(){
                return {
                    a: [1,2,3,{
                      b:11
                    }]
                }
            }
        })
// 效果相同,避免用户操作data还需要通过_data字段
// vm._data.a.shift();
vm.a.shift();
解决思路

通过Object.defineProperty进行一层代理,遍历_data在vm上对所有key定义getset,这样当用户取值赋值时就是操作_data

具体实现链接

vue2.0|具体实现篇|数据劫持