vue-router,MVVM,dom-diff解析

动态路由:
如果需要获取动态路由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')
}
// 页面中
<template>
  <div>
  <!-- 方式1: 通过当前路由规则,获取数据-->
    通过当前路由规则获取: {{ $route. params.id }}
    <br>
    <!-- 方式2:路由规则中开启props 传参-->
    通过开启 props 获取: {{ id }}
  </div>
</template>
<script>
export default {
  name: 'Detail',
  props: ['id']
}
</script>

编程式导航:

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事件
● 根据当前路由地址找到对应组件重新渲染

image.png

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()方法

● 没有事件中心

image.png

● 观察者模式是由具体目标调度,比如当事件触发,Dep就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的。
● 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。

●  乞丐版vue
●  把data中的成员注入到Vue实例,并且把data中的成员转成getter/setter

● Observer
● 能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知Dep


Vue
● 功能
● 负责接收初始化的参数(选项)
● 负责把data中的属性注入到Vue实例,转换成getter/setter
● 负责调用observer监听data中所有属性的变化
● 负责调用compiler 解析指令/差值表达式


image.png

Observer
● 功能
● 负责把data选项中的属性转换成响应式数据
● data中的某个属性也是对象,把该属性转换成响应式数据
● 数据变化发送通知


image.png

Compiler
● 功能
● 负责编译模板,解析指令/差值表达式
● 负责页面的首次渲染
● 当数据变化后重新渲染视图


image.png
image.png

Dep(Dependency)依赖
● 功能
● 收集依赖,添加观察者(watcher)
● 通知所有观察者

image.png
image.png

● 功能
● 当数据变化触发依赖,dep 通知所有的Watcher实例更新视图
● 自身实例化的时候往dep对象中添加自己


image.png
image.png

● 问题
● 给属性重新赋值成对象,是否是响应式的?
● 给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)等


image.png

Virtual DOM 库
● Snabbdom
● Vue 2.x 内部使用的 Virtual DOM 就是改造的 Snabbdom
● 大约 200 SLOC (single line of code)
● 通过模块可扩展
● 源码使用TypeScript 开发
● 最快的 Virtual DOM 之一
● virtual dom

image.png

打包工具为了方便使用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<Partial<Module>>, 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 过程只进行同层级比较


image.png
image.png
image.png

updateChildren
● 功能:
● diff算法的核心,对比新旧节点的children,更新DOM
● 执行过程:
● 要对比两棵树的差异,我们可以取第一棵树的每个节点依次和第二棵树的每一个节点比较,但是这样的时间复杂度为O(n^3)
● 在DOM操作的时候我们很少很少会把一个父节点移动/更新到某一个子节点
● 因此只需要找同级别的子节点依次比较,然后再找下一-级别的节点比较,这样算法的时间复杂度为O(n)


image.png

● 在进行同级别节点比较的时候,首先会对新老节点数组的开始和结尾节点设置标记索引,遍历的过程中移动索引
● 在对开始和结束节点比较的时候,总共有四种情况
● oldStartVnode / newStartVnode (旧开始节点/新开始节点)
● oldEndVnode / newEndVnode (旧结束节点/新结束节点)
● oldStartVnode / oldEndVnode (旧开始节点/新结束节点)
● oldEndVnode / newStartVnode (旧结束节点/新开始节点)


image.png

● 开始节点和结束节点比较,这两种情况类似
● oldStartVnode / newStartVnode (旧开始节点/新开始节点)
● oldEndVnode / newEndVnode (旧结束节点/新结束节点)
● 如果oldStartVnode和newStartVnode是sameVnode (key和sel相同)
● 调用patchVnode()对比和更新节点
● 把旧开始和新开始索引往后移动oldStartldx++ / oldEndldx++


image.png

● oldStartVnode / newEndVnode (旧开始节点/新结束节点)相同
● 调用patchVnode()对比和更新节点
● 把oldStartVnode对应的DOM元素,移动到右边
● 更新索引


image.png

● oldEndVnode / newStartVnode (旧结束节点/新开始节点)相同
● 调用patchVnode()对比和更新节点
● 把oldEndVnode对应的DOM元素,移动到左边
● 更新索引


image.png

如果不是以上四种情况
● 遍历新节点,使用newStartNode的key在老节点数组中找相同节点
● 如果没有找到,说明newStartNode是新节点
● 创建新节点对应的DOM元素,插入到DOM树中
● 如果找到了
● 判断新节点和找到的老节点的sel选择器是否相同
● 如果不相同,说明节点被修改了
● 重新创建对应的DOM元素,插入到DOM树中
● 如果相同,把elmToMove对应的DOM元素,移动到左边


image.png

循环结束
● 当老节点的所有子节点先遍历完(oldStartldx > oldEndldx),循环结束
● 新节点的所有子节点先遍历完(newStartldx > newEndldx),循环结束
● 如果老节点的数组先遍历完(oldStartldx >oldEndldx), 说明新节点有剩余,把剩余节点批量插入到右边


image.png

● 如果新节点的数组先遍历完(newStartldx > newEndldx),说明老节点有剩余,把剩余节点批量删除


image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342