Vue渲染器(二):挂载与更新

news2024/12/30 1:09:26

渲染器(二):挂载与更新

前面介绍了渲染器的基本概念和整体框架,接下来就可以介绍渲染器的核心功能:挂载与更新。

1.挂载子节点和元素的属性:

vnode.children的值为字符串类型时,会把它设置为元素的文本内容。一个元素除了有文本子节点外,还可以包含其他元素子节点,并且子节点可以是多个。为了描述元素的子节点,我们将vnode.children定义为数组。

下面代码描述的是 “一个div具有一个子节点,子节点是p标签”。

const vnode = {
    type: 'h1',
    children: [
        {
            type: 'p',
            children: 'hello'
        }
    ]
}

可以看到,vnode.children是一个数组,它的每一个元素都是一个独立的vnode对象,这样就形成了树形结构,即虚拟DOM树。

修改mountElement完成子节点的渲染:

    function mountElement(vnode, container) {
        const el = createElement(vnode.type);
        if (typeof vnode.children === 'string') {
            setElementText(el, vnode.children);
        } else if (Array.isArray(vnode.children)) {
            vnode.children.forEach(child => {
                patch(null, child, el);
            })
        }
        insert(el, container);
    }

增加新的判断分支,判断vnode.children是否是数组,如果是数组则循环遍历它,并调用patch函数挂载数组中的虚拟节点。在挂载子节点时,注意两点:

  • 传递给patch函数的第一个参数是null,因为是挂载阶段,没有旧vnode。
  • 传递给patch函数的第三个参数是挂载点。由于我们正在挂载的子元素是div标签的子节点,所以需要把刚刚创建的div元素作为挂载点,这样才能保证这些子节点挂载到正确位置。

完成子节点的挂载后,来看看如何用vnode描述一个标签的属性,以及如何渲染这些属性。

HTML标签中有很多属性,有些属性是通用的,如id、class等,而有些属性是特定元素才有,有form元素的action属性。我们先来看看最基本的属性处理,定义vnode.props字段描述元素的属性。

const vnode = {
    type: 'h1',
    // 使用 props描述一个元素的属性
    props: {
        id: 'foo'
    },
    children: [
        {
            type: 'p',
            children: 'hello'
        }
    ]
}

vnode.props是一个对象,它的键代表元素的属性名称,值代表对应属性的值。这样就可以通过遍历props对象的方式,把这些属性渲染到对应的元素上,如下代码:

    function mountElement(vnode, container) {
        const el = createElement(vnode.type);
        if (typeof vnode.children === 'string') {
            setElementText(el, vnode.children);
        } else if (Array.isArray(vnode.children)) {
            vnode.children.forEach(child => {
                patch(null, child, el);
            })
        }
        if (vnode.props) {
            for (const key in vnode.props) {
                el.setAttribute(key, vnode.props[key]);
                // 直接设置
                el[key] = vnode.props[key];
            }
        }
        insert(el, container);
    }

检查vnode.props字段是否存在,存在则遍历它,并调用setAttribute函数将属性设置到元素上,或者通过DOM对象直接设置。

这两种方式都存在缺陷,其实为元素设置属性比想象中要复杂。不过在讨论具体缺陷以前,有必要先搞清楚两个重要的概念: HTML AttributesDOM Properties

2.HTML Attributes 和 DOM Properties:

理解HTML AttributesDOM Properties之间的差异和关联非常重要,这能帮我们合理地设计虚拟节点的结构,更是正确为元素设置属性的关键。

<input id="my-input" type="text" value="foo" />

HTML Attributes指的就是定义在 HTML标签上的属性,这里指的是 id="my-input" type="text" value="foo"。当浏览器解析这段HTML代码,会创建一个与之对应的DOM元素对象,我们可以通过JS来获取该DOM对象:

const el = document.querySelector('#my-input');

这个DOM对象包含很多属性,如下图所示:
在这里插入图片描述

DOM对象下的属性就是所谓的 DOM Properties。很多 HTML Attributes在DOM对象上有与之同名的 DOM Properties,例如 id=“my-input” 对应 el.id ,type="text"对应 el.type ,value=“foo” 对应 el.value等。

但是它们之间的名字不全是一模一样的,例如:

<div class="foo"></div>

class="foo" 对应的 DOM Properties则是 el.className。另外不是所有的HTML Attributes都有与之对应的 DOM Properties。例如:

<div aira-valuenow="75"></div>

aira-*类的 HTML Attributes就没有与之对应的 DOM Properties

类似地,不是所有 DOM Properties都有与之对应的 HTML Attributes,例如可以用 el.textContent来设置元素的文本内容,但没有与之对应的 HTML Attributes来完成同样的工作。

HTML Attributes的值与 DOM Properties的值之间是有关联的,例如:

<div id="foo"></div>

上面代码描述了一个具有 id属性的div标签。其中 id="foo"对应 DOM Properties是 el.id,并且值为字符串 "foo"

这种HTML AttributesDOM Properties具有相同名称(即id)的属性看作是直接映射。

但不是所有都是直接映射关系。例如:<input value="foo" />

  • 当我们没有修改文本框内容时,通过el.value读取对应的 DOM Properties的值就是字符串 foo
  • 如果修改了文本框的值,那么 el.value的值就是当前文本框的值。

修改文本框的内容为’bar’,这时:

console.log(el.getAttribute('value')); // 仍然是foo
console.log(el.value); // bar

用户对文本框内容的修改不会影响 el.getAttribute('value')的返回值,这个现象蕴含HTML Attributes所代表的意义。即HTML Attributes的作用是设置与之对应的DOM Properties的初始值。一旦值改变,DOM Properties始终存储当前值,而getAttribute()得到的仍然是初始值。

我们也可以通过 el.defaultValue来访问初始值,如:

console.log(el.getAttribute('value')); // 仍然是foo
console.log(el.value); // bar
console.log(el.defaultValue); // foo

这说明一个 HTML Attributes可能关联多个 DOM Properties,例如上面的 value="foo"与 el.value 和 el.defaultValue都有关联。

虽然我们可以认为 HTML Attributes是用来设置与之对应的 DOM Properties的初始值的,但是有些值是受限制的,就好像浏览器内部做了默认值校验。

// 通过HTML Attributes提供的默认值不合法,
// 浏览器使用内建的和法治作为对应的 DOM Properties默认值
<input type="foo" />
console.log(el.type); // 'text'

它们两者之间的关系很复杂,但记住一个核心原则即可:HTML Attributes是用来设置与之对应的 DOM Properties的初始值的。

3.正确地设置元素属性:

前面详细讨论了HTML Attributes 和 DOM Properties相关的内容,因为它们会影响DOM属性的添加方式。

  • 对于普通的HTML文件来说,当浏览器解析HTML代码后,会自动分析 HTML Attributes并设置合适的 DOM Properties
  • 但是用户编写在Vue.js的单文件组件中的模板不会被浏览器解析,这意味着原来需要浏览器来完成的工作,现在需要框架来完成。

举例说明,如下禁用按钮:

<button disabled>Button</button>

浏览器解析这段HTML代码时,发现这个按钮存在一个叫做 disabledHTML Attributes,于是将该按钮设置为禁用状态,并将它的 el.disabled这个 DOM Properties的值设置为true,这一切都是浏览器帮我们完成的。

但是同样的代码出现在 Vue.js模板中,这情况有所不同。首先这个html模板会被编译成vnode,等价于:

const button = {
    type: 'button',
    props: {
        disabled: ''
    }
}

这里的 props.disabled值为空字符串,如果在渲染器中调用 setAttribute函数设置属性,则相当于: el.setAttribute('disabled', '')。这样做没问题,浏览器会将按钮禁用。

但考虑如下模板:

<button :disabled="false">Button</button>

它对应的vnode为:

const button = {
    type: 'button',
    props: {
        disabled: false
    }
}

用户的本意是"不禁用"按钮,但如果渲染器仍然使用 setAttribute()设置属性值,则会产生意外效果,即按钮被禁用了:

el.setAttribute('disabled', false)

在浏览器上面运行这句代码,可以发现浏览器仍然将按钮禁用了。这是因为使用 setAttribute()设置的值总会被字符串化,所以它等价于:

el.setAttribute('disabled', 'false')

对于按钮来说,它的 el.disabled属性值是布尔类型的,并且它不关心具体的 HTML Attributes的值是什么。只要disabled属性存在,按钮就会被禁用。

所以我们发现,渲染器不应该总是使用 setAttribute()vnode.props对象中的属性设置到元素上。那么应该怎么解决呢?

思路:优先设置 Dom Properties,例如:el.disabled = false

这样是可以正确工作,但是又有新的问题出现了。以一开始的模板为例:

<button disabled>Button</button>
// 对应的vnode:
const button = {
    type: 'button',
    props: {
        disabled: ''
    }
}

观察可以发现,模板经过编译后得到的 vnode对象中,props.disabled的值是一个空字符串。如果直接用它设置元素的 DOM Properties,就相当于:el.disabled = '',此时浏览器会将它的值矫正为布尔类型的值,即false。等价于: el.disabled = false

这违背了用户本意,用户希望禁用按钮,但是值为false则是不禁用的意思。

这么看下来,其实无论是使用 setAttribute(),还是直接设置元素的 DOM Properties都存在缺陷。

想彻底解决这个问题就要做特殊处理,即优先设置元素的 DOM Properties,但当值为空字符串时,要手动将值矫正为true。只有这样才能保证代码行为符合预期。如下代码:

    function mountElement(vnode, container) {
        const el = createElement(vnode.type);
        if (typeof vnode.children === 'string') {
            setElementText(el, vnode.children);
        } else if (Array.isArray(vnode.children)) {
            vnode.children.forEach(child => {
                patch(null, child, el);
            })
        }
        if (vnode.props) {
            for (const key in vnode.props) {
                // 用 in 判断key是否存在对应的 DOM Properties
                if (key in el) {
                    // 获取该 DOM Properties 的类型
                    const type = typeof el[key];
                    const value = vnode.props[key];
                    // 如果是布尔类型,并且value为空字符串,矫正为true
                    if (type === 'boolean' && value === '') {
                        el[key] = true;
                    } else {
                        el[key] = value;
                    }
                } else {
                    // 如果要设置的属性没有对应的DOM Properties,
                    // 则使用 setAttribute()设置属性
                    el.setAttribute(key, vnode.props[key]);
                }
            }
        }
        insert(el, container);
    }

我们检查每一个 vnode.props中的属性。

  • 是否存在对应的 DOM Properties,如果存在则优先设置,同时对布尔类型的DOM Properties做了值的矫正,当要设置的值为空字符串时,将其矫正为布尔值true。
  • 如果不存在对应的DOM Properties,则仍然使用 setAttribute()完成属性的设置。

但是其实上面的代码还不够完善,因为有一些DOM Properties是只读的(如el.form),修改现有逻辑:

    function shouldSetAsProps(el, key, value) {
        // 特殊处理
        if (key === 'form' && el.tagName === 'INPUT') return false;
        // 兜底
        return key in el;
    }
    function mountElement(vnode, container) {
        const el = createElement(vnode.type);
        if (typeof vnode.children === 'string') {
            setElementText(el, vnode.children);
        } else if (Array.isArray(vnode.children)) {
            vnode.children.forEach(child => {
                patch(null, child, el);
            })
        }
        if (vnode.props) {
            for (const key in vnode.props) {
                const value = vnode.props[key];
                // 使用 shouldSetAsProps()判断是否应该作为 DOM Properties设置
                if (shouldSetAsProps(el, key, value)) {
                    const type = typeof el[key];
                    if (type === 'boolean' && value === '') {
                        el[key] = true;
                    } else {
                        el[key] = value;
                    }
                } else {
                    // 如果要设置的属性没有对应的DOM Properties,
                    // 则使用 setAttribute()设置属性
                    el.setAttribute(key, vnode.props[key]);
                }
            }
        }
        insert(el, container);
    }

如上代码所示,提取一个 shouldSetAsProps(),该函数返回一个布尔值,代表属性是否应该作为 DOM Properties被设置。

  • 如果返回true,则代表应该作为DOM Properties被设置,否则使用 setAttribute来设置。

其实 shouldSetAsProps内的特殊处理 不止对 <input form="xxx">这一种,无论是 <input/>标签,所有表单元素都具有 form属性,它们都应该作为 HTML Attributes被设置。当然我们不可能列举出所有情况,只需要掌握处理问题的思路就可以了。

最后将属性的设置变成与平台无关,将相关代码提取到渲染器选项中:

// 在创建renderer时传入配置项
const renderer = createRenderer({
    createElement(tag) {
        return document.createElement(tag);
    },
    setElementText(el, text) {
        el.textContent = text;
    },
    insert(el, parent, anchor = null) {
        parent.insertBefore(el, anchor);
    },
    // 将属性设置相关操作封装到这里,并作为渲染器选项传递
    patchProps(el, key, prevValue, nextValue) {
        if (shouldSetAsProps(el, key, nextValue)) {
            const type = typeof el[key];
            if (type === 'boolean' && nextValue === '') {
                el[key] = true;
            } else {
                el[key] = nextValue;
            }
        } else {
            el.setAttribute(key, nextValue);
        }
    }
})

此时的 mountElement函数:

    function mountElement(vnode, container) {
        const el = createElement(vnode.type);
        if (typeof vnode.children === 'string') {
            setElementText(el, vnode.children);
        } else if (Array.isArray(vnode.children)) {
            vnode.children.forEach(child => {
                patch(null, child, el);
            })
        }
        if (vnode.props) {
            for (const key in vnode.props) {
                // 调用 pathchProps即可
                patchProps(el, key, null, vnode.props[key]);
            }
        }
        insert(el, container);
    }

这样就把属性相关的渲染逻辑从渲染器的核心中抽离出来了。

4.class的处理:

在前面中,我们讲解了如何正确地把 vnode.props中定义的属性设置到DOM元素上。

但是有一些属性需要特殊处理,比如class。在 Vue.js中对class进行了增强,所以我们需要对它特殊处理。在 Vue.js中为元素设置类型有以下几种方式:

方式一:指定class为一个字符串值。

<div class="foo bar "></div>
// 模板对应的vnode:
const vnode = {
    type: 'p',
    props: {
        class: 'foo bar'
    }
}

方式二:指定class为一个对象值。

<p :class="cls"></p>
  • 假设对象cls的内容如下:const cls = { foo: true, bar: flase }

对应的vnode则为:

const vnode = {
    type: 'p',
    props: {
        class: { foo: true, bar: false },
    }
}

方式三:class是包含上述两种类型的数组: <p :class="arr"></p>

这个数组可以是字符串值与对象值的组合:

const arr = [
    // 字符串
    'foo bar',
    // 对象
    {
        baz: true
    }
]

对应的vnode则为:

const vnode = {
    type: 'p',
    props: {
        class: [
            'foo bar',
            { baz: true }
        ],
    }
}

可以看到,class的值可以是多种类型,所以我们必须在设置元素的class之前将值归一化为统一的字符串类型,再用该字符串作为元素的class值取设置。

因此我们封装 normalizeClass函数,用它来将不同类型的class值正常化为字符串,如:

const vnode = {
    type: 'p',
    props: {
        class: normalizeClass([
            'foo bar',
            { baz: true }
        ])
    }
}
// 等价于
const vnode = {
    type: 'p',
    props: {
        class: 'foo bar baz'
    }
}

normalizeClass的实现:

function normalizeClass(value) {
    let res = '';
    if (isString(value)) {
        res = value;
    }
    else if (isArray(value)) {
        for (let i = 0; i < value.length; i++) {
            const normalized = normalizeClass(value[i]);
            if (normalized) {
                res += normalized + ' ';
            }
        }
    }
    else if (isObject(value)) {
        for (const name in value) {
            if (value[name]) {
                res += name + ' ';
            }
        }
    }
    return res.trim();
}

接下来就开始将正常化后的 class值设置到元素上。在浏览器中为一个元素设置class有三种方式,即使用 setAttribute、el.className 或 el.classlist,接下来对比它们的性能发现 el.className的性能最优。调整 patchProps函数的实现,如:

const renderer = createRenderer({
    createElement(tag) {
        return document.createElement(tag);
    },
    setElementText(el, text) {
        el.textContent = text;
    },
    insert(el, parent, anchor = null) {
        parent.insertBefore(el, anchor);
    },
    patchProps(el, key, prevValue, nextValue) {
        // 对class进行特殊处理
        if (key === 'class') {
            el.className = nextValue || ''
        } else if (shouldSetAsProps(el, key, nextValue)) {
            const type = typeof el[key];
            if (type === 'boolean' && nextValue === '') {
                el[key] = true;
            } else {
                el[key] = nextValue;
            }
        }
        else {
            el.setAttribute(key, nextValue);
        }
    }
})

通过对 class的处理,能发现 vnode.props对象中定义的属性值类型并不总是与 DOM元素属性的数据结构保持一致,这取决于上层API的设计。

Vue.js中允许对象类型的值作为 class是为了方便开发者,在底层实现上,必然需要对值进行正常化后再使用。如果需要进行大量的正常化操作,则会消耗更多性能。

5.卸载操作:

前面讨论的都是挂载操作,接下来就来看卸载操作,卸载操作发生在更新阶段。

  • 更新:初次挂载完成后,后续渲染触发更新。

更新的情况有几种,我们逐个来看。当后续调用 render()渲染空内容(即null):

// 初次挂载
renderer.render(vnode, document.querySelector('#app'))
// 新vnode为null,意味着卸载之前渲染的内容
renderer.render(null, document.querySelector('#app'))

传递null作为新vnode,意味着什么都不渲染,这时就需要卸载之前渲染的内容。回顾前面实现的render函数:

    function render(vnode, container) {
        if (vnode) {
            // 新vnode存在,将其与旧vnode一起传递给patch函数,进行更新
            patch(container._vnode, vnode, container);
        } else {
            if (container._vnode) {
                // 旧vnode存在,且新vnode不存在,说明是unmount(卸载)操作
                // 只需要将container内的DOM清空即可
                container.innerHtml = '';
            }
        }
        // 把 vnode存储到 container._vnode下,即后续渲染中的旧vnode
        container._vnode = vnode;
    }

可以看到,当vnode为null,且container._vnode属性存在,直接通过 innerHtml清空容器是不严谨的,原因有三:

  • 容器的内容可能是由某个或多个组件渲染的,当卸载操作发生时,应该正确调用这些组件的 beforeUnmountunmounted等生命周期函数。
  • 即使内容不是由组件渲染的,有的元素存在自定义指令,我们应该在卸载操作发生时正确执行对应的指令钩子函数。
  • 使用 innerHTML清空容器元素内容的另一个缺陷是:它不会移除绑定在DOM元素上的事件处理函数。

正如上述三点原因,我们不能简单地使用 innerHTML来完成卸载操作。

正确的卸载方式是:根据vnode对象获取与其相关联的真实DOM元素,然后使用原生DOM操作方法将该DOM元素移除。

修改 mountElement函数,在vnode与真实DOM元素之间建立联系:

    function mountElement(vnode, container) {
        // 让 vnode.el引用真实 DOM元素
        const el = vnode.el = createElement(vnode.type);
        if (typeof vnode.children === 'string') {
            setElementText(el, vnode.children);
        } else if (Array.isArray(vnode.children)) {
            vnode.children.forEach(child => {
                patch(null, child, el);
            })
        }
        if (vnode.props) {
            for (const key in vnode.props) {
                patchProps(el, key, null, vnode.props[key]);
            }
        }
        insert(el, container);
    }

当调用 createElement函数创建真实DOM元素时,会把真实DOM元素赋值给 vnode.el属性。这样在vnode与真实DOM元素之间就建立了联系,就可以通过 vnode.el来获取该虚拟节点对应的真实DOM元素。

有了这些,当卸载操作发生时,只需要根据 vnode.el取得真实DOM元素,再将其从父元素中移除即可:

    function render(vnode, container) {
        if (vnode) {
            patch(container._vnode, vnode, container);
        } else {
            if (container._vnode) {
                // 根据 vnode获取要卸载的真实DOM元素
                const el = container._vnode.el;
                // 获取 el的父元素
                const parent = el.parentNode;
                // 调用 removeChild 移除元素
                if (parent) parent.removeChild(el);
            }
        }
        container._vnode = vnode;
    }

其中 container._vnode代表旧 vnode,即要被卸载的vnode。然后通过 container._vnode.el取得真实DOM元素,并调用 removeChild函数将其从父元素中移除即可。

由于卸载操作是比较常见且基本的操作,所以我们将它封装到 unmount函数中,以便后续代码可以复用它,unmount实现:

    function unmount(vnode) {
        const parent = vnode.el.parentNode;
        if (parent) parent.removeChild(vnode.el);
    }

unmount函数接收一个虚拟节点作为参数,并将该虚拟节点对应的 真实DOM元素从父元素中移除。现在这个代码非常简单,后续再慢慢完善它。

此时的render函数:

    function render(vnode, container) {
        if (vnode) {
            patch(container._vnode, vnode, container);
        } else {
            if (container._vnode) {
                // 调用unmount函数卸载 vnode
                unmount(container._vnode);
            }
        }
        container._vnode = vnode;
    }

通过将卸载操作封装的unmount中,有两点额外好处:

  • 在unmount函数内,有机会调用绑定在DOM元素上的指令钩子函数,例如 beforeUnmountunmounted等。
  • unmount函数执行时,有机会检测虚拟节点 vnode的类型。如果该虚拟节点描述的是组件,则有机会调用组件相关的生命周期函数。

6.区分vnode的类型:

在前面中,当调用 render渲染空内容null时,会执行卸载操作。

如果在后续渲染时,为render 函数传递了新的vnode,则不会进行卸载操作,而是会把新旧vnode 都传递给patch 函数进行打补丁。回顾前面的patch函数:

    function patch(n1, n2, container) {
        // 如果n1不存在,意味着挂载,调用 mountElement()完成挂载
        if (!n1) {
            mountElement(n2, container);
        } else {
            // n1存在,意味着更新,暂时省略
        }
    }

n1:旧vnode,n2:新vnode。如果旧vnode存在,则需要在新旧vnode之间打补丁。

但在具体执行打补丁操作之前,需要保证新旧vnode所描述的内容相同。来看个例子,如果初次渲染的vnode是一个p元素:

const vnode = {
    type: 'p'
}
renderer.render(vnode, document.querySelector('#app'))

后续渲染:

const vnode = {
    type: 'input'
}
renderer.render(vnode, document.querySelector('#app'))

这就造成了新旧vnode所描述的内容不同,即 vnode.type属性的值不同。对于上例来说,p元素和input元素之间不存在打补丁的意义。因为对于不同元素,每个元素都有特有的属性,例如:

    <p id="foo"></p>
    <!-- type属性是input标签特有的,p标签没有该属性 -->
    <input type="submit" />

在这种情况下,正确的更新操作是:先将p元素卸载,再将input元素挂载到容器中。调整patch函数:

    function patch(n1, n2, container) {
        // 如果n1存在,则对比 n1 和n2的类型
        if (n1 && n1.type !== n2.type) {
            // 如果新旧vnode的类型不同,则直接将旧vnode卸载
            unmount(n1);
            n1 = null;
        }
        if (!n1) {
            mountElement(n2, container);
        } else {
            // 更新
        }
    }

如上面代码所示,在真正执行更新操作之前优先检查新旧 vnode所描述的内容是否相同。如果不同则直接调用 unmount函数将 旧vnode卸载,卸载完后将 n1的值重置为null,这样就能确保后续挂载操作正确执行了。

即使新旧vnode描述的内容相同,仍然需要进一步确认它们的类型是否相同。

一个vnode可以用来描述普通标签,也可以用来描述组件,还可以用来描述 Fragment等。对于不同类型的vnode,我们需要提供不同的挂载或打补丁的处理方式。

所以继续修改patch函数的代码以满足需求:

    function patch(n1, n2, container) {
        if (n1 && n1.type !== n2.type) {
            unmount(n1);
            n1 = null;
        }
        // 代码运行到这里,证明 n1 和 n2所描述的内容相同
        const { type } = n2;
        // 如果 n2.type的值是字符串类型,则它描述的是普通标签元素
        if (typeof type === 'string') {
            if (!n1) {
                mountElement(n2, container);
            } else {
                patchElement(n1, n2);
            }
        } else if (typeof type === 'object') {
            // 如果 n2.type的值类型是对象,则它描述的是组件
        } else if (type === 'xxx') {
            // 处理其他类型的vnode
        }
    }

实际上,在前面我们一直假设 vnode类型是普通标签元素,但严谨做法是根据 vnode.type进一步确认它们的类型是什么,从而使用相应的处理函数进行处理。例如:

  • typeof type === 'string',则它描述的是普通标签元素,调用 mountElementpatchElement完成挂载和更新操作;
  • typeof type === 'object',则它描述的是组件,则调用与组件相关的挂载和更新方法。

7.事件的处理:

这节开始讨论如何处理事件,包括如何在虚拟节点中描述事件,如何事件添加DOM元素上,以及如何更新事件。

先来解决第一个问题,即如何在虚拟节点中描述事件。

  • 事件可以视作一种特殊的属性,因此我们可以约定,在 vnode.props对象中,凡是以字符串 on 开头的属性都视作事件。如:
const vnode = {
    type: 'p',
    props: {
        onClick: () => {
            alert('clicked')
        }
    },
    children: 'text'
}

解决了事件在虚拟节点层面的描述问题后,再来看看如何将事件添加到DOM元素上。

这不难,只需要在 patchProps中调用 addEventListener()来绑定事件即可,如:

    patchProps(el, key, prevValue, nextValue) {
        // 匹配以on开头的属性,视其为事件
        if (/^on/.test(key)) {
            // 根据属性名得到对应事件名,如 onClick -> click
            const name = key.slice(2).toLowerCase();
            // 绑定事件,nextValue为事件处理函数
            el.addEventListener(name, nextValue);
        } else if (key === 'class') {
            el.className = nextValue || ''
        } else if (shouldSetAsProps(el, key, nextValue)) {
            const type = typeof el[key];
            if (type === 'boolean' && nextValue === '') {
                el[key] = true;
            } else {
                el[key] = nextValue;
            }
        }
        else {
            el.setAttribute(key, nextValue);
        }
    }

那么更新事件该如何处理呢?我们选择一种性能更优的方式来完成事件更新。

在绑定事件时,绑定一个伪造的事件处理函数 invoker,然后把真正的事件处理函数设置为 invoker.value属性的值。这样当更新事件时,就不需要调用 removeEventListener函数来移除上一次绑定的事件,只需要更新 invoker.value的值即可,如下代码:

    patchProps(el, key, prevValue, nextValue) {
        // 匹配以on开头的属性,视其为事件
        if (/^on/.test(key)) {
            let invoker = el._vei;
            const name = key.slice(2).toLowerCase();
            if (nextValue) {
                if (!invoker) {
                    // 如果没有 invoker,则伪造invoker缓存到 el._vei中
                    // vei是vue event invoker的首字母缩写
                    invoker = el._vei = (e) => {
                        // 当伪造的事件处理函数执行时,会执行真正的事件处理函数
                        invoker.value(e);
                    }
                    // 将真正的事件处理函数赋值给 invoker.value
                    invoker.value = nextValue;
                    // 绑定 invoker作为事件处理函数
                    el.addEventListener(name, invoker);
                } else {
                    // 如果invoker存在,直接更新 invoker.value的值即可
                    invoker.value = nextValue;
                }
            } else if (invoker) {
                // 新的事件绑定函数不存在,且之前绑定的invoker存在,移除绑定
                el.removeEventListener(name, invoker);
            }
        } else if (key === 'class') {
            el.className = nextValue || ''
        } else if (shouldSetAsProps(el, key, nextValue)) {
            const type = typeof el[key];
            if (type === 'boolean' && nextValue === '') {
                el[key] = true;
            } else {
                el[key] = nextValue;
            }
        }
        else {
            el.setAttribute(key, nextValue);
        }
    }

事件绑定主要分为两个步骤:

  • 先从 el._vei中读取对应的 invoker,如果它不存在则将伪造的 invoker作为事件处理函数,并将它缓存到 el._vei属性中。
  • 把真正的事件处理函数赋值给 invoker.value,然后把伪造的 invoker函数作为事件处理函数绑定到元素上。可以看到,当事件触发时,实际上执行的是伪造的事件处理函数,在其内部间接执行了真正的事件处理函数 invoker.value(e)

当更新事件时,由于 el._vei已经存在,所以我们只用将 invoker.value的值修改为新的事件处理函数就行。这样在更新时间时就避免一次 removeEventListener()的调用,从而提升了性能。它的作用其实很挺多的,还可以用它解决事件冒泡与事件更新之间相互影响的问题,后文详解。

但目前的实现仍然存在问题,现在只需要将事件处理函数缓存在 el._vei属性中,问题是同一时刻只能缓存一个事件处理函数。这意味着如果一个元素同时绑定了多种事件,将会出现事件覆盖的现象。为了解决这个现象,我们需要重新设计 el._vei的数据结构,我们将它设计为一个对象,键是事件名称,值是对应的事件处理函数,这样就不会发生事件覆盖的现象。如下代码:

    patchProps(el, key, prevValue, nextValue) {
        if (/^on/.test(key)) {
            // 定义 el.vei为一个对象,存在事件名称到事件处理函数的映射 
            let invokers = el._vei || (el._vei = {});
            // 根据事件名获取 invoker
            let invoker = invokers[key];
            const name = key.slice(2).toLowerCase();
            if (nextValue) {
                if (!invoker) {
                    // 将事件处理函数缓存到 el._vel[key]下,避免覆盖
                    invoker = el._vei[key] = (e) => {
                        // 当伪造的事件处理函数执行时,会执行真正的事件处理函数
                        invoker.value(e);
                    }
                    invoker.value = nextValue;
                    el.addEventListener(name, invoker);
                } else {
                    invoker.value = nextValue;
                }
            } else if (invoker) {
                el.removeEventListener(name, invoker);
            }
        } else if (key === 'class') {
            el.className = nextValue || ''
        } else if (shouldSetAsProps(el, key, nextValue)) {
            const type = typeof el[key];
            if (type === 'boolean' && nextValue === '') {
                el[key] = true;
            } else {
                el[key] = nextValue;
            }
        }
        else {
            el.setAttribute(key, nextValue);
        }
    }

另外一个元素不仅可以绑定多种类型的事件,对于同一事件而已,还可以绑定多个处理函数。为了描述同一个事件的多个事件处理函数,我们需要调整 vnode.props对象中事件的数据结构:

const vnode = {
    type: 'p',
    props: {
        onClick: [
            () => { alert('clicked 1') },
            () => { alert('clicked 2') }
        ]
    },
    children: 'text'
}

用一个数组来描述事件,数组中的每个元素都是一个独立的事件处理函数,并且这些事件处理函数都能够正确地绑定到对应元素上,修改 patchProps

    patchProps(el, key, prevValue, nextValue) {
        if (/^on/.test(key)) {
            let invokers = el._vei || (el._vei = {});
            let invoker = invokers[key];
            const name = key.slice(2).toLowerCase();
            if (nextValue) {
                if (!invoker) {
                    invoker = el._vei[key] = (e) => {
                        // 如果 invoker.value是数组,则遍历它并逐个调用事件处理函数
                        if (Array.isArray(invoker.value)) {
                            invoker.value.forEach(fn => fn(e));
                        } else {
                            // 否则直接作为函数调用
                            invoker.value(e);
                        }
                    }
                    invoker.value = nextValue;
                    el.addEventListener(name, invoker);
                } else {
                    invoker.value = nextValue;
                }
            } else if (invoker) {
                el.removeEventListener(name, invoker);
            }
        } else if (key === 'class') {
            el.className = nextValue || ''
        } else if (shouldSetAsProps(el, key, nextValue)) {
            const type = typeof el[key];
            if (type === 'boolean' && nextValue === '') {
                el[key] = true;
            } else {
                el[key] = nextValue;
            }
        }
        else {
            el.setAttribute(key, nextValue);
        }
    }

在这段代码中,修改了 invoker函数的实现。当它执行时,在调用真正事件处理函数之前,要先检查 invoker.value是否是数组,如果是则遍历它,并逐个调用定义在数组中的事件处理函数。

8.事件冒泡与更新时机问题:

在前面,介绍了基本的事件处理。这节将讨论事件冒泡与更新时机相结合所导致的问题。先来看一个小例子:

import { ref } from '../reactivity/ref'
import { effect } from '../reactivity/effect';

const bol = ref(false);
effect(() => {
    // 创建vnode
    const vnode = {
        type: 'div',
        props: bol.value ?
            { onClick: () => { alert('父元素 clicked') } }
            : {},
        children: [
            {
                type: 'p',
                props: {
                    onClick: () => { bol.value = true }
                },
                children: 'text'
            }
        ]
    }
    // 渲染vnode
    renderer.render(vnode, document.querySelector('#app'));
})

创建一个响应式数据bol,接着创建一个副作用函数,并在内部调用渲染函数来渲染vnode。

这里重点在于该vnode对象,它描述了一个 div元素,并且该div元素具有一个p元素作为子节点,来看一下它们的特点:

  • div元素:它的props对象的值是由一个三元表达式决定的。在首次渲染时,由于 bol.value的值为false,所以它的props的值是一个空对象。
  • p元素:它具有click点击事件,并且点击它时,事件处理函数会将 bol.value的值设置为true。

来思考一个问题:当首次渲染完成后,点击p元素,会触发父级的click事件的事件处理函数执行吗?

  • 答案其实很明显,在首次渲染完成之后,由于 bol.value的值为false,所以渲染器并不会为div元素绑定点击事件。当用鼠标点击p元素时,即使 click事件可以从p元素冒泡到父级div元素,但由于div元素没有绑定click事件的事件处理函数,所以什么都不会发生。
  • 但是事实是,当你尝试运行上面这段代码并点击p元素时,会发现父级div元素的click事件的事件处理函数居然执行了。这和更新机制有关。

来分析一下,当点击p元素时,发生了什么?

  • 当点击p元素时,绑定到它身上的click函数会执行,于是 bol.value = true

  • 接下来由于bol是一个响应式数据,当它的值发生变化时,会触发副作用函数重新执行。由于此时 bol.value = true,所以在更新阶段,渲染器会为父级div元素绑定click事件处理函数。

  • 当更新完成后,点击事件才从p元素冒泡到父级div元素。由于此时div元素已经绑定了 click事件的处理函数,就发生了上述奇怪的现象。可看下图:
    在这里插入图片描述

观察上图可以发现,之所以出现前面的现象,是因为更新操作发生在事件冒泡之前,即:为div元素绑定事件处理函数发生在事件冒泡之前。并且可以发现,触发事件的时间与绑定事件的时间之间是有联系的,如下图:

在这里插入图片描述

看上图可以发现,事件触发的时间要早于事件处理函数被绑定的时间。

这意味着当一个事件触发时,目标元素上还没有绑定相关的事件处理函数,我们可以根据这个特点来解决问题:屏蔽所有绑定时间晚于事件触发时间的事件处理函数的执行。调整 patchProps()中关于事件的代码:

    patchProps(el, key, prevValue, nextValue) {
        if (/^on/.test(key)) {
            let invokers = el._vei || (el._vei = {});
            let invoker = invokers[key];
            const name = key.slice(2).toLowerCase();
            if (nextValue) {
                if (!invoker) {
                    invoker = el._vei[key] = (e) => {
                        // e.timeStamp:事件发生的时间
                        // 如果事件发生的时间早于事件处理函数绑定的时间,则不执行事件处理函数
                        if (e.timeStamp < invoker.attached) return;
                        if (Array.isArray(invoker.value)) {
                            invoker.value.forEach(fn => fn(e));
                        } else {
                            invoker.value(e);
                        }
                    }
                    invoker.value = nextValue;
                    // 添加 invoker.attached属性,存储事件处理函数被绑定的时间
                    invoker.attached = performance.now();
                    el.addEventListener(name, invoker);
                } else {
                    invoker.value = nextValue;
                }
            } else if (invoker) {
                el.removeEventListener(name, invoker);
            }
        } else if (key === 'class') {
            el.className = nextValue || ''
        } else if (shouldSetAsProps(el, key, nextValue)) {
            const type = typeof el[key];
            if (type === 'boolean' && nextValue === '') {
                el[key] = true;
            } else {
                el[key] = nextValue;
            }
        }
        else {
            el.setAttribute(key, nextValue);
        }
    }

如上代码,我们只是在原来的基础上添加了两行代码。添加 invoker.attached属性用来存储事件处理函数被绑定的时间。然后在 invoker执行的时候,通过事件对象的 e.timeStamp获取事件发生的时间,然后再进行比较。

9.更新子节点:

前几节中讲解了元素属性的更新,包括普通标签属性和事件。接下来讨论如何更新元素的子节点。先回顾以下子节点是如何被挂载的:

    function mountElement(vnode, container) {
        // 让 vnode.el引用真实 DOM元素
        const el = vnode.el = createElement(vnode.type);
        if (typeof vnode.children === 'string') {
            setElementText(el, vnode.children);
        } else if (Array.isArray(vnode.children)) {
            vnode.children.forEach(child => {
                patch(null, child, el);
            })
        }
        if (vnode.props) {
            for (const key in vnode.props) {
                patchProps(el, key, null, vnode.props[key]);
            }
        }
        insert(el, container);
    }

在挂载子节点时,首先要区分其类型:

  • 如果 vnode.children是字符串,则说明元素具有文本子节点;
  • 如果 vnode.children是数组,则说明元素具有多个子节点。

这里为什么要区分子节点的类型呢?其实这是一个规范性的问题,因为只有子节点的类型是规范化的,才有利于我们编写更新逻辑。那应该设定怎么样的规范呢?

其实对一个元素来说,它的子节点无非有以下三种情况:

  • 没有子节点,此时 vnode.children的值为 null
  • 具有文本子节点,此时 vnode.children的值为字符串,代表文本的内容;
  • 其他情况无论是单个元素子节点,还是多个子节点(可能是文本和元素的混合),都可以用数组来表示。

如下代码:

vnode = { type: 'div', children: null };
vnode = { type: 'div', children: 'Text' };
vnode = {
    type: 'div',
    children: [
        { type: 'p' },
        'Text'
    ]
};

现在就规范化了 vnode.children的类型。既然一个vnode子节点可能有三种情况,那么当渲染器执行更新时,新旧子节点都分别是三种情况之一。那么可以总结出更新子节点时全部九种情况,如下图:在这里插入图片描述

但落实到代码中可以发现,其实不需要完全覆盖这九种可能。代码实现:

function patchElement(n1, n2) {
    const el = n2.el = n1.el;
    const oldProps = n1.props;
    const newProps = n2.props;
    // 第一步:更新props
    for (const key in newProps) {
        if (newProps[key] !== oldProps[key]) {
            patchProps(el, key, oldProps[key], newProps[key]);
        }
    }
    for (const key in oldProps) {
        if (!(key in newProps)) {
            patchProps(el, key, oldProps[key], null);
        }
    }
    // 第二步:更新children
    patchChildren(n1, n2, el);
}

如上面代码所示,更新子节点是对一个元素进行打补丁的最后一步操作。将它封装到 patchChildren函数中:

function patchChildren(n1, n2, container) {
    // 判断新子节点的类型是否是文本节点
    if (typeof n2.children === 'string') {
        // 旧子节点的类型的三种可能
        // 只有当旧子节点为一组子节点时,才需要逐个卸载
        if (Array.isArray(n1.children)) {
            n1.children.forEach((c) => unmount(c));
        }
        // 最后将新的文本节点内容设置给容器元素
        setElementText(container, n2.children);
    } else if (Array.isArray(n2.children)) {
        // 说明新子节点是一组子节点
        if (Array.isArray(n1.children)) {
            // 代码运行到这,说明新旧子节点都是一组子节点,这里涉及到核心Diff算法
        } else {
            // 此时旧子节点不是一组子节点,清空容器,并逐个挂载新子节点
            setElementText(container, '');
            n2.children.forEach(c => patch(null, c, container));
        }
    } else {
        // 代码运行到这里,说明新子节点不存在
        if (Array.isArray(n1.children)) {
            // 旧子节点是一组子节点,逐个卸载
            n1.children.forEach(c => unmount(c));
        } else if (typeof n1.children === 'string') {
            // 旧子节点是文本子节点,清空内容即可
            setElementText(container, '');
        }
        // 如果也没有旧子节点,什么都不做
    }
}

分别用三个分支来表示上面提到的三种情况,这样做能实现需求,但不是最优解,最优解在后面讲解如何使用Diff算法高效更新两组子节点。

10.文本节点和注释节点:

在前面,只说了描述普通标签的vnode,接下来将讨论如何使用vnode描述注释节点和文本节点。

我们知道,vnode.type属性能够代表一个vnode的类型。

  • 如果它是字符串类型,则它描述的是普通标签,并且该值就代表标签的名称。
  • 但注释节点与文本节点不同于普通标签节点,它们不具有标签名称,所以我们需要人为创造一些唯一标识,这时自然而然就想到了使用 Symbol
const Text = Symbol();
const newVnode = {
    type: Text,
    children: '我是文本内容'
}

const Comment = Symbol();
const newVnode = {
    type: Comment,
    children: '我是注释内容'
}

vnode.type属性的值我们使用文本节点和注释节点创建,这样就能够用vnode来描述文本节点和注释节点了。有了用于描述文本节点和注释节点的vnode对象后,就可以使用渲染器来渲染它们了,如下代码:

    function patch(n1, n2, container) {
        if (n1 && n1.type !== n2.type) {
            unmount(n1);
            n1 = null;
        }
        // 代码运行到这里,证明 n1 和 n2所描述的内容相同
        const { type } = n2;
        // 如果 n2.type的值是字符串类型,则它描述的是普通标签元素
        if (typeof type === 'string') {
            if (!n1) {
                mountElement(n2, container);
            } else {
                patchElement(n1, n2);
            }
        } else if (typeof type === 'Text') {
            // 如果新vnode的类型是Text,则它描述的是文本节点
            // 如果没有旧节点,则进行挂载
            if (!n1) {
                //使用 createTextNode 创建文本节点
                const el = n2.el = document.createTextNode(n2.children);
                // 将文本节点插入到容器中
                insert(el, container);
            } else {
                // 如果旧vnode存在,只需要用新文本节点的文本内容更新旧文本节点即可
                const el = n2.el = n1.el;
                if (n2.children !== n1.children) {
                    el.nodeValue = n2.children;
                }
            }
        } else if (type === 'xxx') {
            // 处理其他类型的vnode
        }
    }

可以看到 patch函数依赖浏览器平台特有的API,所以需要将 createTextsetText 抽离封装到渲染器选项中:

const renderer = createRenderer({
    createElement(tag) {
        return document.createElement(tag);
    },
    setElementText(el, text) {
        el.textContent = text;
    },
    insert(el, parent, anchor = null) {
        parent.insertBefore(el, anchor);
    },
    patchProps(el, key, prevValue, nextValue) {
        if (/^on/.test(key)) {
            let invokers = el._vei || (el._vei = {});
            let invoker = invokers[key];
            const name = key.slice(2).toLowerCase();
            if (nextValue) {
                if (!invoker) {
                    invoker = el._vei[key] = (e) => {
                        // e.timeStamp:事件发生的时间
                        // 如果事件发生的时间早于事件处理函数绑定的时间,则不执行事件处理函数
                        if (e.timeStamp < invoker.attached) return;
                        if (Array.isArray(invoker.value)) {
                            invoker.value.forEach(fn => fn(e));
                        } else {
                            invoker.value(e);
                        }
                    }
                    invoker.value = nextValue;
                    // 添加 invoker.attached属性,存储事件处理函数被绑定的时间
                    invoker.attached = performance.now();
                    el.addEventListener(name, invoker);
                } else {
                    invoker.value = nextValue;
                }
            } else if (invoker) {
                el.removeEventListener(name, invoker);
            }
        } else if (key === 'class') {
            el.className = nextValue || ''
        } else if (shouldSetAsProps(el, key, nextValue)) {
            const type = typeof el[key];
            if (type === 'boolean' && nextValue === '') {
                el[key] = true;
            } else {
                el[key] = nextValue;
            }
        }
        else {
            el.setAttribute(key, nextValue);
        }
    },
    createText(text) {
        return document.createTextNode(text);
    },
    setText(el, text) {
        el.nodeValue = text;
    }
})

此时的patch函数:

    function patch(n1, n2, container) {
        if (n1 && n1.type !== n2.type) {
            unmount(n1);
            n1 = null;
        }
        const { type } = n2;
        if (typeof type === 'string') {
            if (!n1) {
                mountElement(n2, container);
            } else {
                patchElement(n1, n2);
            }
        } else if (typeof type === 'Text') {
            if (!n1) {
                const el = n2.el = createTextNode(n2.children);
                insert(el, container);
            } else {
                const el = n2.el = n1.el;
                if (n2.children !== n1.children) {
                    setText(el, n2.children);
                }
            }
        } else if (type === 'xxx') {
            // 处理其他类型的vnode
        }
    }

注释节点的处理方式与文本节点的处理方式类似,不同在于注释节点是使用 document.createComment函数创建注释节点元素。

11.Fragment:

Fragment(片断)是Vue.js3中新增的一个vnode类型。在讨论实现之前,我们先来了解一下为什么需要它?来看场景,我们需要封装一组列表组件:

<List>
    <Items />
</List>

整体由两个组件构成,List组件渲染一个ul标签作为包裹层:

    <!-- List.vue -->
    <template>
      <ul>
        <slot />
      </ul>
    </template>

而Items组件负责渲染一组li列表:

    <!-- Items.vue -->
    <template>
      <li>1</li>
      <li>2</li>
      <li>3</li>
    </template>

这在 Vue.js2中是无法实现的。在Vue.js2中,组件的模板不允许存在多个根节点,这意味着:一个items组件最多只能渲染一个li标签。因此在Vue.js2中,我们通常需要配合 v-for指令来达到目的:

<List>
    <Items v-for="item in list" />
</List>

类似的组合还有 select标签和 option标签。

而Vue3中支持多根节点模板,所以不存在上述问题。那么Vue3如何用 vnode来描述多根节点模板?答案是使用 Fragment,如下代码:

const Fragment = Symbol();
const vnode = {
    type: Fragment,
    children: [
        { type: 'li', children: '1' },
        { type: 'li', children: '2' },
        { type: 'li', children: '3' }
    ]
}

与文本节点和注释节点类似,片段也没有所谓的标签名称,因此需要用到Symbol来创建唯一标识,即Fragment。有了它之后,就可以用它来描述 Items.vue组件的模板了。

对于如下模板对应的vnode则为:

<List>
    <Items />
</List>

const vnode = {
    type: 'ul',
    children: [
        {
            type: Fragment,
            children: [
                { type: 'li', children: '1' },
                { type: 'li', children: '2' },
                { type: 'li', children: '3' }
            ]
        }
    ]
}

当渲染器渲染 Fragment类型的虚拟节点时,由于它本身不会渲染任何内容,所以渲染器只会渲染它的子节点,如下代码:

    function patch(n1, n2, container) {
        if (n1 && n1.type !== n2.type) {
            unmount(n1);
            n1 = null;
        }
        const { type } = n2;
        if (typeof type === 'string') {
            if (!n1) {
                mountElement(n2, container);
            } else {
                patchElement(n1, n2);
            }
        } else if (typeof type === 'Text') {
            if (!n1) {
                const el = n2.el = createTextNode(n2.children);
                insert(el, container);
            } else {
                const el = n2.el = n1.el;
                if (n2.children !== n1.children) {
                    setText(el, n2.children);
                }
            }
        } else if (type === 'Fragment') {
            if (!n1) {
                // 如果旧vnode不存在,只需要将 Fragment的children逐个挂载即可
                n2.children.forEach(c => patch(null, c, container));
            } else {
                // 如果旧vnode存在,更新Fragment的children即可
                patchChildren(n1, n2, container);
            }
        }
    }

其实观察上面代码,可以发现渲染Fragment很简单。本质原因就是 渲染 Fragment与渲染普通元素的区别:它本身并不渲染任何内容,所以只需要处理它的子节点即可。

unmount也支持 Fragment类型的虚拟节点的卸载,故需要进行处理:

    function unmount(vnode) {
        // 卸载的vnode类型为Fragment时,则需要卸载其children
        if (vnode.type === Fragment) {
            vnode.children.forEach(c => unmount(c));
            return;
        }
        const parent = vnode.el.parentNode;
        if (parent) parent.removeChild(vnode.el);
    }

12.总结:

在前面,首先讨论了如何挂载子节点,以及节点的属性。节点的属性其实挺复杂的,涉及到的两个概念,HTML Attributes 和 DOM Properties,记住一个核心原则即可:HTML Attributes是用来设置与之对应的 DOM Properties的初始值的。

该如何正确设置元素属性,取决于被设置属性的特点,以及对特殊属性class的处理。

又该如何正确卸载,避免直接使用 innerHTML来清空容器元素。

还有vnode的类型,该如何区分,什么时候才有打补丁的必要。即使它们所描述的内容相同,也需要进一步检查它们的类型(即vnode.type),根据这个来判断它描述的具体内容是什么。(字符串/对象/文本/注释/片段)

后面就是如何处理事件以及更新时即的问题。

至此,这一章就结束了,后面将介绍渲染器的核心Diff算法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/86848.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

019 | 在线电影娱乐网站系统设计含论文 | 大学生毕业设计 | 极致技术工厂

作为一个在线电影娱乐网站系统&#xff0c;它展示给浏览者的是各种电影信息&#xff0c;把这些信息能够按用户的需要友好的展示出来是很重要的&#xff0c;同时&#xff0c;能够实现对这些信息的有条不紊的管理也是不可以忽视的。对浏览者和会员的功能而言叫做前台实现&#xf…

[附源码]Node.js计算机毕业设计电子购物商城Express

项目运行 环境配置&#xff1a; Node.js最新版 Vscode Mysql5.7 HBuilderXNavicat11Vue。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等等。 环境需要 1.运行环境&#xff1a;最好是Nodejs最新版&#xff0c;我…

[附源码]计算机毕业设计电商小程序Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; Springboot mybatis MavenVue等等组成&#xff0c;B/S模式…

【GRU时序预测】基于卷积神经网络结合门控循环单元CNN-GRU实现时间序列预测附matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

代码随想录训练营第49天|LeetCode 121. 买卖股票的最佳时机、122.买卖股票的最佳时机II

参考 代码随想录 题目一&#xff1a;LeetCode 121. 买卖股票的最佳时机 注意这个题只买卖一次&#xff01;&#xff01; 贪心 class Solution { public:int maxProfit(vector<int>& prices) {int low INT_MAX;int result 0;for(int i 0; i < prices.size(…

Redis框架(十):大众点评项目 订单功能 Redis实现全局唯一ID、 秒杀基本环境

大众点评项目 订单功能 秒杀基本环境需求&#xff1a;订单功能 秒杀基本环境Redis实现全局唯一ID业务实现代码总览总结SpringCloud章节复习已经过去&#xff0c;新的章节Redis开始了&#xff0c;这个章节中将会回顾Redis实战项目 大众点评 主要依照以下几个原则 基础实战的Dem…

揭秘!全球2022年Salesforce不同招聘职位的平均薪资

Salesforce可以说是发展最快的企业软件公司。此外&#xff0c;还一直被评选为全球最佳工作场所之一&#xff0c;2021年赢得了Glassdoor评选的最佳工作场所&#xff0c;并且在《财富》杂志的100家最佳工作公司中排名第四。除了非常重视员工福利&#xff0c;强调工作与生活的平衡…

在WSL中配置GPU环境

首先需要明确一点&#xff0c;虽然我们通过安装WSL获得了linux开发环境&#xff0c;但是我们最终使用的GPU还是在windows当中的&#xff0c;所以还是需要在系统中安装对应的驱动。 第一步&#xff1a;在window上根据显卡型号和版本安装驱动 这里参考之前的步骤就行 第二步&a…

HPC走出全新路线:《开放架构HPC技术与生态白皮书》注解

文|智能相对论 作者|叶远风 数字经济大时代&#xff0c;创新驱动大环境&#xff0c;HPC已不再是阳春白雪&#xff0c;而受到越来越多人的关注。 HPC&#xff0c;即High Performance Computing&#xff0c;高性能计算。对普通人来说&#xff0c;没有听过HPC&#xff0c;但肯定…

使用docker-compose搭建mysql主从复制

使用docker-compose搭建mysql主从复制&#xff1a; Mysql的主从【快速】搭建编写脚本编写 master.sh 脚本编写 slave.sh 脚本编写docker-compose.yml文本测试阶段主从同步效果主从环境重启容器被删除&#xff0c;重新启动从节点关闭&#xff0c;主节点继续写入数据从节点删除&a…

[附源码]计算机毕业设计的云网盘设计Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; Springboot mybatis MavenVue等等组成&#xff0c;B/S模式…

阻塞队列BlockingQueue与同步队列SynchronousQueue

阻塞队列&#xff08;BlockingQueue&#xff09; 什么情况下我们会使用阻塞队列&#xff1f; 多线程并发处理&#xff0c;线程池&#xff01; 使用队列 添加 移除 BlockingQueue四组API 方式有返回值&#xff0c;抛出异常 有返回值&#xff0c;不抛出异常 阻塞等待 超时等待 …

Kubelet v1.25.x源码——StatusManager

1. 环境说明 Kubernetes源码版本&#xff1a;remotes/origin/release-1.25 Kubernetes编译出来的Kubelet版本&#xff1a;Kubernetes v1.24.0-beta.0.2463ee7799bab469d7 Kubernetes集群实验环境&#xff1a;使用Kubernetes v1.25.4二进制的方式搭建了一个单节点集群 K8S 单节…

Linux redhat8.0 NFS共享目录

简介&#xff1a; NFS允许一个系统在网络上与它人共享目录和文件。通过使用NFS&#xff0c;用户和程序可以像访问本地文件一样访问远端系统上的文件。 作用&#xff1a; 多个机器共享一台CDROM或者其他设备。这对于在多台机器中安装软件来说更加便宜跟方便。 员工都可以操作…

流辰信息技术企业凭匠心,助力企业打造专属低代码解决方案!

从建立之初开始&#xff0c;流辰信息技术企业就凭借一股热劲和对低代码开发平台的执着热爱精神&#xff0c;在业界打磨升级&#xff0c;成为众多新老客户朋友青睐和信任的服务商。 在稳定发展的近些年中&#xff0c;流辰信息一直信奉品质和服务是紧跟市场发展步伐的法宝&#x…

maven工程,mybatis,spring

maven 简介&#xff1a;Maven主要用于解决导入依赖于Java类的jar和编译Java项目的主要问题&#xff0c;其使用pom文件将自动管理下载的jar包 创建maven工程 1. groupid:包名 artifactid:工程名 location:存放位置 2.结构 工程目录结构&#xff1a;srcmain主程序区java后端代…

ARM的八种工作模式

ARM有八种工作模式&#xff0c;有些处理器可能是七种&#xff0c;这个要看处理器的版本&#xff0c;早期的处理器如ARM9有七种工作模式&#xff0c;到了后来的Cortex系列新增了一种Secure Monitor模式。对工作模式的理解需要记住以下三点&#xff1a; 不同模式拥有不同的权限不…

基于冲突搜索算法的多机器人路径规划(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

Java Request学习笔记

1.获取请求部分的信息方法&#xff1a; Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {//设置响应的内容为html&#xff0c;指定编码为utf-8response.setContentType("text/html;…

如何利用ArcGIS探究环境与生态因子对水体、土壤、大气污染物等影响

如何利用ArcGIS实现电子地图可视化表达&#xff1f;如何利用ArcGIS分析空间数据&#xff1f;如何利用ArcGIS提升SCI论文的层次&#xff1f;制图是地理数据展现的直观形式&#xff0c;也是地理数据应用的必要基础。本次课程从ArcGIS的基本操作、ArcGIS 的空间数据分析及ArcGIS 的…