带你一行一行手写Vue2.0源码系列(一) -响应式数据原理

121 阅读5分钟

前言

作为一名开发人员,阅读源码是非常好的学习方式,尤其Vue又是当下很受欢迎的前端框架,随着用的人越来越多,不断推动着源码地完善。同时现在市面上的企业招聘都需要你或多少了解一些Vue的原理,所以想要在简历上写上精通二字,这步路还是要走的。

我个人推荐的看源码的方式最好是通过视频文章自己能手写一边然后再去看,否则就会造成这样的结果。

看不懂.jpeg

“ 真的是卷... ”

本文章主要讲解的源码中的核心内容,并不包括服务端渲染、跨平台、构建相关的内容。

正文

Vue前端框架的特性主要体现在两个方面:

  1. 数据驱动试图。
  2. 双向数据绑定。

在我们以往的前端开发中如果数据发生变化了去更新页面,我们大概使用的是Jquery去进行Dom操作,所以就需要我们用大把的精力在Dom上,而现在我们只需要关注数据的变化,Dom操作Vue内部就已将帮我们去处理了。

1.数据初始化

我们在使用Vue的时候都是从 new Vue开始的,从new这个关键字我们就不难看出它的祖先其实就是构造函数。我们在构造函数中传入一个options参数,包括data、method等等。在new Vue的过程中主要就是执行了this._init方法进行初始化。

// core/instance/index.js

import { initMixin } from "./init";

function Vue(options) { // 最原始的Vue构造函数
  if(!(this instanceof Vue)) {
    console.warn('必须使用new关键字');
  }
  this._init(options);
}

initMixin(Vue);

export default Vue;
// core/instance/init.js

export function initMixin(Vue) {
  Vue.prototype._init = function (options) {
    const vm = this;
    vm.$options = options; // 把用户传入的options赋给当前实例上
    initState(vm);
  }
}

这里将用户传入的options配置对象赋值给当前实例上,因为后续源码还有很多地方需要用到这里面的参数,赋值给自己以帮助我们去获取。

2. 对象进行数据劫持

// core/instance/state.js

function initData(vm) {
  let data = vm.$options.data; // 获取用户传入的data
  data = typeof data === 'function' ? data.call(vm) : data; // 如果data是函数就执行获取对象
  vm._data = data; // 赋给实例_data上
  observe(data);
}
//  core/observe/index.js

export function observe(data) {
  if (!(typeof data === 'object' && data !== null)) return; // 检测的数据必须是个对象
  if (data.__ob__ instanceof Observer) return data.__ob__; // 说明对象已经被检测过了
  return new Observer(data);
}
//  core/observe/index.js

class Observer {
  constructor(data) {
    // Object.defineProperty只能劫持已存在的属性
    Object.defineProperty(data, '__ob__', { // __ob__不可被枚举
      value: this,
      enumerable: false
    })
    this.walk(data);
  }

  walk(data) { // 循环对象对属性依次劫持
    Object.keys(data).forEach(key => {
      defineReactive(data, key, data[key]);
    })
  }
}
//  core/observe/index.js

export function defineReactive(target, key, value) {

  // value如果还是个对象需要递归
  observe(value);
  Object.defineProperty(target, key, {
    get() {
      return value;
    },
    set(newValue) { // 修改值重新赋值给vlue
      if (value === newValue) return;
      value = newValue;
    }
  })
}

大家都知道Vue中是利用了Object.defineProperty的get和set来进行数据劫持的,但是对于数组这种类型,他会不断循环去get和set,性能上比较差。同时Object.defineProperty无法检测到新增数据和删除的变化差异。

3. 对数组的监听

//  core/observe/index.js

class Observer {
  constructor(data) {
    // Object.defineProperty只能劫持已存在的属性
    Object.defineProperty(data, '__ob__', { //并在当前对象上赋值 __ob__值为当前Observe,并且不可被枚举
      value: this,
      enumerable: false
    })
    
    // 如果是数据
    if (Array.isArray(data)) {
      // 重写数组中的方法 7个遍历方法
      data.__proto__ = newArrayProto;
      this.observeArray(data);
    } else {
      this.walk(data);
    }
  }


  observeArray(data) { // 对数组进行观测
    data.forEach(item => {
      observe(item);
    })
  }
}
//  core/observe/array.js

let oldArrayProto = Array.prototype; // 保存数组原有的原型
export let newArrayProto = Object.create(oldArrayProto); // 创建我们我们自己的数组原型
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(method => {
  newArrayProto[method] = function (...args) { // 重写定义好的7种数组方法
    const result = oldArrayProto[method].call(this, args);

    // 我们对新增的数据再次劫持
    let inserted;
    let ob = this.__ob__; // 获取我们上面存储的Observe响应式实例

    switch (method) { // 对于有新增数据的方法,对新增的数据需要进行响应式
      case 'push':
      case 'unshift': {
        inserted = args;
        break;
      }
      case 'splice': {
        inserted = args.slice(2);
        break;
      }
    }
    if (inserted) { // 对于新数据也进行响应式
      ob.observeArray(inserted);
    }
    return result; // 返回数据方法原始结果
  }
})

在进行响应式的时候也在每个对象上都添加了一个__ob__的对象用来存储响应式实例,同时它也是不可枚举的。大家试想一下如果这个__ob__如果是可枚举的会出现什么结果? 那就会造成无限的递归循环,同时也也可用来观测当前对象是否已经被响应试了,如果已经是响应式,就直接返回。

4.对数据进行代理

大家是不是很好奇,我们在使用Vue进行开发的时候都是直接使用this.某某某,并没有this.data.某某某,这里是因为Vue还是利用Object.defineProperty来对data数据进行了一层代理。

Object.defineProperty是不是很牛逼,哪里都有我😄!

// core/instance/state.js

function initData(vm) {
  let data = vm.$options.data;
  data = typeof data === 'function' ? data.call(vm) : data;
  vm._data = data;
  observe(data);
  for (let key in data) { // 对data上的每个属性都进行代理,访问当前实例上的属性就等于访问data
    proxy(vm, '_data', key);
  }
}

export function proxy(target, sourceKey, key) { // 数据代理
  Object.defineProperty(target, key, {
    get() {
      return target[sourceKey][key];
    },
    set(newValue) {
      target[sourceKey][key] = newValue;
    }
  })
}

如果觉得本文有帮助 记得点赞三连哦 十分感谢!

小结

到此为止,数据响应式已经完结。可能很多朋友会问数据发生变化如何去更新试图呢?对于试图更新的内容后续我们边会接着更新,主要是利用到了WatcherDep采用观察者设计模式进行对每个属性的依赖收集和派发更新。

vue2.0 和 vue3.0系列文章(后续更新)