vue

彻底掌握 Vue 的核心-响应式原理

Vue 响应式原理是面试中必考题,也是前端开发者提升自己技术深度的易学的框架。必须要掌握!!!

  • 面试官:说一说 Vue 的响应式原理?
  • 面试者:Vue 的响应式原理就是利用 Object.defineProperty()来劫持对象属性的 get 和 set,取值时进行依赖收集,设置值时进行派发更新
  • 面试官:能具体说说 Vue.js 源码是怎么实现响应式的吗?(Observer、Dep、Watcher 三者的关系?)
  • 面试者:懵...

Vue.js 为什么要有数据响应式?

我们都知道 Vue 的一个核心特点是 数据驱动视图,数据发生变化视图就要随之更新。

如果按照以往 Jquery 的思想咱们数据变化了想要同步到视图就必须要手动操作 DOM 更新。但是操作真实 DOM 又是非常耗费性能;1. 浏览器的标准就把 DOM 设计的非常复杂;2. DOM 的更新有可能带来页面的重绘或重排;。那么有没有什么解决方案呢?当然是有的。用 JS 的计算性能来换取操作 DOM 所消耗的性能。--- Vue.js 核心:虚拟 DOM

let div = document.createElement('div')
let str = ''
for (const key in div) {
  str += key + '  '
}
console.log(str)

Vue 帮我们做到了数据变动自动更新视图而不用手动操作 DOM 的功能。 Vue 内部就有一个机制能监听到数据变化然后触发更新

接下来让我们一起揭开 响应式数据的原理 的神秘面纱!

第一:手写响应式原理

Rollup 搭建开发环境

Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码, rollup.js 更专注于 Javascript 类库打包 (开发应用时使用 Webpack,开发库时使用 Rollup)

  • rollup: 主要用于打包 js 库
  • rollup-plugin-babel: 在 rollup 中使用 babel 插件
  • @babel/core: babel 核心库
  • @babel/preset-env: 将高级语法转成低级语法,需要按着预设(let const => var、箭头函数、类等的转换)
npm install rollup rollup-plugin-babel rollup-plugin-serve  @babel/preset-env @babel/code cross-env -D

新建 rollup.config.js

import babel from 'rollup-plugin-babel'
// rollup 默认可以导出一个对象,作为打包的配置文件
export default {
  input: './src/index.js', // 入口
  output: {
    // 出口
    file: './dist/vue.js',
    name: 'Vue',
    format: 'umd', // esm es6模块 commonjs iife(自执行函数) umd(统一模块规范兼容AMD commonjs)
    sourcemap: true, // 可以调试源代码
  },
  plugins: [
    babel({
      exclude: 'node_modules/**', // 排除node_modules所有文件
    }),
  ],
}

新建.babelrc 文件

{
  "presets": ["@babel/preset-env"]
}

配置执行脚本

"scripts": {
    "dev": "rollup -cw"
}

实现 Vue 响应式

  1. 导出 Vue 构造函数
import { initMixin } from './init'

function Vue(options) {
  this._init(options)
}

// 初始化
initMixin(Vue) // 扩展了init方法
// 把原型上方法扩展成一个个的函数,通过函数的方式,在其原型上扩展功能

export default Vue
  1. init 方法中初始化 Vue 状态
import { initState } from './state'

// 初始化
export function initMixin(Vue) {
  Vue.prototype._init = function(options) {
    // 在当前的实例上扩展一些属性$options $data...
    const vm = this
    vm.$options = options // 将用户的选项挂载实例上

    // 初始化状态/数据(Vue中的状态:props、data、computed、methods、watch)
    initState(vm)
    // todo...模板编译、创建虚拟dom。。。
  }
}
  1. 根据不同属性进行初始化数据操作
export function initState(vm) {
  const opts = vm.$options
  // vue的数据来源:属性、方法、数据、计算属性、watch
  if (opts.props) {
    initProps(vm)
  }
  if (opts.methods) {
    initMethods(vm)
  }
  if (opts.data) {
    initData(vm)
  }
  if (opts.computed) {
    initComputed(vm)
  }
  if (opts.watch) {
    initWatch(vm)
  }
}

function initProps() {}
function initMethods() {}
function initData(vm) {}
function initComputed() {}
function initWatch() {}
  1. 初始化数据
import { observe } from './observe.js'

function initData(vm) {
  // 初始化数据工作
  let data = vm.$options.data // 用户传递的data
  // data有可能是对象有可能是函数  用户可以通过_data获取数据
  data = vm._data = typeof data === 'function' ? data.call(this) : data
  // 对数据进行劫持
  // Object.defineProperty() 给属性添加get和set
  observe(data) // 响应式原理
}
  1. 递归劫持每个属性
import { isObject } from './util/index'
class Observer {
  constructor(value) {
    // vue 如果数据层数过多,需要递的去解析对象中的属性,依次增加set和get方法
    this.walk(value)
  }
  walk(data) {
    let keys = Object.keys(data)
    keys.forEach((key) => {
      defineReactive(data, key, data[key])
    })
  }
}

function defineReactive(data, key, value) {
  observe(value) // 多层对象,深度递归
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get() {
      return value
    },
    set(newVal) {
      if (newVal === value) return
      observe(newVal) // 用户有可能设置的值是对象也需要监控
      value = newVal
    },
  })
}

export function observe(data) {
  if (!isObject(data)) return

  new Observer(data) // 用来观测数据的
}
  1. 数组的劫持
import { arrayMethods } from './array'
class Observer {
  constructor(value) {
    if (Array.isArray(value)) {
      // 如果是数据的话并不会对索引进行观测,因为会导致性能问题
      // 如果数组里放的是对象我再监控,不是对象就不监控了
      this.observerArray(value)
      // 注意:数组的劫持:1、劫持数组中的每一项,对数组中的7个方法进行重写
    } else {
      this.walk(value)
    }
  }

  observerArray(value) {
    for (let i = 0; i < value.length; i++) {
      observe(value[i]) // 监控数组的每一项
    }
  }
}
  1. 重写数组原型上的方法
// 重写数组的方法:push pop shift unshift reverse sort splice 这些方法都可以改变数组的本身

const oldArrayMethods = Array.prototype
export const arrayMethods = Object.create(oldArrayMethods)

const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']

methods.forEach((method) => {
  arrayMethods[method] = function(...args) {
    console.log(`用户调用的方法:${method}`)
    const result = oldArrayMethods[method].apply(this, args) // 调用原生的方法 AOP
    const ob = this.__ob__
    // 需要判断一下当前用户通过数组添加的值是否是对象
    let inserted // 当前用户插入的元素
    switch (key) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice': // 新增 修改 删除的功能(新增的内容在第三个)
        inserted = args.slice(2)
      default:
        break
    }
    if (inserted) ob.observerArray(inserted)
    return result
  }
})
  1. 增加__ob__属性
class Observer {
    constructor(value) {
        // value.__ob__ = this;不能通过这种方式,会内存溢出 // 我给每一个i监控的对象都新增一个__ob__属性
        Object.defineProperty(value, '__ob__', {
            enumerable: false,
            configurable: false,
            value: this
        })
        ...
    }
}
  1. 数据代理
function initData(vm) {
  // 初始化数据工作
  let data = vm.$options.data // 用户传递的data
  // data有可能是对象有可能是函数  用户可以通过_data获取数据
  data = vm._data = typeof data === 'function' ? data.call(this) : data
  for (let key in data) {
    proxy(vm, '_data', key) // 将_data上的属性全部代理给vm实例
  }
  // 对数据进行劫持
  // Object.defineProperty() 给属性添加get和set
  observe(data) // 响应式原理
}

// 数据代理
function proxy(vm, source, key) {
  Object.defineProperty(vm, key, {
    get() {
      return vm[source][key]
    },
    set(newValue) {
      vm[source][key] = newValue
    },
  })
}

第二:分析 Vue.js 的打包流程

第三:阅读 Vue.js 响应式源码

第四:Vue3 的响应式原理是怎样实现的呢?

Vue 模板编译原理

Vue 虚拟 DOM 原理

传统更新页面,拼接一个完整的字符串innerHTML全部重新渲染。添加虚拟DOM后,可以比较新旧虚拟节点,找到变化在进行更新。虚拟节点就是一个对象,用来描述真实DOM的。

Vue 渲染流程

Vue 生命周期原理

Vue mixin 原理

Vue 依赖收集原理

Vue 异步更新原理

Vue 组件渲染原理

Vue.extend 原理

Vue.use 插件原理

Vue-Diff 原理