动态路由:
如果需要获取动态路由id,建议使用props方式:
// 路由中开启
{
path: '/detail/:id',
name: 'Detail',
//开启props,会把URL中的参数传递给组件
//在组件中通过props 来接收URL 参数
props: true,
// route LeveL code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is Lazy-Loaded when the route is visited.
component: () => import(/* webpackChunkName: "detail" */ '../views/Detail. vue')
}
// 页面中
通过当前路由规则获取: {{ $route. params.id }}
通过开启 props 获取: {{ id }}
编程式导航:
push () {
this.$router.push('/' )
this.$router.push({ name: 'Home'})
}
$router有两种用法,第一种直接添加路由,第二种是添加name,name为vue-router中设置的name
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
]
this.$router.go(-1) 返回历史页面
hash与history
Hash模式是基于锚点,以及onhashchange事件
● History模式是基于HTML5中的History API
● history.pushState() IE 10 以后才支持
● history.replaceState()
History模式的使用
● History 需要服务器的支持
● 单页应用中,服务端不存在http://www.testurl.com/login这样的地址会返回找不到该页面
● 在服务端应该除了静态资源外都返回单页应用的index.html
history需要服务器支持,我们使用node或nginx
node处理history:
const path = require('path')
//导入处理history 模式的模块
const history = require('connect-history-api-fallback')
// 导入 express
const express = require('express')
const app = express()
// 注册处理 history 模式的中间件
app.use(history())
//处理静态资源的中间件,网站根目录../web
app. use(express.static(path. join(_dirname, '../web')))
// 开启服务器, 端口是3000
app.listen(3000, () =>{})
nginx处理history
location / {
root html;
index index.html index.htm;
try_ files $uri $uri/ /index.html
}
try_files
$uri:当前请求路由
这句话意思是尝试请求当前路由,如果请求不到,就返回当前目录下的index.html
vue-router实现
Vue前置知识
● 插件
● 混入
● Vue.observable()
● 插槽
● render 函数
● 运行时和完整版的Vue
Hash模式
● URL中#后面的内容作为路径地址
● 监听hashchange事件
● 根据当前路由地址找到对应组件重新渲染
History模式
● 通过history.pushState()方法改变地址栏
● 监听popstate事件
● 根据当前路由地址找到对应组件重新渲染
Vue的构建版本
● 运行时版:不支持template模板,需要打包的时候提前编译
完整版:包含运行时和编译器,体积比运行时版大10K左右,程序运行的时候把模板转换成render函数
Vue MVVM原理
● 数据驱动
● 响应式的核心原理
● 发布订阅模式和观察者模式
数据驱动
● 数据响应式
● 数据模型仅仅是普通的JavaScript对象,而当我们修改数据时,视图会进行更新,避免了繁琐的DOM操作,提高开发效率
● 双向绑定
● 数据改变,视图改变;视图改变,数据也随之改变
● 我们可以使用v-model在表单元素上创建双向数据绑定
● 数据驱动
● 是Vue最独特的特性之一
● 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图
● 发布/订阅模式
● 订阅者
● 发布者
● 信号中心
我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布" (publish) 一个信号,其他任务可以
向信号中心"订阅" (subscribe) 这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模
式" (publish-subscribe pattern)
// Vue 自定义事件
let vm = new Vue()
// { 'click': [fn1, fn2], 'change': [fn] }
// 注册事件(订阅消息)
vm.$on('dataChange', () => {
console.log('dataChange')
})
vm.$on('dataChange', () => {
console.log('dataChange1')
})
// 触发事件(发布消息)
vm.$emit('dataChange')
兄弟组件通信
// eventBus.js
//事件中心
let eventHub = new Vue()
// ComponentA. vue
//发布者
addTodo: function () {
//发布消息(事件)
eventHub.$emit('add-todo', { text: this.newTodoText })
this.newTodoText = ''
}
// ComponentB.vue
//订阅者
created: function () {
//订阅消息(事件)
eventHub.$on('add-todo', this.addTodo)
}
观察者模式
● 观察者(订阅者) - Watcher
● update(): 当事件发生时,具体要做的事情
● 目标(发布者) -- Dep
● subs数组:存储所有的观察者
● addSub():添加观察者
● notify(): 当事件发生,调用所有观察者的update()方法
● 没有事件中心
● 观察者模式是由具体目标调度,比如当事件触发,Dep就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的。
● 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。
● 乞丐版vue
● 把data中的成员注入到Vue实例,并且把data中的成员转成getter/setter
● Observer
● 能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知Dep
Vue
● 功能
● 负责接收初始化的参数(选项)
● 负责把data中的属性注入到Vue实例,转换成getter/setter
● 负责调用observer监听data中所有属性的变化
● 负责调用compiler 解析指令/差值表达式
Observer
● 功能
● 负责把data选项中的属性转换成响应式数据
● data中的某个属性也是对象,把该属性转换成响应式数据
● 数据变化发送通知
Compiler
● 功能
● 负责编译模板,解析指令/差值表达式
● 负责页面的首次渲染
● 当数据变化后重新渲染视图
Dep(Dependency)依赖
● 功能
● 收集依赖,添加观察者(watcher)
● 通知所有观察者
● 功能
● 当数据变化触发依赖,dep 通知所有的Watcher实例更新视图
● 自身实例化的时候往dep对象中添加自己
● 问题
● 给属性重新赋值成对象,是否是响应式的?
● 给Vue实例新增一个成员是否是响应式的?
为什么使用Virtual DOM
● 手动操作DOM比较麻烦,还需要考虑浏览器兼容性问题,虽然有jQuery等库简化DOM操作,但是随着项目的复杂DOM操作复杂提升
● 为了简化DOM的复杂操作于是出现了各种MVVM框架,MVVM框架解决了视图和状态的同步问题
● 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题,于是Virtual DOM出现了
● Virtual DOM的好处是当状态改变时不需要立即更新DOM,只需要创建一个虚拟树来描述DOM, Virtual DOM内部将弄清楚如何有效(diff)的更新DOM
参考github上virtual dom的描述
● 虚拟DOM可以维护程序的状态,跟踪上一次的状态
● 通过比较前后两次状态的差异更新真实DOM
虚拟DOM的作用
● 维护视图和状态的关系
● 复杂视图情况下提升渲染性能
● 除了渲染DOM以外,还可以实现SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uni- app)等
Virtual DOM 库
● Snabbdom
● Vue 2.x 内部使用的 Virtual DOM 就是改造的 Snabbdom
● 大约 200 SLOC (single line of code)
● 通过模块可扩展
● 源码使用TypeScript 开发
● 最快的 Virtual DOM 之一
● virtual dom
打包工具为了方便使用parcel
创建项目,并安装parcel
# 创建项目目录
md snabbdom-demo
# 进入项目目录
cd snabbdom-demo
# 创建package.json
yarn init -y
#本地安装parcel
yarn add parcel-bundler
配置package.json的scripts
"scripts": {
"dev": "parcel index.html -- open",
"build": "parcel build index.html”
}
导入Snabbdom
Snabbdom文档
● 看文档的意义
● 学习任何一个库都要先看文档
● 通过文档了解库的作用
● 看文档中提供的示例,自己快速实现-个demo
● 通过文档查看API的使用
● 文档地址
● https://github.com/snabbdom/snabbdom
● 中文翻译
安装Snabbdom
yarn add snabbdom
导入Snabbdom
● Snabbdom的官网demo中导入使用的是commonjs模块化语法,我们使用更流行的ES6模块化的语法import
import { init, h, thunk } from 'snabbdom'
● Snabbdom 的核心仅提供最基本的功能,只导出了三个函数init()
、h()
、 thunk()
● init()是一个高阶函数,返回patch()
● h()返回虚拟节点VNode, 这个函数我们在使用Vue.js的时候见过
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
● thunk()是一种优化策略,可以在处理不可变数据时使用
注意:导入时候不能使用import snabbdom from ' snabbdom'
原因: node_ modules/src/snabbdom.ts 末尾导出使用的语法是export
导出API,没有使用export default
导出
默认输出
export { h } from './h'
export { thunk } from './thunk'
export function init(modules: Array>, domApi?: DOMAPI)
模块
Snabbdom的核心库并不能处理元素的属性样式事件等,如果需要处理的话,可以使用模块
常用模块
官方提供了6个模块
● attributes
● 设置DOM元素的属性,使用setAttribute()
● 处理布尔类型的属性
● props
● 和attributes模块相似,设置DOM元素的属性element[attr] = value
● 不处理布尔类型的属性
● class
● 切换类样式
● 注意:给元素设置类样式是通过sel选择器
● dataset
● 设置data-* 的自定义属性
● eventlisteners
● 注册和移除事件
● style
● 设置行内样式,支持动画
● delayed/remove/destroy
如何学习源码
● 先宏观了解
● 带着目标看源码
● 看源码的过程要不求甚解
● 调试
● 参考资料
Snabbdom的核心
● 使用h()函数创建JavaScript对象(VNode)描述真实DOM
● init() 设置模块,创建patch()
● patch()比较新旧两个VNode
● 把变化的内容更新到真实DOM树上
Snabbdom源码
● 源码地址:
● https://github.com/snabbdom/snabbdom
h()函数介绍
● 在使用Vue的时候见过h()函数
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
● h()函数最早见于hyperscript, 使用JavaScript创建超文本
● Snabbdom中的h()函数不是用来创建超文本,而是创建VNode
函数重载
概念
● 参数个数或类型不同的函数
● JavaScript中没有重载的概念
● TypeScript中有重载,不过重载的实现还是通过代码调整参数
重载的示意
function add (a, b) {
console.1og(a + b)
}
function add (a, b, c) {
console.log(a + b + c)
}
add(1, 2)
add(1, 2, 3)
● patch(oldVnode, newVnode)
● 打补丁,把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点
● 对比新旧VNode是否相同节点(节点的key和sel相同)
● 如果不是相同节点,删除之前的内容,重新渲染
● 如果是相同节点,再判断新的VNode是否有text, 如果有并且和oldVnode的text不同,直接更新文本内容
● 如果新的VNode有children,判断子节点是否有变化,判断子节点的过程使用的就是diff 算法
● diff 过程只进行同层级比较
updateChildren
● 功能:
● diff算法的核心,对比新旧节点的children,更新DOM
● 执行过程:
● 要对比两棵树的差异,我们可以取第一棵树的每个节点依次和第二棵树的每一个节点比较,但是这样的时间复杂度为O(n^3)
● 在DOM操作的时候我们很少很少会把一个父节点移动/更新到某一个子节点
● 因此只需要找同级别的子节点依次比较,然后再找下一-级别的节点比较,这样算法的时间复杂度为O(n)
● 在进行同级别节点比较的时候,首先会对新老节点数组的开始和结尾节点设置标记索引,遍历的过程中移动索引
● 在对开始和结束节点比较的时候,总共有四种情况
● oldStartVnode / newStartVnode (旧开始节点/新开始节点)
● oldEndVnode / newEndVnode (旧结束节点/新结束节点)
● oldStartVnode / oldEndVnode (旧开始节点/新结束节点)
● oldEndVnode / newStartVnode (旧结束节点/新开始节点)
● 开始节点和结束节点比较,这两种情况类似
● oldStartVnode / newStartVnode (旧开始节点/新开始节点)
● oldEndVnode / newEndVnode (旧结束节点/新结束节点)
● 如果oldStartVnode和newStartVnode是sameVnode (key和sel相同)
● 调用patchVnode()对比和更新节点
● 把旧开始和新开始索引往后移动oldStartldx++ / oldEndldx++
● oldStartVnode / newEndVnode (旧开始节点/新结束节点)相同
● 调用patchVnode()对比和更新节点
● 把oldStartVnode对应的DOM元素,移动到右边
● 更新索引
● oldEndVnode / newStartVnode (旧结束节点/新开始节点)相同
● 调用patchVnode()对比和更新节点
● 把oldEndVnode对应的DOM元素,移动到左边
● 更新索引
如果不是以上四种情况
● 遍历新节点,使用newStartNode的key在老节点数组中找相同节点
● 如果没有找到,说明newStartNode是新节点
● 创建新节点对应的DOM元素,插入到DOM树中
● 如果找到了
● 判断新节点和找到的老节点的sel选择器是否相同
● 如果不相同,说明节点被修改了
● 重新创建对应的DOM元素,插入到DOM树中
● 如果相同,把elmToMove对应的DOM元素,移动到左边
循环结束
● 当老节点的所有子节点先遍历完(oldStartldx > oldEndldx),循环结束
● 新节点的所有子节点先遍历完(newStartldx > newEndldx),循环结束
● 如果老节点的数组先遍历完(oldStartldx >oldEndldx), 说明新节点有剩余,把剩余节点批量插入到右边
● 如果新节点的数组先遍历完(newStartldx > newEndldx),说明老节点有剩余,把剩余节点批量删除