前言
- 数据劫持:当访问或设置 vue 实例中的成员时,会做的一些干预操作
- 而vue的响应式原理核心就是,通过对数据进行劫持,并将更新反应到对应的dom视图上
- vue2 响应原理依赖 Object.defineProperty ,vue3 中响应原理依赖 Proxy
vue-router
- hash模式:
- history模式:
- IE10才开始支持,pushState(改变URL,但不像服务器发请求,而且会保存到历史记录)、replaceState。
- 需要服务端支持,比如 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
- subs数组:存储所有的观察者
- addSub(): 添加观察者
- notify(): 当事件发生,调用所有观察者的 update() 方法
很多人容易把发布订阅模式和观察者模式混为一谈,事实上,他们是有区别的
- 观察者模式中,目标对象和自己是相互依赖的
- 发布订阅模式中,有一个事件中心做隔离,减少依赖,相对而言更灵活
// 发布者-目标
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 响应式原理分析
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END