前端框架手写实现

1,000 阅读14分钟

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打包工具

  1. 新建文件夹mypack,新建bin文件夹,在bin下创建mypack.js,在这个文件实现打包操作
  2. npm init初始化项目,修改package.json中的bin"mypack": "bin/mypack.js",运行npm link将此命令关联到全局环境下
  3. mypack.js中需要加入#! /usr/bin/env node,告诉当前文件在node环境下运行
  4. 编写mypack.js打包核心代码
  5. 在原有项目文件夹,运行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

到此,已实现全部请求方法的挂载。但还存在很多的问题,比如:

  1. 尚不支持params、query、request body等传参
  2. 未添加安全机制
  3. 其他express api均未实现
  4. ……

手写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.jsrequest.jsresponse.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.urlctx.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.url
  • ctx.path=ctx.request.path——取ctx.path是取的ctx.request.path
  • ctx.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()
})