首页 > 其他分享 >vuejs设计与实现1-3

vuejs设计与实现1-3

时间:2024-07-23 15:10:19浏览次数:12  
标签:const 渲染 vuejs DOM vnode 实现 tag 设计 children

Vue

1. 权衡的艺术

2. 框架设计的核心要素

3. vue.js3设计思路

1. 权衡的艺术

  • 框架设计:在保持可维护性的同时让性能损失最小化;
命令式 VS 声明式
  1. 从范式上来看,视图层框架分为命令式和声明式。
  2. 命令式框架:关注过程,性能优; 声明式框架:关注结果,可维护性好
  3. 框架设计需要考虑可维护性和性能之间的平衡,在保持可维护性的同时让性能损失最小化;
  4. 对于框架来说,为了实现最优的更新性能,需要找到前后的差异并只更新变化的地方;但最终完成更新的代码仍是div.textContent = 'hello'
  5. 声明式代码会比命令式代码多出找出差异的性能消耗;最理想的情况是,当找出差异的性能消耗为0,声明式代码与命令式代码的性能相同,但无法超越,框架本身就是封装了命令式代码才实现面向用户的声明式。
  6. vuejs选择声明式设计方案的原因:声明式代码可维护性更强。在采用命令式代码时候,我们需要维护实现目标的整个过程,包括手动完成DOM元素的创建、更新、删除等工作;而声明式代码展示的是我们要的结果,看上去更直观,做事的过程vue内部实现。
  7. 声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗;

虚拟DOM
  • 虚拟DOM的出现,就是为了最小化找出差异的性能消耗,
innerHTML操作页面 VS 虚拟DOM操作页面
  1. 创建页面
  • (1) 通过innerHTML创建页面性能:HTML字符串拼接计算量 + innerHTML的DOM计算量;(首先将字符串解析为DOM树,DOM层面的计算,性能比JavaScript层面的计算性能差;)
const html = `<div><span>....</span></div>`;
div.innerHTML = html;
  • (2) 虚拟DOM创建页面性能:创建JavaScript对象的计算量 + 创建真实DOM的计算量;(第一步,创建JavaScript对象,即真实DOM的描述;第二步,递归地遍历虚拟DOM树并创建真实DOM;)
  1. 更新页面
  • (1) 使用innerHTML更新页面过程:1. 重新构建HTML字符串;2. 重新设置DOM元素的innerHTML属性(销毁所有的旧DOM元素,再重新创建新的DOM元素);
  • (2) 虚拟DOM更新更新页面过程:1. 重新渲染JavaScript对象(虚拟DOM树);2. 比较新旧虚拟DOM Diff,找到变化的元素并更新它;
  • ps:在更新页面时,虚拟DOM在JavaScript层面的运算要比创建页面时多出一个Diff的性能消耗,然而毕竟是JavaScript层面的运算,不会产生数量级的差异;再观察DOM层面的运算,可以发现虚拟DOM在更新页面时只会更新必要的元素,但innerHTML需要全量更新。
  • innerHTML、虚拟DOM及原生JavaScript(指createElement等方法)在更新页面时的性能对比:(1) innerHTML(模板),心智负担中等,性能差;(2) 虚拟DOM,心智负担小,可维护性强,性能不错; (3) 原生JavaScript,心智负担大(手动创建、删除、修改大量的DOM元素),可维护性差,性能高;

运行时和编译时
  1. 运行时框架,直接为Render函数提供了一个树形结构的数据对象,没有编译的过程,没办法分析用户提供的内容;
// 树形数据结构对象
const obj = {
    tag: 'div',
    children: [
        {
            tag: 'span',
            children: 'hello world'
        }
    ]
}

// 渲染函数
function Render(obj, root) {
    const el = document.createElement(obj.tag);
    if (typeof obj.children === 'string') {
        const text = document.createTextNode(obj.children);
        el.appendChild(text);
    } else if (obj.children) {
        // 数组,递归调用Render,使用el作为root元素
        obj.children.forEach((child) => Render(child, el))
    }

    // 将元素添加到root
    root.appendChild(el);
}

// 渲染到body下
Render(obj, document.body);
  1. 运行时+编译时
  • 怎么用类似HTML标签的方式描述树形结构呢?引入编译手段,把HTML标签编译成树形结构的数据对象,则可以继续使用Render函数
  • 编译过程,可以分析用户提供的内容,看看哪些内容未来会改变,哪些内容永远不会改变,就可以在编译时提取这些信息,然后将其传递给Render函数,Render函数得到这些信息就可以做进一步的优化;
const html = `
    <div>
        <span>hello world</span>
    </div>
`

// 调用compiler编译得到树形结构,把HTML字符串编译成数据对象
const obj = compiler(html)

// 调用render进行渲染
Render(obj, document.body)
  1. 编译时
  • 把HTML字符串编译成命令式代码
  • 纯编译时,不需要任何运行时,而是直接编译成可执行JavaScript代码,性能可能更好,但不灵活,用户提供的内容必须编译后才能用;
const div = document.createElement('div');
const span = document.createElement('span');
span.innerText = 'hello world';
div.appendChild(span);
document.body.appendChild(div);


2. 框架设计的核心要素

  1. 提升用户的开发体验,增加警告信息
  2. 控制框架代码的体积,开发环境为用户提供友好的警告信息
if (__DEV__) {
    console.log('1111');
}
  1. 框架Tree-Shaking
  • 消除那些永远不会被执行的代码;如果一个函数调用会产生副作用(当调用函数的时候会对外部产生影响如修改了全局变量),那么即使它没被执行也不会被移除; /#PURE/,作用就是告诉rollup.js,对于foo函数的调用不会产生副作用,可以对其进行Tree-Shaking;
import {foo} from "./utils";
/*#__PURE__*/ foo()
  • Tree-Shaking必须满足一个条件,即模块必须是ESM(ES Module),因为Tree-Shaking依赖ESM的静态结构;
  1. 框架应该输出什么样的构建产物
  • vue.js会为开发环境和生产环境输出不同的包,如vue.global.js用于开发环境,包含必要的警告信息,vue.global.prod.js用于生产环境;
  • vuejs还会根据使用场景的不同而输出其他形式的产物:(1) 在HTML页面中使用<script src="/vue.global.js">标签引入框架并使用,需要输出一种叫做IIFE格式的资源(立即调用的函数表达式);rollup中通过配置format:'iife'来输出这种形式的资源; (2) 直接引入ESM格式的资源<script type="module" src="/path/to/vue.esm-browser.js"></script>; (3) Node.js中引入vueconst vue = require('vue'), 服务端渲染,Nodejs环境中,资源的模块格式是cjs,rollup.config.js的配置format:'cjs'
// vue.esm-browser.js, vue.runtime.esm-bundler.js
// 带有-bundler字样的ESM资源是给rollup或webpack等打包工具使用的;-browser字样的ESM资源是直接给`<script type="module"></script>`使用的;const __DEV__ = process.env.NODE_ENV !== 'production'
// 在寻找资源时,如果pageage.json中存在module字段,会优先使用module字段指向的资源来代替main字段指向的资源。
{
    "main": "index.js",
    "module": "dist/vue.runtime.esm-bundler.js"
}
  1. 特性开关
  • 对于用户关闭的特性,可以利用Tree-Shaking机制让其不包含在最终的资源中;
  • 该特性为框架设计带来了灵活性,可以通过特性开关任意为框架添加新的特性;
  • 怎么实现特性开关呢?原理同__DEV__常量一样,本质上是利用rollup的预定义常量插件来实现__VUE__OPTIONS__API__
  1. 框架内置错误处理
// vuejs中可以注册统一的错误处理函数
import App from 'App.vue';
const app = createApp(App);
app.config.errorHandler = () => {
    // 错误处理程序
}
  1. 良好的TS支持
  • 使用TS编写框架和框架对TS类型支持友好是两件不同的事;


3. vue.js设计思路

  1. 声明式的描述UI
  • (1) 编写前端页面都涉及哪些内容:1. DOM元素:如div还是a;2. 属性:如a的href属性,id、class属性;3. 事件:如click、keydown;4. 元素的层级结构:如DOM树的层级结构,既有子节点也有父节点;
  • (2) 如何声明式地描述上面内推呢,vuejs解决方案:1. 使用与HTML标签一致的方式来描述DOM元素如<div id="app"></div>; 2. 使用与HTML标签一致的方式来描述属性,如<div id="app"></div>; 3. 使用:或v-bind来描述动态绑定的属性如<div :id="dynamicId"></div>; 4. 使用@或v-on来描述事件如点击事件<div @click="handler"></div>; 5. 使用与HTML标签一致的样式来描述层级结构;如<div><span></span></div>; ps: 除了使用模块来声明式地描述UI外,还能使用JavaScript对象来描述
const title = {
    tag: 'h1',
    props: {
        onClick: handler,
    },
    children: [
        {tag: 'span'}
    ]
}

// 等价于
<h1 @click="handler"><span></span></h1>

// h函数的返回值就是一个对象,其作用是让编写虚拟DOM更轻松
import {h} from "vue";
export default {
    render() {
        return h('h1', {onClick: handler})
    }
}

// 组件的渲染函数,一个组件要渲染的内容是通过渲染函数来描述的,即render函数,vuejs会根据组件的render函数的返回值拿到虚拟DOM,然后就可以把组件的内容渲染出来了;

export default {
    render() {
        return {
            tag: 'h1',
            props: {onClick: handler}
        }
    }
}

  1. 渲染器
  • 虚拟DOM:用JavaScript对象来描述真实的DOM结构
  • 渲染器:把虚拟DOM转为真实DOM;vuejs组件都是依赖渲染器来工作的;
const vnode = {
    tag: 'div',
    props: {
        onClick: () => alert('hello');
    },
    children: 'click me'
}
// vnode虚拟DOM对象;container一个真实DOM元素,作为挂载点,渲染器会把虚拟DOM渲染到该挂载点下;
function renderer(vnode, container) {
    // 创建元素
    const el = document.createElement(vnode.tag);
    // 为元素添加事件和属性
    for(const key in vnode.props) {
        if (/^on/.test(key)) {
            el.addEventListener(
                key.substr(2).toLowerCase(),
                vnode.props[key]
            )
        }
    }
// 处理children
    if (typeof node.children === 'string') {
        el.appendChild(document.createTextNode(vnode.children))
    } else if(Array.isArray(vnode.children)) {
        vnode.children.forEach(child => renderer(child, el))
    }

    container.appendChild(el)
}

renderer(vnode, document.body)
  • 上面还仅仅是创建节点,渲染器的精髓在于更新节点的阶段;对于渲染器来说,需要精确地找到vnode对象的变更点并且只更新变更点的内容;如下children变化,渲染器应该只更新元素的文本内容,而不需要再走一遍完整的创建元素的流程;归根结底,都是使用一些DOM API来完成渲染工作;
const vnode = {
    tag: 'div',
    props: {
        onClick: () => alert('hello')
    },
    children: 'click again'
}
  1. 组件的本质
  • 虚拟DOM除了能够描述真实DOM外,还能描述组件;
  • 组件是一组DOM元素的封装,这组DOM元素就是组件要渲染的内容;因此可以定义一个函数来代表组件,而函数的返回值就是组件要渲染的内容;
// 组件的返回值也是虚拟DOM,它代表组件要渲染的内容
const MyComp = function () {
    return {
        tag: 'div',
        props: {
            onClick: () => alert('hello')
        },
        children: 'click me'
    }
}

const vnode = {
    tag: MyComp, // 用来描述组件
}

// 渲染元素
function renderer(vnode, container) {
    if (typeof vnode.tag === 'string') {
        mountElement(vnode, container)
    // } else if (typeof vnode.tag === 'function') { // 组件是函数,返回虚拟DOM
    } else if (typeof vnode.tag === 'obejct') {  // 组件是对象
        mountComponent(vnode, container)
    }
}

function mountElement(vnode, container) {
    // 使用vnode.tag作为标签名创建DOM元素
    const el = document.createElement(vnode.tag);
    // 遍历vnode.props,将属性、事件添加到DOM元素
    for(const key in vnode.props) {
        if (/^on/.test(key)) {
            el.addEventListener(
                key.substr(2).toLowerCase(),
                vnode.props[key]
            )
        }
    }
// 处理children
    if (typeof children === 'string') {
        el.appendChild(document.createTextNode(vnode.children))
    } else if(Array.isArray(vnode.children)) {
        vnode.children.forEach(child => renderer(child, el))
    }
// 将元素添加到挂载节点下
    container.appendChild(el)
} 


// 渲染组件
function mountComponent(vnode, container) {
    // const subtree = vnode.tag; // 组件是函数,返回虚拟DOM
    const subtree = vnode.tag.render();  // 组件是对象,调用它的render函数得到组件要渲染的内容

    renderer(subtree, container)
}

  1. 模板的工作原理
  • 模板工作原理: 无论是使用模板还是手写渲染函数,对于一个组件来说,它要渲染的内容最终都是通过渲染函数产生的,然后渲染器再把渲染函数返回的虚拟DOM渲染为真实的DOM
  • 编译器:将模板编译为渲染函数;
  • template标签里面的内容就是模板内容,编译器会将模板内容编译成渲染函数并添加到script标签块的组件对象上
<template>
    <dov @click="handler">click me</dov>
</template>
// 最终浏览器里运行的代码
export default {
    data() {},
    methods: {
        handler: () => {}
    },
    render() {
        return h('div', {onClick:handler}, 'click me')
    }
}


// 无论是使用模板还是手写渲染函数,对于一个组件来说,它要渲染的内容最终都是通过渲染函数产生的,然后渲染器再把渲染函数返回的虚拟DOM渲染为真实的DOM,这就是模板工作原理,也是vuejs渲染页面的流程。

// 组件的实现依赖渲染器;模板的编译依赖于编译器;并且编译后生成的代码是根据渲染器和虚拟DOM设计决定的,因此vuejs的各个模块之间是相互关联、互相制约的,共同构成一个整体。

// 渲染器的作用:寻找并且只更新变化的内容;编译器的作用:分析动态内容,并且在编译阶段把这些信息提取出来,然后交给渲染器,则渲染器就不必花费大力气去寻找变更点了。
render() {
    return {
        tag: 'div',
        props: {
            id: 'foo',
            class: cls
        },
        patchFlags: 1, // 假设1代表是动态的,这样渲染器看到这个标志就知道这里属性会发生变化
    }
}






参考&感谢各路大神

1. vue.js设计与实现-霍春阳

标签:const,渲染,vuejs,DOM,vnode,实现,tag,设计,children
From: https://www.cnblogs.com/haimengqingyuan/p/18314513

相关文章

  • 三种语言实现归并排序(C++/Python/Java)
    题目给定你一个长度为......
  • C#知识|账号管理系统:修改登录密码界面的UI设计
    哈喽,你好啊!我是雷工!本节记录添加修改登录密码界面的过程,以下为练习笔记。01 效果演示演示跳转打开修改登录密码子窗体效果:02添加窗体在UI层添加一个Windows窗体,命名为:FrmModifyPwd.cs;03设置窗体属性按照下表的内容设置窗体的相关属性:设置属性属性值备......
  • 基于单片机和MATLAB的FIR滤波器设计
    摘要:随着社会经济的进步和科学技术的不断发展,数字滤波器的优势使其广泛应用于不同领域。但是,数字滤波器设计过程中的复杂性对其发展造成了制约。因此,笔者实现了基于单片机和MATLAB的滤波器的全新设计方法,并对设计后的滤波器进行仿真和实现验证。结果表明,此种方法所设计的......
  • IT实战课堂计算机毕业设计源码精品基于springboot的线上辅导班系统的开发与设计
    项目功能简介:《[含文档+PPT+源码等]精品基于springboot的线上辅导班系统的开发与设计[包运行成功]》该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程、包运行成功以及课程答疑与微信售后交流群、送查重系统不限次数免费查重等福利!软件开发环境及开......
  • Python中如何实现字符串的查询和替换?
    在Python中,字符串的查询和替换是编程中常见的任务,它们可以通过Python的内置方法和库来高效实现。这些操作对于文本处理、数据清洗、日志分析等场景尤为重要。下面,我将详细阐述如何在Python中实现字符串的查询和替换,包括基础方法、高级技巧以及在实际应用中的注意事项。字符......
  • 实现el-table行展开可以定位到指定行功能
    实现方法1.拿到每一行的高度,2.再拿到每一行展开行的高度3.累加起来,让滚动条滚动到对应的高度tableScrollToRow(tableElement,rowIndex){constexpandedRows=tableElement.bodyWrapper.querySelectorAll(".el-table__expanded-cell");consttheTableRows=......
  • 微信小程序 - 最新详细实现 “餐厅食堂外卖点餐“ 全屏左右联动菜单列表功能界面,附带
    前言如果您需要“简约通用”的左右联动功能,请访问这篇文章。在微信小程序开发中,详解实现仿饿了么、美团外卖用户点餐左右联动界面,全屏适配左边菜单分类右侧商品菜单列表数据,顶部是搜索栏可搜索定位对应锚点位置及商品,左侧导航菜单点击时右侧商品跟着变化,反之列表滑动......
  • 鸿蒙 使用 Refresh 实现下拉刷新
    importpromptActionfrom'@ohos.promptAction'@Entry@ComponentstructIndex{@Staterefreshing:boolean=false@Statelist:number[]=Array(20).fill(Date.now())@Buildercontent(){Stack(){Row(){LoadingPro......
  • 【Java常用设计模式】通俗易懂的玩转单例、建造者、工厂、策略模式(保姆篇)
    文章目录单例模式建造者模式工厂模式策略模式本篇小结更多相关内容可查看在一个狂风骤雨的下午,有人突然问了我一句,单例模式是什么,我愣了,相信看完这篇就不会愣了,本文以通俗易懂的方式写的,可能有不严谨的地方......
  • 硅纪元视角 | 类器官智能OI技术实现将人脑植入机器人
    在数字化浪潮的推动下,人工智能(AI)正成为塑造未来的关键力量。硅纪元视角栏目紧跟AI科技的最新发展,捕捉行业动态;提供深入的新闻解读,助您洞悉技术背后的逻辑;汇聚行业专家的见解,分享独到的视角和思考;精选对您有价值的信息,帮助您在AI时代中把握机遇。1分钟速览新闻  人......