(一) 前置知识
(1) 一些单词
identifier:标识符
locator:定位
anchor: 锚
parasitic:寄生
manual:手动的
automatic:自动的
specifier:标志语,指示语
several:几个,一些
composition:组成,组合
crushed:压坏,压碎
shallow:浅的
(2) URL 和 URI
- URL -> Locator -> 统一资源定位符 -> 强调位置
- URI -> Identifier -> 统一资源标识符 -> 强调标识,具有唯一性
(3) URL的组成
http://www.baidu.com:80/stu/index.html?name=xxx&age=25#teacher
- Protocal: 协议
http://
,https://
- Domain: 域名
www.baidu.com
- Port: 端口
:80
- http协议默认端口
:80
- https协议默认端口
:443
- http协议默认端口
- Path: 文件路径
/开头, ?之前的部分
, =>/stu/index.html
- Query: 查询字符串
?开头到结尾, 或者?开头到#之前
,=>?name=xxx&age=25
- Hash: 哈希值
#开头到结尾
=>#teacher
- 总结:protocal, domain, port, path, query, hash
(4) DOMContentLoaded事件,load事件
window.onload === window.addEventListener('load', listener, ...)
- 两者的却别
- DOMConentLoaded: ( DOM加载完成时触发 )
- load: DOM,样式,脚本,图片,视频等所有资源全部加载完成时才会触发,即 ( 整个页面加载完成时触发 )
(5) window.location
window.location对象
属性:
pathname: 返回url的path部分,( /开始 ?之前 ) 或者 ( /开始到结尾 ),如果没有query和hash
origin:protocal + hostname + port 三者之和,相当于协议,域名,端口
protocal:协议 http:// https://
hostnme: 主机名
port:端口号
host:主机 (hostname + port)
search:查询字符串 (?开头到#之前,或者?开头到结尾)
hash:片段字符串 (哈希值,#开头到结尾)
(6) splice(start, count, addElement1, addElement2, ...)
- 功能:splice方法用于删除原数组的一部分成员,可以在删除的位置添加新的数组成员
- 返回值
- 被删除的元素组成的数组
- 返回值是一个数组
- 如果删除0个成员,还添加新成员,返回值还是一个空数组,和添不添加无关
- 参数
- start:
- 删除的起始位置,默认从0开始
如果起始位置是负数,表示从倒数位置开始删
- 如果是添加成员,则会添加到start位置的前面
- 如果splice只提供一个参数,等于将原数组在指定位置拆分成两个数组
- count: 被删除的元素个数
- 后面的参数
- 是要添加到数组的新元素
添加的成员会放在start位置的前面
- start:
改变原数组
const a = [1,2,3,4]
const b = a.splice(0)
a // []
b // [1,2,3,4]
因为splice()改变原数组,a就会变成空数组
(二) 前端路由
(1) Hash路由
- url中的hash以#号开头,原本用来作为锚点,从而定位到页面的特定区域
- 当 hash 发生改变时,页面不会刷新,浏览器也不会向服务器发送请求
- 注意:hash改变时,可以触发 hashchange 事件,在监听函数中可以请求数据,实现页面的更新操作
(1) 作为锚点,定位页面的特定位置
<a href="#anchor1">锚点1</a>
<a href="#anchor2">锚点2</a>
<div id="anchor1">锚点1的位置</div>
<div id="anchor2">锚点2的位置</div>
说明:
- 点击a2,页面会跳转到div2的位置
- 并且页面的hash部分也会改变,即 url 中以 #开头的字符串会改变
- anchor:是锚的意思
- 注意:a标签的name属性已经废弃,用id代替 (因为有的教程使用name属性实现的)
(2) hashchange事件
- 如果监听了 ( hashchange ) 事件,hash改变,( 地址栏的hash部分也会改变 ),同时hashchange也会触发
- 但是 ( 浏览器不会刷新 ),即浏览器的刷新按钮的 ( 圈圈不会转动 )
- 但是可以利用hashchange的回调函数更新页面内容,注意不是刷新页面
<body>
<a href="#anchor1">锚点1</a>
<a href="#anchor2">锚点2</a>
<script>
window.addEventListener('hashchange', function() {
console.log('111111111')
}, false)
</script>
</body>
说明:
- 点击a标签,url中的hash改变,hash改变,hashchange事件触发,则监听函数就会执行,输出111111
(3) 手动实现一个hash路由
- 原理
- ( hash改变 ) 时,地址栏的hash会变化,同时触发 ( hashchange ) 事件
- 在 hashchange 事件的监听函数中去更新视图
- 代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<a href="#/home">home页面</a>
<a href="#/other">other页面</a>
<div id="content">内容部分,即路由替换的部分</div>
<div id="current-hash"></div>
<script>
const routes = [{
path: '/home',
component: 'home页面的内容'
}, {
path: '/other',
component: 'other页面的内容'
}]
class HashRouter {
constructor(routes) {
this.route = {} // 路由映射
this.createRouteMap(routes) // 创建路由映射,为 ( this.route ) 创建 ( map ) 映射;key=path;value=()=>{更新页面}
this.init() // 初始化
}
createRouteMap = (routes) => {
if (routes.length) {
routes.forEach(({ path, component }) => {
this.route[path] = () => {
document.getElementById('content').innerHTML = component // 替换内容
}
})
}
}
init = () => {
window.addEventListener('load', this.updateView, false)
window.addEventListener('hashchange', this.updateView, false)
}
// 更新视图
updateView = () => {
// (1)
// 这里 ( load事件 ) 和 ( hashchange事件 ) 都会触发 ( updateView方法 )
// (2)
// load事件: ( 页面加载完成时触发 ),包括 ( DOM,样式,图片,视频等所有资源都加载完成 )
// DOMContentLoaded事件: 是在 ( DOM加载完成时触发 )
// (3)
// 当load事件触发时,hash并没有改变,即 window.location.hash = '' => ''.slice(1) => ''
const hash = this.getCurrentHash() // 获取hash
// if (Object.keys(this.route).includes(hash)) { // 还有更简单的方法
// this.route[hash]()
// }
if (this.route[hash]) this.route[hash]() // 如果this.route对象中的key对应得值存在,就执行该函数
}
// 获取当前地址栏的 hash
getCurrentHash = () => {
const hash = window.location.hash.slice(1)
this.printHahToHtml(hash) // 该函数是用来在html中显示当前hash的
return hash ? hash : '/home'
// load事件触发时,hash就不存在,hash='',这种情况下即默认情况下返回 '/home' 路由
// load事件触发时,window.location.hash => 返回 '' 空字符串
// ''.slice(1) => 返回''
}
printHahToHtml = (hash) => {
const DOM = document.getElementById('current-hash')
DOM.innerHTML = `当前页面的hash是:=> #${hash}`
DOM.style.setProperty('background', 'yellow')
DOM.style.setProperty('padding', '10px')
}
}
new HashRouter(routes)
</script>
</body>
</html>
(2) history路由
三个方法 pushState()
replaceState()
popstate事件
(1) window.history 对象
- window.history对象的方法:
back()
forward()
go()
pushState()
replaceState()
- pushState() 和 repalceState()
- 都不会触发页面更新,只能导致history发生变化,地址栏的url会有变化
- 都会改变url,不会触发 popstate 事件,地址栏的url会有变化
(2) window.history.pushState(state, title, url)
- 参数
- state: 是一个与添加的记录相关联的对象
- title: 新页面的标题,现在所有浏览器都忽略该参数,可以传入空字符串
- url: 新的url地址,必须与当前页面同一个域,浏览器的地址栏显示这个网址
window.history.pushState({}, null, url)
- pushState不会刷新页面,只会改变history对象,地址栏的url会变化
- 可以通过 History.state 来读取状态
(3) popstate
popstate触发的条件
- 浏览器的前进后退按钮
- history.go()
- history.back()
- history.forward()
- window.history.pushState() 和 window.history.replaceState() 不会触发 popstate 事件
- pushState()和replaceState()
- 可以改变url,而且不向服务器发送请求,不存在#号,比hash美观
- 但是需要服务器的支持,并且需要所有的路由重定向到根页面
原理分析
- 第一步:给每个a标签都绑定一个click事件,click事件触发时,再去触发pushState()事件,将url的path部分改变为a的data-href自定义属性的值即路由的path,传入window.history.pushState({}, null, path),这样地址栏的url就改变了
- 第二步:改变地址栏的url后,通过window.history.pathname获取更新的url的path部分
- 第三步:用 path 和 route对象中的key去匹配,匹配上就去执行更新视图的函数
- 第四步:
这只是一条线:1-3步即点击a标签的情况
还有一条线:浏览器的前进后退,和函数式导航go() back() forward() 则有 popstate 事件来处理,过程差不多
代码
- 封装一个方法,在pushState()和replaceState()改变url后调用,在该方法中获取最新的window.location.pathname,更新页面
- 通过 go() back() forward() 浏览器前进后退等触发 popstate 事件
<!DOCTYPE html>
<html lang="en"
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<a href="javascript:void(0)" data-href="/home">点击去home页面</a>
<a href="javascript:void(0)" data-href="/other">点击去other页面</a>
<div id="content">内容部分,即路由要替换的内容</div>
<script>
const routes = [{
path: '/home',
component: '<h1>home页面</h1>'
}, {
path: '/other',
component: '<h1>other页面</h1>'
}]
class HistoryRouter {
constructor(routes) {
this.route = {} // 路由映射 key=path value=()=>{更新视图}
this.createRouteMap(routes) // 创建路由映射
this.bindEvent() // 绑定事件
this.init() // 初始化
}
createRouteMap = (routes) => {
if (routes.length) {
routes.forEach(({ path, component }) => {
this.route[path] = () => {
document.getElementById('content').innerHTML = component
}
})
}
}
bindEvent = () => {
const a = document.getElementsByTagName('a')
Array.prototype.forEach.call(a, aDom => {
aDom.addEventListener('click', () => {
const path = aDom.getAttribute('data-href')
this.triggerPushState(path) // 触发pushState事件
}, false)
})
}
triggerPushState = (path) => {
window.history.pushState({}, null, path)
// pushState() 可以改变地址栏的url,但是不会触发页面更新,所以要执行下面的更新函数
// (1) 情况1:这只是 ( 点击a标签 ) 的情况,使用的是 pushState() 函数
// (2) 情况2:还有就是 ( 点击浏览器的前进后退按钮 ) 和 ( 函数式k导航 window.history.go() back() forward() 的情况 )
// (3) 情况3:就是初始化时,在 ( load ) 事件触发是的情况,默认path='/'
this.updateView()
}
updateView = () => {
// 因为:在执行该方法之前,已经触发了 pushState() || popstate事件 || load事件
// 所以:可以用window.location.pathname 获取最新的 url中的 path 部分
const currentPath = window.location.pathname
? window.location.pathname
: '/'
if (this.route[currentPath]) this.route[currentPath]()
}
init = () => {
window.addEventListener('load', this.updateView, false) // 页面加载完成时的情况
window.addEventListener('popstate', this.updateView, false) // popstate触发的情况,浏览器前进后退和函数式导航
}
}
new HistoryRouter(routes)
</script>
</body>
</html>
注意:
- history路由是需要启动服务的
- 该html需要用 Live Server 启动,vscode插件
(3) 手动实现一个vue-router (hash路由版本)
- 大体的原理和hash路由的html版本一样,微小区别
- 都是利用 ( a标签 ) 的 ( href ) 中的 ( '#/xxx' ) 这样的hash串
- 只要点击 a标签 => 地址栏的url的hash部分就会改变 => 同时触发 hashchange 事件 => 通过window.location.hash获取最新的hash
- 获取到 hash 在和 routeMap 中的path匹配,匹配后就改变视图
- 区别
- 区别就是 Vue 自己封装了
<router-link>
和<router-view>
组件 - 可以通过 Vue.component() 方法注册上面两个组件
- 通过
vm.name 可以访问到 new Vue({data: {name: xx}})中的name
- 区别就是 Vue 自己封装了
手动实现一个vue-router(hash版)
vue相关前置知识
- <router-link to="#/home">home</router-link> // 点击会跳转到 '#/home' 地址
- <router-view></router-view> // 路由将显示的DOM位置
// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
- 因为组件是可复用的 Vue 实例,所以它们与 new Vue 接收相同的选项
- 例如 data、computed、watch、methods 以及生命周期钩子等。
---------------
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<!-- 引入 Vue 通过CDN引入 -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<!-- 注意 router-link组件具有 to 属性 -->
<router-link to="#/home">home</router-link>
<router-link to="#/other">other</router-link>
<router-view></router-view>
</div>
<script>
// 创建两个vue组件
const Home = {template: '<h1>home页面</h1>'}
const Other = {template: '<h1>other页面</h1>'}
// 创建vue路由数组
const routes = [{
path: '/home',
component: Home
}, {
path: '/other',
component: Other
}]
class VueRouter {
constructor(Vue, option) {
// 参数:
// Vue:Vue构造函数,通过cdn引入的
// option: 配置对象,包含routes路由数组属性
this.$options = option
this.routeMap = {} // 路由映射,是这样的结构 { path: component }
this.createRouteMap(this.$options) // 创建路由映射
this.app = new Vue({
data: {
currentHash: '#/'
}
})
// this.app.currentHash => 可以访问到currentHash的值 '#/'
// 举例
// var data = {name: 'woow_wu7'}
// var vm = new Vue({
// data: data
// })
// vm.name === data.name => true
this.init() // 初始化监听函数
this.initComponent(Vue) // 初始化Vue种的各种组件
}
createRouteMap = (option) => {
// 注意:option 是传入VueRoute的第二个参数,即 {routes: routes}
// 所以:options是一个对象
option.routes.forEach(item => {
this.routeMap[item.path] = item.component
// this.routeMap是这样一个对象:{path: component}
})
}
init = () => {
window.addEventListener('load', this.onHashChange, false)
// 页面加载完成触发,注意区别 DOMContentLoaded
// load:页面加载完成时触发,包括 DOM加载完成,图片,视频等所有资源加载完成
// DOMContentLoaded:DOM加载完成时触发
window.addEventListener('hashchange', this.onHashChange, false)
// 监听 hashchange 事件
// 触发hashchange的条件:hash改变时候
}
onHashChange = () => {
this.app.currentHash = window.location.hash.slice(1) || '/'
// (1)
// 当 hahs没有改变时,load事件触发时
// window.location.hash = '' => window.location.hash.slice(1) = ''
// 所以:此种情况:this.app.currentHash = '/'
// (2)
// hash改变时,window.location.hash有值,是 '#/...' 这样的字符串
}
initComponent = (Vue) => {
// router-link组件
// props to属性
// template 本质上会被处理成a标签,href属性是传入的 to 属性,内容是 slot 插入的内容
Vue.component('router-link', {
props: {
to: {
type: String,
value: ''
}
},
template: '<a :href="to"><slot/></a>'
})
Vue.component('router-view', {
render: (h) => {
const component = this.routeMap[this.app.currentHash] // 拿到最新hash对应的组件
return h(component)
// h(component) 相当于 createElement(component)
// render: function(createElement) { return createElement(App); }
}
})
}
}
new VueRouter(Vue, {
routes
})
new Vue({
el: '#app'
})
</script>
</body>
</html>
(三) 继承
(1) 原型链继承
- 将 ( 子类的prototype ) 指向 ( 父类的实例 ),同时修改 ( 子类的constructor ) 让其重新指向子类
- 缺点:
- 创建子类实例时,不能向父类传参
- 不能实现多继承 ( 继承多个父类 ),主要是因为直接给prototype属性直接赋值
- 多个实例共享父类的属性和父类prototype上的属性,当属性是引用类型时,子类实例间修改会相互影响【特别对于数组】
- 在子类的prototype上挂属性和方法,必须要在修改子类的prototype指向之后
Sub.prototype = new Super()之后,才可以挂载子类实例的原型属性 Sub.prototype.sex = 'man',不然会被新的引用替代
原型链继承再复习
1. 原理
- 将 ( 子类的prototype属性 ) 指向 ( 父类生成的实例 ),那么 new 子类生成的实例就能继承 ( 父类 ) 和 ( 父类prototype ) 上的属性和方法
2. 缺点
- 不能实现多继承 ( 继承多个父类 ),因为 prototype 是直接赋值的
- 创建子类实例时,不能向父类传参
- 子类prototype属性挂载属性,必须是在子类prototype赋值之后
- 多个子类实例是共享父类实例和父类实例原型上的属性和方法,修改引用类型的数据时会相互影响
3. 代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 原型链继承
// 父类
function Super(name) {
this.superName = name
}
Super.prototype.superAge = 20
// 子类
function Sub(name) {
this.subName = name
}
// 原型链继承
// 原理:将子类的prototype指向父类的实例,这样 ( 子类的实例 ) 就能继承 ( 父类实例 ) 和 ( 父类实例原型上 ) 的属性和方法
Sub.prototype = new Super('super')
// 修改prototpe属性后,一定要同时修改constructor的指向,防止引用出错
Sub.prototype.contructor = Sub
Sub.prototype.subAge = 10 // 缺点:挂载属性必须在prototype赋值之后
const sub = new Sub('sub') // 缺点:只能向子类传参,不能向父类传参
console.log('sub.subName', sub.subName)
console.log('sub.subAge', sub.subAge)
console.log('sub.superName', sub.superName)
console.log('sub.superAge', sub.superAge)
</script>
</body>
</html>
(2) 借用构造函数继承 ( 经典继承 )
- 原理
- 在 ( 子类 ) 中通过 call() 方法将 ( 父类中的this绑定为子类的this ),并执行父类的构造函数,即把父类的this换成子类的this
- 优点
- 可以实现多继承,即在子类中call多个父类
- 生成子类实例时,可以向父类传参
- 属性和方法都是生成在子类实现上的,每个实例独享,修改属性不会相互影响
- 缺点
- 子类实例不能继承父类prototype原型链上的属性和方法,因为只是利用了构造函数,而没有通过new命令调用
- 属性和方法生成在子类上,相互之间不共享,造成资源浪费
- 代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 借用构造函数继承
function Super1(name) { // 父类1
this.superName1 = name
}
function Super2(name) { // 父类2
this.superName2 = name
}
Super2.prototype.superAge2 = 20 // 缺点:不能继承父类prototype属性原型链上的属性和方法
function Sub(superName1, superName2, subName) { // 子类
Super1.call(this, superName1) // 借用构造函数继承
Super2.call(this, superName2) // 优点:可以实现多继承
this.subName = subName
}
const sub = new Sub('super1', 'super2', 'sub') // 优点:生成子类实例时,可以向父类传参
console.log('sub.superName1', sub.superName1)
console.log('sub.superName2', sub.superName2)
console.log('sub.subName', sub.subName)
</script>
</body>
</html>
(3) 组合式继承 ( 原型链继承 + 借用构造函数继承 )
- 原理:原型链继承 + 借用构造函数继承
- 优点
既具有借用构造函数继承的优点(向父类传参,多继承,不存在属性共享
又具有原型链继承的优点(继承父类实例原型链上的属性和方法,并且是共享)
- 缺点
会调用两次父构造函数,导致 (子类实例-即借用构造函数继承 ) 和 ( 子类实例的原型链上-即原型链继承 ) 上都有相同的属性和方法
- 本例中:子类实例上有 superName1 属性;子类实例的原型链上也有 superName1 属性
父类被调用了两次,一次是借用构造函数是的call调用,一次是原型链继承时的new调用
因为父类两次调用,所以子类和父类实例原型链上有相同的属性和方法,造成浪费
- 代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 组合式继承 = 借用构造函数继承 + 原型链式继承
// 优点:两者组合,相互补充
// 缺点:
// 1. 会调用两次父构造函数,导致 (子类实例-即借用构造函数继承 ) 和 ( 子类实例的原型链上-即原型链继承 ) 上都有相同的属性和方法
// - 本例中:子类实例上有 superName1 属性;子类实例的原型链上也有 superName1 属性
// 2. 父类被调用了两次,一次是借用构造函数是的call调用,一次是原型链继承时的new调用
// 3. 因为父类两次调用,所以子类和父类实例原型链上有相同的属性和方法,造成浪费
function Super1(name) {
this.superName1 = name
}
function Super2(name) {
this.superName2 = name
}
Super1.prototype.superAge1 = 10
Super2.prototype.superAge2 = 20
function Sub(superName1, superName2, subName) {
// 借用构造函数继承
// 优点:可以向父构造函数传参,多继承,属性不共享
// 缺点:不能继承父类prototype对象原型链上的属性和方法
Super1.call(this, superName1)
Super2.call(this, superName2)
this.subName = subName
}
// 原型链继承
// 优点:可以继承父类实例原型链上的属性和方法,共享属性
// 缺点:在生成子类实例时不能向父类传传参,不能实现多继承,继承的属性是引用类型时,子类实例之间修改会相互影响
Sub.prototype = new Super1()
Sub.prototype.constructor = Sub
Sub.prototype.subAge = 30
const sub = new Sub('super1', 'super2', 'sub')
console.log('sub', sub)
console.log('sub.superName1', sub.superName1)
console.log('sub.superName2', sub.superName2)
console.log('sub.subName', sub.subName)
console.log('sub.superAge1', sub.superAge1)
console.log('sub.subAge', sub.subAge)
</script>
</body>
</html>
(4) 继承组合式继承
- 要解决的问题:在组合继承中两次调用父构造函数的问题
- 主要解决:
- 组合式继承中,父类被多次调用,导致子类实例属性和子类实例原型链上有相同的属性的问题
- 因为父类两次被调用,call和new,构造函数中的属性会两次生成,造成资源的浪费
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 寄生组合式继承
function Super1(name) {
this.superName1 = name
}
function Super2(name) {
this.superName2 = name
}
Super1.prototype.superAge1 = 20
function Sub(superName1, superName2, name) {
Super1.call(this, superName1)
Super2.call(this, superName2)
this.subName = name
}
function Parasitic() { } // 中间函数,本身没有任何属性和方法
Parasitic.prototype = Super1.prototype
// 这样 sub 实例就能继承 Super1.prototype上的属性和方法,而这条继承线不用在继承 super1 实例上的方法
Sub.prototype = new Parasitic()
Sub.prototype.constructor = Sub
Sub.prototype.subAge = 30
const sub = new Sub('super1', 'super2', 'sub')
console.log('sub', sub)
</script>
</body>
</html>
(四) 观察者模式 和 发布订阅模式
(1) 观察者模式
- 对程序中某个对象进行观察,并在发生改变时得到通知
- 存在 ( 观察者对象 ) 和 ( 目标对象 ) 两种角色
目标对象:subject
观察者对象:observer
在观察者模式中,subject和observer相互独立又相互联系
- 一对多:一个 ( subject ) 对象对应多个 ( observer ) 对象
- observer对象在subject对象中 ( 订阅事件 ),目标对象 ( 广播对象 )
Subject对象:维护一个观察者实例组成的数组,并且具有 ( 添加,删除,通知 ) 操作该数组的各种方法
Observer对象:仅仅需要维护收到通知后 ( 更新 ) 操作的方法
代码实现 - ES5
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function Subject() { // 目标对象的构造函数
this.observers = [] // 观察者实例组成的数组
}
Subject.prototype = {
add(...params) { // 添加观察者,可以一次添加多个
this.observers = this.observers.concat(params)
},
delete(obj) { // 删除观察者
const cache = this.observers
cache.forEach((item, index) => {
if (item === obj) {
cache.splice(index, 1) // 删除该对象
}
})
},
notify() { // 通知观察者
if (this.observers.length) {
this.observers.forEach(item => item.update())
}
}
}
// 修改prototype时,一定要记得修改constructor,防止引用出错
Subject.prototype.constructor = Subject
function Observer(fn) { // 观察者对象的构造函数
this.update = fn // 观察者对象只需要维护 ( 更新函数 )
}
// 观察者对象上具有update更新函数,并且观察者对象也只需要维护update方法
const observerObj1 = new Observer(() => console.log('observer update1'))
const observerObj2 = new Observer(() => console.log('observer update2'))
// 目标对象上具有 ( 添加, 删除, 通知 ) 等方法
const subject = new Subject()
subject.add(observerObj1, observerObj2) // 将观察者对象添加到目标对象维护的observers数组中
subject.notify() // 目标对象发送通知,则执行观察者对象上的更新方法
subject.delete(observerObj1)
subject.notify()
</script>
</body>
</html>
- 代码实现 - ES6
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// ES6
class Subject {
constructor() {
this.observers = [] // 观察者对象数组
}
add(...params) {
this.observers = this.observers.concat(params)
}
delete(obj) {
this.observers.forEach((observer, index) => {
if (obj === observer) {
this.observers.splice(index, 1)
}
})
}
notify() {
this.observers.forEach(observer => {
if (observer.update) {
observer.update()
}
})
}
}
class Observer {
constructor(fn) {
this.update = fn
}
}
const observerObj1 = new Observer(() => console.log('observer update1111'))
const observerObj2 = new Observer(() => console.log('observer update2222'))
const subject = new Subject()
subject.add(observerObj1, observerObj2)
subject.notify()
subject.delete(observerObj2)
subject.notify()
</script>
</body>
</html>
(2) 发布订阅模式
角色
- 发布者 Publisher
- 订阅者 Subscriber
- 中介 Topic/Event Channel
- 中介既要接收发布者所发布的消息,又要将消息派发给订阅者
- 通过中介对象,完全解耦了 ( 发布者 ) 和 ( 订阅者 )
发布订阅模式 - ES5实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 中介对象
const pubsub = {}
// 注意:( 小括号 ) 和 ( 中括号 ) 开头的 ( 前一条语句 ) 必须加分号,或者在小括号或中括号的最前面加分号
;(function(pubsub) {
const topic = {}
// 订阅
// subscribe(订阅的事件名, 事件触发的回调函数)
pubsub.subscribe = function(eventName, fn) {
if (!topic[eventName]) topic[eventName] = [];
topic[eventName].push({
fnName: fn.name,
fn,
})
console.log('topic[eventName]', topic[eventName])
}
// 发布
// publish(事件名,事件触发对应的回调函数的参数)
pubsub.publish = function(eventName, params) {
console.log('topic[eventName]', topic[eventName])
if (topic[eventName]) {
topic[eventName].forEach(observer => {
observer.fn(params)
})
}
}
// 取消订阅
// unScribe(需要取消的事件名, 需要取消的回调函数名)
pubsub.unScribe = function(eventName, fnName) {
if (topic[eventName]) {
topic[eventName].forEach((observer, index) => {
if (observer.fnName === fnName) {
topic[eventName].splice(index, 1)
}
})
}
}
})(pubsub)
pubsub.subscribe('go', function go1(address1){console.log(`${address1}one`)})
pubsub.subscribe('go', function go2(address2){console.log(`${address2}two`)})
pubsub.publish('go', 'home')
pubsub.unScribe('go','go1') // 取消订阅go1函数
pubsub.publish('go', 'work')
</script>
</body>
</html>
发布订阅模式 - ES6实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
class PubSub {
constructor() {
this.topic = {}
}
subscribe = (eventName, fn) => {
if (!this.topic[eventName]) this.topic[eventName] = [];
this.topic[eventName].push({
fnName: fn.name,
fn
})
}
publish = (eventName, params) => {
if (this.topic[eventName]) {
this.topic[eventName].forEach(observer => {
observer.fn(params)
})
}
}
unSubscribe = (eventName, fnName) => {
if (this.topic[eventName]) {
this.topic[eventName].forEach((observer, index) => {
if ( observer.fnName === fnName) {
this.topic[eventName].splice(index, 1)
}
})
}
}
}
const pubSub = new PubSub()
pubSub.subscribe('go', function go1(params) { console.log(`${params+'1'}`)})
pubSub.subscribe('go', function go2(params) { console.log(`${params+'2'}`)})
pubSub.publish('go', 'home')
pubSub.unSubscribe('go', 'go1') // 取消订阅
pubSub.publish('go', 'work')
</script>
</body>
</html>
(3) 观察者模式和发布订阅模式的区别和联系
- 区别
- 观察者模式:需要观察者自己定义事件发生改变时的响应函数
- 发布订阅模式:在发布者和订阅者之间加了中介对象
- 联系
- 二者降低了代码的耦合性
- 都具有消息传递的机制,以数据为中心的思想
(4) 手动实现vue的双向数据绑定
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- 引入 Vue 通过CDN引入 -->
<!-- <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> -->
</head>
<body>
<div id="root">
<div type="text" v-text="name">v-text的内容</div>
<input type="text" v-model="name">
<script>
class MyVue {
constructor(options) {
const { el, data } = this.$options = options
this.$el = document.getElementById(el)
this.$data = data
this._directive = {}
// key:data对象中的key
// value:
this._observer(this.$data)
this._compile(this.$el)
}
_observer = (data) => {
for (let [key, value] of Object.entries(data)) {
// key就是data对象中的key; value就是data对象中每个key对应的值
// data: {name: 'woow_wu7'} => key=name,value='woow_wu7'
if (data.hasOwnProperty(key)) {
this._directive[key] = [] // data中每个key都对应一个数组
}
if (typeof value === 'object') this._observer(value);
const that = this
Reflect.defineProperty(this.$data, key, {
enumerable: true,
configurable: true,
get() {
return value
},
set(newValue) {
if (value !== newValue) {
value = newValue
that._directive[key].forEach(item => item._update())
}
}
})
}
}
_compile = (el) => {
for (let [key, value] of Object.entries(el.children)) {
if (value.length) {
this._compile(value)
}
if (value.hasAttribute('v-text')) {
const attrubuteValue = value.getAttribute('v-text')
this._directive[attrubuteValue].push(new Watcher('input', value, this, attrubuteValue, 'innerHTML'))
// 注意:
// attrubuteValue是v-text对应的值 => 其实就是data中的key值,和_observer中的声明保持一致了
}
if (value.hasAttribute('v-model') && value.tagName === 'INPUT' || value.tagName === 'TEXTAREA') {
const attributeValue = value.getAttribute('v-model')
this._directive[attributeValue].push(new Watcher('v-model', value, this, attributeValue, 'value'))
const that = this
value.addEventListener('input', (e) => {
// 1. input事件修改data中的属性
// 2. data中的属性被修改,触发 Reflect.defineProperty 的 setter() 函数
this.$data[attributeValue] = e.target.value
})
}
}
}
}
class Watcher {
constructor(directiveName, el, vm, exp, attr) {
this.name = directiveName // 指令的名字,比如 'v-text','v-model'
this.el = el // 每个具体的DOM节点
this.vm = vm // MyVue实例对象
this.exp = exp // el中的directiveName属性对应的属性值
this.attr = attr // el的属性,需要需改的属性
this._update()
}
_update = () => {
this.el[this.attr] = this.vm.$data[this.exp]
// 将MyVue实例的data属性的最新值更新到ui视图中
}
}
new MyVue({
el: 'root',
data: {
name: 'woow_wu7'
}
})
</script>
</body>
</html>
(五) reflow回流 和 repaint重绘
(1) reflow回流 - 回流也叫(重排)
- reflow概念
- 对DOM的修改引发了DOM几何尺寸的变化(宽高,隐藏)等,浏览器需要重新计算元素的几何属性。同时其他元素的几何属性和位置也会受到影响,浏览器需要将计算结果绘制出来,这个过程叫做回流reflow
- repaint概念
- 对DOM的修改,只是导致了样式变化,并没有改变几何属性,浏览器不需要重新计算几何属性,而是直接绘制新的样式,这个过程叫做重绘repaint
重绘不一定引起回流,但回流一定会引起重绘
(2) 常见的会引起 ( 回流 ) 的操作
- 页面首次渲染
- 浏览器窗口变化
元素尺寸和位置发生变化
元素字体发生变化
添加和删除可见DOM
激活css伪类
- offsetWidth, width, clientWidth, scrollTop/scrollHeight的计算, 会使浏览器将渐进回流队列Flush,立即执行回流
(六)script标签的两个属性 - ( async ) ( defer )
- defer
- 异步加载,不阻塞页面,
在DOM解析完成后才执行js文件
顺序执行,不影响依赖关系
- 异步加载,不阻塞页面,
- async
- 异步加载,加载不阻塞页面,但是
async会在异步加载完成后,立即执行,如果此时html未加载完,也就阻塞页面
- 异步加载,加载不会阻塞页面,执行会阻塞页面
不能保证各个js的执行顺序
- 异步加载,加载不阻塞页面,但是
(七) 前端模块化
(1) 模块的概念
- 将一个复杂程序的各个部分,按照一定的规则(规范)封装不同的块(不同的文件),并组合在一起
- 块 内部的变量和方法是私有的,只会向外暴露一些接口,通过接口与外部进行通信
(2) 非模块化存在的问题
- 对全局变量的污染
- 各个js文件内部变量会造成相互修改,即只存在全局作用域,没有函数作用域
- 各个模块如果存在依赖关系,依赖关系模糊,很难分清是谁依赖谁,而依赖又必须前置
- 难以维护
(3) 模块化的各种方案
- IIFE
- Commonjs规范
- AMD
- CMD
- ES6的模块化方案
- 总结:
- Commonjs用于服务端,同步加载,是nodejs使用的模块化方案,commonjs模块就是对象,输入时必须查找对象属性
- AMD和CMD主要用于浏览器,异步加载
- ES6的模块化方案,用于浏览器和服务器,通用方案,静态化
AMD依赖前置,依赖必须一开始写好,提前加载依赖 ------ RequireJs
CMD依赖就近,需要使用的时候才去加载依赖 ------------ SeaJs
ES6模块化是静态的,在编译时就能确定模块的依赖关系,输入输出的变量;而AMD,CMD, CommonJs必须在运行时才能确定
(4) ES6的模块化方案
(1) ES6的模块化方案 和 CommonJs 的对比
- Commonjs模块是对象,输入时必须遍历对象的属性,运行时确定模块的依赖关系,输入输出变量
- ES6的模块不是对象,而是通过export显示输出,再通过import显示输入
(2) export 命令
- export用于规定模块对外的接口
- import用于输入其他模块提供的功能
- 一个模块就是一个独立的文件,该模块内部的变量,外部无法获取
- 如果外部需要获取模块内的变量就需要使用 ( export ) 命令 ( 输出 ) 该 ( 变量 )
- (
export
) 命令除了输出 (变量
),还可以输出 (函数
) 或者 (类
) - 通常情况export输出的变量就是本来的名字,但是可以使用
as
关键字重命名
(3) import 命令
- import命令输入的变量是只读的,因为它的本质就是输入接口,即不允许在加载模块的脚本里改写接口
- 但是导入的变量是一个对象,修改导入对象的属性是可以的,只是其他模块读取的也将会是改写过后的值,难排错不建议使用
- from的路径:可以是相对路径也可以是绝对路径
- import也具有提升效果,类似于变量提升,因为import命令是在 ( 编译阶段 ) 执行,在代码 ( 运行阶段 ) 之前
(4) export default 命令
- export default指定默认输出,一个模块只能有一个默认输出
(5) export 和 import 的复合写法
export { foo, bar } from 'my_module';
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };
// 注意!!!!!!!!!!
1. foo 和 bar 并没有导入当前模块,只是相当于对外转发了这两个接口
2. 所以当前模块是不能使用 foo 和 bar 变量的
(6) import(specifier) 函数
- 作用
- import() 函数可以实现在运行时加载模块
- 参数
- specifier:指定所要加载模块的位置
- import命令能接收什么参数,import()函数就能接收什么参数,区别是import()是动态的
- 返回值
import()方法返回的是一个 promise 对象
- import()可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用
- 适用场合
- 按需加载
- 条件加载
- 动态的模块路径
(八) PlainObject
- PlainObject表示纯对象,即用对象字面量方式创建( {} ),或者用构造函数创建的对象( new Object() )
(九) redux 和 react-redux源码
(1) 源码仓库
- [redux和react-redux]源码分析仓库
- [源码] Redux React-Redux01 我的掘金文章
(2) redux
(3) react-redux
- Provider
- connect
useSelector
useDispatch
useStore
- createSelectorHook
- createDispatchHook
- createStoreHook
- shallowEqual 浅比较
shallow是浅的意思
- batch
- connectAdvanced
- ReactReduxContext
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!