史上最强vue总结(万字总结Vue包含全家桶)
史上最强vue总结(万字总结Vue包含全家桶)// VuexUse.vue <template> <div> VueX中的属性{{$store.state.age}} <button @click="change2">=100</button> </div> </template> <script> export default { name: 'VuexUse' mounted() { console.log(this.$store) } methods: { change2() { this.$store.state.age = 100 console.log(this.$store)
作者:叫我阿琛
转发链接:https://mp.weixin.qq.com/s/ph3aUt-H4QtBgw9z-VFlHA
目录万字总结Vue(包含全家桶) 希望这一篇可以帮到您(一)
万字总结Vue(包含全家桶) 希望这一篇可以帮到您(二) 本篇
基本Vuex的实现install方法Vuex作为一个 「插件」,首先执行的是install方法,我们希望的是,「任何组件都可以访问到这里面的数据。组件的渲染是由父到子的」,所以我们既可以先进行判断。如果它是跟节点,就把这个属性挂载到根节点上,如果不是,就找它父级的这个属性,然后挂载到这个Vue实例上
// 官方Api 会把Vue作为参数传入
const install = (_vue)=>{
Vue = _vue
Vue.mixin({ // 先父后子
beforeCreate() {
if (this.$options.store) { // 跟节点
this.$store = this.$options.store
} else { // 往子节点传递
this.$store = this.$parent && this.$parent.$store
}
}
})
}
访问state的实现
我们平时使用的过程是是这样的
const store = new Vuex.Store({
state:{
}
})
所以我们发现,我们实际上是new了一个VueX 的 Store 类。接下来我们开始写这个类。
let Vue
class Store{
constructor(options) {
this.state = options.state
this.mutations = {}
this.actions = {}
this.getters = {}
this.actions = {}
}
}
// 下面是install 方法
在其他组件中使用
// VuexUse.vue
<template>
<div>
VueX中的属性{{$store.state.age}}
<button @click="change2">=100</button>
</div>
</template>
<script>
export default {
name: 'VuexUse'
mounted() {
console.log(this.$store)
}
methods: {
change2() {
this.$store.state.age = 100
console.log(this.$store)
}
}
}
</script>
大家会看到 输出10.当点击按钮的时候。再次打印,会发现数据已经发生变化,但是视图并没有刷新。「我们应该让数据更新之后,视图也跟着刷新。这时候我们就应该想到用Vue的特性」。我们改造一下刚才的代码
let Vue
class Store{
constructor(options) {
this.state = new Vue({ data: { state: options.state } }).state
this.mutations = options.mutations || {}
this.actions = options.actions || {}
this.getters = {}
}
}
// 下面是install 方法
这样我们就实现了数据改变,就刷新视图
commit 和dispatch在VueX,中,更改状态一般需要这两个方法,一个是同步,一个是异步,我们来实现一下这两个方法
// 使用的时候
change() {
this.$store.commit('xxx' 10)
}
所以,这两个方法是写在Store类里面的
let Vue
class Store{
constructor(options) {
this.state = new Vue({ data: { state: options.state } }).state
this.mutations = options.mutations || {}
this.actions = options.actions || {}
this.getters = {}
}
commit = (mutationName payload)=>{
this.mutations[mutationName](this.state payload)
}
dispatch = (actionName payload)=>{
this.actions[actionName](this payload)
}
}
commit,我觉得大家都可以看懂,就是找到用户定义的mutations 把参数传入,就可以执行了。
dispatch,为什么要传入this?「原因」,在定义的时候,使用的是ES6的结构赋值,所以这里要把this传入
「注意,这两种方法还可以使用 柯里化来实现,这样传值的时候只用传入 payload,更方便一点」
getter实现首先我们要明白,getter是作什么用的。我 「个人理解」,需要对访问数据进行一定处理。也就是我们访问这个属性的时候,得到这个函数的返回结果。
let Vue
class Store{
constructor(options) {
this.state = new Vue({ data: { state: options.state } }).state
this.mutations = options.mutations || {}
this.actions = options.actions || {}
// 这下面是修改的部分
options.getters && this.handleGetters(options.getters)
}
handleGetters(getters) {
this.getters = {}
Object.keys(getters).forEach(key=>{
Object.defineProperty(this.getters key {
get: ()=>{
return getters[key](this.state)
}
})
})
}
}
解释一下handleGetters这一段代码
- 获取每个函数函数名称
- 根据每个函数的名称 设置对应的返回值
这段代码相对比较简单,这样就实现了getters
模块(module)功能的实现store/indeximport Vue from 'vue'
// import Vuex from 'vuex'
import Vuex from './../vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
age: 10
}
strict: true
getters: {
myAge(state) {
return state.age 30
}
}
mutations: {
change(state payload) {
state.age = payload
}
}
actions: {
// 异步更改state
asyncChange({ commit } payload) {
setTimeout(()=>{
commit('change' payload)
} 1000)
}
}
modules: {
a: {
namespaced: true
state: {
num: 'a1'
}
mutations: {
// 同步更改state 在严格模式下不可以使用异步
change(state payload) {
console.log(state payload) // 是自己这个模块内的state
console.log('a')
}
}
}
b: {
state: {
num: 'b1'
}
mutations: {
// 同步更改state 在严格模式下不可以使用异步
change(state payload) {
console.log('b')
}
}
modules: {
c: {
namespaced: true
state: {
num: 'c1'
}
mutations: {
// 同步更改state 在严格模式下不可以使用异步
change(state payload) {
console.log('c')
}
}
}
}
}
}
})
export default store
接下来这一部分可能会难理解一点。我尽力把我学习到给大家清楚的讲出来。「这部分会对之前的代码进行大量修改」
「先改造一下我们的Store 变回最开始的样子」
class ModuleCollection {
}
let Vue
class Store{
constructor(options) {
this.state = options.state
this.mutations = {}
this.actions = {}
this.getters = {}
this.modules = new ModuleCollection(options)
console.log('收集完成的模块')
console.log(this.modules)
}
}
// 下面是install 方法
现在,我们需要模块化,所以我们要写一个方法来 「格式化数据」,变成我们想要的样子
「思路」,我们要把这边模块进行遍历 注册,如果模块下面还有子类,则继续遍历。「核心方法」 reduce
ModuleCollection/**
* 循环对象的值
* @param obj
* @param cb
*/
function forEach(obj cb) {
Object.keys(obj).forEach(key=>{
cb(key obj[key])
})
}
class ModuleCollection {
constructor(options) {
this.register([] options)
}
register(path rootModule) {
// 格式化模块
const rawModule = {
_raw: rootModule //原来的modules
_children: {} // 孩子
state: rootModule.state // 原来的数据
}
// 双向记录 把格式化之后的数据记录下来
rootModule.rawModule = rawModule
// 判断是不是根的存在
if (!this.root) {
// 第一次肯定不存在
this.root = rawModule
} else {
// 核心 返回的是各个module 对应的格式化后的模块
const parentModel = path.slice(0 -1).reduce((root current)=>{
console.log(current)
return root._children[current]
} this.root)
/----------------------------------------------------/
parentModel._children[path[path.length - 1]] = rawModule
}
// 递归,遍历子代。核心逻辑
if (rootModule.modules) {
forEach(rootModule.modules (moduleName module)=>{
this.register(path.concat(moduleName) module)
})
}
}
}
主要解释一下 /-------------/上下的代码。上面的parentModel,指的是模块
- 第一次 parentModel是this.root,rawModule是a模块的定义
- 第二次 parentModel是this.root,rawModule是b模块的定义
- 第三次 parentModel是b模块,rawModule是c模块的定义
打印一下 this.modules
UTOOLS1592985514369.png
现在我们就把所有的模块进行了 「格式化」。接下来。我们就要对 我们格式化后的数据进行安装。使他们可以访问得到
「总结一下」,这个函数的作用就是把 我们传入的modules进行一个格式化,并且将模块进行分类。
installModule这个函数的作用 「循环遍历子节点,安装 state action mutation getters」
/**
* 安装 state action mutation getters 并
* @param store Vuex 中的store
* @param rootState 根state
* @param path 路径
* @param rawModule 原模块
*/
function installModule(store rootState path rawModule) {
// 安装state
if (path.length > 0) { // 证明是子节点
const parentState = path.slice(0 -1).reduce((root current)=>{
return rootState[current]
} rootState)
// 官方API。
// 向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新
Vue.set(parentState path[path.length - 1] rawModule.state)
}
// rawModule 里面有
// _raw 原来的模块
// _children 孩子
// state 原来的state
// 安装getters
// 注意状态的使用,要使用本模块的状态
const getters = rawModule._raw.getters || {}
if (getters) {
forEach(getters (getterName getterFun)=>{
Object.defineProperty(store.getters getterName {
get: ()=>getterFun(rawModule.state)
})
})
}
// mutations跟actions 差不多。都是把 所有的模块的函数存在 root根模块中 使用的时候直接遍历
const mutations = rawModule._raw.mutations || {}
if (mutations) {
forEach(mutations (mutationName mutationFun)=>{
// 写一个发布订阅模式
const arr = store.mutations[mutationName] || (store.mutations[mutationName] = [])
arr.push((payload)=>{
mutationFun(rawModule.state payload)
})
})
}
const actions = rawModule._raw.actions || {}
if (actions) {
forEach(actions (actionName actionsFun)=>{
const arr = store.actions[actionName] || (store.actions[actionName] = [])
arr.push((payload)=>{
actionsFun(store payload)
})
})
}
// 遍历子节点
forEach(rawModule._children (moduleName rawModule)=>{
// console.log(rawModule) // 一个个子节点
installModule(store rootState path.concat(moduleName) rawModule)
})
}
store 和 rootState始终是。Vuex中的store 和根上面的state
- 第一次 patch是[],rawModule是根模块的定义
- 第二次 patch是['a'],rawModule是a模块的定义
- 第三次 patch是['b'],rawModule是b模块的定义
- 走进来发现 b下面还有modules 所以patch是['b',‘c’],rawModule是c模块的定义
命名空间这个就简单了。只需要在每个方法全面加上x/就可以了
function installModule(store rootState path rawModule) {
// 命名空间的实现 获取命名
let root = store.modules.root // 拿到的是格式化之后的结果
const nameSpace = path.reduce((str current)=>{
root = root._children[current]
str = str (root._raw.namespaced ? current '/' : '')
return str
} '')
// 安装state 这里没有发生变化
if (path.length > 0) {
// 证明是子节点
const parentState = path.slice(0 -1).reduce((root current)=>{
return rootState[current]
} rootState)
Vue.set(parentState path[path.length - 1] rawModule.state)
}
// rawModule 里面有
// _raw 原来的模块
// _children 孩子
// state 原来的state
// 安装getters 把方法前面加上 命名
const getters = rawModule._raw.getters || {}
if (getters) {
forEach(getters (getterName getterFun)=>{
Object.defineProperty(store.getters nameSpace getterName {
get: ()=>getterFun(rawModule.state) // 使用模块中的状态
})
})
}
const mutations = rawModule._raw.mutations || {}
if (mutations) {
forEach(mutations (mutationName mutationFun)=>{
// 写一个发布订阅模式
const arr = store.mutations[nameSpace mutationName] || (store.mutations[nameSpace mutationName] = [])
arr.push((payload)=>{
mutationFun(rawModule.state payload)
})
})
}
const actions = rawModule._raw.actions || {}
if (actions) {
forEach(actions (actionName actionsFun)=>{
const arr = store.actions[nameSpace actionName] || (store.actions[nameSpace actionName] = [])
arr.push((payload)=>{
actionsFun(store payload)
})
})
}
// 遍历子节点
forEach(rawModule._children (moduleName rawModule)=>{
// console.log(rawModule) // 一个个子节点
installModule(store rootState path.concat(moduleName) rawModule)
})
}
从''(空)字符串开始。根节点不需要命名空间
registerModule API的实现class Store {
constructor(options) {
this.state = new Vue({ data: { state: options.state } }).state
this.mutations = {}
this.actions = {}
this.getters = {}
// 模块收集 并格式化
this.modules = new ModuleCollection(options)
console.log('收集完成的模块')
console.log(this.modules)
// 模块的安装并访问 store rootState path 根模块 安装全部模块
installModule(this this.state [] this.modules.root)
}
// 模块开发完之后的写法
commit = (mutationName payload)=>{
this.mutations[mutationName].forEach(fn=>fn(payload))
}
dispatch = (actionName payload)=>{
this.actions[actionName].forEach(fn=>fn(payload))
}
/**
* 自定义注册 module
* @param moduleName
* @param module
*/
registerModule(moduleName module) {
if (!Array.isArray(moduleName)) {
moduleName = [moduleName]
}
this.modules.register(moduleName module)
console.log(this.modules.root)
// 安装当前模块
installModule(this this.state moduleName module.rawModule)
}
}
思路很简单,就是把 注册的module,进行格式化之后。再进行安装就可以
「注意」,要安装位置要确定好哦
辅助函数VueRouter跟Vuex一样,也写过一篇比较简单的实现 VueRouter的简单实现 感觉这一篇写的相对简单一点
「这部分我个人觉得自己掌握的不是特别好。所以 讲述的不太清楚。仅提供一个思路。」
最开始install方法在我们的平常使用过程中 除了router-link和router-view。最常用的可能就是this.$router.push(xx)。所以我们还是跟VueX的做法差不多。在每一个实例上挂在一个属性
const install = (Vue)=>{
Vue.mixin({
beforeCreate() {
if (this.$options.router) {
// console.log(this) // 指的是一个new Vue
this._routerRoot = this // 把vue实例挂载到这个属性上
this._router = this.$options.router // 用户传入得 router
// 路由的初始化
this._router.init(this)
} else {
this._routerRoot = this.$parent && this.$parent._routerRoot
}
}
})
}
export default install
以上代码只做了两件事。
- 挂载属性
- 调用路由的初始化方法。对路由进行初始化
首先我们应该分析。我们这个主文件应该有什么。在我们日常使用的过程中,一般是import VueRouter from 'vue-router'
所以
- 我们应该有一个VueRoter类。
- 上面得有初始化方法install.。
- 在VueRoter类的constructor中,我们应该对用户传入的数据进行处理。还有就是分析它路由模式
- 写init方法,要可以监听到路由变换,然后跳转到对应的 路由。渲染对应的组件
分析完之后。我们就开始着手写
我先把大体框架给大家展示一下
import install from './install'
import createMatcher from './createMatcher'
import HashHistory from './history/hash'
class VueRouter {
constructor(options) {
// matcher 匹配器 处理树形结构 将他扁平化
// 返回两个方法 addStore match 匹配对应结果
this.matcher = createMatcher(options.routes || [])
// 内部需要用 hash history 进行路由的初始化工作
// base 表示基类,实现所有路由的公告方法都放在基本类上 保证不同路由API 有相同的使用方法
this.history = new HashHistory(this)
}
push(location) {
}
init(app) {
// app 是顶层Vue 实例
// 获取到路径 并进行跳转 并渲染对应组件
// 匹配一次完成后,监听路有变化,完成之后的更新操作
}
}
VueRouter.install = install
export default VueRouter
createMatcher方法
「里面出现的方法在下面都会有所解释」
import createRouteMap from './createRouteMap'
import { createRoute } from './history/base'
export default function createMatcher(routes) {
// 开始扁平化数据
const { pathList pathMap } = createRouteMap(routes)
// 重载s
function addRoute(routes) {
createRouteMap(routes pathList pathMap)
}
function match(location) {
console.log('create里面的match' location)
// 从pathMap获取的location
const record = pathMap[location]
// console.log(record)
return createRoute(record {
path: location
})
}
return {
addRoute match
}
}
我们先通过createRouteMap方法,把传入的routes(即用户传入的配置)进行一个格式化处理,得到一个pathList(地址的列表)和pathMap(地址映射,里面有地址,组件等等属性)
官方API中,有一个叫addRotes 也就是再添加进一组路由。
我们还是利用createRouteMap方法。这个方法具体是什么样的看下面
match方法的作用是匹配器,匹配传入的location(地址)。返回相对应的 记录
createRouteMap方法export default function createRouteMap(routes oldPathList oldPathMap) {
const pathList = oldPathList || []
const pathMap = oldPathMap || Object.create(null)
// Object.create(null) 和 {} 区别 前者没有原型链
// 数组扁平化
routes.forEach(route=>{
addRouteRecord(route pathList pathMap)
}
)
return {
pathList pathMap
}
}
function addRouteRecord(route pathList pathMap parent) {
const path = parent ? parent.path '/' route.path : route.path
const record = {
path
component: route.component
parent
// todo
}
if (!pathList[route]) {
pathList.push(path)
pathMap[path] = record
}
if (route.children) {
route.children.forEach(route=>{
addRouteRecord(route pathList pathMap record)
})
}
}
「Object.create(null) 和 {} 区别 前者没有原型链」
「{}」 会存在一堆的属性
UTOOLS1593155710472.png
**Object.create(null)**不存在这些
addRouteRecord 是这个的核心方法。它的工作是
- 先查找父级元素。如果有。则加上父级 例如 about/a。没有就是本身
- 然后生成一条记录record
- 判断你传入的route(即每一项路由)是否在pathList里面。在了就跳过。不在就添加进去。**这个方法就实现了addRoutes**的作用
- 递归遍历。如果有孩子继续添加
「这个方法及其关键!!!!」
原因:比如我们渲染about/a这个路径的组件。我们是不是必须得渲染about,这样才可以渲染a。
所以这个方法的主要作用就是。把路径的父级也都存下来
export function createRoute(record location) {
const res = [] // 如果匹配到路径 就放进来
if (record) {
while (record) {
res.unshift(record)
record = record.parent
}
} // 把父级路径也存放下来
console.log(res location)
return {
...location
matched: res
}
}
history方法
这个即解释this.history = new HashHistory(this)
为什么要单独列出来?因为有不同的 「路由模式」,但是有公共的处理方法。当然还需要有不同的方法来处理不同的路由。
我们这里只考虑hash
base.jsexport function createRoute(record location) {
const res = [] // 如果匹配到路径 就放进来
if (record) {
while (record) {
res.unshift(record)
record = record.parent
}
} // 把父级路径也存放下来
console.log(res location)
return {
...location
matched: res
}
}
class History {
constructor(router) {
this.router = router
this.current = createRoute(null {
path: '/'// 默认路径
})
}
transitionTo(location cb) { // 最好屏蔽一下,以防止多次调用
console.log(location cb)
// 得到路径 开始匹配对应的模板
const r = this.router.match(location)
this.current = r // 对当前路径进行更新
// eslint-disable-next-line eqeqeq
if (location === this.current.path && r.matched.length === this.current.matched) {
return
}
this.cb && this.cb(r)
cb && cb()
}
setupListeners() {
window.addEventListener('hashchange' ()=>{
this.transitionTo(window.location.hash.slice(1))
})
}
listen(cb) {
this.cb = cb
}
}
export default History
可以看出 这个base.js做了几件事
- 初始化了一个默认路由
- 提供了跳转方法
- 监听了路由变化
- listen这个等会再说
transitionTo中间的if判断。是为了防止多次调用的。
hash.js
import History from './base'
function ensureSlash() {
if (window.location.hash) {
return
}
window.location.hash = '/'
}
class HashHistory extends History {
constructor(router) {
super(router) // super === parent.call(this) 向父级传递router
this.router = router
ensureSlash() // 确保有hash值
}
getCurrentLocation() {
return window.location.hash.slice(1) // 除了# 号后面的路径
}
}
export default HashHistory
这个就比较简单了。就不再解释了
重新回到index.jsimport install from './install'
import createMatcher from './createMatcher'
import HashHistory from './history/hash'
class VueRouter {
constructor(options) {
// matcher 匹配器 处理树形结构 将他扁平化
// 返回两个方法 addStore match 匹配对应结果
this.matcher = createMatcher(options.routes || [])
// 内部需要用 hash history 进行路由的初始化工作
// base 表示基类,实现所有路由的公告方法都放在基本类上 保证不同路由API 有相同的使用方法
this.history = new HashHistory(this)
}
match(location) { // 作了一层封装 返回匹配结果
return this.matcher.match(location)
}
push(location) {
this.history.transitionTo(location ()=>{
window.location.hash = location// 这样的话 要渲染两遍 一边transitionTo 一边是hash的监听
}) // hash没有改变 要改变hash
}
init(app) {
// app 是顶层Vue 实例
// console.log(app)
// 获取到路径 并进行跳转 并渲染对应组件
// 匹配一次完成后,监听路有变化,完成之后的更新操作
const history = this.history
const setupHashListener = ()=>{ // 监听之后回调
history.setupListeners() // 监听路由变化 父类
}
history.transitionTo( // 跳转方法 父类
history.getCurrentLocation() // 获取当前路径 分路由 所以是子类
setupHashListener
)
// 订阅好 然后路由 属性变化 更新此方法
history.listen((route)=>{
app._route = route
})
}
}
VueRouter.install = install
export default VueRouter
改造完之后的index.js做的事,
- 监听路由。
- 跳转路由。
- 设置改变_route的函数(这时候 _route还不是动态的)
import RouterView from './components/router-view'
import RouterLink from './components/router-link'
const install = (Vue)=>{
Vue.mixin({
beforeCreate() {
if (this.$options.router) {
// console.log(this) // 指的是一个new Vue
this._routerRoot = this
this._router = this.$options.router // 用户传入得 router
// 路由的初始化
this._router.init(this)
// 将current 定义成 响应式的。数据改变则刷新视图
console.log(this._router)
// 给当前实例创建了 _route 属性, 取自this._router.history.current
Vue.util.defineReactive(this '_route' this._router.history.current)
// 定义之后 更新_route
} else {
this._routerRoot = this.$parent && this.$parent._routerRoot
}
}
})
Object.defineProperty(Vue.prototype '$route' {
get() {
console.log(this._routerRoot._route)
return this._routerRoot._route
}
})
Object.defineProperty(Vue.prototype '$router' {
get() {
return this._routerRoot._router
}
})
Vue.component('RouterView' RouterView)
Vue.component('RouterLink' RouterLink)
}
export default install
回到install方法,初始化之后。把_route设置成动态(有get和set)。
之后数据发生改变,视图就会刷新。
组件RouterViewexport default {
functional: true // 函数式组件 没有状态 没有this
render(h { parent data }) { // 里面有很多options 这是通过解构赋值出来的
// console.log(options)
const route = parent.$route // 被放到了vue 原型上
console.log(route)
let depth = 0
// $vnode表示占位符Vnode
while (parent) {
if (parent.$vnode && parent.$vnode.data.routerView) {
depth
}
parent = parent.$parent
}
data.routerView = true
const record = route.matched[depth]
console.log(record)
if (!record) {
return h()
}
return h(record.component data)
}
}
这段代码中最难理解的就是 depth。
route是属性。这段代码在history/base.js 。createRoute 返回的结果中有一个match。里面存放了所有的 「父级路径」
routerView的解释。自定义属性。看他是否是根节点。第一次进来的时候 ,渲染App组件(里面放有RouterView)。如果存在,证明要渲染的是下一个节点了
router是方法
RouterLinkexport default {
props: {
to: {
type: String
require: true
}
tag: {
type: String
default: 'a'
}
}
methods: {
handle() {
this.$router.push(this.to)
}
}
render(h) {
const tag = this.tag
return <tag onclick = { this.handle } > { this.$slots.default } < /tag>
}
}
我这里用的是jsx语法。觉得看不懂的可以直接用RouterLink.vue正常写来代替
钩子函数(路由守卫)这个就比较像express和koa里面的了。
简单写个思路就是这样。这一点我还没加上。思路比较简单,我还没有去具体实现
// 储存
let deps = []
// 放置
beforeXxx(cb){
this.deps.push(cb)
}
// 使用
// 在视图更新或者跳转前
this.deps.forEach(dep=>{
dep()
})
路由权限
正在努力
Vue3proxy阮一峰老师这一本书关于这部分已经写的很好了。我就不再多做叙述了。
let obj = {
name: {
achen: {
name: '阿琛'
age: 22
}
}
sex:'男'
arr: ['吃' '喝' '玩']
}
let handler = {
// target就是原对象,key是键
get(target key) {
// 懒代理 如果取到了这个对象才会触发,没有取到就不会代理
if (typeof target[key]=== 'object'){
// 递归调用
return new Proxy(target[key] handler)
}
console.log('收集')
// return target[key] 老方法
return Reflect.get(target key)
}
set(target key value) {
console.log('触发更新')
let oldValue = target[key]
console.log(oldValue value key)
if (!oldValue){
console.log('设置属性')
}else if (oldValue!==value){
console.log('修改属性')
}
// target[key] = value
// 有返回值
return Reflect.set(target key value)
}
}
// 兼容性差,但是可以代理13中方法
// defineProperty 他只能对特定属性进行拦截
// 拦截的是整个对象
let proxy = new Proxy(obj handler)
// proxy.sex = 'nv'
// console.log(proxy.sex)
// 数组
// proxy.arr.push(132) // 先走一次obj 再收集 push length 在改值
// proxy.arr[0] = 100 // 直接触发
proxy.xxx = 100
本篇已完结
作者:叫我阿琛
转发链接:https://mp.weixin.qq.com/s/ph3aUt-H4QtBgw9z-VFlHA