PS:对前端技术感兴趣的朋友们,可以关注下我的《致力于前端的技术博客》哦!如果对你有帮助,欢迎赠个⭐️,会常更新内容,敬请期待!❤️❤️
在前端的世界里,不仅要会使用,更要懂原理。下面,让我们一起来手写一些常用的库或框架吧~!
手写webpack简易版
简单实现commonjs规范
// 实现commonjs
// a.js
// module.exports = 'hello'
// b.js
// let str=require('./a.js')
const fs = require('fs')
function req(moduleName) {
let content = fs.readFileSync(moduleName, 'utf8')
// 创建函数
// function(exports, module, require, __dirname, __filename) {
// module.exports = 'hello'
// return module.exports
// }
let fn = new Function('exports', 'module', 'require', '__dirname', '__filename', content + '\n return module.exports')
let module = {
exports: {}
}
return fn(module.exports, module, req, __dirname, __filename)
}
let str = req('./a.js')
console.log(str);
简单实现AMD规范
// define声明模块,通过require使用模块
let fns = {}
function define(moduleName, dependencies, fn) {
fn.dependencies = dependencies //将依赖记到fn上
fns[moduleName] = fn
}
function require(modules, cb) {
let results = modules.map(function(mod) {
let fn = fns[mod]
let exports
// 对依赖模块进行递归
let dependencies = fn.dependencies
require(dependencies, function() {
exports = fn.apply(null, arguments)
})
return exports
})
cb.apply(null, results)
}
define('a', [], function() {
return 'a'
})
define('b', [], function() {
return 'b'
})
define('c', ['a'], function(a) {
return a.concat('c')
})
require(['a', 'b', 'c'], function(a, b, c) {
console.log(a, b, c);
})
webpack打包后的核心代码
(function(modules) {
// moduleId就是文件名
function require(moduleId) {
var module = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, require);
return module.exports;
}
return require('./src/index.js')
})
({
"./src/index.js": (function(module, exports) {
eval("console.log('hello');\n\n//# sourceURL=webpack:///./src/index.js?");
})
});
实现我们自己的wepack打包工具
- 新建文件夹
mypack,新建bin文件夹,在bin下创建mypack.js,在这个文件实现打包操作 npm init初始化项目,修改package.json中的bin为"mypack": "bin/mypack.js",运行npm link将此命令关联到全局环境下- 在
mypack.js中需要加入#! /usr/bin/env node,告诉当前文件在node环境下运行 - 编写
mypack.js打包核心代码 - 在原有项目文件夹,运行
mypack命令即可实现最终的打包
接下来在mypack.js中实现我们自己的webpack。
核心思想:
模板如上,采用模板替换的方式,将代码中的entry、output替换为我们自己的,再将eval的内容换为读取的entry文件内容(模板替换可采用ejs模块实现),最后将替换后的内容写入output文件中。
#! /usr/bin/env node
const fs = require('fs')
const ejs = require('ejs')
const entry = './src/index.js'
const output = './dist/main.js'
const script = fs.readFileSync(entry, 'utf8')
let template = `
(function(modules) {
// moduleId就是文件名
function require(moduleId) {
var module = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, require);
return module.exports;
}
return require("<%-entry%>")
})
({
"<%-entry%>": (function(module, exports) {
eval(\`<%-script%>\`);
})
});
`
let result = ejs.render(template, {
entry,
script
})
// result为替换后的结果,最终要写到output中
fs.writeFileSync(output, result)
console.log('打包成功!');
继续完善:如果打包文件中存在require()引入其他模块的情况,需要进行相关处理,首先我们采用webpack打包看一下原始打包结果,以下是基本骨架:
(function(modules) {
function require(moduleId) {
var module = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, require);
return module.exports;
}
return require(require.s = "./src/index.js");
})
({
"./src/index.js": (function(module, exports, __webpack_require__) {
eval("const result = __webpack_require__(/*! ./a.js */ \"./src/a.js\")\r\nconsole.log(result);\n\n//# sourceURL=webpack:///./src/index.js?");
}),
"./src/a.js": (function(module, exports) {
eval("module.exports = 'hello'\n\n//# sourceURL=webpack:///./src/a.js?");
})
});
可见,在传入的参数中,不仅传入了入口文件entry,还传入了引入的其他模块。因此我们继续修改mypack.js,让其支持模块引入:
#! /usr/bin/env node
const fs = require('fs')
const path = require('path')
const ejs = require('ejs')
const entry = './src/index.js'
const output = './dist/main.js'
let script = fs.readFileSync(entry, 'utf8')
// 新增部分
let modules = []
// 处理依赖关系
// ?代表非贪婪捕获
// require('./a.js')
script = script.replace(/require\(['"](.+?)['"]\)/g, function() {
let name = path.join('./src/', arguments[1]) // ./src/a.js
let content = fs.readFileSync(name, 'utf8')
modules.push({
name,
content
})
return `require('${name}')`
})
// 修改template模板,采用ejs的循环模式
let template = `
(function(modules) {
function require(moduleId) {
var module = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, require);
return module.exports;
}
return require(require.s = "<%-entry%>");
})
({
"<%-entry%>": (function(module, exports,require) {
eval(\`<%-script%>\`);
}),
<%for(let i=0;i<modules.length;i++){
let module=modules[i]%>
"<%-module.name%>": (function(module, exports) {
eval(\`<%-module.content%>\`);
}),
<%}%>
});
`
let result = ejs.render(template, {
entry,
script,
modules // 注意这里需要传入modules
})
// result为替换后的结果,最终要写到output中
fs.writeFileSync(output, result)
console.log('打包成功!');
再次打包,发现文件通过require引入其他模块成功!
接下来,再来支持require('./index.css')引入css样式文件,我们继续修改mypack.js,如果require文件是css文件,则对其内容进行处理:
// 新增css-loader
// source是文件中的内容
// 新建style标签,放入css文件的内容,将style标签插入到head中
let styleLoader = function(source) {
return `
let style=document.createElement('style')
style.innerText=${JSON.stringify(source).replace(/\\r\\n/g,'')}
document.head.appendChild(style)
`
}
script = script.replace(/require\(['"](.+?)['"]\)/g, function() {
let name = path.join('./src/', arguments[1]) // ./src/a.js
let content = fs.readFileSync(name, 'utf8')
// 这里我们做一下拦截
if (/\.css$/.test(name)) {
content = styleLoader(content)
}
modules.push({
name,
content
})
return `require('${name}')`
})
再次打包,发现可以成功引入css文件了!
手写react-router
hash路由原理
window绑定hashchange事件
<body>
<a href="#/a">a</a>
<a href="#/b">b</a>
</body>
<script>
window.addEventListener('hashchange', () => {
console.log(window.location.hash);
})
</script>
history路由原理
window绑定popstate事件
<body>
<a onclick="push('/a')">a</a>
<a onclick="push('/b')">b</a>
</body>
<script>
function push(path) {
history.pushState({
p: path
}, null, path)
}
// 浏览器前进和后退
window.addEventListener('popstate', (e) => {
console.log(e);
})
</script>
react中react-router的使用示例
import React, { Component } from 'react'
import { render } from 'react-dom'
import { HashRouter as Router, Route } from './react-router-dom'
import AAA from './AAA'
import BBB from './BBB'
import CCC from './CCC'
export default class App extends Component {
constructor() {
super()
}
render() {
return (
<Router>
<div>
<Route path='/aaa' component={AAA} exact='true'></Route>
<Route path='/bbb' component={BBB}></Route>
<Route path='/ccc' component={CCC}></Route>
</div>
</Router >
)
}
}
render(<App />, window.root)
实现react-router
下面我们开始实现react-router,开始吧!
新建react-router-dom文件夹,新建index.js,用来导出所有的class。
index.js
import HashRouter from './HashRouter'
import Router from './Router'
import Link from './Link'
import Redirect from './Redirect'
export { HashRouter, Router, Link, Redirect }
context.js
import React from 'react'
let { Provider, Consumer } = React.createContext()
export { Provider, Consumer }
HashRouter.js
import React, { Component } from 'react'
import { Provider } from './context'
export default class HashRouter extends Component {
constructor() {
super()
this.state = {
location: {
pathname: window.location.hash.slice(1) || '/'
}
}
}
componentDidMount() {
window.location.hash = window.location.hash || '/'
// 监听hash变化重新设置状态
window.addEventListener('hashchange', () => {
this.setState({
location: {
...this.state.location,
pathname: window.location.hash.slice(1) || '/'
}
})
})
}
render() {
let value = {
location: this.state.location,
history: {
push(to) {
window.location.hash = to
}
}
}
return (
<Provider value={value}>
{this.props.children}
</Provider>
)
}
}
Router.js
import React, { Component } from 'react'
import { Consumer } from './context'
import pathToReg from 'path-to-regexp'
export default class Router extends Component {
constructor() {
super()
}
render() {
return (
<Consumer>
{state => {
// path component是Route中传递的
let { path, component: Component, exact = false } = this.props
// pathname是location中的
let pathname = state.location.pathname
// 根据path实现正则,通过正则匹配,这里可以使用path-to-regexp第三方包
let reg = pathToReg(path, [], { end: exact }) //end为true是路由精确匹配
let result = pathname.match(reg)
if (result) {
return <Component></Component>
}
return null
}}
</Consumer>
)
}
}
Link.js
import React, { Component } from 'react'
import { Consumer } from './context'
export default class Link extends Component {
constructor() {
super()
}
render() {
s
return (
<Consumer>
{state => {
return <a onClick={() => {
const { to } = this.props //<Link to="home">首页</Link>
state.location.history.push(to)
}}>{this.props.children}</a>
}}
</Consumer>
)
}
}
Redirect.js
import React, { Component } from 'react'
import { Consumer } from './context'
export default class Redirect extends Component {
constructor() {
super()
}
render() {
return (
<Consumer>
{state => {
state.history.push(this.props.to)//重定向
return null
}}
</Consumer>
)
}
}
Switch.js
import React, { Component } from 'react'
import { Consumer } from './context'
import pathToReg from 'path-to-regexp'
export default class Router extends Component {
constructor() {
super()
}
render() {
return (
<Consumer>
{state => {
let pathname = state.location.pathname
let children = this.props.children
for (let i = 0; i < children.length; i++) {
const child = children[i];
const path = child.props.path || ''
let reg = pathToReg(path, [], { end: 'false' })
if (reg.test(pathname)) {
return child
}
return null
}
}}
</Consumer>
)
}
}
下面可以使用我们自己编写的react-router进行测试了~
手写DOM diff算法
dom diff
根据两个虚拟dom创建出补丁, 描述改变的内容, 将这个补丁用来更新dom
dom diff几种优化策略
- 更新时只比较同级,并不会跨层比较
- 同层变化能复用,使用key
index.js
import {
createElement,
render,
renderDOM
} from './element'
import diff from './diff'
import patch from './patch'
let virtualDOM1 = createElement('ul', {
class: 'list'
}, [
createElement('li', {class: 'item'}, ['a']),
createElement('li', {class: 'item'}, ['b']),
createElement('li', {class: 'item'}, ['c'])
])
let virtualDOM2 = createElement('ul', {
class: 'list-group'
}, [
createElement('li', {class: 'item'}, ['1']),
createElement('li', {class: 'item'}, ['2']),
createElement('div', {class: 'item'}, ['c'])
])
let el = render(virtualDOM)
renderDOM(el, window.root)
// 给元素打补丁,重新更新视图
let patches = diff(virtualDOM1, virtualDOM2)
patch(el, patches)
此时的dom diff策略还存在很多问题:
1. 如果同级只是交换节点位置,会导致重新渲染(应该只是交换位置)
2. 新增节点也不会被更新
在index.js中,我们创建了两个虚拟dom,故意修改了一些属性值、标签名、文本,以测试后面要实现的diff、patch方法。
首先我们实现createElement(创建虚拟dom)、render(将虚拟dom转化为真实dom)、renderDOM(将元素节点插入到页面上)这几个方法。
element.js
// 虚拟dom元素
class Element {
constructor(type, props, children) {
this.type = type
this.props = props
this.children = children
}
}
// 创建虚拟dom
function createElement(type, props, children) {
return new Element(type, props, children)
}
// 设置属性
function setAttr(node, key, value) {
switch (key) {
case 'value':
if (node.tagName.toLowerCase === 'input' || node.tagName.toLowerCase === 'textarea') {
node.value = value
} else {
node.setAttribute(key, value)
}
break
case 'style':
node.style.cssText = value
break
default:
node.setAttribute(key, value)
}
}
// 将vnode转化为真实dom
function render(eleObj) {
let el = document.createElement(eleObj.type)
for (let key in eleObj.props) {
// 设置属性的方法
setAttr(el, key, eleObj.props[key])
}
eleObj.children.forEach(child => {
child = (child instanceof Element) ? render(child) : document.createTextNode(child)
el.appendChild(child)
});
return el
}
// 将元素插入到页面内
function renderDOM(el, target) {
target.appendChild(el)
}
export {
createElement,
render,
Element,
renderDOM,
setAttr
}
注意:在设置元素属性时,因为不同类型的元素设置属性方法不同,因此采用setAttr函数统一设置。
目前为止,我们已经实现了创建虚拟dom,并将虚拟dom转化为真实dom渲染到页面中,接下来我们实现核心的diff算法:
首先我们需要制定规则:
- 当节点类型相同时,看属性是否相同,产生属性补丁包,{type;'ATTES',attrs:{class:'list'}}
- 新的dom不存在,{type;'REMOVE',index:xx}
- 节点类型不相同,直接替换,{type;'REPLACE',newNode:newNode}
- 文本内容变化,{type;'TEXT',text:'xxx'}
- ……
diff.js
const ATTRS = 'ATTRS'
const REMOVE = 'REMOVE'
const REPLACE = 'REPLACE'
const TEXT = 'TEXT'
let Index = 0
// 深度遍历
function diff(oldTree, newTree) {
let patches = {}
let index = 0
walk(oldTree, newTree, index, patches)
return patches
}
function walk(oldNode, newNode, index, patches) {
let currentPatch = [] //每个元素都有一个补丁对象
if (!newNode) { // 2. 新节点被删除
currentPatch.push({
type: REMOVE,
index
})
} else if (_isString(oldNode) && _isString(newNode)) { // 4. 判断文本是否变化
if (oldNode !== newNode) {
currentPatch.push({
type: TEXT,
text: newNode
})
}
} else if (oldNode.nodeType === newNode.nodeType) { // 1. 比较属性是否有更改
let attrs = _diffAttr(oldNode.props, newNode.props)
if (Object.keys(attrs).length > 0) {
currentPatch.pusH({
TYPE: ATTRS,
attrs
})
}
// 如果有子节点,遍历子节点
_diffChildren(oldNode.children, newNode.children, index, patches)
} else { //3.节点被替换
currentPatch.push({
type: REPLACE,
newNode
})
}
if (currentPatch.length > 0) { //当前元素确实有补丁
// 将元素和补丁对应起来,放到大补丁包中
patches[index] = currentPatch
}
}
function _diffAttr(oldAttrs, newAttrs) {
let patch = {}
// 直接判断老属性和新属性关系
for (const key in oldAttrs) {
if (oldAttrs[key] !== newAttrs[key]) {
patch[key] = newAttrs[key] //有可能是undefined
}
}
// 老节点没有新节点的属性
for (const key in newAttrs) {
if (!oldAttrs.hasOwnProperty(key)) {
patch[key] = newAttrs[key]
}
}
return patch
}
function _isString(node) {
return Object.props.toString.call(node) === '[object String]'
}
function _diffChildren(oldChildren, newChildren, index, patches) {
oldChildren.forEach((child, idx) => {
// index每次传给walk时,index是递增的,定义全局变量Index,所有的基于同一序号实现
walk(child, newChildren[idx], ++Index, patches)
});
}
export default diff
通过diff方法,我们能对两个虚拟dom产生完整的patches对象(详细记录了更改信息),以便后续的更新操作。
接下来,我们实现patch方法,根据patches对象,完成真实dom的更新工作:
patch.js
import {
Element,
render,
setAttr
} from './element'
let allPatches
let index = 0
const ATTRS = 'ATTRS'
const REMOVE = 'REMOVE'
const REPLACE = 'REPLACE'
const TEXT = 'TEXT'
function patch(node, patches) {
// 给元素打补丁
allPatches = patches
walk(node)
}
function walk(node) {
let currentPatch = allPatches[index++]
let childNodes = node.childNodes
childNodes.forEach(child => {
walk(child)
});
if (currentPatch) {
doPatch(node, currentPatch)
}
}
function doPatch(node, patches) {
patches.forEach(patch => {
switch (patch.type) {
case ATTRS:
for (const key in patch.attrs) {
let value = patch.attrs[key]
if (value) {
setAttr(node, key, value)
} else {// 如果属性值为undefined则直接删除属性
node.removeAttribute(key)
}
}
break
case REMOVE:
node.parentNode.removeChild(node)
break
case REPLACE:
let newNode = patch.newNode instanceof Element ? render(patch.newNode) : document.createTextNode(patch.newNode)
node.parentNode.replaceChild(newNode, node)
break
case TEXT:
node.textContent = patch.text
break
}
});
}
export default patch
终于完成啦!可以愉快地使用index.js进行测试了~
但是此时的dom diff策略还存在很多问题:
- 如果同级只是交换节点位置,会导致重新渲染(应该只是交换位置)
- 新增节点也不会被更新
- ……
手写mvvm模式
index.html
<!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>
</head>
<body>
<div id="app">
<!-- 双向数据绑定 -->
<input type="text" v-model='msg'> {{msg}}
</div>
</body>
<!-- <script src="node_modules/vue/dist/vue.min.js"></script> -->
<script src="./watcher.js"></script>
<script src="./observer.js"></script>
<script src="./compile.js"></script>
<script src="./mvvm.js"></script>
<script>
// mvvm如何实现?
// vue中实现双向绑定 1.模板编译 2.数据劫持 3.Watcher
let vm = new MVVM({
el: '#app', //el:document.getElementById('app')
data: {
msg: 'hello'
}
})
</script>
</html>
mvvm.js
class MVVM {
constructor(options) {
// 先把可用的东西挂载到实例上
this.$el = options.el
this.$data = options.data
// 如果有要编译的模板就开始编译
if (this.$el) {
// 数据劫持,把对象的所有属性改为get、set
new Observer(this.$data)
this.proxyData(this.$data)
// 用数据和元素进行编译
new Compile(this.$el, this)
}
}
// 代理数据,因为用户可能要通过this.msg取值,而不是this.$data.msg取值
proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return data[key]
},
set(newVal) {
data[key] = newVal
}
})
})
}
}
compile.js
class Compile {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
if (this.el) {
// 1. 先把真实的dom移入到内存,放到fragment
let fragment = this.node2Fragment(this.el)
// 2. 编译——提取想要的元素节点和文本节点 v-model {{}}
this.compile(fragment)
// 3. 把编译好的element放回页面
this.el.appendChild(fragment)
}
}
// 辅助方法
isElementNode(node) {
return node.nodeType === 1
}
isDirective(name) {
return name.includes('v-')
}
// 核心方法
// 将el元素内容全部放入内存
node2Fragment(el) {
let fragment = document.createDocumentFragment() //文档碎片
let firstChild
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild)
}
return fragment
}
// 编译
compile(fragment) {
// childNodes拿不到嵌套子节点,需要使用递归
let childNodes = fragment.childNodes
Array.from(childNodes).forEach(node => {
// 元素节点
if (this.isElementNode(node)) {
this.compileElement(node)
this.compile(node) // 需要深入检查, 使用递归
} else {
// 文本节点
this.compileText(node)
}
})
}
// 编译元素 v-model、v-text等
compileElement(node) {
let attrs = node.attributes
Array.from(attrs).forEach(attr => {
// 判断属性名字是否包含v-
let attrName = attr.name
if (this.isDirective(attrName)) {
let expr = attr.value //expr是指令的值
// node this.vm.$data expr
//取到v-后面的名称,如v-model的model,v-text的text等等
// let type = attrName.slice(2)
let [, type] = attrName.split('-')
Compileutil[type](node, this.vm, expr)
}
})
}
// 编译文本,{{}}
compileText(node) {
let expr = node.textContent
let reg = /\{\{([^}]+)\}\}/g //匹配{{}}
if (reg.test(expr)) {
// node this.vm.$data expr
const type = 'text'
Compileutil[type](node, this.vm, expr)
}
}
}
Compileutil = {
// 获取实例上对应的数据,如msg.a.b=>'hello'
// msg.a.b=>this.$data.msg=>this.$data.msg.a=>this.$data.msg.a.b
getVal(vm, expr) {
expr = expr.split('.')
return expr.reduce((prev, next) => {
return prev[next]
}, vm.$data)
},
// 获取编译文本后的结果,如{{msg}}=>'hello'
getTextVal(vm, expr) {
return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
// arguments[1]是正则匹配括号内容,如{{msg}}的msg
return this.getVal(vm, arguments[1])
})
},
// 赋值
// 例如给msg.a.b赋新值,则取到最后再赋value值
setVal(vm, expr, value) {
expr = expr.split('.')
return expr.reduce((prev, next, curIndex) => {
if (curIndex === expr.length - 1) {
return prev[next] = value
}
}, vm.$data)
},
// 文本处理
text(node, vm, expr) {
let updateFn = this.update['textUpdater']
// 拿到{{a}}{{b}}的a、b
expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
new Watcher(vm, arguments[1], newVal => {
// 如果数据变化了, 文本节点需要重新获取依赖的数据来更新文本节点
updateFn && updateFn(node, this.getTextVal(vm, expr))
})
})
let value = this.getTextVal(vm, expr)
updateFn && updateFn(node, value)
},
// 输入框处理
model(node, vm, expr) {
let updateFn = this.update['modelUpdater']
// 这里应该加一个监控,数据变化时,应该调用watcher的callback,将新值传递过来
new Watcher(vm, expr, newVal => {
updateFn && updateFn(node, this.getVal(vm, expr))
})
updateFn && updateFn(node, this.getVal(vm, expr))
node.addEventListener('input', e => {
let newVal = e.target.value
this.setVal(vm, expr, newVal)
})
},
update: {
// 文本更新
textUpdater(node, value) {
node.textContent = value
},
// 输入框更新
modelUpdater(node, value) {
node.value = value
},
}
}
observer.js
class Observer {
constructor(data) {
this.observe(data)
}
// 将data数据原有的属性改为get和set的形式
observe(data) {
if (!data || typeof data !== 'object') return
Object.keys(data).forEach(key => {
// 开始劫持
this.defineReactive(data, key, data[key])
// 如果劫持的是对象,还要对对象内的属性继续劫持
this.observe(data[key])
})
}
// 定义响应式
defineReactive(data, key, value) {
let _this = this
let dep = new Dep() //每个变化的数据都会对应一个数组,这个数组是存放所有更新的操作
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
Dep.target && dep.addSub(Dep.target)
return value
},
set(newValue) {
if (newValue !== value) {
// 设置新值时,如果是对象仍然需要劫持
_this.observe(newValue)
value = newValue
dep.notify() //通知所有订阅者数据变化了
}
}
})
}
}
watcher.js
// 观察者,给需要变化的dom元素增加观察者
// 用新值和旧值进行比对,如果发生变化,执行对应的方法
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
this.value = this.get()
}
// 获取实例上对应的数据,如msg.a.b=>'hello'
getVal(vm, expr) {
expr = expr.split('.')
return expr.reduce((prev, next) => {
return prev[next]
}, vm.$data)
}
get() {
Dep.target = this
let value = this.getVal(this.vm, this.expr)
Dep.target = null
return value
}
// 对外暴露的方法
update() {
let newVal = this.getVal(this.vm, this.expr)
let oldVal = this.value
if (newVal !== oldVal) {
this.cb(newVal)
}
}
}
// 发布订阅
class Dep {
constructor() {
// 订阅数组
this.subs = []
}
// 添加订阅
addSub(watcher) {
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => {
watcher.update()
})
}
}
测试html页面,可以成功实现mvvm,view和model已经完成了双向绑定。
手写express
采用express第三方包实现的话,app.js如下:
const express = require('express')
const app = express()
// 我们在这里模拟一些get、post方法
app.get('/', (req, res) => {
res.end('welcome')
})
app.get('/name', (req, res) => {
res.end('yyyyy')
})
app.get('/age', (req, res) => {
res.end('12')
})
app.post('/name', (req, res) => {
res.end('yes!')
})
app.listen(3000, () => {
console.log('3000');
})
接下来创建express.js,实现自己的express,明确文件要导出一个方法,这个方法需要产生app对象(因为上述代码是采用express()实现的),开始搭建基本骨架:
const http = require('http')
const url = require('url')
function createApplication() {
let app = (req, res) => {}
app.listen = function() {
let server = http.createServer(app)
server.listen(...arguments)
}
return app
}
module.exports = createApplication
接下来,我们实现get请求,思路如下:
- 创建app.routes数组,里面存放每一个路由信息(包括method、path、回调函数handler)
- 通过app.get方法,向app.routes放入每个路由信息
- 拿到传入的mthod和path,遍历app.routes,找到对应的handler并执行
const http = require('http')
const url = require('url')
function createApplication() {
let app = (req, res) => {
// 获取请求的方法
let m = req.method.toLowerCase()
let {
pathname
} = url.parse(req.url, true)
// 取出每一个layer
// 根据方法和路径匹配成功后执行对应的回调函数
for (let i = 0; i < app.routes.length; i++) {
let {
method,
path,
handler
} = app.routes[i]
if (m === method && pathname === path) {
handler(req, res)
return
}
}
res.end('cannot find')
}
app.routes = []
app.get = function(path, handler) {
let layer = {
method: 'get',
path,
handler
}
app.routes.push(layer)
}
app.listen = function() {
let server = http.createServer(app)
server.listen(...arguments)
}
return app
}
module.exports = createApplication
我们实现了get请求了,接下来要实现其他的请求方法,如post、put、delete等等,但是method如此之多,一个个写肯定不是最明智的选择,因此我们选择批量生产方法的形式,通过http.METHODS拿到所有的method,实现批量挂载:
const http = require('http')
const url = require('url')
function createApplication() {
let app = (req, res) => {
// 获取请求的方法
let m = req.method.toLowerCase()
let {
pathname
} = url.parse(req.url, true)
// 取出每一个layer
// 根据方法和路径匹配成功后执行对应的回调函数
for (let i = 0; i < app.routes.length; i++) {
let {
method,
path,
handler
} = app.routes[i]
if (m === method && pathname === path) {
handler(req, res)
return
}
}
res.end('cannot find')
}
app.routes = []
// 批量生产方法
http.METHODS.forEach(method => {
method = method.toLowerCase()
app[method] = function(path, handler) {
let layer = {
method,
path,
handler
}
app.routes.push(layer)
}
})
// console.log(http.METHODS);
// [ 'ACL',
// 'BIND',
// 'CHECKOUT',
// 'CONNECT',
// 'COPY',
// 'DELETE',
// 'GET',
// 'HEAD',
// 'LINK',
// 'LOCK',
// 'M-SEARCH',
// 'MERGE',
// 'MKACTIVITY',
// 'MKCALENDAR',
// 'MKCOL',
// 'MOVE',
// 'NOTIFY',
// 'OPTIONS',
// 'PATCH',
// 'POST',
// 'PROPFIND',
// 'PROPPATCH',
// 'PURGE',
// 'PUT',
// 'REBIND',
// 'REPORT',
// 'SEARCH',
// 'SOURCE',
// 'SUBSCRIBE',
// 'TRACE',
// 'UNBIND',
// 'UNLINK',
// 'UNLOCK',
// 'UNSUBSCRIBE' ]
// restful api
// app.get = function(path, handler) {
// let layer = {
// method: 'get',
// path,
// handler
// }
// app.routes.push(layer)
// }
app.listen = function() {
let server = http.createServer(app)
server.listen(...arguments)
}
return app
}
module.exports = createApplication
到此,已实现全部请求方法的挂载。但还存在很多的问题,比如:
- 尚不支持params、query、request body等传参
- 未添加安全机制
- 其他express api均未实现
- ……
手写koa
const Koa = require('./my-koa/application')
const app = new Koa()
app.use((ctx, next) => {
console.log(ctx.req.url);
// 原生
console.log(ctx.req.url); //ctx.req = req
console.log(ctx.request.req.url); // ctx.request.req = req
console.log(ctx.request.url); //ctx.request是koa自己封装的属性
console.log(ctx.url); //用ctx.url来代替ctx.request.url属性,简化写法
})
app.listen(3000, () => {
console.log('3000');
})
注意,我们需要先理清ctx.req | ctx.request.req | ctx.request | ctx 的关系。打印结果如下:
// 这两组总是一样
console.log(ctx.req.url); //ctx.req = req
console.log(ctx.request.req.url); // ctx.request.req = req
// 这两组总是一样
console.log(ctx.request.url); //ctx.request是koa自己封装的属性
console.log(ctx.url); //用ctx.url来代替ctx.request.url属性,简化写法
由此可以判断,我们实现时应该写
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
加下来,我们实现自己的koa框架:
首先创建一个文件夹,新建application.js文件作为入口文件,再分别创建context.js、request.js、response.js文件。
context.js
let context = {}
module.exports = context
request.js
let request = {}
module.exports = request
response.js
let response = {}
module.exports = response
application.js
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')
class Koa {
constructor() {
this.callbacksFn
this.context = context
this.request = request
this.response = response
}
use(cb) {
this.callbacksFn = cb
}
createContext(req, res) {
// 希望ctx可以拿到context的属性,但是不修改context
let ctx = Object.create(this.context)
ctx.request = Object.create(this.request)
ctx.response = Object.create(this.response)
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
return ctx
}
handlerRequest(req, res) {
let ctx = this.createContext(req, res)
this.callbacksFn(ctx) //这里传入的是ctx
}
listen() {
let server = http.createServer(this.handlerRequest.bind(this))
server.listen(...arguments)
}
}
module.exports = Koa
我们通过app.js检验一下:
console.log(ctx.req.url); //ctx.req = req
console.log(ctx.request.req.url); // ctx.request.req = req
// 这里仍然还是undefined
console.log(ctx.request.url); //ctx.request是koa自己封装的属性
console.log(ctx.url); //用ctx.url来代替ctx.request.url属性,简化写法
因为ctx.request.url、ctx.url输出的还是undefined,因为没有实现,下面开始实现这一部分,核心源码是使用代理来实现:
request.js
const url = require('url')
let request = {
get url() {
return this.req.url
},
get path() {
return url.parse(this.req.url).pathname
}
}
module.exports = request
response.js
let response = {
set body(value) {
this.res.statusCode = 200 //只要调了ctx.body='xxx',就设置状态码为200
this._body = value
},
get body() {
return this._body
}
}
module.exports = response
context.js
实现:
ctx.url=ctx.request.url——取ctx.url是取的ctx.request.urlctx.path=ctx.request.path——取ctx.path是取的ctx.request.pathctx.body=ctx.request.body——取ctx.body是取的ctx.response.body,设置ctx.body是设置的ctx.response.body
let ctx = {}
// 自定义获取器,代理属性
function defineGetter(property, name) {
// 取 ctx.url 取的是 ctx.request.url
// __defineGetter__是原生方法,也可以使用Object.defineProperty实现
ctx.__defineGetter__(name, function() {
return this[property][name]
})
}
function defineSetter(property, name, value) {
ctx.__defineSetter__(name, function(value) {
this[property][name] = value
})
}
defineGetter('request', 'url')
defineGetter('request', 'path')
defineGetter('response', 'body')
defineSetter('response', 'body')
module.exports = ctx
修改application.js
handlerRequest(req, res) {
res.statusCode = 404 //默认页面找不到
let ctx = this.createContext(req, res)
this.callbacksFn(ctx) //这里传入的是ctx,当回调函数执行后,ctx.body值就会发生变化
let body = ctx.body
if (typeof body === 'undefined') {
res.end('Not Found!')
} else if (typeof body === 'string') {
res.end(body)
}
}
完成后,通过app.js进行测试:
const Koa = require('./my-koa/application')
const app = new Koa()
app.use((ctx, next) => {
console.log(ctx.req.url);
// 原生
console.log(ctx.req.url); //ctx.req = req
console.log(ctx.request.req.url); // ctx.request.req = req
// koa封装的
console.log(ctx.request.url); //ctx.request是koa自己封装的属性
console.log(ctx.url); //用ctx.url来代替ctx.request.url属性,简化写法
// koa封装的
console.log(ctx.request.path);
console.log(ctx.path);
ctx.body = 'hello'
})
app.listen(3000, () => {
console.log('3000');
})
打开http://localhost:3000,显示'hello',上述代码没问题了。接下来我们实现多个中间件的功能,核心原理(洋葱模型):
function app() {}
app.middlewares = []
app.use = function(cb) {
app.middlewares.push(cb)
}
app.use((ctx, next) => {
console.log('mid 1-1');
next()
console.log('mid 1-2');
})
app.use((ctx, next) => {
console.log('mid 2-1');
next()
console.log('mid 2-2');
})
app.use((ctx, next) => {
console.log('mid 3-1');
next()
console.log('mid 3-2');
})
function dispatch(index) {
if (index === app.middlewares.length) return
let mid = app.middlewares[index]
mid({}, () => dispatch(index + 1))
}
dispatch(0)
----------------------------
output:
mid 1-1
mid 2-1
mid 3-1
mid 3-2
mid 2-2
mid 1-2
接下来,实现异步,采用async、await
function app() {}
app.middlewares = []
app.use = function(cb) {
app.middlewares.push(cb)
}
// koa可以使用async await
let log = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('ok');
resolve()
}, 1000);
})
}
app.use((ctx, next) => {
console.log('mid 1-1');
next()
console.log('mid 1-2');
})
app.use(async(ctx, next) => {
console.log('mid 2-1');
await log()
next()
console.log('mid 2-2');
})
app.use((ctx, next) => {
console.log('mid 3-1');
next()
console.log('mid 3-2');
})
function dispatch(index) {
if (index === app.middlewares.length) return
let mid = app.middlewares[index]
mid({}, () => dispatch(index + 1))
}
dispatch(0)
------------------------------
output:
mid 1-1
mid 2-1
mid 1-2
ok
mid 3-1
mid 3-2
mid 2-2
想想为什么会出现这个结果?
最后通过这个原理,修改原代码:
application.js(在以上实现的代码基础上修改,只展示新增部分)
class Koa {
constructor() {
//新增
this.middlewares = []
}
//修改
use(cb) {
this.middlewares.push(cb)
}
//新增
compose(ctx, middlewares) {
function dispatch(index) {
// 处理越界
if (index === middlewares.length) return
let mid = middlewares[index]
return Promise.resolve(mid({}, () => dispatch(index + 1))) // 转化为promise
}
return dispatch(0)
}
//修改
handlerRequest(req, res) {
res.statusCode = 404 //默认页面找不到
let ctx = this.createContext(req, res)
// this.callbacksFn(ctx) //这里传入的是ctx,当回调函数执行后,ctx.body值就会发生变化
let composedMiddlewares = this.compose(ctx,this.middlewares)
composedMiddlewares.then(() => {
let body = ctx.body
if (typeof body === 'undefined') {
res.end('Not Found!')
} else if (typeof body === 'string') {
res.end(body)
}
})
}
}
目前已经完成了一个简易版koa,通过app.js即可进行测试!
手写Promise
参考只会用?一起来手写一个合乎规范的Promise 先看看promise的一个基本用法:
function task1() {
return new Promise(function(resolve, reject) {
console.log("task1");
})
}
function task2() {
return new Promise(function(resolve, reject) {
console.log("task2");
})
}
function task3() {
return new Promise(function(resolve, reject) {
console.log("task3");
})
}
// 调用函数
task1()
.then(task2())
.then(task3())
创建Promise实例时,我们传入了一个函数,函数的两个参数(resolve/reject)分别将Promise的状态变为成功态和失败态。首先搭建出基本骨架:
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'
class Promise {
constructor(executor) {
this.state = PENDING
this.value = undefined
this.reason = undefined
}
resolve(value) {
}
reject(reason) {
}
}
Promise.prototype.then = (onFullFilled, onRejected) => {
}
module.exports = Promise
Promise实例中state保存它的状态,分为3种:等待态(pending)成功态(resolved)和失败态(rejected)。因为Promise也可以通过.then进行调用,因此在Promise的原型上绑定了then方法。
接下来分别实现:
- 当实例化Promise时,构造函数中就要马上调用传入的executor函数执行
- 完成resolve和reject方法,已经是成功态或是失败态不可再更新状态
- 实现原型上的then方法,完成Promise.prototype.then函数
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'
class Promise {
constructor(executor) {
this.state = PENDING
this.value = undefined
this.reason = undefined
executor(this.resolve, this.reject)
}
resolve(value) {
if (this.state === PENDING) {
this.value = value
this.state = RESOLVED
}
}
reject(reason) {
if (this.state === PENDING) {
this.reason = reason
this.state = REJECTED
}
}
}
Promise.prototype.then = (onFullFilled, onRejected) => {
if (this.state === RESOLVED) {
if (typeof onFullFilled === 'function') {
onFullFilled(this.value)
}
}
if (this.state === REJECTED) {
if (typeof onRejected === 'function') {
onRejected(this.reason)
}
}
}
module.exports = Promise
目前已经完成了Promise的基本功能,接下来解决异步问题。因为此时的代码还不支持Promise种传入异步函数。 我们可以创建两个数组onFulfilledFunc、onRejectedFunc 分别存放成功的回调和失败的回调,当then方法执行时,若状态还在等待态(pending),将回调函数依次放入数组中,这样在resolve和reject方法中可以分别将数组中的回调函数依次执行(resolve中执行onFulfilledFunc的所有方法,reject中执行onRejectedFunc的所有方法),具体实现如下:
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'
class Promise {
constructor(executor) {
this.state = PENDING
this.value = undefined
this.reason = undefined
this.onFulfilledFunc = []; //保存成功回调
this.onRejectedFunc = []; //保存失败回调
executor(this.resolve, this.reject)
}
resolve(value) {
if (this.state === PENDING) {
this.value = value
this.onFulfilledFunc.forEach(fn => fn(value))
this.state = RESOLVED
}
}
reject(reason) {
if (this.state === PENDING) {
this.reason = reason
this.onRejectedFunc.forEach(fn => fn(reason))
this.state = REJECTED
}
}
}
Promise.prototype.then = (onFullFilled, onRejected) => {
if (this.state === PENDING) {
if (typeof onFulfilled === 'function') {
this.onFulfilledFunc.push(onFulfilled); //保存回调
}
if (typeof onRejected === 'function') {
this.onRejectedFunc.push(onRejected); //保存回调
}
}
if (this.state === RESOLVED) {
if (typeof onFullFilled === 'function') {
onFullFilled(this.value)
}
}
if (this.state === REJECTED) {
if (typeof onRejected === 'function') {
onRejected(this.reason)
}
}
}
module.exports = Promise
现在我们测试实现的Promise类:
function task1() {
return new Promise(function(resolve, reject) {
console.log("task1");
})
}
function task2() {
return new Promise(function(resolve, reject) {
console.log("task2");
})
}
function task3() {
return new Promise(function(resolve, reject) {
console.log("task3");
})
}
// 调用函数
task1()
.then(task2())
.then(task3())
-----------------------
output:
task1
task2
task3
不过目前的Promise还存在一些问题:
- 尚不支持then链式调用
- 异常捕获
- all、race等方法实现
接下来实现链式调用和异常捕获:
- 每个then方法都返回一个新的Promise对象(原理的核心)
- 如果then方法中显示地返回了一个Promise对象就以此对象为准,返回它的结果
- 如果then方法中返回的是一个普通值(如Number、String等)就使用此值包装成一个新的Promise对象返回。
- 如果then方法中没有return语句,就视为返回一个用Undefined包装的Promise对象
- 若then方法中出现异常,则调用失败态方法(reject)跳转到下一个then的onRejected
- 如果then方法没有传入任何回调,则继续向下传递(值的传递特性)。
修改如下:
- 使MyPromise.prototype.then方法返回一个Promise
- 实现resolvePromise方法(核心)
- 重写MyPromise.prototype.then逻辑
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'
class MyPromise {
constructor(executor) {
this.state = PENDING
this.value = undefined
this.reason = undefined
this.onFulfilledFunc = []; //保存成功回调
this.onRejectedFunc = []; //保存失败回调
executor(this.resolve, this.reject)
}
resolve(value) {
if (this.state === PENDING) {
this.value = value
this.onFulfilledFunc.forEach(fn => fn(value))
this.state = RESOLVED
}
}
reject(reason) {
if (this.state === PENDING) {
this.reason = reason
this.onRejectedFunc.forEach(fn => fn(reason))
this.state = REJECTED
}
}
}
/**
* 解析then返回值与新Promise对象
* @param {Object} promise2 新的Promise对象
* @param {*} x 上一个then的返回值
* @param {Function} resolve promise2的resolve
* @param {Function} reject promise2的reject
*/
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
reject(new TypeError('Promise发生了循环引用'));
}
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
//可能是个对象或是函数
try {
let then = x.then;
if (typeof then === 'function') {
let y = then.call(x, (y) => {
//递归调用,传入y若是Promise对象,继续循环
resolvePromise(promise2, y, resolve, reject);
}, (r) => {
reject(r);
});
} else {
resolve(x);
}
} catch (e) {
reject(e);
}
} else {
//是个普通值,最终结束递归
resolve(x);
}
}
MyPromise.prototype.then = (onFullfilled, onRejected) => {
var promise2 = new Promise((resolve, reject) => {})
var self = this
if (this.state === PENDING) {
promise2 = new Promise(function(resolve, reject) {
if (typeof onFullFilled === 'function') {
self.onRejectedFunc.push(function() {
//x可能是一个promise,也可能是个普通值
setTimeout(function() {
try {
let x = onFullfilled(self.value)
resolvePromise(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
});
})
}
if (typeof onRejected === 'function') {
self.onRejectedFunc.push(function() {
//x可能是一个promise,也可能是个普通值
setTimeout(function() {
try {
let x = onRejected(self.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
});
})
}
})
}
if (this.state === RESOLVED) {
if (typeof onFullFilled === 'function') {
promise2 = new Promise(function(resolve, reject) {
//x可能是一个promise,也可能是个普通值
setTimeout(function() {
try {
let x = infulfilled(self.value)
onFullFilled(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
});
})
}
}
if (this.state === REJECTED) {
if (typeof onRejected === 'function') {
promise2 = new Promise(function(resolve, reject) {
//x可能是一个promise,也可能是个普通值
setTimeout(function() {
try {
let x = onRejected(self.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
});
})
}
}
return promise2
}
module.exports = MyPromise
思考 :如何实现Promise.all() Promise.race()方法?
手写ajax封装
搭建基本骨架:
var $ = (function() {
return {
ajax: function(opt) {
},
get: function(url, success) {
},
post: function(url, data, success) {
}
}
})();
这样的话,可以直接通过$.ajax()、$.get()、$.post()调用。接下来完善其中的功能:
var $ = (function() {
var o = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP')
if (!o) {
throw new Error('Your browser cannot support http')
}
function doAjax(opt) {
var opt = opt || {},
type = (opt.type || 'GET').toUpperCase(),
async = opt.async || true,
url = opt.url,
data = opt.data || null,
error = opt.error || function() {},
success = opt.success || function() {},
complete = opt.complete || function() {}
if (!url) {
throw new Error('no url')
}
o.open(type, url, async)
type === 'POST' && o.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
o.send(type === 'GET' ? null : formatDatas(data))
o.onreadystatechange = function() {
if (o.readyState === 4 && (o.status === 200 || status === 304)) {
success(JSON.parse(o.responseText))
} else {
error()
}
complete()
}
}
function formatDatas(obj) {
var str = ''
for (var key in obj) {
str += key + '=' + obj[key] + '&'
}
return str.replace(/&$/, '')
}
return {
ajax: function(opt) {
doAjax(opt)
},
get: function(url, success) {
doAjax({
type: 'GET',
url,
success
})
},
post: function(url, data, success) {
doAjax({
type: 'POST',
url,
data,
success
})
}
}
})();
// 测试
$.ajax({
url:'xxx',
type:'GET',
success:function(){}
})
$.get('xxx',fucntion(){})
$.post('xxx',{a=1,b=2},fucntion(){})
手写深拷贝
浅拷贝:如果属性是值类型,拷贝的是值类型的值;如果属性是引用类型,拷贝的是引用类型的内存地址。其中一个对象改变了,另一个对象也会随之改变。
深拷贝:开辟一个新的区域存放新对象,且修改新对象不会影响原对象
对于深拷贝,我们可以简单地通过JSON.parse(JSON.stringify());这么一段简单地写法实现。但是它还存在着很大的缺陷,例如无法拷贝其他引用类型、拷贝函数、解决循环引用等情况。
1. 基础版本
- 如果是值类型,无需继续拷贝,直接返回
- 如果是引用类型,创建一个新对象,依次遍历将原对象上的属性拷贝到新对象上(采用递归实现)
function clone(target) {
if (typeof target === 'object') {
let cloneTarget = {};
for (const key in target) {
cloneTarget[key] = clone(target[key]);
}
return cloneTarget;
} else {
return target;
}
};
2. 考虑数组
- 如果拷贝的数组,则不应该创建对象{},则是创建数组[],在前面加上判断即可。
function clone(target) {
if (typeof target === 'object') {
let cloneTarget = Array.isArray(target) ? [] : {};
for (const key in target) {
cloneTarget[key] = clone(target[key]);
}
return cloneTarget;
} else {
return target;
}
};
3. 解决循环引用
- 以上的版本如果发生循环引用的话会发生栈内存溢出的情况
- 这时,我们应该额外开辟一个存储空间,用来存放当前对象与拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中查找,如果有的话就直接返回,如果没有的话就继续拷贝
function clone(target, map = new Map()) {
if (typeof target === 'object') {
let cloneTarget = Array.isArray(target) ? [] : {};
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
for (const key in target) {
cloneTarget[key] = clone(target[key], map);
}
return cloneTarget;
} else {
return target;
}
};
在这里,我们也可以通过WeakMap来实现。如果我们创建了强引用的对象,我们只有手动设置为null才能被GC回收,如果是弱引用类型的话,GC会自动帮我们回收。
如果我们要拷贝的对象非常非常大时,使用Map会对内存造成很大的消耗,这时使用WeakMap可以解决这个问题。
4. 性能优化 当遍历数组时,直接使用forEach进行遍历,当遍历对象时,使用Object.keys取出所有的key进行遍历,然后在遍历时把forEach会调函数的value当作key使用:
function clone(target, map = new WeakMap()) {
if (typeof target === 'object') {
const isArray = Array.isArray(target);
let cloneTarget = isArray ? [] : {};
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
const keys = isArray ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone2(target[key], map);
});
return cloneTarget;
} else {
return target;
}
}
5. 考虑其他数据类型 首先,判断是否为引用类型,我们还需要考虑function和null两种特殊的数据类型:
//判断是否为对象
function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}
if (!isObject(target)) {
return target;
}
//获取数据类型
function getType(target) {
return Object.prototype.toString.call(target);
}
下面我们抽离出一些常用的数据类型以便后面使用:
//可以继续遍历的类型
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
//不可以继续遍历的类型
const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
可继续遍历的类型:
function clone(target, map = new WeakMap()) {
// 克隆原始类型
if (!isObject(target)) {
return target;
}
// 初始化
const type = getType(target);
let cloneTarget;
if (deepTag.includes(type)) {
cloneTarget = getInit(target, type);
}
// 防止循环引用
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
// 克隆set
if (type === setTag) {
target.forEach(value => {
cloneTarget.add(clone(value,map));
});
return cloneTarget;
}
// 克隆map
if (type === mapTag) {
target.forEach((value, key) => {
cloneTarget.set(key, clone(value,map));
});
return cloneTarget;
}
// 克隆对象和数组
const keys = type === arrayTag ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone(target[key], map);
});
return cloneTarget;
}
不可继续遍历的类型:
function cloneOtherType(targe, type) {
const Ctor = targe.constructor;
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(targe);
case regexpTag:
return cloneReg(targe);
case symbolTag:
return cloneSymbol(targe);
default:
return null;
}
}
// clone Symbol
function cloneSymbol(targe) {
return Object(Symbol.prototype.valueOf.call(targe));
}
// Clone Regexp
function cloneReg(targe) {
const reFlags = /\w*$/;
const result = new targe.constructor(targe.source, reFlags.exec(targe));
result.lastIndex = targe.lastIndex;
return result;
}
// Clone Function
const isFunc = typeof value == 'function'
if (isFunc || !cloneableTags[tag]) {
return object ? value : {}
}
function cloneFunction(func) {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
if (func.prototype) {
console.log('普通函数');
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if (body) {
console.log('匹配到函数体:', body[0]);
if (param) {
const paramArr = param[0].split(',');
console.log('匹配到参数:', paramArr);
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}
跨域原理
推荐阅读《非常全的跨域实现方案》
跨域方案
- jsonp
- cors
- postMessage
- document.domain
- window.name
- location.hash
- http-proxy
- nginx
- websocket
jsonp实现
- 只能发送get请求,不支持post/put/delete等
- 也不安全,存在xss攻击
// 调用方法:
jsonp({
url: 'xxx',
params: {
wd: 'aa'
},
cb: 'show'
}).then(data => {
console.log(data);
})
function jsonp({
url,
params,
cb
}) {
return new Promise((resolve, reject) => {
window[cb] = function(data) {
resolve(data)
document.body.removeChild(script)
}
params = {
...params,
cb
}
let arrs = []
for (const key in params) {
arrs.push(`${key}=${params[key]}`)
}
let script = document.createElement('script')
script.src = `${url}?${arrs.join('&')}`
document.body.appendChild(script)
})
}
cors实现(最常用)——服务端设置
express示例:
const whiteList = ['http://localhost:3000']
app.use(function(req, res, next) {
let origin = req.headers.origin
if (whiteList.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin) //允许哪个源可以访问我
res.setHeader('Access-Control-Allow-Headers', 'name') //允许携带哪个头访问我
res.setHeader('Access-Control-Allow-Methods', 'POST') //允许哪个方法访问我
res.setHeader('Access-Control-Max-Age', 6) //预检的存活时间
res.setHeader('Access-Control-Allow-Credentials', true) //允许携带cookie
res.setHeader('Access-Control-Expose-Headers', 'name') //允许返回的头
// post/put请求前会发送options请求,试探作用,看服务器是否支持
if (req.method === 'OPTIONS') {
next()
}
}
next()
})