Vue家族源码中的重要部分(Vuex、Vue-router、defineReactive)(15)

69 阅读4分钟

1.Vuex(现以Vuex3版本叙述)

Vuex3之所以能在组件中通过this.$store访问到store 的数据,因为在Vue.use(Vuex),Vuex源码中在install函数中通过applymixin 函数,使用Vue.mixin({beforeCreate:vuexinit } ),vuexinit中给每个组件都注入了store的实例,(this.$store = 我们给Vue.Config 传入的store),包含我们能使用的this.$store.commit这些函数。

大白话就是通过mixin混入到组件的实例中

image.png

image.png

1.1 为何重复install

image.png

这里为何重复install,何时会走入该逻辑?

在cdn引入Vue的时候, <script src="vue"></script>

1.2 Vuex4的注入方式呢?

Vuex 4数据响应方面与3大体相同,鉴于Vue3已经放弃mixin,所以取消了使用Mixin方式混入到实例,使用了单例模式,provide inject

2.Vue-router(手写一个简版的router[hash])

myRouter.js

class MyRouter {
    constructor(config) {
        // 参数组织
        // 路由配置列表
        this._routes = config.routes; // 数组,路由的配置项

        // 路由历史栈
        this.routeHistory = [];
        this.currentUrl = '';
        this.currentIndex = -1;

        // 跳转中间变量
        this.changeFlag = false;

        // 流程调用
        this.init();
    }
    init() {
        // 监听hash变化
        window.addEventListener(
            'hashchange',
            this.refresh.bind(this),
            false
        );

        // load
        window.addEventListener(
            'load',
            this.refresh.bind(this),
            false
        );
    }
    // 单页更新
    refresh() {
        // 1. 路由参数处理
        if (this.changeFlag) {
            this.changeFlag = false;
        } else {
            this.currentUrl = location.hash.slice(1) || '/';
            // 去除分叉路径
            this.routeHistory = this.routeHistory.slice(0, this.currentIndex + 1);
            this.routeHistory.push(this.currentUrl);
            this.currentIndex++;
        }

        // 2. 切换模块
        let path = MyRouter.getPath();
        let currentComponentName = '';
        let nodeList = document.querySelectorAll('[data-component-name]');

        // 查找当前路由名称对应
        // find()
        for (let i = 0; i < this._routes.length; i++) {
            if (this._routes[i].path === path) {
                currentComponentName = this._routes[i].name;
                break;
            }
        }

        // 遍历控制节点模块展示
        nodeList.forEach(item => {
            if (item.dataset.componentName === currentComponentName) {
                item.style.display = 'block';
            } else {
                item.style.display = 'none';
            }
        })
    }

    push(option) {
        if (option.path) {
            MyRouter.changeHash(option.path, option.query);
        } else if (option.name) {
            let path = '';

            for (let i = 0; i < this._routes.length; i++) {
                if (this._routes[i].name === option.name) {
                    path = this._routes[i].path;
                    break;
                }
            }

            if (path) {
                MyRouter.changeHash(path, option.query);
            }
        }
    }

    back() {
        this.changeFlag = true;
        // ……
    }

    front() {
        this.changeFlag = true;
    }

    static getPath() {
        let href = window.location.href;
        let index = href.indexOf('#');
        if (index < 0) {
            return '';
        }
        href = href.slice(index + 1);

        let searchIndex = href.indexOf("?");
        if (searchIndex < 0) {
            return href;
        } else {
            return href.slice(0, searchIndex);
        }
    }

    static changeHash(path, query) {
        if (query) {
            let str = '';
            for (let i in query) {
                str += '&' + i + '=' + query[i];
            }

            window.location.hash
                = str
                    ? path + '?' + str.slice(1)
                    : path;
        } else {
            window.location.hash = path;
        }
    }
}

编写一个html文件引入上述myRouter进行测试使用

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>router</title>
  </head>
  <body>
    <ul>
      <li>
        <a onclick="router.push({name: 'course'})">课程</a>
        <a onclick="router.push({name: 'teacher'})">学子</a>
      </li>
    </ul>
    <div class="main-content">
      <div class="main-box" data-component-name="course">大前端</div>
      <div class="main-box" data-component-name="teacher">LuyolG</div>
    </div>
  </body>
</html>
<script src="./myRouter.js"></script>
<script>
  window.router = new MyRouter({
    routes: [
      {
        path: "/course",
        name: "course",
      },
      {
        path: "/teacher",
        name: "teacher",
      },
    ],
  });
</script>
<style>
  [data-component-name] {
    display: none;
  }
</style>

3.Vue 响应式原理(这里会写个defineReactive)

这里借鉴某博主的一篇文章,加深自己的理解

这是Vue组件化简化的流程,我们可以跟着这个流程去理解响应式到底是在哪一步

image.png

image.png

这是个响应式的思维导图,其关键点在于Observe、Dep、Watcher

image.png

其中我们要知道,Dep有收集、派发功能,对应的就是:

收集:获取数据时(get),会触发依赖收集,这样我们就知道视图哪里用到我们这个数据
派发:我们每次修改数据时(set),只需通知有依赖我们这个数据的地方去更改即可

部分更详细的流程可查阅上方推荐的文章,这里我们手写一下响应式核心文件defineReactive

class Vue {
    constructor(options) {
        const data = options.data
        this._data = data

        // 数据劫持 => initData
        _proxy(this, '_data', data)

        // 核心逻辑
        observe(data)

        new Watch(this, function () {
            return data.name + '创建响应式' // 这里读取data.name,触发get,打印'依赖收集'
        }, function () {
            console.log('watch cb:', this.value)
        })
    }
}

const _proxy = function (vm, sourceKey, data) {
    const keys = Object.keys(data);

    keys.forEach(key => {
        Object.defineProperty(vm, key, {
            get() {
                return vm[sourceKey][key]
            },
            set(val) {
                vm[sourceKey][key] = val
            }
        })
    })
}

const observe = function (data) {
    const ob = new Observer(data)
}

class Observer {
    constructor(data) {
        this.walk(data)
    }
    walk(data) {
        Object.keys(data).forEach(key => {
            defineReactive(data, key)
        })
    }
}

const defineReactive = function (obj, key) {
    let val = obj[key]
    const dep = new Dep()

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            console.log('依赖收集')
            dep.depend()
            return val
        },
        set(newVal) {
            console.log('派发更新')
            val = newVal
            dep.notify()
        }
    })
}

class Dep {
    constructor() {
        this.id = Dep.uid++
        this.subs = []
    }

    addSub(sub) {
        this.subs.push(sub)
    }

    depend() {
        if (Dep.target) {
            Dep.target.addDep(this)
        }
    }

    notify() {
        this.subs.forEach(sub => sub.update())
    }

    removeSub(sub) {
        const subIndex = this.subs.indexOf(sub)
        this.subs.splices(subIndex, 1)
    }
}
Dep.uid = 0
Dep.target = null

class Watch {
    constructor(vm, render, cb) {
        this.vm = vm
        this.render = render
        this.cb = cb

        this.deps = []
        this.depsIds = new Set()
        this.newDeps = []
        this.newDepsIds = new Set()

        this.value = this.get()
        this.cb(this.value)
    }

    get() {
        Dep.target = this
        this.newDeps = []
        this.newDepsIds = new Set()

        const value = this.render()

        Dep.target = null
        this.deps.forEach(oldDep => {
            const notExistInNewDeps = !this.newDepsIds.has(oldDep.id)
            if (notExistInNewDeps) {
                oldDep.removeSub(this)
            }
        })
        this.deps = this.newDeps
        this.depsIds = this.newDepsIds

        return value
    }

    addDep(dep) {
        const depId = dep.id
        if (!this.newDepsIds.has(depId)) {
            this.newDeps.push(dep)
            this.newDepsIds.add(depId)

            if (!this.depsIds.has(depId)) {
                dep.addSub(this)
            }
        }
    }

    update() {
        this.value = this.get()
        this.cb(this.value)
    }
}

let luyolg = new Vue({
    data: {
        name: 'LuyolG',
        study1: 100,
        study2: 99
    }
})

luyolg.study1 = 1
luyolg.study2 = 2

让我们一步步分析这段代码的执行顺序和打印顺序。

  1. 实例化 Vue 对象

    let luyolg = new Vue({
        data: {
            name: 'LuyolG',
            study1: 100,
            study2: 99
        }
    })
    
  2. Vue 构造函数

    • this._data = datadata 赋值给 this._data
    • 调用 _proxy(this, '_data', data),将 data 中的属性代理到 Vue 实例上。
    • 调用 observe(data),开始观察 data 中的属性。
    • 创建 Watch 实例,传入回调函数。
  3. _proxy 函数

    • 遍历 data 的属性,并使用 Object.definePropertydata 的属性代理到 Vue 实例上。
  4. observe 函数

    • 创建 Observer 实例,调用 walk 方法遍历 data 的属性,并调用 defineReactive
  5. defineReactive 函数

    • 使用 Object.definePropertydata 的属性变成响应式属性。
    • get 方法中打印 依赖收集,并调用 dep.depend()
    • set 方法中打印 派发更新,并调用 dep.notify()
  6. Watch 构造函数

    • 调用 this.get(),设置 Dep.target 为当前 Watch 实例。
    • 执行 this.render(),触发 data.nameget 方法,打印 依赖收集
    • 调用 dep.depend(),将当前 Watch 实例添加到 Dep 的订阅者列表中。
    • 执行回调函数 cb,打印 watch cb: LuyolG创建响应式
  7. 修改属性

    luyolg.study1 = 1
    luyolg.study2 = 2
    
    • 触发 study1study2set 方法,分别打印 派发更新

总结打印顺序:

  `依赖收集`
  `watch cb: LuyolG创建响应式`
  `派发更新`
  `派发更新`