[复习笔记-03] 03

449 阅读8分钟

(一) 前置知识

(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
  • 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位置的前面
  • 改变原数组
const a = [1,2,3,4]
const b = a.splice(0)

a // []
b // [1,2,3,4]
因为splice()改变原数组,a就会变成空数组

(二) redux 和 react-redux源码

(1) 源码仓库

(2) redux

(3) react-redux

  • Provider
  • connect
  • useSelector
  • useDispatch
  • useStore
  • createSelectorHook
  • createDispatchHook
  • createStoreHook
  • shallowEqual 浅比较 shallow是浅的意思
  • batch
  • connectAdvanced
  • ReactReduxContext

(三) axios 源码

(1) 源码仓库

(四) webpack-Compiler 源码手写

(1) 源码仓库

前端模块化

DOM - 文档对象模型

  • DOM是Document Object Model的缩写,表示文档对象模型

手写Promise

(五) 前端路由

(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-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元素
  • ( 显示和隐藏 ) 可见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() )