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)
大家会发现,整个调用过程中,没有出现具体的发布者和订阅者(比如上面的
PrdPublisher和DeveloperObserver),全程只有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
- 分析
on和once注册函数,存储起来emit时找到对应的函数,执行off找到对应函数,从存储中删除
- 注意
on绑定的事件可以连续执行,除非offonce绑定的函数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我会分为这么几步来:
- 首先我会定义一个类
Vue,这个类接收的是一个options,那么其中可能有需要挂载的根元素的id,也就是el属性;然后应该还有一个data属性,表示需要双向绑定的数据 - 其次我会定义一个
Dep类,这个类产生的实例对象中会定义一个subs数组用来存放所依赖这个属性的依赖,已经添加依赖的方法addSub,删除方法removeSub,还有一个notify方法用来遍历更新它subs中的所有依赖,同时Dep类有一个静态属性target它用来表示当前的观察者,当后续进行依赖收集的时候可以将它添加到dep.subs中。 - 然后设计一个
observe方法,这个方法接收的是传进来的data,也就是options.data,里面会遍历data中的每一个属性,并使用Object.defineProperty()来重写它的get和set,那么这里面呢可以使用new Dep()实例化一个dep对象,在get的时候调用其addSub方法添加当前的观察者Dep.target完成依赖收集,并且在set的时候调用dep.notify方法来通知每一个依赖它的观察者进行更新 - 完成这些之后,我们还需要一个
compile方法来将HTML模版和数据结合起来。在这个方法中首先传入的是一个node节点,然后遍历它的所有子级,判断是否有firstElmentChild,有的话则进行递归调用compile方法,没有firstElementChild的话且该child.innderHTML用正则匹配满足有/\{\{(.*)\}\}/项的话则表示有需要双向绑定的数据,那么就将用正则new Reg('\\{\\{\\s*' + key + '\\s*\\}\\}', 'gm')替换掉``是其为msg变量。 - 完成变量替换的同时,还需要将
Dep.target指向当前的这个child,且调用一下this.opt.data[key],也就是为了触发这个数据的get来对当前的child进行依赖收集,这样下次数据变化的时候就能通知child进行视图更新了,不过在最后要记得将Dep.target指为null哦(其实在Vue中是有一个targetStack栈用来存放target的指向的) - 那么最后我们只需要监听
document的DOMContentLoaded然后在回调函数中实例化这个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
