28 框架相关

1 将虚拟 Dom 转化为真实 Dom

    {
      tag: 'DIV',
      attrs:{
      id:'app'
      },
      children: [
        {
          tag: 'SPAN',
          children: [
            { tag: 'A', children: [] }
          ]
        },
        {
          tag: 'SPAN',
          children: [
            { tag: 'A', children: [] },
            { tag: 'A', children: [] }
          ]
        }
      ]
    }
    
    把上面虚拟Dom转化成下方真实Dom
    
    <div id="app">
      <span>
        <a></a>
      </span>
      <span>
        <a></a>
        <a></a>
      </span>
    </div>

实现

    // 真正的渲染函数
    function _render(vnode) {
      // 如果是数字类型转化为字符串
      if (typeof vnode === "number") {
        vnode = String(vnode);
      }
      // 字符串类型直接就是文本节点
      if (typeof vnode === "string") {
        return document.createTextNode(vnode);
      }
      // 普通DOM
      const dom = document.createElement(vnode.tag);
      if (vnode.attrs) {
        // 遍历属性
        Object.keys(vnode.attrs).forEach((key) => {
          const value = vnode.attrs[key];
          dom.setAttribute(key, value);
        });
      }
      // 子数组进行递归操作
      vnode.children.forEach((child) => dom.appendChild(_render(child)));
      return dom;
    }

2 实现事件总线结合Vue应用

Event Bus(Vue、Flutter 等前端框架中有出镜)和 Event Emitter(Node中有出镜)出场的“剧组”不同,但是它们都对应一个共同的角色——全局事件总线

全局事件总线,严格来说不能说是观察者模式,而是发布-订阅模式。它在我们日常的业务开发中应用非常广。

如果只能选一道题,那这道题一定是 Event Bus/Event Emitter 的代码实现——我都说这么清楚了,这个知识点到底要不要掌握、需要掌握到什么程度,就看各位自己的了。

在Vue中使用Event Bus来实现组件间的通讯

Event Bus/Event Emitter 作为全局事件总线,它起到的是一个沟通桥梁 的作用。我们可以把它理解为一个事件中心,我们所有事件的订阅/发布都不能由订阅方和发布方“私下沟通”,必须要委托这个事件中心帮我们实现。

在Vue中,有时候 A 组件和 B 组件中间隔了很远,看似没什么关系,但我们希望它们之间能够通信。这种情况下除了求助于 Vuex 之外,我们还可以通过 Event Bus 来实现我们的需求。

创建一个 Event Bus(本质上也是 Vue 实例)并导出:

    const EventBus = new Vue()
    export default EventBus

在主文件里引入EventBus,并挂载到全局:

    import bus from 'EventBus的文件路径'
    Vue.prototype.bus = bus

订阅事件:

    // 这里func指someEvent这个事件的监听函数
    this.bus.$on('someEvent', func)

发布(触发)事件:

    // 这里params指someEvent这个事件被触发时回调函数接收的入参
    this.bus.$emit('someEvent', params)

大家会发现,整个调用过程中,没有出现具体的发布者和订阅者(比如上面的PrdPublisherDeveloperObserver),全程只有bus这个东西一个人在疯狂刷存在感。这就是全局事件总线的特点——所有事件的发布/订阅操作,必须经由事件中心,禁止一切“私下交易”!

实现方式1

    class EventEmitter {
      constructor() {
        // handlers是一个map,用于存储事件与回调之间的对应关系
        this.handlers = {}
      }
    
      // on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数
      on(eventName, cb) {
        // 先检查一下目标事件名有没有对应的监听函数队列
        if (!this.handlers[eventName]) {
          // 如果没有,那么首先初始化一个监听函数队列
          this.handlers[eventName] = []
        }
    
        // 把回调函数推入目标事件的监听函数队列里去
        this.handlers[eventName].push(cb)
      }
    
      // emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数
      emit(eventName, ...args) {
        // 检查目标事件是否有监听函数队列
        if (this.handlers[eventName]) {
          // 如果有,则逐个调用队列里的回调函数
          this.handlers[eventName].forEach((callback) => {
            callback(...args)
          })
        }
      }
    
      // 移除某个事件回调队列里的指定回调函数
      off(eventName, cb) {
        const callbacks = this.handlers[eventName]
        const index = callbacks.indexOf(cb)
        if (index !== -1) {
          callbacks.splice(index, 1)
        }
      }
    
      // 为事件注册单次监听器
      once(eventName, cb) {
        // 对回调函数进行包装,使其执行完毕自动被移除
        const wrapper = (...args) => {
          cb.apply(...args)
          this.off(eventName, wrapper)
        }
        this.on(eventName, wrapper)
      }
    }

实现方式2

  • 分析
    • ononce注册函数,存储起来
    • emit时找到对应的函数,执行
    • off找到对应函数,从存储中删除
  • 注意
    • on绑定的事件可以连续执行,除非off
    • once绑定的函数emit一次即删除,也可以未执行而被off
    class EventBus {
        /**
         * {
         *    'key1': [
         *        { fn: fn1, isOnce: false },
         *        { fn: fn2, isOnce: false },
         *        { fn: fn3, isOnce: true },
         *    ]
         *    'key2': [] // 有序
         *    'key3': []
         * }
         */
        constructor() {
            this.events = {}
        }
    
        on(type, fn, isOnce = false) {
            const events = this.events
            if (events[type] == null) {
                events[type] = [] // 初始化 key 的 fn 数组
            }
            events[type].push({ fn, isOnce })
        }
    
        once(type, fn) {
            this.on(type, fn, true)
        }
    
        off(type, fn) {
            if (!fn) {
                // 解绑所有 type 的函数
                this.events[type] = []
            } else {
                // 解绑单个 fn
                const fnList = this.events[type]
                if (fnList) {
                    this.events[type] = fnList.filter(item => item.fn !== fn)
                }
            }
        }
    
        emit(type, ...args) {
            const fnList = this.events[type]
            if (fnList == null) return
    
            // 注意过滤后重新赋值
            this.events[type] = fnList.filter(item => {
                const { fn, isOnce } = item
                fn(...args)
    
                // once 执行一次就要被过滤掉
                if (!isOnce) return true
                return false
            })
        }
    }

实现方式3:拆分保存 on 和 once 事件

    // 拆分保存 on 和 once 事件
    
    class EventBus {
        constructor() {
            this.events = {} // { key1: [fn1, fn2], key2: [fn1, fn2] }
            this.onceEvents = {}
        }
    
        on(type, fn) {
            const events = this.events
            if (events[type] == null) events[type] = []
            events[type].push(fn)
        }
    
        once(type, fn) {
            const onceEvents = this.onceEvents
            if (onceEvents[type] == null) onceEvents[type] = []
            onceEvents[type].push(fn)
        }
    
        off(type, fn) {
            if (!fn) {
                // 解绑所有事件
                this.events[type] = []
                this.onceEvents[type] = []
            } else {
                // 解绑单个事件
                const fnList = this.events[type]
                const onceFnList = this.onceEvents[type]
                if (fnList) {
                    this.events[type] = fnList.filter(curFn => curFn !== fn)
                }
                if (onceFnList) {
                    this.onceEvents[type] = onceFnList.filter(curFn => curFn !== fn)
                }
            }
        }
    
        emit(type, ...args) {
            const fnList = this.events[type]
            const onceFnList = this.onceEvents[type]
    
            if (fnList) {
                fnList.forEach(f => f(...args))
            }
            if (onceFnList) {
                onceFnList.forEach(f => f(...args))
    
                // once 执行一次就删除
                this.onceEvents[type] = []
            }
        }
    }
    // 测试
    const e = new EventBus()
    
    function fn1(a, b) { console.log('fn1', a, b) }
    function fn2(a, b) { console.log('fn2', a, b) }
    function fn3(a, b) { console.log('fn3', a, b) }
    
    e.on('key1', fn1)
    e.on('key1', fn2)
    e.once('key1', fn3)
    e.on('xxxxxx', fn3)
    
    e.emit('key1', 10, 20) // 触发 fn1 fn2 fn3
    
    e.off('key1', fn1)
    
    e.emit('key1', 100, 200) // 触发 fn2

在日常的开发中,大家用到EventBus/EventEmitter往往提供比这五个方法多的多的多的方法。但在面试过程中,如果大家能够完整地实现出这五个方法,已经非常可以说明问题了,因此这个EventBus希望大家可以熟练掌握。学有余力的同学,推荐阅读FaceBook推出的通用EventEmiiter库的源码 (opens new window),相信你会有更多收获。

3 实现一个双向绑定

defineProperty 版本

    // 数据
    const data = {
      text: 'default'
    };
    const input = document.getElementById('input');
    const span = document.getElementById('span');
    // 数据劫持
    Object.defineProperty(data, 'text', {
      // 数据变化 --> 修改视图
      set(newVal) {
        input.value = newVal;
        span.innerHTML = newVal;
      }
    });
    // 视图更改 --> 数据变化
    input.addEventListener('keyup', function(e) {
      data.text = e.target.value;
    });

proxy 版本

    // 数据
    const data = {
      text: 'default'
    };
    const input = document.getElementById('input');
    const span = document.getElementById('span');
    // 数据劫持
    const handler = {
      set(target, key, value) {
        target[key] = value;
        // 数据变化 --> 修改视图
        input.value = value;
        span.innerHTML = value;
        return value;
      }
    };
    const proxy = new Proxy(data, handler);
    
    // 视图更改 --> 数据变化
    input.addEventListener('keyup', function(e) {
      proxy.text = e.target.value;
    });

4 实现一个简易的MVVM

实现一个简易的MVVM我会分为这么几步来:

  1. 首先我会定义一个类Vue,这个类接收的是一个options,那么其中可能有需要挂载的根元素的id,也就是el属性;然后应该还有一个data属性,表示需要双向绑定的数据
  2. 其次我会定义一个Dep类,这个类产生的实例对象中会定义一个subs数组用来存放所依赖这个属性的依赖,已经添加依赖的方法addSub,删除方法removeSub,还有一个notify方法用来遍历更新它subs中的所有依赖,同时Dep类有一个静态属性target它用来表示当前的观察者,当后续进行依赖收集的时候可以将它添加到dep.subs中。
  3. 然后设计一个observe方法,这个方法接收的是传进来的data,也就是options.data,里面会遍历data中的每一个属性,并使用Object.defineProperty()来重写它的getset,那么这里面呢可以使用new Dep()实例化一个dep对象,在get的时候调用其addSub方法添加当前的观察者Dep.target完成依赖收集,并且在set的时候调用dep.notify方法来通知每一个依赖它的观察者进行更新
  4. 完成这些之后,我们还需要一个compile方法来将HTML模版和数据结合起来。在这个方法中首先传入的是一个node节点,然后遍历它的所有子级,判断是否有firstElmentChild,有的话则进行递归调用compile方法,没有firstElementChild的话且该child.innderHTML用正则匹配满足有/\{\{(.*)\}\}/项的话则表示有需要双向绑定的数据,那么就将用正则new Reg('\\{\\{\\s*' + key + '\\s*\\}\\}', 'gm')替换掉``是其为msg变量。
  5. 完成变量替换的同时,还需要将Dep.target指向当前的这个child,且调用一下this.opt.data[key],也就是为了触发这个数据的get来对当前的child进行依赖收集,这样下次数据变化的时候就能通知child进行视图更新了,不过在最后要记得将Dep.target指为null哦(其实在Vue中是有一个targetStack栈用来存放target的指向的)
  6. 那么最后我们只需要监听documentDOMContentLoaded然后在回调函数中实例化这个Vue对象就可以了

coding :

需要注意的点:

  • childNodes会获取到所有的子节点以及文本节点(包括元素标签中的空白节点)
  • firstElementChild表示获取元素的第一个字元素节点,以此来区分是不是元素节点,如果是的话则调用compile进行递归调用,否则用正则匹配
  • 这里面的正则真的不难,大家可以看一下

完整代码如下:

    <!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>MVVM</title>
      </head>
      <body>
        <div id="app">
          <h3>姓名</h3>
          <p>{{name}}</p>
          <h3>年龄</h3>
          <p>{{age}}</p>
        </div>
      </body>
    </html>
    <script>
      document.addEventListener(
        "DOMContentLoaded",
        function () {
          let opt = { el: "#app", data: { name: "等待修改...", age: 20 } };
          let vm = new Vue(opt);
          setTimeout(() => {
            opt.data.name = "jing";
          }, 2000);
        },
        false
      );
      class Vue {
        constructor(opt) {
          this.opt = opt;
          this.observer(opt.data);
          let root = document.querySelector(opt.el);
          this.compile(root);
        }
        observer(data) {
          Object.keys(data).forEach((key) => {
            let obv = new Dep();
            data["_" + key] = data[key];
    
            Object.defineProperty(data, key, {
              get() {
                Dep.target && obv.addSubNode(Dep.target);
                return data["_" + key];
              },
              set(newVal) {
                obv.update(newVal);
                data["_" + key] = newVal;
              },
            });
          });
        }
        compile(node) {
          [].forEach.call(node.childNodes, (child) => {
            if (!child.firstElementChild && /\{\{(.*)\}\}/.test(child.innerHTML)) {
              let key = RegExp.$1.trim();
              child.innerHTML = child.innerHTML.replace(
                new RegExp("\\{\\{\\s*" + key + "\\s*\\}\\}", "gm"),
                this.opt.data[key]
              );
              Dep.target = child;
              this.opt.data[key];
              Dep.target = null;
            } else if (child.firstElementChild) this.compile(child);
          });
        }
      }
    
      class Dep {
        constructor() {
          this.subNode = [];
        }
        addSubNode(node) {
          this.subNode.push(node);
        }
        update(newVal) {
          this.subNode.forEach((node) => {
            node.innerHTML = newVal;
          });
        }
      }
    </script>

简化版2

    function update(){
      console.log('数据变化~~~ mock update view')
    }
    let obj = [1,2,3]
    // 变异方法 push shift unshfit reverse sort splice pop
    // Object.defineProperty
    let oldProto = Array.prototype;
    let proto = Object.create(oldProto); // 克隆了一分
    ['push','shift'].forEach(item=>{
      proto[item] = function(){
        update();
        oldProto[item].apply(this,arguments);
      }
    })
    function observer(value){ // proxy reflect
      if(Array.isArray(value)){
        // AOP
        return value.__proto__ = proto;
        // 重写 这个数组里的push shift unshfit reverse sort splice pop
      }
      if(typeof value !== 'object'){
        return value;
      }
      for(let key in value){
        defineReactive(value,key,value[key]);
      }
    }
    function defineReactive(obj,key,value){
      observer(value); // 如果是对象 继续增加getter和setter
      Object.defineProperty(obj,key,{
        get(){
            return value;
        },
        set(newValue){
            if(newValue !== value){
                observer(newValue);
                value = newValue;
                update();
            }
        }
      })
    }
    observer(obj); 
    // AOP
    // obj.name = {n:200}; // 数据变了 需要更新视图 深度监控
    // obj.name.n = 100;
    obj.push(123);
    obj.push(456);
    console.log(obj);

5 实现一个迷你版的vue

入口

    // js/vue.js
    class Vue {
      constructor (options) {
        // 1. 通过属性保存选项的数据
        this.$options = options || {}
        this.$data = options.data || {}
        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
            }
          })
        })
      }
    }

实现Dep

    class Dep {
      constructor () {
        // 存储所有的观察者
        this.subs = []
      }
      // 添加观察者
      addSub (sub) {
        if (sub && sub.update) {
          this.subs.push(sub)
        }
      }
      // 发送通知
      notify () {
        this.subs.forEach(sub => {
          sub.update()
        })
      }
    }

实现watcher

    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 (this.oldValue === newValue) {
          return
        }
        this.cb(newValue)
      }
    }

实现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
          })
        }
      }
      // 判断元素属性是否是指令
      isDirective (attrName) {
        return attrName.startsWith('v-')
      }
      // 判断节点是否是文本节点
      isTextNode (node) {
        return node.nodeType === 3
      }
      // 判断节点是否是元素节点
      isElementNode (node) {
        return node.nodeType === 1
      }
    }

实现Observer

    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 val
          },
          set (newValue) {
            if (newValue === val) {
              return
            }
            val = newValue
            that.walk(newValue)
            // 发送通知
            dep.notify()
          }
        })
      }
    }

使用

    <!DOCTYPE html>
    <html lang="cn">
    <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>Mini Vue</title>
    </head>
    <body>
      <div id="app">
        <h1>差值表达式</h1>
        <h3>{{ msg }}</h3>
        <h3>{{ count }}</h3>
        <h1>v-text</h1>
        <div v-text="msg"></div>
        <h1>v-model</h1>
        <input type="text" v-model="msg">
        <input type="text" v-model="count">
      </div>
      <script src="./js/dep.js"></script>
      <script src="./js/watcher.js"></script>
      <script src="./js/compiler.js"></script>
      <script src="./js/observer.js"></script>
      <script src="./js/vue.js"></script>
      <script>
        let vm = new Vue({
          el: '#app',
          data: {
            msg: 'Hello Vue',
            count: 100,
            person: { name: 'zs' }
          }
        })
        console.log(vm.msg)
        // vm.msg = { test: 'Hello' }
        vm.test = 'abc'
      </script>
    </body>
    </html>

6 实现Vue reactive响应式

    // Dep module
    class Dep {
      static stack = []
      static target = null
      deps = null
      
      constructor() {
        this.deps = new Set()
      }
    
      depend() {
        if (Dep.target) {
          this.deps.add(Dep.target)
        }
      }
    
      notify() {
        this.deps.forEach(w => w.update())
      }
    
      static pushTarget(t) {
        if (this.target) {
          this.stack.push(this.target)
        }
        this.target = t
      }
    
      static popTarget() {
        this.target = this.stack.pop()
      }
    }
    
    // reactive
    function reactive(o) {
      if (o && typeof o === 'object') {
        Object.keys(o).forEach(k => {
          defineReactive(o, k, o[k])
        })
      }
      return o
    }
    
    function defineReactive(obj, k, val) {
      let dep = new Dep()
      Object.defineProperty(obj, k, {
        get() {
          dep.depend()
          return val
        },
        set(newVal) {
          val = newVal
          dep.notify()
        }
      })
      if (val && typeof val === 'object') {
        reactive(val)
      }
    }
    
    // watcher
    class Watcher {
      constructor(effect) {
        this.effect = effect
        this.update()
      }
    
      update() {
        Dep.pushTarget(this)
        this.value = this.effect()
        Dep.popTarget()
        return this.value
      }
    }
    
    // 测试代码
    const data = reactive({
      msg: 'aaa'
    })
    
    new Watcher(() => {
      console.log('===> effect', data.msg);
    })
    
    setTimeout(() => {
      data.msg = 'hello'
    }, 1000)

7 实现模板字符串解析功能

    let template = '我是{{name}},年龄{{age}},性别{{sex}}';
    let data = {
      name: '姓名',
      age: 18
    }
    render(template, data); // 我是姓名,年龄18,性别undefined
    function render(template, data) {
      const reg = /\{\{(\w+)\}\}/; // 模板字符串正则
      if (reg.test(template)) { // 判断模板里是否有模板字符串
        const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段
        template = template.replace(reg, data[name]); // 将第一个模板字符串渲染
        return render(template, data); // 递归的渲染并返回渲染后的结构
      }
      return template; // 如果模板没有模板字符串直接返回
    }

8 实现一下hash路由

基础的html代码:

    <html>
      <style>
        html, body {
          margin: 0;
          height: 100%;
        }
        ul {
          list-style: none;
          margin: 0;
          padding: 0;
          display: flex;
          justify-content: center;
        }
        .box {
          width: 100%;
          height: 100%;
          background-color: red;
        }
      </style>
      <body>
      <ul>
        <li>
          <a href="#red">红色</a>
        </li>
        <li>
          <a href="#green">绿色</a>
        </li>
        <li>
          <a href="#purple">紫色</a>
        </li>
      </ul>
      </body>
    </html>

简单实现:

    <script>
      const box = document.getElementsByClassName('box')[0];
      const hash = location.hash
      window.onhashchange = function (e) {
        const color = hash.slice(1)
        box.style.background = color
      }
    </script>

封装成一个class:

    <script>
      const box = document.getElementsByClassName('box')[0];
      const hash = location.hash
      class HashRouter {
        constructor (hashStr, cb) {
          this.hashStr = hashStr
          this.cb = cb
          this.watchHash()
          this.watch = this.watchHash.bind(this)
          window.addEventListener('hashchange', this.watch)
        }
        watchHash () {
          let hash = window.location.hash.slice(1)
          this.hashStr = hash
          this.cb(hash)
        }
      }
      new HashRouter('red', (color) => {
        box.style.background = color
      })
    </script>

9 实现redux中间件

简单实现

    function createStore(reducer) {
      let currentState
      let listeners = []
    
      function getState() {
        return currentState
      }
    
      function dispatch(action) {
        currentState = reducer(currentState, action)
        listeners.map(listener => {
          listener()
        })
        return action
      }
    
      function subscribe(cb) {
        listeners.push(cb)
        return () => {}
      }
      
      dispatch({type: 'ZZZZZZZZZZ'})
    
      return {
        getState,
        dispatch,
        subscribe
      }
    }
    
    // 应用实例如下:
    function reducer(state = 0, action) {
      switch (action.type) {
        case 'ADD':
          return state + 1
        case 'MINUS':
          return state - 1
        default:
          return state
      }
    }
    
    const store = createStore(reducer)
    
    console.log(store);
    store.subscribe(() => {
      console.log('change');
    })
    console.log(store.getState());
    console.log(store.dispatch({type: 'ADD'}));
    console.log(store.getState());

2. 迷你版

    export const createStore = (reducer,enhancer)=>{
    	if(enhancer) {
    		return enhancer(createStore)(reducer)
    	}
    	let currentState = {}
    	let currentListeners = []
    
    	const getState = ()=>currentState
    	const subscribe = (listener)=>{
    		currentListeners.push(listener)
    	}
    	const dispatch = action=>{
    		currentState = reducer(currentState, action)
    		currentListeners.forEach(v=>v())
    		return action
    	}
    	dispatch({type:'@@INIT'})
    	return {getState,subscribe,dispatch}
    }
    
    //中间件实现
    export applyMiddleWare(...middlewares){
    	return createStore=>...args=>{
    		const store = createStore(...args)
    		let dispatch = store.dispatch
    
    		const midApi = {
    			getState:store.getState,
    			dispatch:...args=>dispatch(...args)
    		}
    		const middlewaresChain = middlewares.map(middleware=>middleware(midApi))
    		dispatch = compose(...middlewaresChain)(store.dispatch)
    		return {
    			...store,
    			dispatch
    		}
    	}
    
    // fn1(fn2(fn3())) 把函数嵌套依次调用
    export function compose(...funcs){
    	if(funcs.length===0){
    		return arg=>arg
    	}
    	if(funs.length===1){
    		return funs[0]
    	}
    	return funcs.reduce((ret,item)=>(...args)=>ret(item(...args)))
    }
    
    
    //bindActionCreator实现
    
    function bindActionCreator(creator,dispatch){
        return ...args=>dispatch(creator(...args))
    }
    function bindActionCreators(creators,didpatch){
        //let bound = {}
        //Object.keys(creators).forEach(v=>{
       //     let creator = creator[v]
         //   bound[v] = bindActionCreator(creator,dispatch)
        //})
        //return bound
        
        return Object.keys(creators).reduce((ret,item)=>{
    	    ret[item] = bindActionCreator(creators[item],dispatch)
        	return ret
        },{})
    }

10 实现redux-thunk

redux-thunk 可以利用 redux 中间件让 redux 支持异步的 action

    // 如果 action 是个函数,就调用这个函数
    // 如果 action 不是函数,就传给下一个中间件
    // 发现 action 是函数就调用
    const thunk = ({ dispatch, getState }) => (next) => (action) => {
      if (typeof action === 'function') {
        return action(dispatch, getState);
      }
    
      return next(action);
    };
    export default thunk
Last Updated:
Contributors: leeguooooo