学习Vue数据劫持

64 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app" style="color: red;">
      hello{{name}}
    </div>
    <script src="./vue.js"></script>
    <script>
      const vm = new Vue({
        data() {
          return {
            name: '阿伟',
            age: 26,
          }
        },
        el: '#app',
      })
    </script>
  </body>
</html>
// index.js

import { initMixin } from './init'

// option是用户的选项,data、methods等
function Vue(options) {
  // 调用原型中的方法
  this._init(options)
}

在Vue原型中添加_init方法

// init.js  

export function initMixin(Vue) {
  // 初始化
  Vue.prototype._init = function (options) {
    // 将选项保存到实例上,方便在其他原型方法中使用
    const vm = this
    vm.$options = options

    // 初始化状态
    initState(vm)
  }
}

initState方法判断options中的data是否存在,存在就执行initData
initData函数中先判断data是函数还是对象,如果是函数的话就用call绑定vue的实例,循环data绑定到vue实例上就可以用vm.属性拿到vm.data上的数据
将data传入observe函数中

// state.js 

import { observe } from './observe/index'

export function initState(vm) {
  // 获取所有选项
  const opts = vm.$options
  // 如果data存在就执行初始化data操作
  if (opts.data) {
    initData(vm)
  }
}

// 对data初始化
function initData(vm) {
  let data = vm.$options.data
  // data可能是一个函数、对象,函数需要修改修改this并执行
  data = typeof data === 'function' ? data.call(vm) : data

  // 将data复制给实例的_data
  vm._data = data
  // 对数据进行劫持
  observe(data)
   
  // 将_data中的数据代理到vm,相当于vm.name = vm._data.name
  for (const key in data) {
    proxy(vm, '_data', key)
  }
}

function proxy(vm, target, key) {
  // 用vm.来取属性时,会到vm._data中取
  Object.defineProperty(vm, key, {
    get() {
      return vm[target][key]
    },
    set(newVal) {
      vm[target][key] = newVal
    },
  })
}

observe先判断data是不是一个对象,如果不是就直接返回,是对象就执行Observe类
Observe类先判断data是否是数组对象还是obj对象,如果是数组对象就把data的隐式原型(proto)指向重写数组方法对象的显示原型(prototype),如果数组的值是对象的话就传入observeArray,observeArray把值递归observe,如果是obj对象walk方法将data遍历执行defineReactive
defineReactive函数首先会判断value传入observe递归判断是否是对象,如果是对象就递归。在获取值时会赋值一个newVal,首先判断新值跟旧值是否一样,再传入observe递归判断是否是对象

// observe/index.js  

import { newArrayProto } from './array'

export function observe(data) { // {name: '阿伟',age: 26}
  // 只对对象劫持
  if (typeof data !== 'object' || data === null) {
    return
  }

  // 执行劫持方法类
  return new Observe(data)
}

class Observe {
  constructor(data) { // {name: '阿伟',age: 26}
    // 将Observe的this保存data自定义属性中,方便在执行数组方法中调用Observe的observeArray
    // 给数据加了一个标识,如果数据有__ob__就说明这个属性已经被观测过了
    Object.defineProperty(data, '__ob__', {
      value: this,
      // 将__ob__变成不可枚举,解决死循环问题
      enumerable: false,
    })
    
    // 如果是数组
    if (Array.isArray(data)) {
      // 保留数组原有的特性,但是重写了7个可以修改原数组的方法(push、shift等)
      data.__proto__ = newArrayProto
      // 处理数组对象[{a:1}]
      this.observeArray(data)
    } else {
      this.walk(data)
    }
  }

  // 循环对象,对属性依次劫持
  walk(data) {
    Object.keys(data).forEach((key) => {
      defineReactive(data, key, data[key])
    })
  }
  
  // 循环数组,对属性依次劫持
  observeArray(data) { 
    // 数组中的值也要劫持
    data.forEach((item) => {
      observe(item)
    })
  }
}

export function defineReactive(data, key, val) {
  // 对所有的对象都进行属性劫持
  observe(val)
  
  Object.defineProperty(data, key, {
    // 取值时会执行get
    get() {
      // console.log('获取值')
      return val
    },
    // 修改值时会执行set
    set(newVal) {
      // console.log('设置值')
      if (newVal === val) {
        return
      }
      observe(newVal)
      val = newVal
    },
  })
}

先获取到数组的显示原型(prototype)
使用Object.create创建一个空对象,这个空对象的隐式原型(proto)执行数组的显示原型
给newArrayProto添加push等修改原数组的方法

// array.js

// 获取数组原型
let oldArrayProto = Array.prototype

// 创建一个新对象,这个新对象的原型指向oldArrayProto,newArrayProto.__proto__ === oldArrayProto==>(Array.prototype),在newArrayProto添加push等方法,也不会修改数组原型上的方法
export let newArrayProto = Object.create(oldArrayProto)

// 可以修改原数组的方法
let methods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice']

methods.forEach((item) => {
  newArrayProto[item] = function (...args) {
    // 执行数组中的方法,并将this绑定到执行该方法的数据(xxx.push,this为xxx)上
    const result = oldArrayProto[item].call(this, ...args)
    let inserted
    let ob = this.__ob__
    // 使用push等方法时需要对传入的参数进行劫持
    switch (item) {
      // 将传递的参数保存
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
      default:
        break
    }
    // 如果添加的数据是对象类型的话也要进行劫持
    if (inserted) {
      ob.observeArray(inserted)
    }
    return result
  }
})