vue-router、响应式理解

前言

  • 数据劫持:当访问或设置 vue 实例中的成员时,会做的一些干预操作
  • 而vue的响应式原理核心就是,通过对数据进行劫持,并将更新反应到对应的dom视图上
  • vue2 响应原理依赖 Object.defineProperty ,vue3 中响应原理依赖 Proxy

vue-router

  • hash模式:
  • history模式:
  1. IE10才开始支持,pushState(改变URL,但不像服务器发请求,而且会保存到历史记录)、replaceState。
  2. 需要服务端支持,比如 www.baidu.com/foo.html 这个页面不存在,则会返回默认404。所以刷新需要全部重定向到index.html。而客户端去修改url 链接

服务器端支持
node: 开启app.use(history()) 就可以了

vue.use()
会调用函数,或者对象的 install 方法

let _Vue = null
export default class VueRouter {
  // static 只需要通过 类名 即可访问该方法,无需 new
  static install (Vue) {
    // 检测是否有被安装过
    if (VueRouter.install.installed) return
    VueRouter.install.installed = true
    // 将 vue 挂载到全局对象上
    _Vue = Vue
    // 将 router 挂载到 vue 对象上
    _Vue.mixin({
      beforeCreate () {
        const router = this.$options.router
        if (router) _Vue.prototype.$router = router
      }
    })
  }

  constructor (options) {
    this.options = options
    this.routeMap = {} // 查看最简单的路由映射关系
    this.data = _Vue.observable({
      // 创建响应式对象, 默认是当前路由地址
      current: '/'
    })
    this.init()
  }

  init () {
    this.createRouteMap()
    this.initComponents(_Vue)
    this.initEvent()
  }

  createRouteMap () {
    this.options.routes.forEach(route => {
      this.routeMap[route.path] = route.component
    })
  }

  initComponents (Vue) {
    // 初始化 router-link router-view
    Vue.component('router-link', {
      props: {
        to: String
      },
      render (h) {
        return h('a', {
          attrs: {
            href: this.to
          },
          on: {
            click: this.clickHandler
          }
        }, [this.$slots.default])
      },
      methods: {
        clickHandler (e) {
          // history.pushState({}, '', '#' + this.to)
          window.location.hash = '#' + this.to
          // 把当前路由地址中的组件变更,改为当前视图 拿到当前自定义路由信息
          this.$router.data.current = this.to
          e.preventDefault()
        }
      }
    })

    const self = this
    Vue.component('router-view', {
      render (h) {
        const component = self.routeMap[self.data.current]
        return h(component)
      }
    })
  }

  // 点击浏览器返回键需要处理组件变化
  initEvent () {
    window.addEventListener('hashchange', e => {
      this.data.current = e.newURL.split('#')[1]
    })
  }
}
复制代码

Object.defineProperty 对比 Proxy

下面进行,2者的实现模拟:

// 模拟 vue 中 data 选项
let data = {
  msg: 'hello'
}

// 模拟 vue 的实例
let vm = {}

// 数据劫持:当访问或者设置 vm 的成员时,做一些干预操作
object.defineProperty(vm, 'msg', {
  // 可枚举
  enumerable: true,
  // 可配置(可以使用 delete 删除,通过 defineProperty 重新定义)
  configurable:true,
  // 当获取值得时候执行
  get() {
    console.log('get', data.msg)
    return data.msg
  },
  // 设置值得时候执行
  set(newVal) {
    if (newVal === data.msg) return 
    data.msg = newVal
    document.querySelector('#app').textContent = data.msg
  }
})


let vm = new Proxy(data, {
  // 执行代理函数
  // 当访问 vm 成员时会执行
  get(target, key) {
    console.log('get, key', key, target[key])
    return target[key]
  },
  // 当设置 vm 中值得时候执行
  set(target, key, newValue) {
    console.log(target, key, newValue)
    if (target[key] === newValue) return 
    target[key] = newValue
    document.querySelector('#app').textContent = newValue
  }
})

// 测试
vm.msg = 'HELLO WORLD'
console.log(vm.msg)
复制代码

我们可以发现:
使用 Proxy 还是要简洁得多,因为 Proxy 使用的是代理整个对象,这个对象所有属性在访问设置时都会触发 get、set;而我们使用 defineProperty 我们需要循环。另外 proxy 由浏览器优化,性能会好很多。

发布订阅模式

相信很多人使用过$on 和 $emit, 但关于怎么实现的,却不是很清楚。以及使用起来需要监听,是不是特别耗性能。vue 事件机制就是采用发布订阅模式。那么,下面我们先来实现一个$on 和 $emit的基础版本

class EventEmitter {
  // 先定义一个对象,用于存放对应事件的数组
  constructor () {
    this.subs = Object.create(null) // 没有原型链,更节省性能
  }

  // 执行事件
  $emit(eventType) {
    if (!this.subs[eventType]) return
    this.subs[eventType].forEach(handler => {
      handler()
    })
  }

  // 触发事件
  $on(eventType, handler) {
    this.subs[eventType] = this.subs[eventType] || []
    this.subs[eventType].push(handler)
  }
}

let em = new EventEmitter()
em.$on('click', () => {
    console.log('click1')
})

em.$emit('click')
复制代码

由以上代码可以看出,发布订阅,其实就是把一连串的函数 FN 先放到一个数组,等到需要执行的时候,再通过数组遍历,一个个执行。因此上面说的非常损耗性能其实是不存在的

观察者模式

  • 观察者(订阅者)–watch
    update: 当事件发生时,具体要做的事情

  • 目标(发布者)–Dep

    1. subs数组:存储所有的观察者
    2. addSub(): 添加观察者
    3. notify(): 当事件发生,调用所有观察者的 update() 方法

很多人容易把发布订阅模式和观察者模式混为一谈,事实上,他们是有区别的

  • 观察者模式中,目标对象和自己是相互依赖的
  • 发布订阅模式中,有一个事件中心做隔离,减少依赖,相对而言更灵活

image.png

// 发布者-目标
class Dep {
  constructor() {
    // 记录所有的订阅者
    this.subs = []
  }
  // 添加订阅者
  addSub(sub) {
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  // 发布通知
  notify() {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}

// 订阅-观察者
class Watcher {
  update() {
    console.log('update')
  }
}

// 测试
const dep = new Dep()
const watcher = new Watcher()

dep.addSub(watcher)
dep.notify()
复制代码

vue 响应式原理分析

image.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享