Sphere
类用于创建三维球体对象,它提供了丰富的参数和方法来定制球体的外观和行为。

球体在制作三维动画时,具有广泛的应用场景。

比如:

  • 展示几何概念
    :通过创建不同大小、颜色和透明度的球体,可以直观地展示几何中的体积、表面积等概念
  • 物理模拟
    :在模拟物理现象(如重力、碰撞等)时,可以使用Sphere类来代表球体物体
  • 天文模拟
    :在展示天文现象(如行星运动、星系结构等)时,Sphere类可以用于创建星球、恒星等天体模型

1. 主要参数

Sphere
类的主要参数包括:

参数名称 类型 说明
center Point3D 球体的中心位置
radius float 球体的半径
resolution int 球体的细分程度。这个参数通常用于控制球体表面网格的密度或光滑程度。
u_range [float] 定义了球体在u方向上的参数化范围
v_range [float] 定义了球体在v方向上的参数化范围

resolution
参数设置较大时,会生成更精细、更光滑的球体表面,但也会增加计算量和内存使用。

2. 使用示例

Sphere
类的使用比较简单,下面的示例重点展示其参数的不同使用方式。

2.1. 基本球体

这个示例创建了一个位于原点、半径为1的基本球体。

s = Sphere(radius=1)

2.2. 位置和颜色

在这个示例中,创建了一个球体,并对其位置和颜色进行了自定义。

球心放置在三维空间中的特定点(2, -1, 1),并且球体被填充为红色。

s = Sphere(
    radius=1.5,
    center=np.array([2, -1, 1]),
)
s.set_color(RED)

2.3. 分辨率和透明度

此示例展示了如何调整球体的分辨率和透明度。

通过增加分辨率,球体表面看起来更加光滑;通过设置透明度,让球体呈现出透明效果,使得其后的物体或背景部分可见。

# 高分辨率,透明度高
s1 = Sphere(
    radius=0.5,
    resolution=(32, 32),
    center=np.array([-1, -1, 1]),
    fill_opacity=0.2,
)
s1.set_color(BLUE)

# 中分辨率,中等透明度
s2 = Sphere(
    radius=0.5,
    resolution=(8, 8),
    fill_opacity=0.6,
)
s2.set_color(YELLOW)

# 低分辨率,不透明
s3 = Sphere(
    radius=0.5,
    resolution=(4, 4),
    center=np.array([1, 1, -1]),
    fill_opacity=1,
)
s3.set_color(RED)

2.4. 球体动画

在这个示例中,不仅创建了一个球体,还为其添加了一系列动画效果。

球体首先以淡入效果出现,然后移动到三维空间中的另一个位置,接着绕一个轴旋转,最后进行缩放。

s = Sphere(
    radius=1,
    center=np.array([1, 0, -1]),
)
self.play(FadeIn(s))
self.play(s.animate.move_to(np.array([-1.5, -1, 1])))
self.play(s.animate.rotate(PI / 2, axis=OUT))
self.play(s.animate.scale(1.5))

3. 附件

文中的代码只是关键部分的截取,完整的代码共享在网盘中(
sphere.py
),

下载地址:
完整代码
(访问密码: 6872)

vue简介

vue中文官网

动态构建用户界面的渐进式 JavaScript 框架

vue的特点:

  1. 遵循MVVM模式
  2. 采用组件化模式,提高代码复用率,让代码更好维护
  3. 声明式编码,无需直接操作DOM,提高开发效率,编码简洁、体积小,运行效率高
  4. 本身只关注UI,也可以引入其他三方库开发项目
  5. 使用虚拟DOM+优秀的Diff算法,尽量复用DOM节点

环境准备

1.导入vue
  1. 在页面以CDN包的形式导入

    <!-- cdn链接 -->
    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
    
    <!--生产环境,推荐链接到一个明确的版本号和构建文件,以避免新版本造成的不可预期的破坏 -->
    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16"></script>
    
  2. 下载Js源代码文件通过script导入

    <script src="本地源代码路径"></script>
    
  3. npm


    在用 Vue 构建大型应用时推荐使用 npm 安装 。npm 能很好地和诸如webpack 或 Rollup 模块打包器配合使用


    # 最新稳定版
    npm install vue@next
    
  4. 脚手架工具(CLI)


    Vue 提供了一个官方的 CLI,为单页面应用 (SPA) 快速搭建繁杂的脚手架。它为现代前端工作流提供了功能齐备的构建设置。只需要几分钟的时间就可以运行起来,并带有热重载、保存时 lint 校验,以及生产环境可用的构建版本


    npm install -g @vue/cli
    
  5. Vite


    Vite 是尤雨溪新开发的一个 web 开发构建工具,由于其原生 ES 模块导入方式,可以实现闪电般的冷服务器启动,通过在终端中运行以下命令,可以使用 Vite 快速构建 Vue 项目


    npm init vite@latest <project-name> --template vue
    cd <project-name>
    npm install
    npm run dev
    
2.vue全局配置

Vue.config
是一个对象,包含 Vue 的全局配置,可以在启动应用之前修改

配置详解

    // 阻止 vue 在启动时生成生产提示
    Vue.config.productionTip = false

vue实例

  • 想让Vue工作,就必须创建一个Vue实例,且要传入一个配置对象
  • 容器里的代码依然符合html规范,只不过混入了一些特殊的Vue语法
  • 容器里的代码被称为Vue模板
  • Vue实例和容器是一一对应的,一个vue实例只能和一个实例对应
  • 真实开发中只有一个Vue实例,并且会配合着组件一起使用
  • {{插值语法}}中可以写js表达式(1+1、Data.now()),且表达式可以自动读取到data中的所有属性
<body>
<!--创建ID为root的容器 -->
<div id="root">

<!--  插值语法,关联创建的vue实例data对象中的name字段  -->
<h1>{{name}}</h1>
</div>


</body>
<script>
    // 阻止 vue 在启动时生成生产提示
    Vue.config.productionTip = false

    // 创建Vue实例,参数是一个配置对象
    new Vue({
        // el是element的缩写,el用于指定当前Vue实例为哪个容器服务,值通常为css选择器字符串
        // 也可以使用el:document.getElementById("root")
        // 关联ID是root的容器
        el:"#root",
        // data是用于存储数据,数据供el指定的容器去使用
        data:{
            name:"vue"
        }
    })


</script>

模版语法

Vue模板语法有2大类:

  1. 插值语法:
    功能:用于解析标签体内容。
    写法:{{xxx}},xxxjs表达式,且可以直接读取到data中的所有属性。

  2. 指令语法:
    功能:用于解析标签(标签属性、标签体内容、绑定事件等等)

    ​ 写法:Vue中有很多的指令且形式都是:v-xxxx:表达式, xxx是具体的指令,指令中同样写js表达式

<body>
<!--创建ID为root的容器 -->
<div id="root">

    <!--  插值语法-->
    <h1>{{name}}</h1>
    <!--指令语法-->
    <!--   v-bind表示绑定,将data的url绑定给href,使用v-bind之后href的""就不再是字符串,而是js表达式  -->
    <!--   等同于     <h2><a href="https://www.baidu.com">百度一下</a></h2>  -->
    <h2><a v-bind:href="url">百度一下</a></h2>
    <!--    v-bind简写形式,直接使用: -->
    <h2><a :href="url">百度一下简写</a></h2>


</div>


</body>
<script>

    new Vue({
        el: "#root",
        data: {
            name: "vue",
            url: "https://www.baidu.com"
        }
    })

</script>

数据绑定

Vue中有2种数据绑定的方式:

  1. 单向绑定(v-bind):数据只能从data流向页面。
  2. 双向绑定(v-model):数据不仅能从data流向页面,还可以从页面流向data。
    • 双向绑定一般都应用在表单类元素(输入类元素)
    • v-model:value 可以简写为 v-model,因为v-model默认收集的就是value值
<body>
<div id="root">

    单向数据绑定: <input type="text" v-bind:value="name">
    单向数据绑定简写: <input type="text" :value="name">
    <br>
    双向数据绑定: <input type="text" v-model:value="info.category">
    <br>
    双向数据绑定简写: <input type="text" v-model="info.category">
 
<!--    双向数据绑定-有一处地方数据发生变化,所有引用的地方都会发生变化。
        当数据模型中的数据发生变化时,与之绑定的视图会自动更新以反映新的数据状态;
        反之,当用户在视图层通过交互操作(如在输入框中输入内容、选择下拉菜单选项等)改变了数据,数据模型也会相应地更新-->
    
</div>
</body>

<script type="text/javascript">
    Vue.config.productionTip = false

    new Vue({
        el:'#root',
        data:{
            name:'vue',
            info:{
                category:"分类"

            }
        }
    })
</script>

vue实例创建的两种写法

  1. 第一种方式
	new Vue({
      // 第一种写法:创建实例的时候直接挂载对应的容器
			el:'#root',
      // 第一种写法:对象式
			data:{
				name:'vue'
			}
		})
  1. 第二种方式
    // 创建Vue实例
    const vm = new Vue({
        // 第二种方式:函数式,必须返回一个对象
        data:function (){
            // 此处的this是Vue实例对象
            return{name:"vue"}
        }
    })
    // 通过$mount方法绑定容器,$mount是vue原型上的方法,作用是将vue挂载指定容器
    vm.$mount("#root")

  • el使用哪种方式都可以,data的创建后续使用组件的时候,必须使用函数式
  • 由Vue管理的函数,一定不要写箭头函数,一旦写了箭头函数,this就不再是Vue实例,而是Window

MVVM模型

  1. M:模型(Model) :data中的数据
  2. V:视图(View) :模板代码
  3. VM:视图模型(ViewModel):Vue实例
    • data中所有的属性,最后都出现在了vm身上。
    • vm身上所有的属性 及 Vue原型上所有属性,在Vue模板中都可以直接使用

Js的Object.defineProperty

用于在一个对象上定义一个新属性,或者修改一个已经存在的属性。它允许精确地添加或修改对象的属性,包括属性的值、可枚举性、可配置性和可读写性等

// 创建一个对象
let user = {
    name: "test",
    age: 18
}


// 使用:Object.defineProperty(obj, prop, descriptor)
// 参数
//   obj:是要定义属性的目标对象。
//   prop:是要在目标对象上定义或修改的属性的名称。
//   descriptor:是一个对象,用于描述属性的各种特征。它可以包含以下键值对:
//              value:属性的值。例如,{ value: 42 }会将属性的值设置为 42。
//              writable:一个布尔值,表示属性是否可写。默认为false,则不能重新赋值。例如,{ writable: false, value: 'immutable' }定义了一个不可写的属性。
//              enumerable:一个布尔值,表示属性是否可枚举。默认为false,在使用for...in循环或者Object.keys()方法等列举对象属性时,该属性不会被列出
//              configurable:一个布尔值,表示属性是否可配置。默认为false,则不能删除该属性,并且除了value和writable之外的其他属性描述符(如enumerable和configurable)也不能被修改
//
//              getter和setter方法,读和写,和上述部分属性是互斥的


// 向user对象中添加一个num属性,值是20
Object.defineProperty(user, "num", {
    value: 20,
    writable: true,
    enumerable: true,
    configurable: true,

})






let test = 10
// 向user对象中添加一个test属性
Object.defineProperty(user, "test", {

    // value/writable与get/set互斥
    // 当使用Object.defineProperty定义属性时,不能同时指定value(或writable)和get/set。
    // 原因是它们代表了两种不同的属性定义模式。value和writable用于定义一个简单的数据属性,其值是直接存储的,读取和修改操作是基本的赋值和取值
    // 而get和set定义的是访问器属性,通过自定义函数来控制属性的读取和写入逻辑,属性值可能存储在其他地方,不是简单地通过value来存储


    configurable: true,

    // 当读取user的test的属性的时候get(getter)函数就会被调用,返回值是num的值
     get: function () {
        console.log("test属性被调用")
        return test
    },
    // 当修改user的test的属性的时候set(setter)函数就会被调用,且会收到修改的值
     set: function (value) {
        console.log(`test属性被修改,值是${value}`)
        test = value
    }


})


数据代理

数据代理:通过一个对象代理对另一个对象中属性的操作(读/写)

数据代理概念
let user1 = {num:100}
let user2  = {}

// 向user2中添加一个num属性
Object.defineProperty(user2,"num",{
    // 读取user2的num属性的时候,返回的是user1的num
    get() {
        return user1.num
    },

    // 修改user2的num属性的时候,修改的是user1的num
    set(value){
        user1.num = value
    }
})
vue中的数据代理
  1. Vue中的数据代理:
    通过vm对象来代理data对象中属性的操作(读/写)
  2. Vue中数据代理的好处:
    更加方便的操作data中的数据
  3. 基本原理:
    通过Object.defineProperty()把data对象中所有属性添加到vm上。
    为每一个添加到vm上的属性,都指定一个getter/setter。
    在getter/setter内部去操作(读/写)data中对应的属性
  1. 原理示例
// 1. 创建vue实例的时候,data对象是存储在_data属性中
// 2. 使用Object.defineProperty向vue中添加_data对象中的属性,定义getter和setter方法
// 		定义的getter和setter方法,返回和修改的是_data中对应的属性,所以我们可以通过vue实例对象访问代理数据


// 模拟vue的代理机制
// 模拟Vue实例(这里简化为一个普通对象来理解原理),实际的_data实现了数据劫持
const vm = {
    // 用于存储数据的属性,模拟Vue的_data属性
    _data: {},

    // 构造函数,模拟Vue实例创建过程
    constructor() {
        // 将传入的数据对象赋值给_data属性,类似Vue内部的处理
        this._data = {...dataObject };

        // 遍历_data中的所有属性,为它们创建数据代理
        for (let key in this._data) {
            Object.defineProperty(this, key, {
                get() {
                    return this._data[key];
                },
                set(v) {
                    this._data[key] = v;
                }
            });
        }
    }
}
  1. 使用代理数据
// 结合vue实现数据代理的原理,我们可以使用下面两种方式访问和修改数据
    const vm = new Vue({
        el: "#root",
        data: {name: "vue"}
    })

		// 通过_data对象
    console.log(vm._data.name)
		// 
    console.log(vm.name)

事件处理

  1. 使用v-on:xxx 或 @xxx 绑定事件,其中xxx是事件名;
  2. 事件的回调需要配置Vue选项参数的methods对象中,最终会在vm上;
  3. methods中配置的函数,不要用箭头函数!否则this就不是vm了;
  4. methods中配置的函数,都是被Vue所管理的函数,this的指向是vm 或 组件实例对象;
基本使用
<div id="root">

    <!--    绑定点击事件-方式一-->
    <button v-on:click="info">显示信息</button>
    <!--    绑定点击事件-方式二-->
    <button @click="info">显示信息2</button>
    <!--    绑定点击事件-传参的回调函数 使用$event传递event对象,如果不需要使用event,可以不传,只传自定义参数-->
    <button @click="info2($event,66666)">显示信息3</button>
  
   <!-- 回调函数可以不传event对象,然后在函数内部可以直接使用-->
    <button @click="info3">显示信息4</button>

</div>
</body>

<script type="text/javascript">
    const vm = new Vue({
        el: "#root",
        data: {name: "vue"},
        // 配置回调方法,回调方法最终也是存储在vm上,没有做数据代理
        methods: {
            // 回调方法
            info() {
                alert("hello world")
            },
            
            info2(event,number){
                console.log(this) // 此处的this是vm,如果使用箭头函数this是Window
                alert(event.target + number.toString())
            },
            info3(e){
                console.log(e.target)
                
            }
          
        }
    })


</script>
事件修饰符
  • prevent:阻止默认事件
  • stop:阻止事件冒泡
  • once:事件只触发一次
  • capture:使用事件的捕获模式
  • self:只有event.target是当前操作的元素时才触发事件
  • passive:事件的默认行为立即执行,无需等待事件回调执行完毕
    <!--    a标签配置点击的事件回调-触发info方法-点击事件的行为设置了prevent, -->
    <!--    prevent-阻止默认事件,即点击超链接不会触发跳转效果 -->
    <a href="https://www.baidu.com" @click.prevent="info">百度一下</a>
    <a href="https://www.baidu.com" v-on:click.prevent="info">百度一下2</a>


    <!--   一个div绑定了点击事件,内部有一个按钮绑定了点击事件,会触发事件冒泡 -->
    <div @click="info" style="width: 100px;height: 100px;background-color: yellow">
        <!--    stop-阻止事件冒泡-->
        <button @click.stop="info">点点</button>
    </div>

    <!--    按钮绑定了点击事件-->
    <!--    once-事件只会触发一次 -->
    <button @click.once="info">只有一次</button>


    <!--    div绑定了点击事件-->
    <!--使用事件的捕获模式,捕获阶段触发事件-->
    <div @click.capture="info" style="width: 100px;height: 100px;background-color: yellow">
        <div @click style="width: 50px;height: 50px;background-color: gold"></div>

    </div>


    <!--     只有event.target是当前操作的元素时,才触发事件-->
    <div @click.self="info" style="width: 100px;height: 100px;background-color: yellow">
        <button>按钮</button>
    </div>

    <!--    事件触发的时候立刻触发默认行为,不会等事件回调执行完 -->
    <div style="height: 200px; overflow: auto" @scroll.passive="handleScroll">
键盘事件

键盘事件有:

  • keyup:按下键盘上的一个键并松开时触发

  • keydown:用户按下键盘上的键时触发

  • keypress:按下一个可打印字符(如字母、数字、标点符号等)的键并松开后触发

基本使用
<body>  
<!--    键盘事件-打印按下的按键-->
    <input type="text" @keyup="info">
</body>
<script type="text/javascript">
    const vm = new Vue({
        el: "#root",
        data: {name: "vue"},
        // 配置回调方法
        methods: {
            // 回调方法
            info(e) {
                console.log(e.target.value)
            },

        }
    })


</script>
键盘的按键别名
 <!--    vue常用的按键别名 -->
    <!--    回车 => enter-->
    <!--    删除 => delete (捕获“删除”和“退格”键)-->
    <!--    退出 => esc-->
    <!--    空格 => space-->
    <!--    换行 => tab (特殊,必须配合keydown去使用)-->
    <!--    上 => up-->
    <!--    下 => down-->
    <!--    左 => left-->
    <!--    右 => right-->
    <!--    键盘事件-按下按键后不会触发,需要按下回车键的时候才触发-->
    <input type="text" @keyup.enter="info">
    <!--也可以通过键码触发对应的按键-不推荐-已弃用 -->
    <input type="text" @keyup.20="info">

通过按键名字触发事件

<div id="root">



    <!--    Vue未提供别名的按键,可以使用按键原始的key值去绑定-->
    <!--    在回调中通过event.target.key 和event.target.key分别获取按键名字和按键编码 -->
    <!--    可以通过按键名字(event.key)获取键名,如果是大驼峰的名字,需要转换小写每个单词-隔开-->
    <!--按下大小写的按键的时候触发- CapsLock === caps-lock -->
    <input type="text" @keyup.caps-lock="info">

</div>
</body>

<script type="text/javascript">
    const vm = new Vue({
        el: "#root",
        data: {name: "vue"},
        // 配置回调方法
        methods: {
            // 回调方法
            info(e) {
                console.log(e.key) // 键名  CapsLock
                console.log(e.keyCode) // 键码 20
            },

        }
    })
自定义定制键名

Vue.config.keyCodes.自定义键名 = 键码,可以去定制按键别名

   // 设置大小写的别名为daxiaoxie
    Vue.config.keyCodes.daxiaoxie = 20

    const vm = new Vue({
        el: "#root",
        data: {name: "vue"},
        // 配置回调方法
        methods: {
            // 回调方法
            info(e) {
                console.log(e.key) // 键名  CapsLock
                console.log(e.keyCode) // 键码 20
            },

        }
    })
键盘的系统修饰键

系统修饰键(用法特殊):ctrl、alt、shift、meta
(1).配合keyup使用:按下修饰键的同时,再按下其他键,随后释放其他键,事件才被触发。
(2).配合keydown使用:正常触发事件

计算属性

  • 定义:要用的属性不存在,要通过已有属性计算得来,即通过已有的属性计算生成一个新属性
  • 原理:底层借助了Objcet.defineproperty方法提供的getter和setter
  • 优势:与methods实现相比,内部有缓存机制(复用),效率更高,调试方便
  • 计算属性最终会出现在vm上,直接读取使用即可
  • 如果计算属性要被修改,那必须写set函数去响应修改,且set中要引起计算时依赖的数据发生改变
基本使用
<body>
<div id="root">

姓氏:<input type="text" v-model="firstName">
    <br>
名字:<input type="text" v-model="lastName">
    <br>
全名: <span>{{allName}}</span>



</div>
</body>

<script type="text/javascript">

    const vm = new Vue({
        el: "#root",
        data: {firstName: "vue",lastName:"html"},
        methods:{},
        // 配置计算属性
        computed:{
            // 定义一个allName计算属性
            allName:{
                // 当有人读取allName时,get就会被调用,返回值作为allName的返回值
                // 被调用时机:1.初次读取allName的时候 2.依赖的数据发生变化的时候
               // 初次计算后会将数据缓存,后续调用不用重复计算,除非有依赖数据发生变化
                get(){
                    // this是vm
                    return this.firstName + "-" + this.lastName
                },
                // set在allName被修改的时候调用
                set(v){
                    // abc-bcd格式的字符串,通过-切割
                    const arr = v.split("-")
                    // 将切割的两个值分别赋值给两个属性,然后计算属性联动变化
                    this.firstName = arr[0]
                    this.lastName = arr[1]

                 }

            }

        }

    })
简写方式

如果计算属性只读取,不修改的话,可以简写

<body>
<div id="root">

    姓氏:<input type="text" v-model="firstName">
    <br>
    名字:<input type="text" v-model="lastName">
    <br>
    全名: <span>{{allName}}</span>


</div>
</body>

<script type="text/javascript">

    const vm = new Vue({
        el: "#root",
        data: {firstName: "vue", lastName: "html"},
        methods: {},
        // 配置计算属性
        computed: {
            // 定义属性以函数的形式,该函数就是getter的作用
            allName() {
                // 直接返回计算后的属性
                return this.firstName + "-" + this.lastName
            }

        }
    })


</script>

监视属性

  • 当被监视的属性变化时, 回调函数自动调用, 进行相关操作
  • 监视的属性必须存在,才能进行监视
  • 监视的两种写法:
    1. new Vue时传入watch配置
    2. 通过vm.$watch监视
基本使用
  1. 第一种写法

        const vm = new Vue({
            el: "#root",
            data: {firstName: "vue", lastName: "html"},
            methods: {},
            // 配置计算属性
            computed: {
                 allName() {
                    return this.firstName + "-" + this.lastName
                }
            },
    
            // 配置监视属性
            watch:{
                // 要监视的属性名字
                firstName:{
                    // 初始化的时候是否让handler调用一次,默认值是false
                    immediate: true,
                    // 当监视的属性发生变化的时候,handler调用
                    handler(newValue,oldValue){
                        console.log(`firstName被修改了`,newValue,oldValue)
                    }
    
                }
            }
    
        })
    
  2. 第二种写法

      // 配置监视属性
        // 要监视的属性名字,配置对象
        vm.$watch("firstName",{
            immediate: true,
             handler(newValue,oldValue){
                console.log(`firstName被修改了`,newValue,oldValue)
            }
    
        })
    
深度监视
  • Vue中的watch默认不监测对象内部值的改变,只监视一层
  • 配置deep:true可以监测对象内部值改变,监视多层
  • Vue自身可以监测对象内部值的改变,但Vue提供的watch默认不可以
  • 使用watch时根据数据的具体结构,决定是否采用深度监视
  • 当被监视的对象是一个复杂对象(如包含嵌套属性的对象)时,默认情况下,watch只会监视对象的引用是否发生变化。而深度监视则会递归地监视对象内部的每一个属性,一旦内部属性发生变化,就会触发监听器的回调函数
const vm = new Vue({
        el: "#root",
        data: {
            info: {
                a: 1,
                b: 2
            }
        },
        methods: {},
        // 配置计算属性
        computed: {},
        // 监视属性
        watch:{
            // 监视整个info,如果不深度监视,只会监视info对应的对象地址值引用有没有发生变化,而不会具体到里面的某个值
            info:{
                // 开启深度监视-监视多层结构中每个属性的变化
                deep:true,
                handler(){

                    console.log("info-",this.info)
                }
            },

            // 监视多级结构中某个属性的变化-只监视info里面的a属性
            // 对象的key是字符串,所以需要写成字符串形式
            'info.a':{
                handler() {
                    console.log("a",this.info.a)
                }
            }

        }
    })

监视属性简写

如果不需要深度监视和初始化时调用,可以简写

  // 监视属性
        watch:{
            // 监视info,以函数的形式,函数相当于handler
           info(newV,oldV){
               console.log(newV,oldV)
           }
        }
  // 要监视的属性,回调函数,不允许写箭头函数
    vm.$watch("info",function (newV,oldV) {
        console.log(newV,oldV)

    })
计算属性和监视属性对比

computed和watch之间的区别:

  1. computed能完成的功能,watch都可以完成
  2. watch能完成的功能,computed不一定能完成,例如:watch可以进行异步操作
  3. 两个重要的小原则:
    • 所被Vue管理的函数,最好写成普通函数,这样this的指向才是vm 或 组件实例对象。
    • 所有不被Vue所管理的函数(定时器的回调函数、ajax的回调函数等、Promise的回调函数,最好写成箭头函数这样this的指向才是vm 或 组件实例对象

绑定样式

  • 在应用界面中, 某些元素的样式是变化的

  • class/style 绑定就是专门用来实现动态样式效果的技术

class绑定

使用:class进行绑定

<style>
    .box {
        width: 100px;
        height: 100px;
    }


    .box-color {
        background-color: yellow;

    }

    .box-border {
        border: 1px solid black;

    }


</style>

<body>
<div id="root">
    <!-- 绑定class样式,ex是一个字符串,适用于样式不确定、需要动态指定-->
    <!--    最终class和:class的对应类会拼成一个class 最终是 class="box box-color "-->
    <div class="box" :class="ex">box</div>

    <!--    绑定class样式,exArr是一个数组,适用于要使用的样式个数不确定、名字也不确定-->
    <div class="box" :class="exArr">box</div>

    <!--    绑定class样式,exObj是一个对象,适用于要绑定的样式个数确定、名字也确定,但要动态决定用不用-->
    <div  :class="exObj">box</div>


</div>
</body>

<script type="text/javascript">

    const vm = new Vue({
        el: "#root",
        data: {
            ex: "box-color",
            exArr: ["box-color", "box-border"],
            exObj:{
                box:true,  // 根据true、false决定是否启动该样式
                // 有特殊字符的key值,使用下面两种写法都可以
                ["box-color"]:true,
                "box-border":true
            }


        }
    })

 </script>
Style绑定
<body>
<div id="root">
    <!-- 绑定style样式--对象写法,单个动态样式 -->
    <div :style="styleObj">box</div>
    <!-- 绑定style样式--数组写法,多个动态样式 -->
    <div :style="styleArr">box</div>




</div>
</body>

<script type="text/javascript">

    const vm = new Vue({
        el: "#root",
        data: {
            styleObj:{
                width:"100px",
                height:"100px",
            },
            styleArr: [
                {
                    width:"100px",
                    height:"100px",
                },
                {
                    // background-color 这种属性 改为小驼峰写法
                    // 也可以使用字符串key   "background-color"
                   backgroundColor:"yellow"
                }
            ]


        }
    })

 </script>

条件渲染

v-show
<div id="root">

    <!--    v-show可以控制显示、隐藏元素 -->
    <!--    v-show并不会删除,只是页面不显示,相当于操作display: none;-->
    <!--    适用于切换频率较高的场景-->
    

    <!--  布尔值,true显示,false隐藏-->
    <h1 v-show="false">title1</h1>
    <!--也可以写表达式值是布尔值-->
    <h1 v-show="2>1">title2</h1>
    <!--   从vue data动态设置 -->
    <h1 v-show="showIf">title3</h1>


</div>
</body>

<script type="text/javascript">

    const vm = new Vue({
        el: "#root",
        data: {
            showIf: true
        }
    })

</script>
v-if
<body>
<div id="root">

    <!--    v-if会删除元素,而不是页面隐藏,适用于:切换频率较低的场景-->


    <!--  布尔值,true显示,false隐藏-->
    <h1 v-if="false">title1</h1>
    <!--也可以写表达式值是布尔值-->
    <h1 v-if="2>1">title2</h1>
    <!--   从vue data动态设置 -->
    <h1 v-if="showIf">title3</h1>
    

    <!--    v-if可以和v-else-if、v-else一起使用,但要求结构不能被打断-->
    <!--    如果 1和"1" 相等 显示title4 -->
    <h1 v-if="num===1">title4</h1>
    <!--    如果1大于2 显示title5-->
    <h1 v-else-if="num>2">title5</h1>
    <!--    否则显示title6,,4和5条件都不成立-->
    <h1 v-else>title6</h1>

    <!--  等同于
        if(num===1){

        }else if(num>2){

        }else {

        }
        -->

</div>
</body>

<script type="text/javascript">


    const vm = new Vue({
        el: "#root",
        data: {
            showIf: true,
            num:10
        }
    })

</script>
			<!-- v-if只能和template的配合使用,不能和v-show配合使用 -->
			<template v-if="num === 20">
				<h2>1</h2>
				<h2>2</h2>
				<h2>3</h2>
			</template>
  • template用于定义一个模板片段,在这个模板片段中的内容在默认情况下不会被直接渲染到页面上,而是作为一种 “模板” 存储起来,以便在需要的时候通过 JavaScript 进行克隆或者其他操作来使用其中的内容。
  • 例如,在构建一个动态列表时,可以将列表项的模板定义在
    <template>
    标签中,然后根据数据动态地克隆和填充这个模板

列表渲染

基本渲染
<body>
<div id="root">

    <!--    遍历数组-->
    <ul>
        <!--    等同于  for (let u of users)-->
        <!--        有多少条数据会填充多少条li-->
        <!--         也可以使用 u in users-->
        <!--         :key用来指定这个li节点的唯一标识 -->
        <li v-for="u of users" :key="u.id">
            {{u.name}}---{{u.tag}}
        </li>

    </ul>
    <ul>
        <!--index是下标-->
        <li v-for="(u,index) of users" :key="u.id">
            {{u.name}}---{{u.tag}}---{{index}}
        </li>
    </ul>


    <!--    遍历对象 -->
    <ul>
        <li v-for="(value,key) of info" :key="key">
            {{key}} -- {{value}}
        </li>
    </ul>

    <!--    遍历字符串 -->
    <ul>
        <li v-for="(char,index) of strFor" :key="index">
            {{char}} -- {{index}}
        </li>
    </ul>
    <!-- 遍历指定次数 -->
    <ul>
        <!--        遍历5次,number是从1开始的值,index是下标 -->
        <li v-for="(number,index) of 5" :key="index">
            {{number}}--{{index}}
        </li>
    </ul>
</div>
</body>

<script type="text/javascript">


    const vm = new Vue({
        el: "#root",
        data: {
            users: [
                {id: 1, name: "vue", "tag": "js"},
                {id: 2, name: "python", "tag": "py"},
                {id: 3, name: "golang", "tag": "go"},
            ],
            info: {
                name: "vue",
                count: 10,
                price: 1000

            },
            strFor: "helloworld"
        }
    })

</script>
v-for key的原理和作用

虚拟DOM中key的作用:

key是虚拟DOM对象的标识,当数据发生变化时,Vue会根据【新数据】生成【新的虚拟DOM,随后Vue进行【新虚拟DOM】与【旧虚拟DOM】的差异比较,比较规则如下:

  1. 旧虚拟DOM中找到了与新虚拟DOM相同的key:


    • 若虚拟DOM中内容没变, 直接使用之前的真实DOM!

    • 若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM。

  2. 旧虚拟DOM中未找到与新虚拟DOM相同的key


    • 创建新的真实DOM,随后渲染到到页面


用index作为key可能会引发的问题:

  1. 若对数据进行:逆序添加、逆序删除等破坏顺序操作:
    会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。

  2. 如果结构中还包含输入类的DOM:
    会产生错误DOM更新 ==> 界面有问题

image-20241114145515446

image-20241114145821110


开发中如何选择key:

  1. 最好使用每条数据的唯一标识作为key, 比如id、手机号等唯一值
  2. 如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没问题的
  3. 如果不指定:key,vue默认是使用下标作为key的
列表过滤
使用watch实现
<body>
<div id="root">

    <!--    双向绑定过滤关键字-->
    <input type="text" v-model="keyWord">
    <ul>
        <!--       遍历模糊搜索过滤的数组 -->
        <li v-for="u of filUsers" :key="u.id">
            {{u.name}}---{{u.tag}}
        </li>

    </ul>
</div>
</body>

<script type="text/javascript">


    const vm = new Vue({
        el: "#root",
        data: {
            // 过滤的关键字
            keyWord: "",
            users: [
                {id: 1, name: "vue", "tag": "html", "price": 30},
                {id: 2, name: "python", "tag": "server", "price": 40},
                {id: 3, name: "golang", "tag": "server", "price": 50},
            ],
            // 定义一个新数组,用于存储过滤后的数据
            filUsers: []
        },
        // 配置监视属性
        watch: {
            keyWord:{
                // 初始化时调用一下,过滤空串,初显全部数据
                immediate: true,
                handler(newValue) {
                    // 从users的tag过滤 包含newValue的所有数据,重新赋值给this.users
                    this.filUsers = this.users.filter((p) => {
                        // 返回tag包含newValue的数据
                        // indexOf 不包含 返回-1 空串 返回0
                        return p.tag.indexOf(newValue) !== -1
                    })

                }

            }

        }
    })

</script>
用computed实现
    const vm = new Vue({
        el: "#root",
        data: {
            // 过滤的关键字
            keyWord: "",
            users: [
                {id: 1, name: "vue", "tag": "html", "price": 30},
                {id: 2, name: "python", "tag": "server", "price": 40},
                {id: 3, name: "golang", "tag": "server", "price": 50},
            ],

        },
        // 配置计算属性
        computed:{
            // 计算属性依赖keyword,keyword发生变化的时候,触发计算出新的数组
            filUsers(){
                return this.users.filter((p)=>{
                    return p.tag.indexOf(this.keyWord) !== -1
                })
            }
        }
    })
列表排序
<body>
<div id="root">

    <!--    双向绑定过滤关键字-->
    <input type="text" v-model="keyWord">
		<!-- 按钮点击的时候 修改sortType的值 -->
    <button @click="sortType = 2">price 升序</button>
    <button @click="sortType = 1">price 降序</button>
    <button @click="sortType = 0">原顺序</button>

    <ul>
        <!--       遍历模糊搜索过滤的数组 -->
        <li v-for="u of filUsers" :key="u.id">
            {{u.name}}---{{u.tag}}--{{u.price}}
        </li>

    </ul>
</div>
</body>

<script type="text/javascript">


    const vm = new Vue({
        el: "#root",
        data: {
            // 过滤的关键字
            keyWord: "",
            // 定义一个关键字,0表示原顺序  1表示降序 2表示升序
            sortType: 0,
            users: [
                {id: 1, name: "vue", "tag": "html", "price": 30},
                {id: 2, name: "python", "tag": "server", "price": 40},
                {id: 3, name: "golang", "tag": "server", "price": 50},
            ],

        },
        // 配置计算属性
        computed: {
            // 计算属性依赖keyword,keyword发生变化的时候,触发计算出新的数组
            filUsers() {
                // 定义一个数组接收过滤后的数据
                const arr = this.users.filter((p) => {
                    return p.tag.indexOf(this.keyWord) !== -1
                })
                // 先过滤 再排序
                // 判断是否需要排序
                if (this.sortType) {
                    arr.sort((a, b) => {
                        // 如果type===1 降序 否则升序
                        return this.sortType === 1 ? b.price - a.price : a.price - b.price
                    })
                }
                // 返回排序后的数组
                return arr
            }
        }
    })

</script>

Vue监测数据原理

Vue监测对象
  1. Vue监测对象的原理


    当创建一个 Vue 实例时,Vue 会遍历
    data
    选项中的所有属性。对于每个属性,它使用
    Object.defineProperty
    方法来进行数据劫持。这个方法允许 Vue 重新定义属性的
    get

    set
    访问器


    vm的数据代理,data数据给
    _data
    前,先将数据加工,然后再赋值给_data,更好的实现响应式,对象中每个属性都有对应的getter和setter


    // 模拟实现一个vm的数据监测
    
        function  Observer(obj){
            // 获取对象中所有的属性,生成一个数组
            const keys = Object.keys(obj)
    
            // 遍历所有属性
            keys.forEach((k)=>{
                // 将对应的属性添加到当前实例身上
                Object.defineProperty(this,k,{
                    get(){
                        // 返回对应的值
                        return obj[k]
                    },
                    set(v){
                        // 数据被修改,去解析模版,生成虚拟DOM比对,更新页面
                        obj[k] = v
                    }
    
                })
    
            })
    
        }
    
    
    let data = {
            name:"vue",
            tag:"js"
        }
    
        // 创建一个监视的实例对象,用于监视data中属性的变化
        const  obs = new Observer(data)
        
        // 准备一个的vm实例对象
        let vm = {}
        vm._data = data = obs
        // 可以通过vm._data去获取和修改属性
    
    
  2. 问题示例:点击按钮触发回调更新数据,第一种方法可以更新成功,第二个方法更新不成功(数据改了但是没有响应式)

    <button @click="updateInfo1" type="button">按钮</button>
    <button @click="updateInfo2" type="button">按钮</button>
    <ul>
        <!--       遍历模糊搜索过滤的数组 -->
        <li v-for="u of users" :key="u.id">
            {{u.name}}---{{u.tag}}--{{u.price}}
        </li>

    </ul>
</div>
</body>

<script type="text/javascript">

    const vm = new Vue({
        el: "#root",
        data: {
            users: [
                {id: 1, name: "vue", "tag": "html", "price": 30},
                {id: 2, name: "python", "tag": "server", "price": 40},
                {id: 3, name: "golang", "tag": "server", "price": 50},
            ],

        },
        // 回调
        methods:{
            updateInfo1(){
                // 可以更新成功
                this.users[0].name = "js"
                this.users[0].price = 40
            },
            updateInfo2(){
                // 数据更新成功,但是页面没有响应式更新,原因看监测数组原理
                this.users[0] =    {id: 1, name: "css", "tag": "html", "price": 40}
            }
        }
    })

</script>

Vue.set()方法

如果我们想直接给vm或者
_data
上加一个新属性,通过vm.xxx = xxx 或者vm._data.xxx = xx是没有响应式的

Vue的数据的响应式是通过
Object.defineProperty
来实现的,每个属性都有对应的getter和setter,而直接添加属性是没有数据实现

Vue给我们提供了set和$set方法,给vm或者_data添加属性, 并且完成数据加工

  • Vue.set是全局的 API,它的调用方式是Vue.set(target, key, value)


    • 其中target是要添加属性的目标对象(响应式对象),key是要添加的属性名,value是属性的值
    • 这种调用方式不依赖于特定的 Vue 组件实例,适用于在组件外部或者一些工具函数等场景中使用
  • this.$set是 Vue 实例(组件)的一个实例方法,它只能在 Vue 组件内部使用


    • 通过this.$set来调用。this指向当前的 Vue 组件实例,调用方式为this.$set(target, key, value)
    • 参数含义与Vue.set相同。这种方式更符合在组件内部操作数据的习惯,因为它与组件实例紧密相关,可以方便地访问组件的this上下文,如组件的数据(data)、计算属性(computed)等
  • Vue.set() 和 vm.$set() 不能给vm 或 vm的根数据对象 添加属性

    const vm = new Vue({
        el: "#root",
        data: {
            user:{
                name:"vue",
                info:{
                    price:100
                }
            }
        },
        methods:{
            // 事件回调 给info天添加一个tag数学
            addInfo(){
                Vue.set(this.user.info,"tag1","html")

                this.$set(this.user.info,"tag2","html")
            }
        }

    })
Vue监测数组

vm上数组的数据,没有getter和setter

vm监测数组数据变化实现响应式,是通过调用Vue数组(非原生方法)方法实现的

  • Vue在数据原型上实现了数组的增删改查方法(push、pop等)
  • 内部先调用了原生数组的增删改查方法来实现数据变化
  • 然后解析模版实现响应式

通过数组索引修改数据,只是修改了数据,没有重新解析模版,所以页面不会跟随更新

  // 使用this.$set来更新数组元素,实现响应式更新
  this.$set(this.numbers, 0, 10);
  
   // 使用splice方法来更新数组元素,实现响应式更新
  this.numbers.splice(0, 1, 10);

vue收集表单数据和修饰符

<body>
<div id="root">


    <form>
        <!--        有输入框相关的,v-Model收集的是value值,用户输入的东西就是value值-->


        <!--
        .trim修饰符
         去除收集数据的首尾空格
         -->
        账号:<input type="text" v-model.trim="account"> <br/><br/>
        密码:<input type="password" v-model="password"> <br/><br/>

        <!--  .number修饰符,用户输入vue收集过来会默认转换成number类型
               type="number" 是只能输入number,
               如果其他type 输入字符串“123”,修饰符收集过来会转换成number123
               如果输入123abc,修饰符收集过来是123
        -->
        年龄:<input type="number" v-model.number="age"> <br/><br/>

        <!-- radio类型没有输入框    v-model收集的是value值,需要给标签配置value值   -->
        性别:
        男<input type="radio" name="sex" value="male" v-model="sex">
        女<input type="radio" name="sex" value="female" v-model="sex"> <br/><br/>


        <!--
        checkbox类型
        如果没有配置value属性,v-model收集的就是标签的checked属性,true 或者false
        如果配置了value属性
           1. v-model的初始值是非数组,那么收集的就是checked(勾选 or 未勾选,布尔值)
           2. v-model的初始值是数组,那么收集的的就是value组成的数组
               初始值就是vue双向绑定给的初始值

        -->
        爱好:
        学习<input type="checkbox" v-model="hobby" value="study">
        打游戏<input type="checkbox" v-model="hobby" value="game">
        吃饭<input type="checkbox" v-model="hobby" value="eat">
        <br/><br/>
        所属校区
        <select v-model="city">
            <option value="">请选择校区</option>
            <option value="beijing">北京</option>
            <option value="shanghai">上海</option>
            <option value="shenzhen">深圳</option>
            <option value="wuhan">武汉</option>
        </select>
        <br/><br/>
        其他信息:

        <!--
        .lazy修饰符
         默认收集数据是实时的,每输入一个字符都会直接收集
         使用 .lazy修饰符,会在失去焦点的时候统一收集,而不是实时收集
         -->
        <textarea v-model.lazy="other"></textarea> <br/><br/>

        <input type="checkbox" v-model="agree">阅读并接受<a href="https://www.baidu.com">《用户协议》</a>
        <button>提交</button>
    </form>
</div>
</body>

<script type="text/javascript">

    const vm = new Vue({
        el: "#root",
        data: {
            account: "",
            password: "",
            age: 20, // 默认20
            sex: "male", // 默认选中 value为male
            // 复选:v-model初始值设置为数组,收集所选数据
            hobby: [],

            city: "beijing", // 默认选择北京
            other: "",
            agree: "" // 复选:不需要收集具体数,设置为字符串,收集checked状态


        }

    })


</script>

过滤器

过滤器不会改变原本的数据, 是产生新的对应的数据

局部过滤器
<body>
<div id="root">
    <!--   要展示的属性 |  要使用的过滤器 -->
    <!--   过滤器原理: 先读取time,把time当做参数传给过滤器,过滤器的返回值替换{{}}插值语法中的内容-->
    <h2>{{time  | timeFormat }}</h2>
    <!--    过滤器可以传递参数,第一个参数默认就是value,然后第二个参数需要使用形参接受-->
    <h2>{{time  | timeFormat(true) }}</h2>

    <!--    多个过滤器可以一起使用  多个过滤器使用 | 分割 , 插值 | 过滤器 |过滤器 ..-->
    <!--    原理:先将time交给timeFormat处理,然后把timeFormat处理的结果交给testFilter,最后使用testFilter的值进行替换-->
    <h2>{{time | timeFormat | testFilter}}</h2>


</div>
</body>

<script type="text/javascript">

    

    const vm = new Vue({
        el: "#root",
        data: {
            time: Date.now() // 当前时间的时间戳
        },
        // 配置局部过滤器,只有当前vue实例可以使用,其他实例不可以使用
        filters: {
            // 过滤器就是函数
            timeFormat(value, status) {
                if (status) {
                    console.log(value)
                }
                // 将时间戳格式化 然后返回
                return dayjs(value).format("YYYY年MM月DD日 HH:mm:ss")

            },
            testFilter(value) {
                return value[0]

            }

        }

    })


</script>
全局过滤器
  // 全局过滤器 必须在Vue实例创建之前配置
    Vue.filter("myFilter", function (value) {
        return value.slice(0, 4)
    })

    // 如果要多个全局过滤器,需要多声明
    Vue.filter("myFilter2", function (value) {
        return value.slice(0, 4)
    })

    const vm = new Vue({
        el: "#root",
        data: {
            time: Date.now() // 当前时间的时间戳
        },
        // 配置局部过滤器,只有当前vue实例可以使用,其他实例不可以使用
        filters: {
            // 过滤器就是函数
            timeFormat(value, status) {
                if (status) {
                    console.log(value)
                }
                // 将时间戳格式化 然后返回
                return dayjs(value).format("YYYY年MM月DD日 HH:mm:ss")

            },
            testFilter(value) {
                return value[0]
            }
        }
    })
<!--在单向数据绑定的时候,也可以使用过滤器 -->
    <input :value="time | timeFormat">

内置指令

v-bind : 单向绑定解析表达式, 可简写为 :xxx
v-model : 双向数据绑定
v-for : 遍历数组/对象/字符串
v-on : 绑定事件监听, 可简写为@
v-if : 条件渲染(动态控制节点是否存存在)
v-else : 条件渲染(动态控制节点是否存存在)
v-show : 条件渲染 (动态控制节点是否展示)

v-text

作用:向其所在的节点中渲染文本内容。
与插值语法的区别:v-text会替换掉节点中的内容,插值语法则不会替换

<body>
<div id="root">

    <!-- 123vue-->
    <h1>123{{name}}</h1>
  
  
    <!-- vue  -->
    <!--    v-text会拿到data里的值替换掉整个标签里的内容-->
    <!-- v-text会将数据当成字符串渲染,不会解析标签,如果值是标签内容 也会当字符串显示-->
    <h1 v-text="name">123</h1>
    
</div>
</body>

<script type="text/javascript">
    const vm = new Vue({
        el: "#root",
        data: {
            name: "vue"
        }
    })
</script>
v-html

向指定节点中渲染包含html结构的内容

v-html有安全性问题:

  1. 在网站上动态渲染任意HTML是非常危险的,容易导致XSS攻击
  2. 一定要在可信的内容上使用v-html,永不要用在用户提交的内容上
<div id="root">

    <!--<h1>666</h1>-->
    <div>{{name}}</div>
    <!--    <h1>666</h1>-->
    <div v-text="name"></div>
    
    <!--h1效果的666 ,v-html可以解析标签 ,如果标签中有其他内容,也会整个替换,和v-text一样-->
    <div v-html="name"></div>


</div>
</body>

<script type="text/javascript">
    const vm = new Vue({
        el: "#root",
        data: {
            name: "<h1>666</h1>"
        }
    })
</script>
v-clock

本质是一个特殊属性,Vue实例创建完毕并接管容器后,会删掉v-cloak属性。
使用css配合v-cloak可以解决网速慢时页面展示出{{xxx}}的问题

<style>

    /*  当vue没有创建成功的时候,插值语法还没有值,标签有v-cloak属性,选中这些标签 将该元素隐藏 */
    /*  当vue创建成功的之后,标签的v-cloak都被删掉,这些元素就不会隐藏了 */
    [v-cloak] {
        display: none;
    }</style>
<body>
<div id="root">

    <!--     vue实例没有创建成功的时候,标签内v-cloak,当实例创建成功之后,会删除所有 所有标签内的v-cloak -->
    <h1 v-cloak>{{name}}</h1>
    <h1 v-cloak>{{name}}</h1>

</div>
</body>

<script type="text/javascript">
    const vm = new Vue({
        el: "#root",
        data: {
            name: "vue"
        }
    })
</script>
v-once

v-once所在节点在初次动态渲染后,就视为静态内容了。
以后数据的改变不会引起v-once所在结构的更新,可以用于优化性能

    <!--    使用v-once,只会读取一次num值,后续不会在更新-->
    <h2 v-once>{{num}}</h2>

    <!--     每点一次 按钮 num增加1 -->
    <h2>当前的n值是:{{num}}</h2>
    <button @click="num++">点我n+1</button>
v-pre
  • 跳过其所在节点的编译过程
  • 可利用它跳过:没有使用指令语法、没有使用插值语法的节点,会加快编译
  <!--  页面显示  {{name}}} 不会解析了 使用了插值语法、指令的地方,不使用 -->
    <div v-pre>{{name}}</div>

    <!--  对于不需要vue渲染的 可以使用该指令 -->
    <div v-pre>内容</div>

自定义指令

指令对象式里面的方法、函数式的this都是Window

函数式
<div id="root">

    <div style="height: 100px" v-test="num" ></div>



</div>
</body>

<script type="text/javascript">
    const vm = new Vue({
        el: "#root",
        data: {
            num: 10
        },
        // 配置自定义指令
        directives:{
            // 函数形式写法-自定义一个test指令,函数名字不需要v-,但是使用的时候需要v-

            // element:使用该指令所绑定的 DOM 元素 比如 <div style=""></div>,真实DOM而非虚拟DOM
            // binding:一个对象,它包含了指令的相关信息
            //         def:包含了指令在生命周期不同阶段执行的函数,主要是bind和update
            //         update:当指令所绑定的数据发生变化时,update函数会被调用。它用于根据新的数据更新指令相关的操作。例如,如果指令是将数据绑定到元素的文本内容,update函数会更新文本内容以反映数据的变化
            //         expression:指令中使用的表达式,v-指令="xxx"中的xxx
            //         modifiers:一个对象,存储指令的修饰符。当指令在模板中有修饰符时,这个对象会记录相关信息
            //         name: 自定义指令的名称,这里是test
            //         rawName:自定义指令的原始名称,包含v-,这里是v-test
            //         value:指令的表达式绑定的值,通常是由expression计算得出或者直接从绑定的数据获取的,会在指令的逻辑中用于各种操作,比如更新元素的属性或者作为条件判断的依据


            // 指定触发时机 1.指令与元素成功绑定时(此时元素不一定已经放入页面) 
            //              如果一些操作比如使用element获取焦点,如果元素还没放入页面,就会失败
            //              函数式存在指令和元素绑定成功但是vue还没将模版(html)渲染的问题
            //            2.指令所在的模板被重新解析时
            test(element,binding){
                console.log(binding)
                // element的对应的dom元素,向该元素写入数据, 从binding.value获取值,然后做处理
                element.innerText = binding.value *10

            }

        }

    })
</script>

对象式
<body>
<div id="root">


    <div style="height: 100px" v-test="num"></div>

    <!--    指令名如果是多个单词,要使用v-xxx-xxx命名方式,不要用小驼峰命名-->
    <input type="text" v-test-obj:value="num">


</div>
</body>

<script type="text/javascript">
    const vm = new Vue({
        el: "#root",
        data: {
            num: 10
        },
        // 配置自定义指令
        directives: {
            test(element, binding) {
                // element的对应的dom元素,向该元素写入数据, 从binding.value获取值,然后做处理
                element.innerText = binding.value * 10
            },
            // 对象式-可以解决函数式的绑定元素&页面元素未放入的时机问题
            // bind和update方法大部分情况是一样的,函数式相当于实现了bind和update方法,没有inserted方法
            testObj: {
                //  调用时机-指令与元素成功绑定时
                bind(element, binding) {
                    element.value = binding.value * 10
                },
                //  调用时机-指令所在元素被插入页面时调用
                inserted(element, binding) {
                    // 获取input的焦点,触发时机是所在元素被插入页面时,所以dom的一些操作都会成功
                    element.focus()
                },
                //  调用时机-指令所在的模版呗重新解析时调用
                update(element, binding) {
                    element.value = binding.value * 10
                }

            },
            // 官方推荐直接使用 多个单词-连接的方式
            "test-object":{
                bind(element, binding) {
                },
                inserted(element, binding) {
                },
                update(element, binding) {
                }
            }
        }
    })
</script>
自定义指令-全局指令

在vue实例上配置的指令,都是局部指令,只能在当前vue实例使用

   // 在vue实例之前   
   // 指令名,配置对象
    Vue.directive("test",{}) 
    // 指令名,回调函数
    Vue.directive("test1",function () {})

生命周期

image-20241118102140212

Vue 的生命周期是指 Vue组件从创建到销毁的整个过程中所经历的一系列阶段。在每个阶段,组件会触发相应的生命周期钩子函数,开发者可以通过在这些钩子函数中编写代码来实现特定的功能,例如初始化数据、发送网络请求、操作 DOM 元素、清理资源等

  • 又名:生命周期回调函数、生命周期函数、生命周期钩子。
  • Vue在关键时刻帮我们调用的一些特殊名称的函数
  • 生命周期函数的名字不可更改,但函数的具体内容是根据需求编写的
  • 生命周期函数中的this指向是vm 或 组件实例对象
    const vm = new Vue({
            el: "#root",
            data: {
                opacity: 1 // 设置透明度
            },
            // mounted是一个生命周期钩子函数
            // 当Vue完成模版解析并把初始的真实DOM放入页面后(挂载完毕)调用mounted
            mounted() {
                setInterval(()=>{
                    this.opacity -= 0.01
                    if(this.opacity <= 0)
                        this.opacity = 1
                },16)
            }
        }
    )
挂载流程

image-20241118102213359

    // new vue实例
    const vm = new Vue({
            el: "#root",
            data: {},
            /**   挂载流程   **/

            //  1.初始化生命周期和事件之后,调用该方法,此数据代理还未开始,不能通过vm访问data、methods
            beforeCreate() {
                console.log("beforeCreate")
            },

            // 2.当数据检测、数据代理等初始化完成之后调用该方法,此时可以通过vm访问data、methods中的方法
            created() {
                console.log("created")
            },

            // 3.Vue开始解析模版,生成虚拟DOM(内存中),此时页面还不能显示解析好的内容
            //   判断new vue实例的时候有没有el选项,如果没有,就等待vm.$mount调用再后续执行
            //   判断new vue实例的时候是否有template选项,如果有的话,编译模版到render函数

            // 4.上方步骤之后,调用该方法,页面呈现的是未经Vue编译的DOM结构,所有对DOM的操作,此时都不奏效
            beforeMount() {
                console.log("beforeMount")

            },

            // 5.将内存中的虚拟DOM转为真实DOM插入页面

            // 6.当Vue完成模版解析并把初始的真实DOM放入页面后(挂载完毕)调用mounted
            //  此时对DOM的操作均有效,但是尽量不在此时操作DOM
            //  一般可以在这个时候开启定时器、发送请求、定向消息、绑定自定义事件等初始化操作
            mounted() {
                console.log("mounted")

            },

            /**   更新流程   **/

            // 1.当有数据改变的时候(更新之前),调用该方法
            //   此时数据是新的,但是页面还是旧的,页面还未和数据保持同步
            beforeUpdate() {
                console.log("beforeUpdate")
            },

            // 2.根据新数据,生成新的虚拟DOM,与旧的虚拟DOM进行比较,然后完成页面更新,Model->View的更新


            // 3. 更新完成之后,调用该方法,此时数据是新的,页面也是新的
            updated() {
                console.log("updated")
            },

            /**   销毁流程   **/
            // 当调用vm.$destroy()之后,触发销毁流程
            //
            //


            // 1. 在组件即将被销毁之前调用的钩子函数。这个阶段组件仍然是完整的,还没有开始实际的销毁过程
            //     目前vm中的方法属性等都处于可以用状态,但是修改不会再生效,一般在此时关闭定时器、取消消息解绑自定义事件等收尾操作
            beforeDestroy() {
                console.log("beforeDestroy")

            },

            // 2. 完全销毁一个实例,清理它与其他实例的连接,解绑它的全部指令及监听
            //    vue不再管理页面,但是之前生成的数据还在

            // 3.在组件已经完全被销毁之后调用
            destroyed() {
                console.log("destroyed")
            }


        }
    )

执行模型

执行模型(Processing Model)定义了数据库系统如何执行一个查询计划。

Iterator Model

基本思想:采用树形结构组织操作符,然后中序遍历执行整棵树,最终根结点的输出就是整个查询计划的结果。

每个操作符(Operator)实现如下函数:

  • Next()
    • 返回值:一个tuple或者EOF。
    • 执行流程:循环调用孩子结点的
      Next()
      函数。
  • Open()

    Close()
    :类似于构造和析构函数。

image-20241118105113714

输出从底部向顶部(Bottom-To-Top)汇聚,且支持流式操作,所以又称为Valcano Model,Pipeline Model。

Materialization Model

基本思想:操作符不是一次返回一个数据,暂存下所有数据,一次返回给父结点。

相比于Iterator Model,减少了函数调用开销,但是中间结果可能要暂存磁盘,IO开销大。

image-20241118105733041

可以向下传递一些暗示(hint),如
Limit
,避免扫描过多的数据。

更适用于OLTP而不是OLAP。

Vectoriazation Model

基本思想:操作符返回一批数据。

结合了Iterator Model和Materialization Model的优势,既减少了函数调用,中间结果又不至于过大。

可以采用SIMD指令加速批数据的处理。

image-20241118110928540

对比

特性 Iterator Model Materialization Model Vectorization Model
数据处理单位 单条记录(tuple-at-a-time) 整个中间结果(table-at-a-time) 批量记录(vector/batch-at-a-time)
性能 函数调用开销高,效率低 延迟高,内存/I/O 开销大 函数调用开销低,SIMD 加速性能优异
内存使用 内存需求低 内存需求高 中等
I/O 开销 中等
缓存利用率
复杂性 实现简单 中等 实现复杂
适用场景 小型数据集,流式处理 中间结果复用的复杂查询 大型数据集,需高性能计算的场景

数据访问方式

主要有三种数据访问方式:

  1. 全表扫描(Sequential Scan)
  2. 索引扫描(Index Scan)
  3. 多索引扫描(Multi-Index Scan)

Sequential Scan

全表扫描的优化手段:

image-20241118113337122

Data Skipping方法:

  1. 只需要大致结果:采样估计。
  2. 精确结果:Zone Map

image-20241118113508953

Zone Map基本思想:化整为零,提前对数据页进行聚合。

执行
Select * From table Where val > 600
时,下面的页可以直接跳过。

image-20241118113722074

Index Scan

如何确定使用哪个索引:数据分布。

image-20241118114047331

Multi-Index Scan

基本思想:根据每个索引上的谓词,独立找到满足条件的数据记录(Record),然后根据连接谓词进行操作(并集,交集,差集等)。

image-20241118114343292

Halloween Problem

对于UPDATE语句,需要追踪更新过的语句,否则会出现级联更新的问题。

image-20241118114850271

<999, Andy>执行更新,走索引扫描:

  1. 移除索引
  2. 更新Tuple,<1099, Andy>
  3. 插入索引
  4. (约束检查)

此时,如果不对<1099, Andy>进行标记,他满足Where子句,会被重新更新一次。

表达式求值

基本思想:采用树形结构,构建表达式树,用中序遍历方式执行所有求值动作,根结点的求值结果就是最终值。

image-20241118115507962

数据库中哪些地方采用了树结构:

  • B+树:存储。
  • 树形结构+中序遍历求值:查询计划,表达式求值。

优化手段:JIT Compilatoin。将热点表达式计算结点视为函数,编译为内联机器码,而不是每次都遍历结点。

image-20241118120356183

教程名称:使用 C# 入门深度学习

作者:痴者工良

地址:

https://torch.whuanle.cn

微积分

由于学习微积分需要一定的基础知识,但是由于本教程不是数学书,所以不能一一详细介绍基础知识,读者需要自行了解学习初等函数、三角函数等基础知识。

极限

极限的符号是
\(\lim\)
,在高等数学中,主要是数列极限和函数极限,限于篇幅,本文只讨论函数存在极限时的一些情况。

数学上有正无穷大(
\(+\infty\)
)和负无穷大( $ -\infty $ )的概念,大家都知道无穷大的意思,但是比较容易理解错无穷小、负无穷大,
无穷小指的是无限接近 0,而不是负数的无穷大。


举个例子你就明白了,当 $ x \to + \infty$ 时,
\(\frac{1}{x}\)
的值,我们都知道 x 越大,
\(\frac{1}{x}\)
越小,但是不可能为 0,只能越来越接近于 0。


求解极限,一般会碰到这几种情况,当 x 无穷大时,y 是多少。

例如下图所示,当 x 无穷大时,y 逐渐贴近 x 轴,即 y 越来越接近,我们使用
\(y\to 0\)
表示趋近于 0 或者说接近 0。

image-20241110092447001

图片来自《高等数学上册》第一章第三节函数极限的定义与计算,同济大学数学系编著。


所以:

\[\lim_{x \to \infty} f(x) = \lim_{x \to \infty} \frac{1}{x} = 0
\]

使用
C#
表示时,我们使用一个极大的数表示无穷大。

var x = torch.tensor(double.MaxValue);
var y = 1 / x;

var lim = (int)y.item<double>();
Console.WriteLine(lim);


上面使用了
y.item<double>()
将张量转换为标量,我们也可以使用函数
y.ToScalar().ToInt32();
转换。


再比如下图所示,当 x 无穷大时,y 越来越接近
\(\frac{\pi}{2}\)
,所以 :

\[\lim_{x \to \infty} \arctan x = \frac{\pi}{2}
\]

image-20241110092718306

图片来自《高等数学上册》第一章第三节函数极限的定义与计算,同济大学数学系编著。


上面求极限时,是当 $\lim_{x \to \infty} $ 或 $\lim_{x \to 0} $ 时的情况,在实际中更多的是给出某点,求其极限,例如:

\[\lim_{x \to x_{0}} f(x) = \lim_{x \to x_{0}} \frac{1}{x}
\]



\(x=1\)
时,我们直接计算其实可以得到
y=1
,极限就是 1,或者换句话来说,我们求一个函数在
\(x_{0}\)
的极限时,如果你可以直接计算出
\(y_{0}\)
的值,那么这个值就是该点的极限。

这种函数计算极限很简单,因为可以直接通过
\(y=f(x)\)
计算出来。


下面这道题是也是同济大学《高等数学上册》中的两道题。

image-20241110095607086


当 x 解决 0 时,分子是 0,0 除以任何数都是 0,所以极限是 0?肯定不是呀。

当碰到这种
\(x\to0\)
分子或分母为 0 的情况,就不能直接计算了。这两道题的解答过程:

image-20241110095620201


由于本文不是数学教程,因此这里不再深入讨论细节。

在高等数学中,有两种非常重要的极限:

\[\lim_{x \to 0} \frac{\sin x}{x} = 1 ,x \in (0,\frac{\pi }{2})
\]

\[\lim_{x \to 0} (1 + x)^{\frac{1}{x}} = e
\]


导数

给定一个函数,如何计算函数在某个区间上的变化率?

如图所示,函数
\(y = x^{2}\)
在区间
\([1,3]\)
的起点 A 和 终点 B。

image-20241110102423654

那么平均变化率就是:

\[\frac{\bigtriangleup y}{\bigtriangleup x} = \frac{9-1}{3-1} = \frac{8}{2} = 4
\]


但是当这个
\(\bigtriangleup{x}\)

\(\bigtriangleup{y}\)
非常小时,事情就会变得非常复杂。如果我们要求
\(x=9\)
附近的平均变化率,则:

\[\frac{y + \bigtriangleup y}{x+ \bigtriangleup x} = \frac{9 + \bigtriangleup y}{3 + \bigtriangleup x}
\]



\(\frac{\bigtriangleup y}{\bigtriangleup x}\)
非常小时,实际上反映了函数在
\(x=9\)
时的瞬时变化率。那么这个瞬时变化率,我们可以过 A、B 点使用切线表示。

切线是轻轻接触函数一点的一条线,由图可知,当 x 越来越大时,
\(y_{2} = x+1\)

\(y_{1} = x\)
大很多,比如
\(5^2\)

\(4^{2}\)

\(3^{2}\)
之间的差,越来越大。

那么切线可以反映这种变化率。如图所示,B 点的切线角度比 A 的的切线大。

image-20241110103552859


因此,出现了一种新的函数,叫原函数的导函数,简称导数,导数也是一个函数,通过导数可以计算原函数任一点的瞬时变化率。

导数的表示符号有多种,例如:

\[\frac{\bigtriangleup y}{\bigtriangleup x} = f'(x) = y' = \frac{dy}{dx} = \frac{df(x)}{x}= \frac{df}{x}
\]

d 是微分符号,例如 dy 是对 y 的微分,dx 是对 x 的微分。


如果要求在某点
\(x_{0}\)
的瞬时变化率,则:

\[\frac{\bigtriangleup y}{\bigtriangleup x} \big|_{x_{0}} = f'(x) \big|_{x_{0}} = y' \big|_{x_{0}} = \frac{dy}{dx} \big|_{x_{0}} = \frac{df(x)}{x} \big|_{x_{0}} = \frac{df}{x} \big|_{x_{0}}
\]


读者应该都有一定的数学基础吧,前面两种应该很容易理解,而后面三种也很重要,在积分和微积分的学习中,我们将会大量使用这种方式。

我们可以这样理解:

\[dy = \bigtriangleup y
\]

\[dx = \bigtriangleup x
\]


在 Pytorch 中,我们可以通过微分系统进行计算,例如我们要计算
\(d(x^2) \big|_{x=3}\)

// 定义 y = x^2 函数
var func = (torch.Tensor x) => x.pow(2);

var x = torch.tensor(3.0, requires_grad: true);
var y = func(x);

// 计算导数
y.backward();

// 转换为标量值
var grad = x.grad.ToScalar().ToDouble();
Console.WriteLine(grad);


不要搞错,计算导数后,要使用 x 输出导数值,而不是使用 y,因为 y 是函数结果。为什么求导的时候不直接输出求导结果呢?因为 Pytorch 自动求导系统是非常复杂的,计算的是偏导数,对于一元函数来说,对 x 的偏导数就是 y 的导数,在后面的偏导数和梯度时,会更多介绍这方面的知识。

另外创建 x 的张量类型时,需要添加
requires_grad: true
参数。


求导公式

下面是同济大学《高等数学》中的一些基本求导公式。

image-20241110105635460


例如,我们求
\(y = x^2\)
的导数,使用上图的 (2)式,得到
\(y = 2x\)


对于复合函数和复杂函数的求导会很麻烦,这里不再赘述。对于复杂的函数,还存在高阶导数,即导数的导数,二阶导数公式如下:

\[f''(x) = y'' = \frac{d^2y}{dx^2} = \frac{d^2f(x)}{x^2}= \frac{d^2f}{x^2}
\]


乘除求导例题

主要例题有乘法求导、商求导、指数求导几种。

① 求下面函数的导函数。

\[f(x) = e^x \cos x
\]

解:

\[\begin{align}
f'(x) &= (e^x \cos{x})' \\
&= (e^x)'\cos{x} + e^x (\cos{x})' \\
&= e^x \cos{x} - e^x \sin{x}
\end{align}
\]


求下面函数的导数:

\[y = \frac{x+1}{\ln x}
\]

解:

\[\begin{align}
y' &= \frac{(x+1)'\ln{x} - (x+1)(\ln{x})}{(\ln{x})^2} \\
&= \frac{\ln{x} - (x+1)^{\frac{1}{x}}}{(\ln{x})^2} \\
&= \frac{x \ln{x} -(x+1)}{x(\ln{x})^2}
\end{align}
\]


复合函数求导的链式法则

如果
\(y=f(u)\)
在点
\(u\)
处可导,
\(u=g(x)\)
在点
\(x\)
出可导,则复合函数
\(y=f[g(x)]\)
在点
\(x\)
处可导,且有:

\[\frac{dy}{dx} = \frac{dy}{du} \cdot \frac{du}{dx}
\]


如果函数比较复杂,还可以推广到有限个复合函数的情况,例如:

\[\frac{dy}{dx} = \frac{dy}{du} \cdot \frac{du}{dv} \cdot \frac{dv}{dx}
\]


例题,求
\(y = e^{2x}\)
的导函数。


\(u=2x\)
,则:

\[y' = (e^u)' = e^u \cdot (u)' = e^{2x} \cdot (2x)' = 2e^{ex}
\]


Sigmoid 函数的导数

经过上面的学习,我们知道由复合函数求导公式可知:

\[\big (\frac{u}{v} \big )' = \frac{u' \dot v - u \dot v'}{v^2}
\]


所以对于
\(\big ( \frac{1}{f(x)} \big )'\)
此类函数的求导,可得出:

\[\big ( \frac{1}{f(x)} \big )' = - \frac{f'(x)}{f(x)^2}
\]


Sigmoid 函数
\(σ(x)\)
是神经网络中最有名的激活函数之一,其定义如下:

\[\sigma (x) = \frac{1}{1+e^{-x}}
\]

image-20241114203340853


在后面学习梯度下降时,需要对 Sigmoid 函数进行求导,使用下面的公式求导会比较方便:

\[\sigma '(x) = \sigma (x)(1 - \sigma (x) )
\]


当然,你也可以使用分数的求导方法慢慢推导。

\[\begin{align}
\sigma '(x) &= (\frac{1}{1+e^{-x}})' \\
&= \frac{1}{1+e^{-x}} - \frac{1}{(1+e^{-x})^2} \\
&= \frac{1}{1+e^{-x}} (1 - \frac{1}{1+e^{-x}}) \\
&= \sigma (x)(1 - \sigma (x) )
\end{align}
\]


求最小值问题

函数的斜率有一个性质,当函数的斜率为 0 时,该点 a 取得极值,即
\(f'(a) = 0\)
,该点的切线平行于 x 轴。

画出
\(y = 3x^4 - 4x^3 - 12x^2 + 32\)
的图形如下所示,由图可知分别在
\(x = -1\)

\(0\)

\(2\)
三个点存在极值,此时斜率都是 0 ,其中当
\(x = 2\)
时,函数取得最小值,该函数无最大值。

image-20241113213533007


那么如果给定一个函数,我们如果取得这个函数的所有极值和最小最大值?这里可以使用穿针引线法。

首先对函数进行求导并化简。

\[\begin{align}
y' &= (3x^4 - 4x^3 - 12x^2 + 32)' \\
&= 12x^3 - 12x^2 - 24x \\
&= 12x(x^2 - x - 2) \\
&= 12x(x + 1)(x - 2)
\end{align}
\]


由此可知,该函数在
\(x = -1,0,2\)
三点的斜率为 0,然后分别计算在这三点的值,做出如下图所示。

image-20241113215104872


然后计算导数在区间的正负,例如当
\(x<-1\)
时,由于导数的结果是负数,所以
\(f(x)\)
递减区间。根据导数的正负,确定
\(f(x)\)
的递增递减区间,然后作出新的表格。

image-20241113215631143


接下来就简单了,根据三个点的值,在坐标轴上描点,最后按照递减区间连线,因此最小值是 0。

手画图不需要准确,主要是知道递增递减区间和极值就行。

e951f1d6da6d09477880fc13f655b23


微分

下面是同济大学《高等数学》中的一张图。

image-20241110110700577


由图可知,在正方形 A 中,其面积是
\(A = (x_{0})^2\)
,而大正方形的面积是
\((x_{0}+ \bigtriangleup x)^2\)
,或者通过多个矩形面积相加得出大正方形面积为:

\[S = x_{0}^2 + 2x_{0} \bigtriangleup x + (\bigtriangleup x)^2
\]


那么,在边长增加了
\(\bigtriangleup x\)
的时候,面积增加了多少呢?

\[\bigtriangleup S = 2x_{0} \bigtriangleup x + (\bigtriangleup x)^2
\]


我们可以使用下面的公式来表示当 $y = f(x)$ 满足一定关系时,其增量的表达式:

\[\bigtriangleup y = A \bigtriangleup x + O(\bigtriangleup x)
\]


前面在讲解导数时,我们知道
\(\bigtriangleup y =f(x + \bigtriangleup x) - f(x)\)
,所以:

\[\bigtriangleup y =f(x + \bigtriangleup x) - f(x) = A \bigtriangleup x + O(\bigtriangleup x)
\]



\(\bigtriangleup x\)
非常小时,并且
\(A \not= 0\)
时,可以忽略
\(O(\bigtriangleup x)\)
,我们使用 $A \bigtriangleup x $ 近似计算 $\bigtriangleup y $ 的值,这就是微分的定义,其中
\(A = f'(x)\)

\[dy = f'(x)\bigtriangleup x
\]


举个例子,求
\(y = x^3\)

\(x=1\)
时,
\(\bigtriangleup x = 0.01\)

\(\bigtriangleup x = 0.001\)
的增量。

上题本质就是求
\(x = 1.01\)

\(x = 1\)

\(\bigtriangleup y\)
以及
\(x = 1.001\)

\(x = 1\)

\(\bigtriangleup y\)

先求:

\[(1.01)^3 = 1.030301
\]

\[(1.001)^3 = 1.003003001
\]


所以两个增量方便是 0.030301、0.003003001。

但是如果只需要求近似值,那么我们使用微分方式去求,首先求出导数:

\[y' = dy = (x^3)' = 3x^2
\]


所以:

\[dy = 3x^2 \bigtriangleup x
\]


所以
\(\bigtriangleup x = 0.01\)
时,有:

\[dy = 3*(1)^2 * 0.01 = 3 * 0.01 = 0.03
\]


所以
\(\bigtriangleup x = 0.001\)
时,有:

\[dy = 3*(1)^2 * 0.001 = 3 * 0.001 = 0.003
\]


所以可以这样通过微分 dy 的方式近似计算函数的增量。

因为:

\[\bigtriangleup y = \frac{dy}{dx}
\]


我们使用 dy 近似代替
\(\bigtriangleup y\)
,这就是微分的应用场景之一。


积分

前面介绍了导数,我们知道
\(y = x^3\)
的导数是
\(y = 3x^2\)

那么反过来,我们知道一个函数
\(F(x)\)
的导数是
\(y=x^3\)
,对于幂函数,我们很容易反推出
\(\frac{1}{4} x^4\)
的导数是
\(x^3\)
,但是
\(\frac{1}{4} x^4 + 1\)

\(\frac{1}{4} x^4 + 666\)
的导数都是
\(x^3\)
,所以
\(x^3\)
的原函数是不确定的,所以反推得出的积分公式,又叫不定积分,我们使用
\(C\)
来表示这个不确定的常数。


假设原函数是 F(x),导数是
\(f(x)\)
,由于常数在求导时会被消去,所以求积分时,需要出现加上这个不确定的常数,所以:

\[\int f(x)dx = F(x) + C
\]


下面是同济大学《高等数学》给出一些积分公式。

image-20241110135308642


前面介绍了微分的作用,这里也给出导数在平面中的简单应用场景。


如图所示,图中的是
\(y = x^2\)
函数的封闭区域,和
\(x=0\)

\(x=2\)
两个直线围成了一个封闭区域,求 ABC 所围成的封闭区域的面积。

image-20241110140743986


首先求出其原函数为
\(y = \frac{1}{3}x^3\)
。使用积分区间表示求解的面积:

\[\int_{1}^{2} x^2=\frac{1}{3}x^3 \big|_{1}^{2} =\frac{1}{3}2^3 - \frac{1}{3}1^3=\frac{7}{3}
\]


对于上面求解的问题,使用的是积分公式,如下公式所示,∫ 表示积分符号,
\(f(x)\)
表示被积函数,
\(dx\)
表示积分变量增量(微分),
\(a\)

\(b\)
表示积分的下限和上限,即积分区间。

\[\int_{a}^{b} f(x) dx
\]


下面再来一道简单的题目,求 $y = 2x+3$ 和 $y = x^2$ 所围成的面积。

image-20241110143455304


首先要求得积分区间,即两者的两个交点,由
\(x^2=2x+3\)
得:

\[x^2 - 2x -3 = 0
\]


根据十字相乘法,得:

\[(x + 1)(x - 3) = 0
\]


所以
\(x_{1} = -1\)

\(x_{2} = 3\)


我们先求
\(y = 2x + 3\)
在这两个点之间围成的面积。

\[\int_{-1}^{3} 2x+3 = x^2+3x \big|_{-1}^{3} = (9 + 9) - (1 - 3) = 20
\]



\(y = x^2\)
在这两个点所围成的面积。

\[\int_{-1}^{3} x^2 = \frac{1}{3}x^3 \big|_{-1}^{3} = 9 - (-\frac{1}{3}) = 9 + \frac{1}{3}
\]


所以围成的面积是:
\(20 - (9+\frac{1}{3}) = \frac{32}{3}\)


在数学上,我们可以更加方便表示这种两个函数加减的方法,即:

\[\int_{-1}^{3} (2x+3 - x^2) = \int_{-1}^{3} (2x+3) - \int_{-1}^{3} (x^2)
\]


偏导数

偏导数属于多元函数的微分学,最常见的是求解空间问题,在初高中基本只涉及一元函数,在这里我们引入二元函数,记作:

\[z = f(x,y)
\]


在一元函数中,导数是函数沿着 x 轴的变化率,而在多元函数中由于有多个变量,不能直接计算导数,要针对某个轴方向进行求导,所以叫偏导数,接下来,我们将逐渐学习偏导数的一些基础知识。


多元函数定义域

下面给出一个二元函数构成的图形。

\[z=\sqrt{1-x^2-y^2}
\]

image-20241110145138140


下面提个问题,怎么求这个
\(z=\sqrt{1-x^2-y^2}\)
的定义域?

我们知道 $1 \ge x^2 + y^2 $ ,在设
\(y = 0\)
时, $1 \ge x^2 $ ,则
\(-1 \le x \le 1\)
,由于
\(1-x^2 \ge y^2\)
,所以
\(-\sqrt{1-x^2} \le y \le \sqrt{1 - x^2}\)

所以定义域:

\[-1 \le x \le 1 \\
-\sqrt{1-x^2} \le y \le \sqrt{1 - x^2} \\
z \ge 0
\]


这个函数是二元函数,求定义域还是比较简单的,z 是
\(f(x,y)\)
的函数,我们先求出 x 的定义域,然后求出 y 定义域。推广到
\(u=f(x,y,z)\)
三元函数,一般 x 定义域是常数,y 的定义域由 x 的函数组成,而 z 的定义域由 x、y 的函数组成。

求解空间中两个立体图形组成的封闭的空间体积时,就是使用定积分去求,计算定积分需要知道定义域,就是这种求法,本文不再赘述。


多元函数的值

已知函数
\(f(x,y) = \frac{xy}{x^2+y^2}\)
,求
\(f(1,2)\)

其实也很简单,方便使用
\(x=1,y=2\)
替代进去即可:

\[f(1,2) = \frac{2}{1^2+2^2} = \frac{1}{5}
\]


多元函数的极限

前面提到极限的时候,涉及到的都是一元函数,对于多元函数的极限,计算则复杂一些,我们可以使用以下公式表示二元函数在某点的极限值。

\[\lim_{_{y \longrightarrow y_{0}}^{x \longrightarrow x_{0}}} f(x,y) = A
\]


求二元函数的极限,称为二重极限。

例如求下面函数的二重极限。

\[\lim_{_{y \longrightarrow 2}^{x \longrightarrow 1}} \ln{(x+y^2)} = \ln{(1+2^2)} = \ln{5}
\]


偏导数

对多元函数求导的时候,由于函数有多个未知变量,例如 $z = x^2 + y^2 $ ,由于里面有 x、y 两个变量,因此函数也就有两个变化方向,求导的时候要设定是往哪个方向,例如要知道往 x 轴方向的变化率,那就是要针对 x 进行求导,求在
\(z=f(x_{0},y_{0})\)
时 x 的导数,这个就叫对 x 的偏导数。

偏导数使用符号
\(\partial\)
表示,那么对 x 的偏导数可以记作:

\[\frac{\partial z}{\partial x} \big|_{y=y_{0}}^{x=x_{0}}
\]


当然还有很多变体,Markdown 敲数学公式超级累,这里贴个图省事儿。

image-20241110151831018


下面给个简单函数的偏导数,方法很简单,当对 x 求偏导数时,把 y 当常数处理即可。

\[z = x^2 + y^2
\]

\[\frac{\partial z}{\partial x} = 2x,\frac{\partial z}{\partial y} = 2y
\]


再如:

\[z = x^2 + yx + y^2
\]

\[\frac{\partial z}{\partial x} = 2x + y,\frac{\partial z}{\partial y} = 2y +x
\]


前面提到积分可以求解平面中两个函数所组成的封闭区域的面积,偏导数则可以计算空间中立体几何和平面组成的封闭区域面积,这里就不再深入。


全微分

设二元函数
\(z = f(x,y)\)
则其全增量公式为:

\[\bigtriangleup z =A\bigtriangleup x + B \bigtriangleup y + O(\beta)
\]


那么关于 z 的微分:

\[dz=f_{x}(x,y)dx + f_{y}(x,y)dy
\]


求全微分,其实就是先求出所有偏导数,然后再进行计算。

例如求
\(z = e^{2x+3y}\)
的全微分。

389b1a036ebf0d36c4c2233c49c3556


给个例题,求函数
\(z = f(x,y) = \frac{x^2}{y}\)
在 点
\((1,-2)\)
出,当
\(\bigtriangleup x=0.02\)

\(\bigtriangleup y = -0.01\)
时的全增量。


先求函数的两个偏导数得出。

\[dz = \frac{2x}{y} \bigtriangleup x - \frac{x^2}{y^2} \bigtriangleup y
\]



\(\bigtriangleup x=0.02\)

\(\bigtriangleup y = -0.01\)
代入,得
\(-0.0175\)


下面是这个函数的图像。

image-20241110154210542


由微分和全微分的基础知识可知,在数学中进行一些计算时,其精确度会有所丢失。


偏导数求最小值

在学习导数时,我们知道当
\(f'(a) = 0\)
时,该函数取得极值,推广到多元函数中,也可以通过偏导数来求取极值。例如,对于二元函数
\(z = f(x,y)\)
,当符合下面条件时,可以取得极值:

\[\frac{\partial z}{\partial x} = 0,\frac{\partial z}{\partial y} = 0
\]


这是因为此时 x、y 切线的斜率都是 0,这里就不给出推理过程了,直接记住该方法即可。

如下图是函数
\(z=x^2 + y^2\)
的图像,求当 x、y 为何值时,函数取得最小值。

image-20241114204528433

很明显,当 x、y 都是 0 的时候,函数取得最小值,但是我们要通过数学来推到,不能只凭图像得出结论。

先求偏导数:

\[\frac{\partial z}{\partial x} = 2x \\
\frac{\partial z}{\partial y} = 2y
\]


可知,当
\(x=0,y=0\)
时,两个偏导数结果都是 0,所以
\(z=f(x,y)\)
只有在
\((0,0)\)
处有唯一的极值。

因为
\(z=x^2 + y^2 \ge 0\)
,所以可知,
\(z=f(0,0)\)
时取得最小值。


当 x、y 的斜率越来越接近 0 时,可以看到曲面切线越来越光滑。

image-20241114220105658


拉格朗日乘数法

有一个二元函数
\(z=(x,y)\)
,以及附加条件
\(\varphi (x,y) = 0\)
,而拉格朗日乘数法就是用来求解这种有条件限制的多元函数极值问题。

公式如下:

\[F(x,y,\lambda) = f(x,y) + \lambda{\varphi{(x,y)}}
\]


其中 $\lambda $ 是一个参数,也是我们要求解的值,求出
\(\lambda\)
后可以求得 z 的最小值。

首先将上面的公式进行偏导数求导,并且求出为 0 的条件:

\[F'_{x}(x,y,\lambda )= f'_{x}(x,y) +\lambda{\varphi{'_{x}(x,y)}} = 0
\]

\[F'_{y}(x,y,\lambda ) = f'_{y}(x,y) + \lambda{\varphi{'_{y}(x,y)}} = 0
\]

\[F'_{\lambda}(x,y,\lambda ) = \varphi{(x,y)} = 0
\]


通过上述方程求出 x、y、
\(\lambda\)
之后,代入
\(f(x,y)\)
求得极值。


例题
\(a + b = 1\)
,求
\(\frac{1}{a} + \frac{4}{b}\)
的最小值。

首先,二元函数是
\(z=f(a,b) = \frac{1}{a} + \frac{4}{b}\)

约束条件
\(\varphi (a,b) = a + b - 1=0\)

所以:

\[F(a,b,\lambda) = f(a,b)+ \lambda{\varphi{(a,b)}} = \frac{1}{a} + \frac{4}{b} + \lambda{(a + b - 1)}
\]


现在开始求偏导数。

\[F'_{a}(a,b,\lambda )= f'_{a}(a,b) +\lambda{\varphi{'_{a}(a,b)}} = -\frac{1}{a^2} + \lambda = 0 \qquad (1)
\]

\[F'_{b}(a,b,\lambda ) = f'_{b}(a,b) + \lambda{\varphi{'_{b}(a,b)}} = -\frac{4}{b^2} + \lambda= 0 \qquad (2)
\]

\[F'_{\lambda}(a,b,\lambda ) = \varphi{(a,b)} = a + b - 1= 0 \qquad (3)
\]


由 (1)、(2)、(3) 解得:

image-20241114212937793


代入
\(z=f(a,b) = \frac{1}{a} + \frac{4}{b}\)
,求得
\(z_{min} = f(\frac{1}{3},\frac{2}{3}) = 9\)
,所以最小值是 9。


梯度

在本节中,我们将学习深度学习里面重点之一的梯度下降法,梯度下降法要学习的知识比较多,本文的内容基本都是为梯度下降法做铺垫。

百度百科:方向导数本质上研究的是函数在某点处沿某特定方向上的变化率问题,梯度反映的是空间变量变化趋势的最大值和方向。


方向导数

前面提到导数,在一元函数中,
\(y=f(x)\)
,导数是反映了其在某点的变化率,而在
\(z = f(x,y)\)
中,两个偏导数 $\frac{\partial z}{\partial x} $ 、
\(\frac{\partial z}{\partial y}\)
则是反映函数沿着平行于 x 轴 、y 轴方向上的变化率。偏导数反映的是往某个轴方向的变化率,而方向导数则是某个方向的变化率,而不是某个轴方向。

image-20241110161833098


如上图所示,设
\(l\)
是一条有
\(P(x,y)\)
引出来的一条射线,
\(Q(x + \bigtriangleup x,y + \bigtriangleup y)\)

\(l\)
上的一点,设
\(\rho\)

\(P\)

\(Q\)
两点之间的距离,则:

\[\frac{\bigtriangleup z}{\rho}
\]


该公式反映函数在了
\(P\)

\(Q\)
两点之间沿着
\(l\)
方向的平均变化率,如果当
\(Q\)
趋近于
\(P\)
时,极限存在,则该极限值称为点
\(P\)
沿方向
\(l\)
的方向导数。


由于:

\[\bigtriangleup x = \rho \cos \alpha , \bigtriangleup y = \rho \cos \beta
\]


所以方向导数可以表示为:

\[\begin{align}
\frac{\partial z}{\partial l} &= \\
&= \frac{\partial z}{\partial x} \bigtriangleup x + \frac{\partial z}{\partial y} \bigtriangleup y \\
&= \frac{\partial z}{\partial x} \cos \alpha + \frac{\partial z}{\partial y} \cos \beta
\end{align}
\]


如果使用
\(i\)

\(j\)
表示 x、y 上的分量,也可以表示为:

\[\frac{\partial z}{\partial l} = \frac{\partial z}{\partial x}i + \frac{\partial z}{\partial y}j
\]


如果我们使用向量表示,也可以表示为:

\[(\frac{\partial z}{\partial x}, \frac{\partial z}{\partial y})
\]


梯度

梯度是指函数的值在哪个方向增长最快,后面学习的梯度下降则是相反的,是函数值下降最快的方向。


在空间中的一点,当点
\(P\)
固定时,方向
\(l\)
变化时,函数的方向导数 $\frac{\partial u}{\partial l} $ 也随之变化,说明了对于固定的点,函数在不同方向上的变化率也有所不同。那么对于点
\(P\)
,在什么方向上可以使得函数的变化率达到最大?这里需要引入梯度的概念。


下图是一个半球。

image-20241117094156647

问,怎么给定任意一点,怎么最快地达到顶部?很明显,垂直往上走,可以最快到底顶部,但是对于实际中凹凸不平的图像来说,是不能直接得出结论的,不过我们这里可以先简单讨论。

就像上面的图形,给定可微的二元函数
\(z = f(x,y)\)
,有一点
\((x_{0},y_{0})\)
,这个点可以往各种方向走,每个方向的方向导数都不一样,现在假设有个方向可以让方向导数最大,这个就是梯度 $gradf(x_{0},y_{0}) $。


如图所示,
\(A(x_{0},y_{0})\)
往 B 方向可以让 A 最快到达顶点,也就是变化率最大。而 A 有各种方向,其中一个是往 C 走。

往 B 方向的方向导数最大,就是梯度 $gradf(x_{0},y_{0}) $ 。由图所示,从 A 开始的任意一个方向导数,跟 $ \overrightarrow{AB}$ 都有一个夹角,因为是在空间,所以这个夹角表示起来有点麻烦,就是各个方向的余弦值,我们也是有向量表示:
\(n_{e} = (\cos \alpha ,\cos \beta)\)
,那么方向导数、梯度的关系:

\[\frac{\partial z}{\partial l} =gradf(x_{0},y_{0}) \cdot n_{e}
\]

\[\frac{\partial z}{\partial l} = \frac{\partial z}{\partial x} \cos \alpha + \frac{\partial z}{\partial y} \cos \beta =
gradf(x_{0},y_{0})\cdot n_{e}
\]

image-20241117094843430


如下图所示,当
\(\alpha = 0\)
时,
\(gradf(x_{0},y_{0})\)

\(e_{1}\)
重合, 由于
\(\cos \alpha = 1\)
,所以方向导数也达到最大值
\(|gradf(x_{0},y_{0})|\)
。也就是,沿着梯度方向的方向导数可以达到最大值。

所以:

\[\begin{align}
gradf(x_{0},y_{0}) &= \frac{\partial z}{\partial x} \cos \alpha + \frac{\partial z}{\partial y} \cos \beta \\
&= \frac{\partial z}{\partial x}i + \frac{\partial z}{\partial y}j \\
&= (\frac{\partial z}{\partial x}, \frac{\partial z}{\partial y})
\end{align}
\]

image-20241110164906093


例题,求函数
\(z = \ln(x^2 + y^2)\)
的梯度。

image-20241110165653935

来源:《高等数学工本》陈兆斗。


再来一道实际意义的题目。

image-20241110170325278

来源:《高等数学工本》陈兆斗。


使用
C#
求解该题,得:

// 定义 u = x^2 + y^2 + z^2 函数在 (2,1,-1) 点的值
var x = torch.tensor(2.0, requires_grad: true);
var y = torch.tensor(1.0, requires_grad: true);
var z = torch.tensor(-1.0, requires_grad: true);
var u = x.pow(2) + y.pow(2) + z.pow(2);

// 求导
u.backward();

var ux = x.grad;
var uy = y.grad;
var uz = z.grad;

Console.WriteLine($"gradu(2,1,-1) = {"{"}{ux.ToScalar().ToDouble()},{uy.ToScalar().ToDouble()},{uz.ToScalar().ToDouble()} {"}"}");
gradu(2,1,-1) = {4,2,-2 }


梯度下降法的基本公式

建议读者阅读这篇文章,这样很容易理解什么是梯度下降:
https://www.zhihu.com/question/434600945


前面提到,梯度是向上最快,那么梯度下降就是向下最快,跟梯度相反就是最快咯。


梯度下降法是神经网络的武器,相信大家在了解深度学习时,也最常出现梯度下降的相关知识,所以本小节将讲解梯度下降法的一些基础知识。

在偏导数求最小值一节中,我们学习到最小值需要满足以下条件:

\[\frac{\partial z}{\partial x} = 0,\frac{\partial z}{\partial y} = 0
\]


如果可以直接通过偏导数计算出梯度,那么问题就简单了,直接计算出最小值,都是对于实际场景要计算出来是比较可能的,尤其在神经网络里面。所以大佬们使用另一种方法来求出最小值的近似值,叫梯度下降法。

画出一个三维图像如图所示:

image-20241114223740021


假如你正在最高位置,将你蒙上眼睛后,你要从最上面移动到最底的位置,每次只能移动一个格。

我们要最快下降到底部,肯定要选择最徒的路径,但是因为蒙着眼睛,无法跳过一个格知道后面的格的位置,所以只能先从附近的格对比后,找到最徒的格,然后再走下一步。但是不可能所有的格都走一次吧?可以先选几个格,然后判断哪个格最徒,接着走下一步,然后再选几个格,再走下一步。


在前面学习梯度时,我们知道:

\[\frac{\partial z}{\partial l} = \frac{\partial z}{\partial x} \bigtriangleup x + \frac{\partial z}{\partial y} \bigtriangleup y
\]


即:

\[\bigtriangleup z = \frac{\partial z}{\partial x} \bigtriangleup x + \frac{\partial z}{\partial y} \bigtriangleup y
\]


如果我们把这个公式当作两个向量的内积,可以得出:

\[\bigtriangleup z = (\frac{\partial z}{\partial x},\frac{\partial z}{\partial y}) \cdot (\bigtriangleup x,\bigtriangleup y)
\]


image-20241115204555682


当以下向量方向相反时,
\(\bigtriangleup z\)
取得最小值。

image-20241115205039736


让我们回顾向量知识,当两个向量的方向相反时,向量内积取得最小值。由于:

\[a \cdot b = |a||b| \cos \theta
\]


所以向量 b 满足:

\[b = -ka \qquad
\]

(k 为正的常数)



\(b= (\bigtriangleup x,\bigtriangleup y)\)

\(a= (\frac{\partial z}{\partial x},\frac{\partial z}{\partial y})\)

\(k=\eta\)
,所以:

\[(\bigtriangleup x,\bigtriangleup y) = -\eta (\frac{\partial z}{\partial x},\frac{\partial z}{\partial y} ) \qquad ( \eta 为正的微小常数)
\]


这个公式称为二变量函数的梯度下降法基本公式,如果推广到三个变量以上:

\[(\bigtriangleup x_{},\bigtriangleup x_{2},...,\bigtriangleup x_{n}) = -\eta (\frac{\partial x}{\partial x_{1}},\frac{\partial z}{\partial x_{2}},...,,\frac{\partial z}{\partial x_{n}} )

\]


前面学习方向导数和梯度的时候,我们知道沿着梯度的方向导数最大,此时梯度是
\((\frac{\partial z}{\partial x},\frac{\partial z}{\partial y})\)
,也就是向上是最徒的。

由于
\((\bigtriangleup x,\bigtriangleup y)\)
是跟梯度相反的向量,所以向下是下降最快的,所以这就是梯度下降法求使得下降最快的向量。


回顾使用偏导数求最小值
\(z=x^2 + y^2\)
,求:当 x 从 1 变成
\(1+\bigtriangleup x\)
、y 从 2 变到
\(2 + \bigtriangleup y\)
时,求出使得这个函数减小最快的向量
\((\bigtriangleup x,\bigtriangleup y)\)

首先求出偏导数:

\[\frac{\partial z}{\partial x} = 2x \\
\frac{\partial z}{\partial y} = 2y
\]


根据梯度下降法的基本公式得出:

\[(\bigtriangleup x,\bigtriangleup y) = -\eta (2x,2y ) \qquad (\eta 为正的微小常数)
\]

由题意当
\(x=1\)

\(y=2\)
时,得出:

\[(\bigtriangleup x,\bigtriangleup y) = -\eta (2,4 ) \qquad
\]

(
\(\eta\)
为正的微小常数)


在本小节中,还有一个
\(\eta\)
没有讲解,它是一个非常小的正数,就像下山问题中的一个格,即移动的步长。在使用计算机进行计算时,需要确定一个合适的
\(\eta\)
值,
\(\eta\)
值过小或过大都会导致一些问题,而在神经网络中,
\(\eta\)
称为学习率,没有明确的方法求出
\(\eta\)
值,只能通过反复实验来寻找合适的值。


哈密算子
\(\bigtriangledown\)

当梯度下降法推广到多个变量时,下面的公式会显示非常复杂:

\[(\bigtriangleup x_{},\bigtriangleup x_{2},...,\bigtriangleup x_{n}) = -\eta (\frac{\partial x}{\partial x_{1}},\frac{\partial z}{\partial x_{2}},...,\frac{\partial z}{\partial x_{n}} )
\]


所以数学上经常使用 $\bigtriangledown $ 符号简化公式。

\[\bigtriangledown f = (\frac{\partial x}{\partial x_{1}},\frac{\partial z}{\partial x_{2}},...,\frac{\partial z}{\partial x_{n}} )
\]


替换到梯度下降法公式就是:

\[(\bigtriangleup x_{},\bigtriangleup x_{2},...,\bigtriangleup x_{n}) = -\eta \bigtriangledown f
\]


梯度下降法求最小值的近似值

在学习梯度下降法的基本公式时,提到了
\(\eta\)
,那么继续回顾
\(z = x^2 + y^2\)
的问题,我们如果设置学习率
\(\eta = 0.1\)
,那么根据梯度下降法,我们怎么使用这个算法求最小值呢?假设初始点是
\((3,2)\)
,根据梯度:

\[(\bigtriangleup x,\bigtriangleup y) = -0.1 (2x,2y ) \\
\bigtriangleup x = -0.2x \\
\bigtriangleup y = -0.2y
\]


代入
\((3,2)\)
,得:

第几次运算 当前位置 当前位置 梯度 梯度 位移向量 位移向量 函数值
i x y ∂z/∂x ∂z/∂y ∆x ∆y z
0 3.00 2.00 6.00 4.00 -0.60 -0.40 13.00


所以,点
\((3.00,2.00)\)
已经移动到
\((2.40,1.60)\)
,所以:

第几次运算 当前位置 当前位置 梯度 梯度 位移向量 位移向量 函数值
i x y ∂z/∂x ∂z/∂y ∆x ∆y z
0 3.00 2.00 6.00 4.00 -0.60 -0.40 13.00
1 2.40 1.60


重新计算梯度等步骤,得出:

第几次运算 当前位置 当前位置 梯度 梯度 位移向量 位移向量 函数值
i x y ∂z/∂x ∂z/∂y ∆x ∆y z
0 3.00 2.00 6.00 4.00 -0.60 -0.40 13.00
1 2.40 1.60 4.80 3.20 -0.48 -0.32 8.32

反复执行运算,最终可以算出最小值,如果步骤越少,那么下降的速度最快。


在 Pytorch 中,梯度下降算法有很多种,这里不再赘述,读者感兴趣可以参考这篇文章:
https://zhuanlan.zhihu.com/p/619988672

作者:来自 vivo 互联网大前端团队- Ke Jie

介绍 App 包体积优化的必要性,游戏中心 App 在实际优化过程中的有效措施,包括一些优化建议以及优化思路。

一、包体积优化的必要性

安装包大小与下载转化率的关系大致是成反比的,即安装包越大,下载转换率就越差。Google 曾在 2019 的谷歌大会上给出过一个统计结论,包体积体大小每上升 6MB,应用下载转化率就会下降 1%,在不同地区的表现可能会有所差异。

APK 减少 10MB,在不同国家转化率增长

(注:数据来自于
googleplaydev:Shrinking APKs, growing installs

二、游戏中心 APK 组成

APK 包含以下目录:

  • META-INF/:包含 CERT.SF 、CERT.RSA 签名文件、MANIFEST.MF 清单文件。

  • assets/:包含应用的资源。

  • res/:包含未编译到 resources.arsc 中的资源。

  • lib/:支持对应 CPU 架构的 so 文件。

  • resources.arsc:资源索引文件。

  • classes.dex:可以理解的 dex 文件就是项目代码编译为 class 文件后的集合。

  • AndroidManifest.xml:包含核心 Android 清单文件。此文件列出了应用的名称、版本、访问权限和引用的库文件。

发现占包体积比较大的主要是 lib、res、assets、resources 这几个部分,优化主要也从这几个方面入手。

三、包体积检测工具

Matrix-ApkChecker 作为 Matrix 系统的一部分,是针对 Android 安装包的分析检测工具,根据一系列设定好的规则检测 APK 是否存在特定的问题,并输出较为详细的检测结果报告,用于分析排查问题以及版本追踪。

配置游戏中心的 Json,主要检测 APK 是否经过了资源混淆、不含 Alpha 通道的 PNG 文件、未经压缩的文件类型、冗余的文件、无用资源等信息。

对于生成的检测文件进行分析,可以优化不少体积。

工具 Matrix Apkcheck 介绍:
https://github.com/Tencent/matrix/wiki/Matrix-Android-ApkChecker

四、包体积优化措施

4.1 不含 Alpha 通道的 PNG 大图

项目中存在较多这种类型的图,可以替换为 JPG 或者 WebP 图,能减少不少体积。

4.2 代码做减法

随着业务的迭代,很多业务场景是不会再使用了,涉及到相关的资源和类文件都可以删除掉,相应的 APK 中 res 和 dex 都会相应减少。游戏中心这次去掉了些经过迭代后没有使用的业务场景和资源。

4.3 资源文件最少化配置

针对内销的项目,本地的 string.xml 或者 SDK 中的 string.xml 文件中的多语言,是根本用不到的。这部分资源可以优化掉,能减少不少体积。

在 APP 的 build.gradle 中下添加 resConfigs "zh-rCN", "zh-rTW", "zh-rHK"。这样配置不影响英文、中文、中国台湾繁体、中国香港繁体语言的展示。

资源文件最少化配置前

资源文件最少化配置后

4.4 配置资源优化

很多项目为了适配各种尺寸的分辨率,同一份资源可能在不同的分辨率的目录下放置了各种文件,然后现在主流的机型都是 xxh 分辨率,游戏游戏中心针对了内置的 APK,配置了优先使用"xxhdpi", "night-xxhdpi"。

这么配置如果 xxhdpi、night-xxhdpi 存在资源文件,就会优先使用该分辨率目录下文件,如果不存在则会取原来分辨率目录下子资源,能避免出现资源找不到的情形。

defaultConfig {
        resConfigs isNotBaselineApk ? "" : ["xxhdpi", "night-xxhdpi"]
}

4.5 内置包去除 v1 签名

同样对于内置包来说,肯定都是 Android 7 及以上的机型了,可以考虑去掉 v1 签名。

signingConfigs {
    gameConfig {
        if (isNotBaselineApk) {
            print("v1SigningEnabled true")
            v1SigningEnabled true
        } else {
            print("v1SigningEnabled false")
            v1SigningEnabled false
        }
        v2SigningEnabled true
    }
}

去掉 v1 签名后,上图的三个文件在 APK 中会消失,也能较少 600k 左右的体积。

4.6 动效资源文件优化

发现项目中用了不少的 GIF、Lottie 文件、SVG 文件,占用了很大一部分体积。考虑将这部分替换成更小的动画文件,目前游戏中心接入了 PAG 方案。替换了部分 GIF 图和 Lottie 文件。

PAG 文件采用可扩展的二进制文件格式,可单文件集成图片音频等资源,导出相同的 AE 动效内容,在文件解码速度和压缩率上均大幅领先于同类型方案,大约为 Lottie 的 0.5 倍,SVG 的 0.2 倍。

实际上可能由于设计导出的 Lottie 或者 GIF 不规范,在导出 PAG 文件时会提醒优化点,实际部分资源的压缩比率达到了 80~90%,部分动效资源从几百 K 降到了几十 K。

具体可以参考 PAG 官网:
https://github.com/Tencent/libpag/blob/main/README.zh_CN.md

游戏中心这边将比较大的 GIF 图,较多的 Lottie 图做过 PAG 替换。

举例

(1)游戏中心的榜单排行页上的头图,UI 那边导出的符合效果的 GIF 大小为 701K,替换为 PAG 格式后同样效果的图大小为 67K,只有原来的 1/10 不到。

(2)游戏中心的入口空间 Lottie 动效优化。

一份 Lottie 动效大概是这样的,一堆资源问题加上 Json 文件。像上述动效的整体资源为 112K,同样的动效格式转换为 PAG 格式后,资源大小变成 6K,只有原大小的 5%左右。之后新的动效会优先考虑使用 PAG。

4.7 编译期间优化图片

以游戏中心 App 为例,图片资源约占用了 25%的包体积,因此对图片压缩是能立杆见效的方式。

WebP 格式相比传统的 PNG 、JPG 等图片压缩率更高,并且同时支持有损、无损、和透明度。

思路就是在是在 mergeRes 和 processRes 任务之间插入 WebP 压缩任务,利用 Cwebp 对图片在编译期间压缩。

(注:图片来源于
https://booster.johnsonlee.io/zh/guide/shrinking/png-compression.html#pngquant-provider

已有的解决方法

(1)可以采用滴滴的方案 booster,booster-task-compression-cwebp 。

参考链接:
https://github.com/didi/booster

(2)公司内部官网模块也有类似基于 booster 的插件,基于 booster 提供的 API 实现的图片压缩插件。压缩过后需要对所有页面进行一次点检,防止图片失真,针对失真的图片,可以采用白名单的机制。

4.8 动态化加载 so

同样以游戏中心为例,so 的占比达到了 45.1%,可以对使用场景较少和较大的 so 进行动态化加载的策略,在需要使用的场景下载到本地,动态去加载。

使用的场景去服务端下载到本地加载的流程可以由以下流程图表示。

流程可以归纳为下载、解压、加载,主要问题就是解决 so 加载问题。

载入 so 库的传统做法是使用:

System.loadLibrary(library);

经常会出现 UnsatisfiedLinkError,Relinker 库能大幅减小报错的概率:

ReLinker.loadLibrary(context, "mylibrary")

具体可以参考:
https://github.com/KeepSafe/ReLinker

按需加载的情形,风险与收益是并存的,有很多情况需要考虑到,比如下载触发场景、网络环境、加载失败是否有降级策略等等,也需要做好给用户的提示交互。

4.9 内置包只放 64 位 so

目前新上市的手机 CPU 架构都是 arm64-v8a, 对应着 ARMV8 架构,所以在打包的时候针对内置项目,只打包 64 位 so 进去。

ndk {
            if ("64" == localMultilib)
                abiFilters "arm64-v8a"
            else if ("32" == localMultilib)
                abiFilters "armeabi"
            else
                abiFilters "armeabi", "arm64-v8a"
        }
//其中localMultilib为配置项变量
 
String localMultilib = getLocalMultilib()
String getLocalMultilib() {
    def propertyKey = "LOCAL_MULTILIB"
    def propertyValue = rootProject.hasProperty(propertyKey) ? rootProject.getProperty(propertyKey) : "both"
    println " --> ${project.name}: $propertyKey[$propertyValue], $propertyKey[${propertyValue.class}]"
    return propertyValue
}

4.10 开启代码混淆、移除无用资源、ProGuard 混淆代码

android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
        }
    }
}

shrinkResources 和 minifyEnabled 必须同时开启才有效。

特别注意:这里需要强调一点的是开启之后无用的资源或者图片并没有真正的移除掉,而是用了一个同名的占位符号。

可以通过 ProGuard 来实现的,ProGuard 会检测和移除代码中未使用的类、字段、方法和属性,除此外还可以优化字节码,移除未使用的代码指令,以及用短名称混淆类、字段和方法。

proguard-android.txt 是 Android 提供的默认混淆配置文件,在配置的 Android sdk /tools/proguard 目录下,proguard-rules.pro 是我们自定义的混淆配置文件,我们可以将我们自定义的混淆规则放在里面。

android {
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),'proguard-rules.pro'
        }
    }
}

4.11 R 文件内联优化

如果我们的 App 架构如下:

编译打包时每个模块生成的 R 文件如下:

R_lib1 = R_lib1;
R_lib2 = R_lib2;
R_lib3 = R_lib3;
R_biz1 = R_lib1 + R_lib2 + R_lib3 + R_biz1(biz1本身的R)
R_biz2 = R_lib2 + R_lib3 + R_biz2(biz2本身的R)
R_app = R_lib1 + R_lib2 + R_lib3 + R_biz1 + R_biz2 + R_app(app本身R)

可以看出各个模块的 R 文件都会包含下层组件的 R 文件内容,下层的模块生成的 id 除了自己会生成一个 R 文件外,同时也会在全局的 R 文件生成一个,R 文件的数量同样会膨胀上升。多模块情况下,会导致 APK 中的 R 文件将急剧的膨胀,对包体积的影响很大。

由于 App 模块目前的 R 文件中的资源 ID 全部是 final 的, Java 编译器在编译时会将 final 常量进行 inline 内联操作,将变量替换为常量值,这样项目中就不存在对于 App 模块 R 文件的引用了,这样在代码缩减阶段,App 模块 R 文件就会被移除,从而达到包体积优化的目的。

基于以上原理,如果我们将 library 模块中的资源 ID 也转化为常量的话,那么 library 模块的 R 文件也可以移除了,这样就可以有效地减少我们的包体积。

现在有不少开源的 R 文件内联方法,比如滴滴开源的 booster 与字节开源的 bytex 都包含了 R 文件内联的插件。

booster 参考:

https://booster.johnsonlee.io/zh/guide/shrinking/res-index-inlining.html#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8

bytex 参考:

https://github.com/bytedance/ByteX/blob/master/access-inline-plugin/README-zh.md

五、优化效果

5.1 优化效果

上述优化措施均在游戏中心实际中采用,以游戏中心某个相同的版本为例子,前后体积对比如下图所示:

(1)包体积优化的比例达到了 31%,包体积下降了 20M 左右,从长久来说对应用的转换率可以提升 3%的点左右。

(2)启动速度相对于未优化版本提升 2.2%个点。

5.2 总结

(1)读者想进行体积优化之前,需先分析下 APK 的各个模块占比,主要针对占比高的部分进行优化,比如:游戏中心中 lib、res、assets、resources 占比较高,就针对性的进行了优化;

(2)动效方案的切换、so 动态加载、编译期间图片优化等措施是长久的,相比于未进行优化,时间越长可能减少的体积越明显;

(3)资源文件最小化配置、配置资源优化,简单且效果显著;

(4)后续会对 dex 进行进一步探索,目前项目中代码基本上都在做加法,越来越复杂,很少有做减法,导致 dex 逐渐增大,目前还在探索怎么进一步缩小 dex 体积。