• 【前端源码解析】指令和生命周期


    参考:Vue 源码解析系列课程

    本章源码:https://gitee.com/szluyu99/vue-source-learn/tree/master/Directive_Study

    系列笔记:

    JavaScript 知识储备

    documentFragment

    Document.createDocumentFragment() - Web API 接口参考 | MDN (mozilla.org)

    document.createDocumentFragment() 用来创建一个虚拟的节点对象(文档片段对象)

    fragment 认为是一个 dom 节点的收容器,原本要向 dom 中插入多少个节点就需要浏览器回流多少次,fragment 之后把要插入的节点先放入这个收容器中,这样再一次性插入,可以使浏览器只回流一次。

    使用这个来模拟 Vue 中的 AST 抽象语法树(简化版的 AST)

    DOM nodeType

    nodeType 属性返回节点类型(只读)

    • 元素节点,nodeType 属性返回 1
    • 属性节点, nodeType 属性返回 2
    • 文本节点,nodeType 属性返回 3
    • 注释节点,nodeType 属性返回 8

    类数组

    产生情况

    类数组就是形式像数组一样,但是没有数组的方法(有 length 属性)的对象

    // 这是个类数组
    let arrayLike = {
       0: "java",
       1: "C++",
       2: "javascript",
       length: 3
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    JavaScript 在以下情况中的对象是 类数组

    • 函数里面的参数对象 arguments
    • 使用 getElementsByName / TagName / ClassName 获得到的 HTMLCollection
    • querySelector 获得的 NodeList

    arguments

    function sum(a, b, c) {
        console.log(arguments)
        console.log(typeof arguments) // object
    	console.log(Object.prototype.toString.call(arguments)) // [object Arguments]
    	console.log(arguments.callee)
    }
    sum(1, 2, 3)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    HTMLCollection

    <div class="son">
       张三
    </div>
    <div class="son">
       李四
    </div>
    <div class="son">
       王五
    </div>
    
    <script>
    	var arrayList = document.getElementsByClassName("son")
    	console.log(arrayList)
    	console.log(Array.isArray(arrayList)) // false
    	console.log(typeof arrayList) // object
    	console.log(Object.prototype.toString.call(arrayList)) // [object HTMLCollection]
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    NodeList

    <div class="son">
       张三
    </div>
    <div class="son">
       李四
    </div>
    <div class="son">
       王五
    </div>
    
    <script>
        var arrayList = document.querySelector(".son")
    	console.log(Array.isArray(arrayList)) // fasle
        console.log(arrayList) // object
        console.log(typeof arrayList) // [object HTMLDivElement]
        console.log(Object.prototype.toString.call(arrayList))
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    应用场景

    遍历参数操作:

    function sum() {
          var sum = 0, len = arguments.length
          for (var i = 0; i < len; i++) {
            sum += arguments[i]
          }
    	return sum
    }
    
    console.log(sum(1,2,3)) // 6
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    定义连接字符串函数:

    类数组是没有 join 方法的,只能将其先转换为数组,再使用 join 方法

    function myConcat(separa){
    	// 类数组转数组
    	var args = Array.prototype.slice.call(arguments, 1)
    	return args.join(separa)
    }
    
    console.log(myConcat(",", "张三", "李四", "王五")) // 张三,李四,王五
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    将参数从一个函数传递到另一个函数:

    function bar(a, b, c) {
       console.log(a, b, c)
    }
    
    function foo() {
       bar.apply(this, arguments)
    }
    
    bar(1, 2, 3) // 1 2 3
    foo(1, 2, 3) // 1 2 3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    类数组 转 数组

    方法1:使用 ES6 中的 Array.from()

    function fun () {
    	let arr = Array.from(arguments)
    	console.log(Array.isArray(arr)) // true
    }
    
    • 1
    • 2
    • 3
    • 4

    方法2:使用 ES6 中的 ...

    function fun () {
    	let arr = [...arguments]
    	console.log(Array.isArray(arr)) // true
    }
    
    • 1
    • 2
    • 3
    • 4

    方法3:借数组方法

    通过 [] 创建一个空数组,使用 slice 方法返回了一个新的数组

    function fun () {
        let arr = [].slice.call(arguments)
        console.log(Array.isArray(arr))
    }
    
    • 1
    • 2
    • 3
    • 4
    function fun () {
        let arr = Array.prototype.slice.call(arguments)
        console.log(Array.isArray(arr))
    }
    
    • 1
    • 2
    • 3
    • 4

    手写一个简易版 Vue

    本章源码:https://gitee.com/szluyu99/vue-source-learn/tree/master/Directive_Study

    这里很多内容基于前面的基础上进行

    简易版 Vue 的使用效果:

    <div id="app">
    	{{a}}
    	<button onclick="add()">增加数字</button>
    	<input type="text" v-model="a">
    </div>
    
    <script src="/xuni/bundle.js"></script>    
    
    <script>
    	let vm = new Vue({
    		el: '#app',
    		data: {
    			a: 10,
    		},
    		watch: {
    			a() {
    				console.log('a 变化了');
    			}
    		}
    	})
    	console.log(vm);
    
    	function add() {
    		vm.a++
    	}
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    Vue 这个类应当挂载在 windows 对象上:

    window.Vue = Vue
    
    • 1

    Vue.js:简易版的 Vue 类

    export default class Vue {
        constructor(options) {
            this.$options = options || {}
            this._data = options?.data || undefined 
            // 将数据变成响应式
            observe(this._data)
            // 将数据添加到实例中
            this._initData()
            // 调用默认的 watch
            this._initWatch()
            // 模板编译
            new Compile(options.el, this)
        }
    
        /**
         * 将数据添加到实例中
         */
        _initData() {
            let self = this
            Object.keys(this._data).forEach(key => {
                Object.defineProperty(self, key, {
                    get() {
                        return self._data[key]
                    },
                    set (newVal) {
                        self._data[key] = newVal
                    }
                })
            })
        }
    
        /**
         * 初始化 watch
         */
        _initWatch() {
            let self = this
            let watch = this.$options.watch 
            Object.keys(watch).forEach(key => {
                new Watcher(self, key, watch[key])
            })
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    Compile.js:模板编译和指令解析

    /**
     * 模板编译类,指令解析
     */
    export default class Compile {
        constructor(el, vue) {
            // vue 实例
            this.$vue = vue
            // 挂载点
            this.$el = document.querySelector(el)
            if (this.$el) {
                // fragment 是 JS 提供的虚拟节点(弱化版 AST)
                let $fragment = this.node2Fragment(this.$el)
                // 编译
                this.compile($fragment)
                // 编译后内容,上树
                this.$el.appendChild($fragment)
            }
        }
    
        /**
         * DOM 节点转换成 Fragment
         */
        node2Fragment(el) {
            let fragment = document.createDocumentFragment()
            let child
            while (child = el.firstChild)
                fragment.append(child)
            return fragment
        }
        /**
         * 模板编译
         */
        compile(el) {
            let self = this
            let childNodes = el.childNodes
    
            // 匹配 {{val}} 中 val 
            let reg = /\{\{(.*)\}\}/ 
    
            childNodes.forEach(node => {
                let text = node.textContent
    
                if (node.nodeType === 1) {
                    // console.log('是元素节点');
                    self.compileElement(node)
                } else if (node.nodeType === 3 && reg.test(text)) {
                    // console.log('是文本节点');
                    let name = text.match(reg)[1] // 获取 {{val}} 中 val
                    // console.log(`文本节点:${name}`);
                    self.compileText(node, name)
                }
            })
        }
        /**
         * 编译元素
         */
        compileElement(node) {
            let self = this
            // 获取到节点的属性
            let nodeAttrs = node.attributes;
            // console.log(nodeAttrs)
    
            // nodeAttrs 是类数组,转成数组
            [].slice.call(nodeAttrs).forEach(attr => {
                // 例如:class="title"
                let attrName = attr.name // class
                let value = attr.value // title
    
                // 分析指令,都是 v- 开头的,如:v-if、v-model
                if (attrName.startsWith('v-')) { // 判断是指令
                    let dir = attrName.substring(2)
                    if (dir == 'model') {
                        // console.log(`v-model 指令:${value}`);
                        // 实现 v-model 的双向绑定功能
                        new Watcher(self.$vue, value, newVal => {
                            node.value = newVal
                        })
    
                        let v = self.getVueVal(self.$vue, value)
                        node.value = v
                        node.addEventListener('input', e => {
                            let newVal = e.target.value
                            self.setVueVal(self.$vue, value, newVal)
                            v = newVal
                        })
                    } else if (dir == 'if') {
                        // console.log(`v-if 指令:${value}`);
                    }
                }
            })
        }
        /**
         * 编译文本
         */
        compileText(node, name) {
            // console.log('compileText: ' + name);
            console.log(`getVueVal: ${this.getVueVal(this.$vue, name)}`);
    
            node.textContent = this.getVueVal(this.$vue, name)
            // 创建一个观察者,监听数据变化
            new Watcher(this.$vue, name, value => {
                node.textContent = value
            })
        }
        /**
         * 根据 a.b.c 形式的表达式从 vue 实例中获取值 
         */
        getVueVal(vue, exp) {
            let val = vue
            exp.split('.').forEach(key => 
                val = val[key]
            )
            return val
        }
        /**
         * 根据 a.b.c 形式的表达式给 vue 实例设置值
         */
        setVueVal(vue, exp, value) {
            let val = vue
            exp.split('.').forEach((key, index) => {
                if (index < key.length - 1) {
                    val = val[key]
                } else {
                    val[key] = value
                }
            })
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
  • 相关阅读:
    springboot vue街球社区网站java
    74道高级Java面试合集,java开发模式面试题
    kafka+zookeeper群集
    【人脸识别】MVFace:一个优于CosFace和ArcFace的人脸识别损失
    NPDP 02组合管理
    牛客首发半小时点击过热,300MB 实战 SpringBoot 笔记 GitHub 上被爆赞
    HFSS脚本建模入门
    在Anaconda或者Linux系统中导入或导出requirements.txt中的代码环境
    Java IO 之 BIO、NIO 和 AIO
    SpringSecurity(十七)---OAuth2的运行机制(下)-实现一个简单的单点登录应用程序
  • 原文地址:https://blog.csdn.net/weixin_43734095/article/details/125530676