Vue 相关原理学习笔记

响应式原理

方案一:Object.defineProperty

基于 Object.defineProperty 通过 setter/getter 方法来监听数据的变化。 getter 进行依赖收集,setter 在数据变更的时候通知订阅者,递归调用监听对象的所有属性。

缺点:

  • 无法检测到对象属性的添加或删除
  • 不能监听数组的变化,需要重写数组方法
function defineReactive(obj, key, value) {
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            console.log('get', obj, key, value)
            return value
        },
        set(newVal) {
            observe(newVal)
            if (newVal !== value) {
                console.log('set', obj, key, newVal)
                value = newVal
            }
        }
    })
}

function observe(obj) {
    if (!obj || typeof obj !== 'object') {
        return
    }
    Object.keys(obj).forEach(key => {
        const value = obj[key]
        observe(value)
        defineReactive(obj, key, obj[key])
    })
}

const data = { a: { b: 1 } }
observe(data)
data.a.b = 2

方案二:ES6 Proxy

优点:

  • 针对整个对象,而不是某个属性,所以不需要遍历,但是仍需要递归
  • 支持数组变化监听
const handler = {
    get(target, key) {
        if (typeof target[key] == 'object' && target[key] !== null) {
            return new Proxy(target[key], handler)
        }
        console.log('set', key)
        return Reflect.get(target, key)
    },
    set(target, key, value) {
        if (key === 'length') return true
        console.log('set', value)
        return Reflect.set(target, key, value)
    }
}

const data = { a: { b: 1 } }
const proxy = new Proxy(data, handler)
proxy.a.b = 2

如何收集依赖

两个关键类:

  • Watcher:封装一个观察订阅的依赖,触发 getter 事件
  • Dep:存放 Watcher 数组,需要通知变更的时候遍历 Watcher 发送通知

流程:

  • 对于某个属性,先声明一个 Dep 对象,用来存放依赖
  • Watcher 初始化的时候,会临时将 Dep.target 指向 this ,然后调用 getter 方法
  • 在 getter 中,将 Dep.target 指向的 Water 存入依赖数组
  • 在 setter 中,通过 dp.notify() 通知所有依赖对象

Vue Router 原理

  • hash mode:根据不同 hash 值渲染不同的组件和数据,通过在 window.onhashchange 监听 hash 改变实现路由切换
  • history mode:利用了 HTML5 History API 中的 pushState() 和 replaceState() 方法,虽然改变了当前的 URL ,但浏览器不会向后端发送请求。后台需要配置一个解析规则,避免 404 出现

Vuex 原理

vuex 中的 store 本质就是没有 template 的 vue 组件

Nuxt 原理

在打包之前,有两者的入口文件:

  • Server entry:服务端入口文件主要是返回新创建的 Vue 实例。为了避免在一个单例对象中的多次请求对状态的污染,所以每次渲染都会重复创建 Vue 实例。在这个地方,也会进行组件状态数据的预获取,路由处理等。
  • Client entry:客户端入口文件就是将 Vue 实例挂载到指定的 DOM 元素上。

在打包之后,有两个 bundle 文件:

  • Server bundle:服务器按照请求通过 Node 去生成预渲染的 HTML 字符串返回到请求的客户端,完成初始的渲染。
  • Client bundle:客户端拿到 HTML 之后,会使用这个 bundle 进行混合,使其变为由 Vue 管理的动态 DOM,为之后能够响应后续的数据变化。

同构渲染:同一份代码,服务端先渲染生成 HTML(Dehydrate),客户端拿到代码后运行 JS ,进行客户端激活(Client-Side Hydration,CSH)的过程。