美文网首页vue
手写Vue2核心(一): 搭建环境与对象/数组劫持

手写Vue2核心(一): 搭建环境与对象/数组劫持

作者: 羽晞yose | 来源:发表于2021-02-04 14:21 被阅读0次

    主要记录关键知识点,并非源码,仅适合想了解vue底层原理或准备面试者。

    准备工作:rollup安装


    与webpack之间得选择:
    类库或工具库 - rollup,打包结果不会有依赖(runtime与bundle)
    项目开发 - webpack

    一、安装相关依赖

    npm i rollup @rollup/plugin-babel @babel/core @babel/preset-env rollup-plugin-serve -D

    • @rollup/plugin-babel:rollup和babel的桥梁
    • @babel/core:babel核心模块
    • @babel/preset-env:es6转es5
    • rollup-plugin-serve:启动webpack服务

    二、新增命令行

    package.json中增加shell命令:"dev": "rollup -c -w"

    • -c 使用配置文件
    • -w 监听变化,同--watch

    三、编写rollup配置

    rollup-config.js配置

    import serve from 'rollup-plugin-serve'
    import babel from '@rollup/plugin-babel'
    
    // 用于打包的配置
    export default {
        input: './src/index.js',
        output: {
            file: 'dist/vue.js',
            name: 'Vue', // 全局名字就是vue
            format: 'umd', // window.Vue
            sourcemap: true // es6->es5
        },
        plugins: [
            babel({
                exclude: 'node_modules/**' // 该目录不需要用babel转换
            }),
            serve({
                open: true,
                openPage: '/public/index.html',
                port: 3000,
                contentBase: '' // 指定根目录,不写会报错
            })
        ]
    }
    

    .babelrc配置

    {
        "presets": [
            "@babel/preset-env"
        ]
    }
    

    四、增加入口点与index.html

    根目录下创建public\index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
        <script src="/dist/vue.js"></script>
        <script>
            const vm = new Vue({ // options api
                data () {
                    return {}
                },
                methods: {
    
                },
                computed: {
    
                },
                watch: {
    
                }
            })
        </script>
    </body>
    </html>
    

    根目录下创建src\index.js

    function Vue () {
    
    }
    
    export default Vue
    

    五、执行命令,进入源码开发

    执行npm run dev,查看是否有报错,根目录下是否正确生成dist目录

    Vue初始化状态流程及对象劫持


    vue2.X版本中,vue是一个构造函数

    vue2中就是一个构造函数,而不是class
    使用class入口文件将会非常臃肿,不符合模块化开发的思想。虽然也能使用Vue.prototype进行混入,但这么做也挺奇葩了

    class Vue {
        constructor (options) {
            this._init()
        }
        _init () {}
        _render () {}
    }
    

    options

    用户传入的数据,缺点是无法tree-shaking,vue2缺陷,比如methods中有写入未被使用的代码,但vue2中是无法判断该代码是否有被用到,因此没法tree-shaking掉

    函数拓展原型

    创建src\init.js,用于向Vue原型上拓展方法,实现模块化拆分

    // 通过原型混合的方式,往vue的原型添加方法
    export default function initMixin (Vue) {
        Vue.prototype._init = function (options) {
        
        }
    }
    

    vm.$options

    vue上所有的属性都可以通过$options获取(代码就不写了,也就是简单的赋值)

    初始化状态流程,响应式数据变化

    或者叫数据代理,底层原理是通过Object.defineProperty

    1. 将所有初始化方法写入initMixin中(初始化对象 -> 加入混合(initMixin) -> 初始化状态(initState) -> 初始化数据(initData))
    2. 由于data有可能是对象,也有可能是函数,需要对data类型进行判断,并赋值到vm._data
      data = vm._data = typeof data === 'function' ? data.call(vm) : data
    3. 为了避免用户设置与取值的时候需要通过vm._data,而是可以直接通过vm来设置获取data中的值,所以将vm._data中的数据做一层代理
    // 数据代理
    function Proxy (vm, source, key) {
        Object.defineProperty(vm, key, {
            get () {
                return vm[source][key]
            },
            set (newValue) {
                vm[source][key] = newValue
            }
        })
    }
    
    1. 通过observe方法将对象进行劫持(Object.defineProperty)
    class Observer {
        constructor (value) { // 需要对value属性重新定义
            this.walk(value)
        }
        walk (data) {
            // 将对象中所有的key 重新用 defineProperty定义成响应式的
            Object.keys(data).forEach((key) => {
                defineReactive(data, key, data[key])
            })
        }
    }
    
    export function defineReactive (data, key, value) { // 该实现也是为什么vue2中数据嵌套不要过深,过深浪费性能
        // value可能也是一个对象
        observe(value) // 对结果递归拦截
    
        Object.defineProperty(data, key, {
            get () {
                return value
            },
            set (newValue) {
                // 值没变化,无需重新设置
                if (newValue === value) return
                observe(newValue) // 如果用户设置的是一个对象,就继续将用户设置的对象变成响应式的
                value = newValue
            }
        })
    }
    
    export function observe (data) {
        if (typeof data !== 'object' || data == null) return
    
        // 通过类来实现对数据的观测,类可以方便拓展,会产生实例
        return new Observer(data)
    }
    

    Vue数组劫持


    虽然walk中可以对数组进行监听,但这样得处理方式相当低效,因为数组元素相对较多
    因此对数组劫持是劫持的数组方法(AOP切片编程),通过Object.create(Array.prototype)来继承数组原型

     // 不能直接改写数组原方法,也就是不能直接 Array.prototype.push = fn 直接改写,这样数组原功能也会被覆盖掉
    // 需要通过 Object.create(Array.prototype) 来创建一个对象,通过原型链来获取到数组的方法
    let oldArrayMethods = Array.prototype
    
    export let arrayMethods = Object.create(Array.prototype)
    // 7个会改变原数组的方法,而其他诸如concat slice等都不会改变原数组
    let methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort']
    
    // AOP切片编程
    methods.forEach(method => {
        arrayMethods[method] = function (...args) {
            console.log('数组变化了,这里是劫持数组当中')
            // 调用数组原有方法执行
            const result = oldArrayMethods[method].call(this, ...args)
            return result
        }
    })
    

    劫持到数组方法之后,在observe中Object.setPrototypeOf()来将数组类型的原型链指向改写后的拦截数组

    class Observer {
        constructor (value) { // value 最初为 data 传入的每一项数据
            // value可能是对象 也可能是数组,需要分开处理
            if (Array.isArray(value)) {
                // 这一句是为了在 arrayMethods中可以使用 observeArray 方法,如果是数组,则会在数组上挂载一个 Observer 实例
                // 在数组arrayMethods拦截中可以使用 observeArray 来对数组进行观测
                value.__ob__ = this
    
                // 数组不用defineProperty来进行代理 性能不好
                // 如果是数组,则将数组原型链指向被劫持后的数组,这样如果是改变数组的方法则会先被劫持,否则通过原型链使用数组方法
                Object.setPrototypeOf(value, arrayMethods)
                this.observeArray(value) // 原有数组中的对象
                // value.__proto__ = arrayMethods // 同上,但这种写法非标准。个人文章:https://www.jianshu.com/p/28a0164b0d63
            } else {
                this.walk(value)
            }
        }
        // 监控数组中是否为对象,如果是则进行劫持
        observeArray (value) {
            for (let i = 0; i < value.length; i++) {
                observe(value[i])
            }
        }
        walk (data) {
            // 将对象中所有的key 重新用 defineProperty定义成响应式的
            Object.keys(data).forEach((key) => {
                defineReactive(data, key, data[key])
            })
        }
    }
    

    如果初始化数组数据中有对象,还需要对对象进行劫持

    // 监控数组中是否为对象,如果是则进行劫持
    observeArray (value) {
        for (let i = 0; i < value.length; i++) {
            observe(value[i])
        }
    }
    

    此时还仅是对初始化的数据进行,还需要对插入的数据也进行观测(如果是对象或数组也需要继续进行观测)
    拦截数组arrayMethods中需要使用Observer的observeArray方法,因此需要将Observer挂在到该数组的__ob__中,这样在arrayMethods中就可以使用observeArray

    // observer\index.js
    class Observer {
        constructor (value) {
    +       // 这一句是为了在 arrayMethods中可以使用 observeArray 方法,如果是数组,则会在数组上挂载一个 Observer 实例
    +       // 在数组arrayMethods拦截中可以使用 observeArray 来对数组进行观测
    +       value.__ob__ = this
        }
    }
    
    // observer\array.js
    methods.forEach(method => {
        arrayMethods[method] = function (...args) {
            // code...
    
            // 如果有值则需要使用 observeArray 方法,通过 Observer 中对每一项进行监控时,如果为数组则会在该数组属性上挂上数组遍历方法
    +        if (inserted) {
    +            ob.observeArray(inserted)
    +        }
    
            // 调用数组原有方法执行
            const result = oldArrayMethods[method].call(this, ...args)
            return result
        }
    })
    

    __ob__其实就是Observer,那么去到walk的时候,进入属性监控,而__ob__就是其本身Observer,那么就会无限递归,因此需要将其设置为不可枚举

    // observer\index.js
    class Observer {
        constructor (value) {
            // 这一句是为了在 arrayMethods中可以使用 observeArray 方法
            // 在数组 arrayMethods 拦截中可以使用 observeArray 来对数组进行观测
    -       value.__ob__ = this
    +       Object.defineProperty(value, '__ob__', {
    +           value: this,
    +           enumerable: false, // 不能被枚举,否则会导致死循环
    +           configurable: false // 不能删除此属性
    +       })
        }
    }
    

    通过该属性__ob__,可以在observe方法中进行判断,如果已经检测过了则直接return即可,不用每次更改都进行一次监听

    export function observe (data) {
    +   if (data.__ob__) return // 如果有__ob__,证明已经被观测了
    }
    

    相关文章

      网友评论

        本文标题:手写Vue2核心(一): 搭建环境与对象/数组劫持

        本文链接:https://www.haomeiwen.com/subject/qmhbtltx.html