前言:最近由于疫情原因,导致久久未开学,在家本着闲着也是闲着的心情,就开始研究起了vue的源码。看了一下,才发现所谓的源码并没有想象的那么难,就是代码量比较多,都是一些基础的代码,难就难在作者的设计思想,挺佩服作者的设计思想的。话不多说,直接进入正题:
在这里插入图片描述

vue渲染页面的两种方式:

第一种:通过{{}}的形式
第二种:通过指令的形式:如v-text、v-html、v-model等
实现的细节:
1.利用文档碎片,减少页面的重绘和回流

解释:因为vue在渲染页面的时候,需要把{{}}和指令替换成对面的变量值,这样就需要进行大量的dom的操作,很消耗性能。所以vue就把要渲染的区域先获取过来(这就是为什么vue要让我们申明一个区域),把该区域放进文档碎片中,对该区域渲染完后再放回页面中,这样我们整个过程就操作了两次dom,大大的优化了浏览器性能。(文档碎片:在我看来,就是用来存储dom元素的变量,该变量的所有的属性和方法都和dom元素的一样。)

2.利用元素节点和文本节点去区分{{}}和指令

元素节点:例如:<h1 v-text='msg'>2121</h1>,这就是一个元素节点。
文本节点:上面那个例子的2121,这就是一个文本节点。
解释:当我们知道元素节点和文本节点的基本概念,我们就可以这么想了:{{}}是在文本节点中,指令一般以属性的形式放在元素节点中的。我们使用dom元素.nodeType属性来区分它是元素节点还是文本节点。区分出来就可以进行对应得渲染。

3.渲染指令

明确一下我们需要的东西:

  1. 指令的名称:例如text、html、model。
  2. 变量名:例如v-text=’msg‘中的msg变量名。

需要灵活的使用操作字符串:

  1. 使用字符串的split()方法:例如:我们需要在v-text中分离出text,我们就可以使用'v-text'.split('-'),把字符串'v-text'分割成数组['v','text']这种形式。

需要理解es6中的结构赋值:

  1. 我们获取元素节点的时候,使用dom.attributes的属性,就元素节点的所有属性都获取过来,但是你要知道,attributes这个属性返回的是一个伪数组,由于我们需要遍历这个数组,所以要把这个数组转成真数组,使用es6的解构赋值:[...dom.attributes]

需要灵活的使用数组的reduce方法:

  1. 我们在获取到变量名的时候,有时候获取到的是msg这样一层的,有时候会遇到person,name这样两层的。这个时候如果我们直接vm.$data[变量名]vm.$data是用户传过来的data),获取msg是获取的到的,但是person.name`是获取不到的。
  2. 这时我们就需要利用split('.')以点的形式把获取到的变量名弄成数组,利用reduce方法去一层一层的获取。
3.渲染页面上的{{}}

我们需要的的知识点:

  1. 字符串的replace()方法
  2. 正则表达式

我在这说一下replace()和正则表达式中的括号的配合使用:
注:我们在使用replace方法的时候,我们一般都认为第一个参数是要替换的字符或者正则表达式,第二个参数是换成什么的字符串。其实第二个参数,还可以写成函数的形式。我举个例子:

		let span = '<span>123abc'
        span.replace(/<.+?>/g, (...args) => {
            console.log(args)
        })

结果:
在这里插入图片描述
我们会发现第二参数写成函数的形式的时候,这个函数有个默认参数,这个参数我们扩展运算符来接收,不限制参数的个数。我们发现:

  1. 第一个参数是截取到的字符串
  2. 第二参数是截取到字符串的开始下标
  3. 第三个参数是整个参数

有些时候,我们有以下需求,我们需要获取<>包裹中的内容,即上面的例子的span。我们就可以结合正则表达式的括号来获取了。(在我们需要的地方加个括号)

		let span = '<span>123abc'
        span.replace(/<(.+?)>/g, (...args) => {
            console.log(args)
        })

结果:
在这里插入图片描述
正则表达式加了括号之后,我们获取到的参数就多了一个,获取到的args[1]就是我们需要的<>包裹的内容。
如果你能理解上面的知识点,你就可以看的懂渲染的代码了
html代码:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title></title>
</head>

<body>
    <div id="app">
        <h2>{{person.name}} -- {{person.age}}</h2>
        <h3>{{person.fav}}</h3>
        <ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
        </ul>
        <h3>{{msg}}</h3>
        <div v-text='msg'></div>
        <div v-text='person.fav'></div>
        <div v-html='htmlStr'></div>
        <div v-html='person.fav'></div>
        <input type="text" v-model='person.name'>
        <button v-on:click='add'>v-on</button>
        <button @click='add'>@click</button>
        <p v-text='af'></p>
    </div>
</body>
<script src="./My.js"></script>
<script>
    let vm = new Vue({
        el: '#app',
        data: {
            person: {
                name: '小明',
                age: 15,
                fav: '女'
            },
            msg: '你好',
            htmlStr: '<h3>html字符串</h3>'
        },
        methods: {
            add() {
                console.log('add方法调用了')
            }
        }
    })
</script>

</html>

vue渲染实现js代码:

const compileUtil = {
    getVal (arg, vm) {
        // 使用reduce一层一层的取.如果你直接vm.$data[arg],其中arg是person.fav这样的话,取不到的。一层就可以取到。
        return arg.split('.').reduce((data, currentData) => {
            return data[currentData]
        }, vm.$data)
    },
    on (node, methodName, vm, eventName) {
        // 1.获取方法
        // 2.添加事件
        let method = vm.options.methods && vm.options.methods[methodName]
        node.addEventListener(eventName, method)
    },
    text (node, arg, vm) {
        // 1.获取值
        // 2.更新页面
        let textContent
        if (arg.indexOf('{{') !== -1) {
            // 正则表达式中的()是为了标识子字符串,replace函数的第二个参数用得到。
            textContent = arg.replace(/\{\{(.+?)\}\}/g, (...args) => {
                return this.getVal(args[1], vm)
            })
        } else {
            textContent = this.getVal(arg, vm)
        }
        node.textContent = textContent
    },
    html (node, arg, vm) {
        // 1.获取值
        // 2.更新页面
        const htmlContent = this.getVal(arg, vm)
        node.innerHTML = htmlContent
    },
    model (node, arg, vm) {
        // 1.获取值
        // 2.更新页面
        const inputVal = this.getVal(arg, vm)
        node.value = inputVal
    }
}
class Compile {
    constructor(el, vm) {
        this.el = el.nodeType === 1 ? el : document.querySelector(el)
        this.vm = vm
        // 1.创建一个文档碎片,方便操作document,减少性能消耗
        this.f = this.createFragment(this.el)
        // 2.编译模板
        this.compile(this.f)
        // 3.将文档碎片放回容器中
        this.el.appendChild(this.f)
    }
    // 创建文档碎片
    createFragment (node) {
        let f = document.createDocumentFragment()
        let firstChild
        // 循环遍历根元素的子节点添加到文档碎片中
        while (firstChild = node.firstChild) {
            // 注:appendChild()方法会把已存在的节点删除
            f.appendChild(firstChild)
        }
        return f
    }
    compile (node) {
        // 编译文档步骤:
        // 1.遍历每一个子节点
        // 2.编译指令
        // 3.编译文本节点中的'{{}}'
        const childNodes = node.childNodes
        // 遍历每一个子节点
        childNodes.forEach(item => {
            // 元素节点
            if (item.nodeType === 1) {
                // 递归遍历每一个子节点
                this.compile(item)
                // 编译属性上的指令:v-text、v-html、v-model、v-on、@等等
                this.compileDir(item)
            } else { // 文本节点
                // 编译文本中的{{}}
                this.compileText(item)
            }
        })
    }
    // 编译指令
    compileDir (node) {
        // node.attributes是伪数组,使用es6的解构赋值弄成数组
        const attributes = [...node.attributes]
        attributes.forEach(item => {
            // item是个object属性,有name和value,所以我们再结构赋值。item:{name:v-text,value: msg}
            let { name, value } = item
            if (name.startsWith('v-')) {  // 处理v-开头的指令。假如指令是v-on:click,下面的值则是:
                let directive = name.split('-')[1]  // on:click
                let dirName = directive.split(':')[0] // on
                let eventName = directive.split(':')[1] // click
                compileUtil[dirName](node, value, this.vm, eventName)
            } else if (name.startsWith('@')) { // @click
                let eventName = name.split('@')[1] // click
                compileUtil.on(node, value, this.vm, eventName)
            }
        })
    }
    // 编译{{}}
    compileText (node) {
        // 获取文本
        const text = node.textContent
        if ((/\{\{.+?\}\}/).test(text)) { // 匹配有{{}}的字符串
            compileUtil.text(node, text, this.vm)
        }
    }
}
class Vue {
    constructor(options) {
        // 绑定数据
        this.$el = options.el
        this.$data = options.data
        this.options = options
        if (this.$el) {
            // 2.实现编译器
            new Compile(this.$el, this)
        }
    }
}

Logo

前往低代码交流专区

更多推荐