theme: vue-pro
Hash 和 History 模式的区别
不管那种方式,都是客户端路由的方式,不会像服务器发送请求。
表现形式的区别:
history 模式需要服务端配置的支持。
原理的区别:
history.push() 方法 和 pushState 方法的区别是 push 方法路径会发生变化,这时候要向服务端发送请求;而 pushState 方法不会像服务端发送请求,只改变地址栏中的地址,并且把这个地址记录到历史记录中。
History 模式的使用
node.js 服务器端配置
nginx 服务器配置:
将打包好的前端项目放入 nginx 的 html 文件夹
启动 nginx:
想要在nginx 配置 history 模式需要修改配置文件
在文件中的 server 中的 location 添加一行代码:
VueRouter 实现原理
首先回顾一下核心代码:
VueRouter 的类图:
let _Vue = null
export default class VueRouter {
static install(Vue) {
// 1. 判断当前插件是否已经被安装
if (VueRouter.install.installed) {
return
}
VueRouter.install.installed = true
// 2. 把 Vue 构造函数记录到全局变量
_Vue = Vue
// 3. 把创建 Vue 实例时候传入的 router 对象注入到 Vue 实例上
// 混入
_Vue.mixin({
// 混入中的 this 指向的是 Vue 实例
beforeCreate() {
// 混入的选项,所有实例中都会有,比如组件中也会执行 beforeCreate
// 而我们给 Vue 挂载 $router 只需要让他执行一次,所以需要判断组件就不执行,Vue 实例才执行
// 只有 Vue 的 $options 中才有 router 属性,所以判断 this.$options.router 是否存在就行了
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router
this.$options.router.init()
}
}
})
}
constructor(options) {
this.options = options
// 把 options 中的 routes 解析出来存储到 routeMap
// 键值对的形式,键就是路由地址,值就是路由组件
// router-view 这个组件中,会根据当前的路由地址,来 routeMap 里找到对应的路由组件,把它渲染到浏览器中。
this.routeMap = {}
// data 是一个响应式的对象,里边有一个属性 current 用于记录当前的路由地址
// 默认情况下路由地址是 /,也就是根目录
// Vue 的静态方法 observable 用于创建一个响应式的对象
this.data = _Vue.observable({
current: '/'
})
}
init() {
this.createRouteMap()
this.initComponents(_Vue)
this.initEvent()
}
// createRouteMap 方法是将构造函数中传入的参数 options 中的 routes 处理成键值对的形式存储到 routeMap 中
createRouteMap() {
// 遍历所有的路由规则,把路由规则解析成键值对的形式,存储到 routeMap
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
}
// initComponents 方法创建两个组件: router-link, router-view
/**
* 注意:Vue 的构建版本分为 运行时版 和 完整版
* 运行时版:不支持 template 模板,需要打包的时候提前编译。使用 render 函数创建虚拟 dom,然后把它渲染到视图。
* 完整版:包含运行时和编译器,体积比运行时版大 10k 左右,程序运行的时候把模板转换成 render 函数,性能不如运行时版本。
* vue-cli 创建的项目默认使用的是运行时版本
* 使用两种方法:
* 1. 使用完整版
* 1)在根目录下创建文件 vue.config.js
* 2)vue.config.js 文件中配置:
* module.exports = {
* runtimeCompiler: true
* }
* 3)以下代码
*/
// initComponents(Vue) {
// Vue.component('router-link', {
// props: {
// to: String
// },
// template: ' '
// })
// }
/**
* 2. 使用运行时版本
* 1) 删除 vue.config.js 文件
* 2)如下代码
*/
initComponents(Vue) {
const self = this
Vue.component('router-link', {
props: {
to: String
},
render(h) {
// h 函数是 Vue 传过来的,它用来创建虚拟dom,render 函数中调用 h 函数并把它的结果返回
return h('a', {
attrs: {
href: self.options.mode === 'history' ? this.to : `/#${this.to}`
},
on: {
// 注册点击事件
click: this.clickHandler
}
}, [this.$slots.default])
},
methods: {
clickHandler(e) {
const to = self.options.mode === 'history' ? this.to : `/#${this.to}`
if (self.routeMap[this.to]) {
// 改变浏览器路径且不向服务端发送请求
history.pushState({}, '', to)
// 加载当前路由组件:将当前路由存储到 data 的current 中
// this.data 是响应式对象,当 current 值改变,会将视图更新到浏览器
this.$router.data.current = this.to
} else {
history.pushState({}, '', '/#/*')
this.$router.data.current = '*'
}
e.preventDefault()
}
}
// template: ' '
})
// router-view 相当于一个占位符
// 我们要根据当前路由地址,获取到路由组件,渲染到 router-view 的位置
Vue.component('router-view', {
render(h) {
const component = self.routeMap[self.data.current]
// h 函数将组件转换为虚拟 dom 并返回
return h(component)
}
})
}
// 点击浏览器的前进后退按钮时,渲染当前地址栏中的地址对应的组件
initEvent() {
if (this.options.mode === 'history') {
window.addEventListener('popstate', () => {
this.data.current = this.routeMap[window.location.pathname] ? window.location.pathname : '*'
})
} else {
window.addEventListener('hashchange', () => {
const hash = window.location.hash
this.data.current = hash ? (this.routeMap[hash.substr(1)] ? hash.substr(1) : '*') : '/'
})
}
}
}
数据响应式
数据响应式的核心原理 - Vue 2.x
在 Vue 2.x 中,当把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法降级处理(shim)的特性,所以不支持 IE8和更低版本浏览器。
数据响应式的核心原理 - Vue 3.x
- 使用 Proxy
- 直接监听对象,而非属性
- ES6 中新增的,IE不支持,性能由浏览器优化
模拟 Vue 响应式原理
Vue - 负责把 Vue 中的 data 注入到 Vue 实例,并且调用 Observer 和 Compiler。
Observer - 负责数据劫持,也就是监听数据的变化,把 data 中的属性转换成 getter 和 setter。
Compiler - 负责解析差值表达式和指令。
在 Vue 的响应式机制中,需要使用到观察者模式来监听数据的变化,所以可以看到 Dep 和 Watcher。
Dep - 观察者模式中的发布者(目标),Dependency 的缩写,在 getter 方法中收集依赖。每一个响应式的属性,都会创建一个 Dep 对象,负责收集所有依赖于该属性的地方,所有依赖该属性的位置都会创建一个 Watcher 对象,所以依赖收集的就是依赖于该属性的 Watcher 对象。在 setter 方法中会去通知依赖,当属性中的值发生变化的时候,调用 Dep 的 notify 发送通知,调用 Watcher 对象的 update 方法
Watcher - 负责更新对应的视图
Vue
- 负责接收初始化的参数(选项)
- 负责把 data 中的属性注入到 Vue 实例,转换成 getter/setter
- 负责调用 observer 监听 data 中所有属性的变化
- 负责调用 compiler 解析指令/差值表达式
类图:
class Vue {
constructor (options) {
// 1. 通过属性保存选项的数据
this.$options = options || {}
this.$data = options.data || {}
// options.el 可以是 dom 对象也可以是字符串,也就是一个选择器,所以要判断一下
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
// 2. 把 data 中的成员转换成 getter 和 setter,注入到 Vue 实例中
this._proxyData(this.$data)
// 3. 调用 observer 对象,监听数据的变化
new Observer(this.$data)
// 4. 调用 compiler 对象,解析指令和差值表达式
new Compiler(this)
}
_proxyData(data) {
// 遍历 data 中的所有属性
Object.keys(data).forEach(key => {
// 把 data 的属性注入到 Vue 实例中
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key]
},
set(newValue) {
if (newValue === data[key]) return
data[key] = newValue
}
})
})
}
}
Observer
- 负责把 data 选项中的属性转换成响应式数据
- data 中的某个属性也是对象,把该属性转换成响应式数据
- 数据变化发送通知
类图:
class Observer {
constructor(data) {
this.walk(data)
}
walk(data) {
// 1. 判断 data 是否是对象
if (!data || typeof data !== 'object') return
// 2. 遍历 data 对象的所有属性
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
defineReactive(obj, key, val) {
let that = this
// 负责收集依赖,并发送通知
let dep = new Dep()
// 如果 val 是对象,把 val 内部的属性转换成响应式数据
this.walk(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 收集依赖
Dep.target && dep.addSub(Dep.target)
// 不可以 return obj[key],
// 因为在获取 obj[key] 的过程中又会调
// 用当前的 get 方法,形成死递归,造成内存溢出
return val
},
set(newValue) {
if (newValue === val) return
val = newValue
that.walk(newValue)
// 发送通知
dep.notify()
}
})
}
}
Compiler
- 负责编译模板,解析指令/差值表达式
- 负责页面的首次渲染
- 当数据变化后重新渲染视图
类图:
// Compiler:
// 负责编译模板,解析指令、差值表达式
// 负责页面的首次渲染
// 当数据变化后重新渲染视图
class Compiler {
constructor(vm) {
this.el = vm.$el
this.vm = vm
this.compile(this.el)
}
// 编译模板,处理文本节点和元素节点
compile(el) {
let childNodes = el.childNodes
Array.from(childNodes).forEach(node => {
// 处理文本节点
if(this.isTextNode(node)) {
this.compileText(node)
} else if(this.isElementNode(node)) {
this.compileElement(node)
}
// 判断 node 节点是否有子节点,如果有子节点,要递归调用 compile
if(node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
// 编译元素节点,处理指令
compileElement(node) {
// console.log(node.attributes)
// 遍历所有的属性节点
Array.from(node.attributes).forEach(attr => {
// 判断是否是指令
let attrName = attr.name
if(this.isDirective(attrName)) {
// v-text --> text
attrName = attrName.substr(2)
let key = attr.value
this.update(node,key,attrName)
}
})
}
update(node,key,attrName) {
let updateFn = this[attrName + 'Updater']
updateFn && updateFn.call(this,node,this.vm[key],key)
}
// 处理 v-text 指令
textUpdater(node,value,key) {
node.textContent = value
new Watcher(this.vm,key,(newValue) => {
node.textContent = newValue
})
}
// v-model
modelUpdater(node,value,key) {
node.value = value
new Watcher(this.vm,key,(newValue) => {
node.value = newValue
})
// 双向绑定
node.addEventListener('input',() => {
this.vm[key] = node.value
})
}
// 编译文本节点,处理差值表达式
compileText(node) {
// console.dir(node)
// {{ msg }}
let reg = /\{\{(.+?)\}\}/
let value = node.textContent
if(reg.test(value)) {
let key = RegExp.$1.trim()
node.textContent = value.replace(reg,this.vm[key])
// 创建 watcher 对象,当数据改变更新视图
new Watcher(this.vm,key,(newValue) => {
node.textContent = newValue
})
}
}
// 判断元素属性是否是指令 - 是否 v- 开头
isDirective(attrName) {
return attrName.startsWith('v-')
}
// 判断节点是否是文本节点
isTextNode(node) {
return node.nodeType === 3
}
// 判断节点是否是元素节点
isElementNode(node) {
return node.nodeType === 1
}
}
Dep
- 收集依赖,添加观察者(watcher)
- 通知所有观察者
类图:
class Dep {
constructor () {
// 存储所有的观察者
this.subs = []
}
// 添加观察者
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 发送通知
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
Watcher
- 当数据变化触发依赖,dep 通知所有的 Watcher 实例更新视图
- 自身实例化的时候往 dep 对象中添加自己
类图:
class Watcher {
constructor (vm, key, cb) {
this.vm = vm
// data 中的属性名称
this.key = key
// 回调函数负责更新视图
this.cb = cb
// 把 watcher 对象记录到 Dep 类的静态属性 target
Dep.target = this
// 触发 get 方法, 在 get 方法中会调用 addSub
this.oldValue = vm[key]
Dep.target = null
}
// 当数据发生变化的时候更新视图
update() {
let newValue = this.vm[this.key]
if (newValue === this.oldValue) return
this.cb(newValue)
}
}
虚拟 dom(Virtual DOM)
什么是 Virtual DOM
- Virtual DOM(虚拟DOM),是由普通的JS对象来描述DOM对象
如上图,我们将真实DOM成员遍历并打印,打印的结果如下图。可以看到打印的成员是非常多的,也就是要创建一个dom的成本是非常高的。
再看虚拟dom(下图),它就是一个js对象,创建一个虚拟dom,它的成员非常少,也就是要创建一个虚拟dom 的成本要比创建真实dom小很多。
为什么要使用虚拟dom
前端发展迅速,如今需要大量的操作dom。虚拟 DOM 可以跟踪状态变化,当状态改变的时候不需要立即更新 dom,只需要创建一个虚拟 dom 树来描述真实的 dom 树,它内部会使用 diff 算法来保存状态的差异,只更新变化的部分。
虚拟 DOM 的作用
- 维护视图和状态的关系
- 复杂视图情况下提升渲染性能
- 跨平台
- 浏览器平台渲染DOM
- 服务端渲染 SSR (Nuxt.js/Next.js)
- 原生应用(weex/React Native)
- 小程序(mpvue/uni-app)等
虚拟 DOM 库
- Snabbdom
- Vue.js 2.x 内部使用的虚拟 DOM 就是改造的 Snabbdom
- 大约 200 SLOC (single line of code)
- 通过模块可扩展
- 源码使用 TypeScript 开发
- 最快的 Virtual DOM 之一
- virtual-dom
Snabbdom 基本使用
创建项目
安装 Snabbdom,我的版本为:
目录结构:
index.html:
在 src 下创建 01-basicusage.js 文件,用来将 index.html 中的 div#app 替换为我们想要展示的内容。在 01-basicusage.js 中我们将展示替换成一个内容为字符串的div。
基本使用
Snabbdom 的核心:
- init() 设置模块,创建 patch() 函数
- 使用 h() 函数创建 JavaScript 对象(VNode) 描述真实 DOM
- patch() 比较新旧两个 VNode
- 把变化的内容更新到真实 DOM 树
01-basicusage.js:
import { init } from 'snabbdom/build/init'
import { h } from 'snabbdom/build/h'
const patch = init([])
// h 函数用来创建虚拟dom,这里创建的是 VNode 的虚拟节点,VNode 的作用是用来描述真实dom
// 第一个参数:标签+选择器
// 第二个参数:如果是字符串就是标签中的文本内容
let vnode = h('div#container.cls', 'hello world')
let app = document.querySelector('#app')
// patch 将虚拟dom 转换成真实dom 挂载到 dom 树上
// 第一个参数:旧的 VNode,可以是DOM元素,如果是DOM元素会将这个DOM元素转换成VNode
// 第二个参数:新的VNode
// 返回新的 VNode
// patch 会对比两个VNode,将差异更新到真实dom,并且把第二个参数返回
// 以下这行代码是将html中的 ‘div#app’ 修改为 vnode
let oldVnode = patch(app, vnode)
vnode = h('div#container.xxx', 'hello snabbdom')
patch(oldVnode, vnode)
运行 npm run dev,查看结果,可以看到页面中展示的 hello snabbdom。
将 index.html 中引入的 js 改为 02-basicusage.js,用来将 div#app 更新为 一个内部为其他元素的 div。
02-basicusage.js:
import { init } from 'snabbdom/build/init'
import { h } from 'snabbdom/build/h'
const patch = init([])
// 如果想要内容是标签,第二个元素就是数组,每一项都是元素,可以再用h函数
let vnode = h('div#container', [
h('h1', 'hello snabbdom'),
h('p', '这是一个p')
])
let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)
setTimeout(() => {
// vnode = h('div#container', [
// h('h1', 'hello world'),
// h('p', 'hello p')
// ])
// patch(oldVnode, vnode)
// 清除div和div中的内容
patch(oldVnode, h('!'))
}, 2000)
模块的使用
将 index.html 中引入的 js 文件改为 03-modules.js。
03-modules.js:
import { init } from 'snabbdom/build/init'
import { h } from 'snabbdom/build/h'
// 1. 导入模块
import { styleModule } from 'snabbdom/build/modules/style'
import { eventListenersModule } from 'snabbdom/build/modules/eventlisteners'
// 2. 注册模块
const patch = init([
styleModule,
eventListenersModule
])
// 3. 使用h() 函数的第二个参数传入模块中使用的数据(对象)
let vnode = h('div', [
h('h1', { style: { backgroundColor: 'red' } }, 'hello world'),
h('p', { on: { click: eventHandler } }, 'hello p')
])
function eventHandler() {
console.log('别点我,疼')
}
let app = document.querySelector('#app')
patch(app, vnode)
Snabbdom 的核心
由以上可以看出,Snabbdom 的核心就是: