数据驱动
前提:
- 你一定得用过 vue
- 如果没有使用过的 可以去 官网 去看一看 使用教程
使用步骤:
- 编写 页面 模板
- 直接在 HTML 标签中写 标签
- 使用 template
- 使用 单文件 ( )
- 创建 Vue 的实例
- 在 Vue 的构造函数中提供: data, methods, computed, watcher, props, ...
- 将 Vue 挂载到 页面中 ( mount )
- 获得模板: 模板中有 "坑"
- 利用 Vue 构造函数中所提供的数据来 "填坑", 得到可以在页面中显示的 "标签了"
- 将标签替换页面中原来有坑的标签
- 怎么将真正的 DOM 转换为 虚拟 DOM
- 怎么将虚拟 DOM 转换为 真正的 DOM
- 科里化: 一个函数原本有多个参数, 之传入一个参数, 生成一个新函数, 由新函数接收剩下的参数来运行得到结构.
- 偏函数: 一个函数原本有多个参数, 之传入一部分参数, 生成一个新函数, 由新函数接收剩下的参数来运行得到结构.
- 高阶函数: 一个函数参数是一个函数, 该函数对参数这个函数进行加工, 得到一个函数, 这个加工用的函数就是高阶函数.
-
判断元素
-
虚拟 DOM 的 render 方法
-
判断元素:
- 模板 -> AST
- AST -> VNode
- VNode -> DOM
- 虚拟 DOM 的 render 方法
- 页面一开始加载需要渲染
- 每一个属性 ( 响应式 ) 数据在发生变化的时候 要渲染
- watch, computed 等等
- 虚拟 DOM 可以降级理解为 AST
- 一个项目运行的时候 模板是不会变 的, 就表示 AST 是不会变的
- 这样的闭包会内存泄漏吗老师?
- 性能一定是会有问题
- 尽可能的提高性能
- 原生的好多东西都忘记了,不知道从哪学起?
- 没明白柯里化怎么就只要循环一次。昨天 讲的
- 缓存一部分行为
- mountComponent 这个函数里面的内容 没太理解 ( 具体 )
- call
- 我们在使用 Vue 时候, 赋值属性获得属性都是直接使用的 Vue 实例
- 我们在设计属性值的时候, 页面的数据更新
- push
- pop
- shift
- unshift
- reverse
- sort
- splice
- 在改变数组的数据的时候, 要发出通知
- Vue 2 中的缺陷, 数组发生变化, 设置 length 没法通知 ( Vue 3 中使用 Proxy 语法 ES6 的语法解决了这个问题 )
- 加入的元素应该变成响应式的
- 使用一个临时的函数名存储函数
- 重新定义原来的函数
- 定义扩展的功能
- 调用临时的那个函数
- 直接修改 prototype 不行
- 修改要进行响应式化的数组的原型 ( proto )
- 作业
- 代理方法 ( app.name, app._data.name )
- 事件模型 ( node: event 模块 )
- vue 中 Observer 与 Watcher 和 Dep
- 如果代码中没有自定义组件, 那么在比较算法的时候, 我们会将全部的模板 对应的 虚拟 DOM 进行比较.
- 如果代码中含有自定义组件, 那么在比较算法的时候, 就会判断更新的是哪一些组件中的属性, 只会判断更新数据的组件, 其他组件不会更新.
- 老板提供一个 账簿( 数组 )
- 我可以根据需求订阅我的商品( 老板要记录下 谁 定了什么东西, 在数组中存储 某些东西 )
- 等待, 可以做其他的事情
- 当货品来到的时候, 老板就查看 账簿, 挨个的打电话 ( 遍历数组, 取出数组的元素来使用 )
- 有一个 event 对象
- on, off, emit 方法
- event 是一个全局对象
- event.on( '事件名', 处理函数 ), 订阅事件
- 事件可以连续订阅
- 可以移除: event.off()
- 移除所有
- 移除某一个类型的事件
- 移除某一个类型的某一个处理函数
- 写别的代码
- event.emit( '事件名', 参数 ), 先前注册的事件处理函数就会依次调用
- 描述发布订阅模式
- 后面会使用到事件
- 中间的全局的容器, 用来存储可以被触发的东西( 函数, 对象 )
- 需要一个方法, 可以往容器中传入东西 ( 函数, 对象 )
- 需要一个方法, 可以将容器中的东西取出来使用( 函数调用, 对象的方法调用 )
- 如果页面中只有一个组件 ( Vue 实例 ), 不会有性能损失
- 但是如果页面中有多个组件 ( 多 watcher 的一种情况 ), 第一次会有 多个组件的 watcher 存入到 全局watcher 中.
- 如果修改了局部的数据( 例如其中一个组件的数据 )
- 表示只会对该组件进行 diff 算法, 也就是说只会重新生成该组件的 抽象语法树
- 只会访问该组件的 watcher
- 也就表示再次往全局存储的只有该组件的 watcher
- 页面更新的时候也就只需要更新一部分
- 无法处理数组
- 响应式无法在中间集成 Watcher 处理
- 我们实现的 rectify 需要和实例紧紧的绑定在一起, 分离 ( 解耦 )
- observe 还没对单独的数组元素做处理吧?
- 模型 ( 图 )
- 关于 this 的问题
- 只考虑修改后刷新 ( 响应式 )
- 再考虑依赖收集 ( 优化 )
- get() 用来进行计算或执行处理函数
- update() 公共的外部方法, 该方法会触发内部的 run 方法
- run() 运行, 用来判断内部是使用异步运行还是同步运行等, 这个方法最终会调用内部的 get 方法
- cleanupDep() 简单理解为清除队列
- vue 项目中包含很多的组件, 各个组件是自治
- 那么 watcher 就可能会有多个
- 每一个 watcher 用于描述一个渲染行为 或 计算行为
- 子组件发生数据的更新, 页面需要重新渲染 ( 真正的 Vue 中是局部渲染 )
- 例如 vue 中推荐是使用 计算属性 代替复杂的 插值表达式.
- 计算属性是会伴随其使用的属性的变化而变化的
name: () => this.firstName + this.lastName
- 计算属性 依赖于 属性 firstName 和 属性 lastName
- 只要被依赖的属性发生变化, 那么就会促使计算属性 重新计算 ( Watcher )
- 依赖收集与派发更新是怎么运行起来的
- 在全局 准备一个 targetStack ( watcher 栈, 简单的理解为 watcher "数组", 把一个操作中需要使用的 watcher 都存储起来 )
- 在 Watcher 调用 get 方法的时候, 将当前 Watcher 放到全局, 在 get 之前结束的时候(之后), 将这个 全局的 watcher 移除. 提供: pushTarget, popTarget
- 在每一个属性中 都有 一个 Dep 对象
- 属性引用了当前的渲染 watcher, 属性知道谁渲染它
- 当前渲染 watcher 引用了 访问的属性 ( Dep ), 当前的 Watcher 知道渲染了什么属性
- 足够简单, 合适使用
- 可以为已有的项目, 进行优化
- 为 ts 可以做一个铺垫
- 使用命令行工具
- 编写代码, 执行命令检查如果有问题 则提示, 没问题跳过 ( 和传统编译型语言 非常类似 )
- 使用 IDE 插件 ( 推荐 )
- 所见即所得
- flow-bin 它就是 flow 的静态类型检查工具 ( 主程序 ).
- 编译器 ( compiler ), 例如 flow-remove-types, 将类型的语法结构删除掉, 还原成 纯 js 的文件.
- 代码中添加 一个 注释
// @flow
或者/* @flow */
- 在运行 flow 之前, 使用
npx flow init
初始化 - 版本, 生成文件的版本
- 使用模块化的语法是 ES6 语法 ( http://es6.ruanyifeng.com/#docs/module )
- 安装 ( 局部 安装 )
- rollup 源文件的路径 --file 生成文件的路径 --format umd --name 生成的库的名字
- 各个文件夹的作用
- Vue 的初始化流程
- compiler 编译用的
- vue 使用字符串作为模板
- 在编译文件夹中存放对 模板字符串的 解析的算法, 抽象语法树, 优化等
- core 核心, vue 构造函数, 以及生命周期等方法的部分
- platforms 平台
- 针对 运行的 环境 ( 设备 ), 有不同的实现
- 也是 vue 的入口
- server 服务端, 主要是将 vue 用在服务端的处理代码 ( 略 )
- sfc, 单文件组件 ( 略 )
- shared 公共工具, 方法
- vue 源码
- Observer
- watch 和 computed
- 简单的说明一下 patch
- array.js 创建含有重写 数组方法的数组, 让所有的 响应式数据数组继承自 该数组
- dep.js Dep 类
- index.js Observer 类, observe 的工厂函数.
- scheduler.js vue 中的 任务调度的工具, watcher 执行的核心
- traverse.js 递归遍历响应式数据. 目的是触发依赖收集.
- watcher.js Watcher 类
- 不使用 JSON.stringify 实现将 对象转换为 JSON 格式的字符串
Vue 的执行流程
Vue 利用 我们提供的数据 和 页面中 模板 生成了 一个新的 HTML 标签 ( node 元素 ), 替换到了 页面中 放置模板的位置.
我们该怎么实现???
目标:
思路与深拷贝类似
参考资料:
概念:
为什么要使用科里化? 为了提升性能. 使用科里化可以缓存一部分能力.
使用两个案例来说明:
Vue 本质上是使用 HTML 的字符串作为模板的, 将字符串的 模板 转换为 AST, 再转换为 VNode.
那一个阶段最消耗性能?
最消耗性能是字符串解析 ( 模板 -> AST )
例子: let s = "1 + 2 * ( 3 + 4 * ( 5 + 6 ) )" 写一个程序, 解析这个表达式, 得到结果 ( 一般化 ) 我们一般会将这个表达式转换为 "波兰式" 表达式, 然后使用栈结构来运算
在 Vue 中每一个标签可以是真正的 HTML 标签, 也可以是自定义组件, 问怎么区分???
在 Vue 源码中其实将所有可以用的 HTML 标签已经存起来了.
假设这里是考虑几个标签:
let tags = 'div,p,a,img,ul,li'.split(',');
需要一个函数, 判断一个标签名是否为 内置的 标签
function isHTMLTag( tagName ) { tagName = tagName.toLowerCase(); if ( tags.indexOf( tagName ) > -1 ) return true; return false; }
模板是任意编写的, 可以写的很简单, 也可以写到很复杂, indexOf 内部也是要循环的
如果有 6 中内置标签, 而模板中有 10 个标签需要判断, 那么就需要执行 60 次循环
思考: vue 项目 模板 转换为 抽象语法树 需要执行几次???
我们昨天写的代码 每次需要渲染的时候, 模板就会被解析一次 ( 注意, 这里我们简化了解析方法 )
render 的作用是将 虚拟 DOM 转换为 真正的 DOM 加到页面中
我们可以将代码进行优化, 将 虚拟 DOM 缓存起来, 生成一个函数, 函数只需要传入数据 就可以得到 真正的 DOM
问题:
makeMap( [ 'div', 'p' ] ) 需要遍历这个数据 生成 键值对
let set = { div: true p: true } set[ 'div' ] // ture set[ 'Navigator' ] // undefined -> false
但是如果是使用的函数, 每次都需要循环遍历判断是不是数组中的
Object.defineProperty( 对象, '设置什么属性名', { writeable configurable enumerable: 控制属性是否可枚举, 是不是可以被 for-in 取出来 set() {} 赋值触发 get() {} 取值触发 } )
// 简化后的版本 function defineReactive( target, key, value, enumerable ) { // 函数内部就是一个局部作用域, 这个 value 就只在函数内使用的变量 ( 闭包 ) Object.defineProperty( target, key, { configurable: true, enumerable: !!enumerable, get () { console.log( `读取 o 的 ${key} 属性` ); // 额外 return value; }, set ( newVal ) { console.log( `设置 o 的 ${key} 属性为: ${newVal}` ); // 额外 value = newVal; } } ) }
实际开发中对象一般是有多级
let o = { list: [ { } ], ads: [ { } ], user: { } }
怎么处理呢??? 递归
对于对象可以使用 递归来响应式化, 但是数组我们也需要处理
要做什么事情呢?
技巧: 如果一个函数已经定义了, 但是我们需要扩展其功能, 我们一般的处理办法:
扩展数组的 push 和 pop 怎么处理呢???
已经将对象改成响应式的了. 但是如果直接给对象赋值, 赋值另一个对象, 那么就不是响应式的了, 怎么办? ( 作业 )
// 继承关系: arr -> Array.prototype -> Object.prototype -> ... // 继承关系: arr -> 改写的方法 -> Array.prototype -> Object.prototype -> ...
任务:
代理方法, 就是要将 app._data 中的成员 给 映射到 app 上
由于需要在更新数据的时候, 更新页面的内容 所以 app._data 访问的成员 与 app 访问的成员应该时同一个成员
由于 app._data 已经是响应式的对象了, 所以只需要让 app 访问的成员去访问 app._data 的对应成员就可以了.
例如:
app.name 转换为 app._data.name app.xxx 转换为 app._data.xxx
引入了一个函数 proxy( target, src, prop ), 将 target 的操作 映射到 src.prop 上 这里是因为当时没有
Proxy
语法 ( ES6 )我们之前处理的 rectify 方法已经不行了, 我们需要一个新的方法来处理
提供一个 Observer 的方法, 在方法中对 属性进行处理 可以将这个方法封装发到 initData 方法中
app._data.name // vue 设计, 不希望访问 _ 开头的数据 // vue 中有一个潜规则: // - _ 开头的数据是私有数据 // - $ 开头的是只读数据 app.name // 将 对 _data.xxx 的访问 交给了 实例 // 重点: 访问 app 的 xxx 就是在访问 app._data.xxx
假设:
var o1 = { name: '张三' }; // 要有一个对象 o2, 在访问 o2.name 的时候想要访问的是 o1.name Object.defineProperty( o2, 'name', { get() { return o1.name } } );
访问 app 的 xxx 就是在访问 app._data.xxx
Object.defineProperty( app, 'name', { get() { return app._data.name }, set( newVal ) { app._data.name = newVal; } } )
将属性的操作转换为 参数
function proxy( app, key ) { Object.defineProperty( app, key, { get() { return app._data[ key ] }, set( newVal ) { app._data[ key ] = newVal; } } ) }
问题:
在 vue 中不仅仅是只有 data 属性, properties 等等 都会挂载到 Vue 实例上
function proxy( app, prop, key ) { Object.defineProperty( app, key, { get() { return app[ prop ][ key ] }, set( newVal ) { app[ prop ][ key ] = newVal; } } ) }; // 如果将 _data 的成员映射到 实例上 proxy( 实例, '_data', 属性名 ) // 如果要 _properties 的成员映射到 实例上 proxy( 实例, '_properties', 属性名 )
目标: 解耦, 让各个模块之间没有紧密的联系
现在的处理办法是 属性在更新的 时候 调用 mountComponent 方法.
问题: mountComponent 更新的是什么??? (现在) 全部的页面 -> 当前虚拟 DOM 对应的页面 DOM
在 Vue 中, 整个的更新是按照组件为单位进行 判断, 已节点为单位进行更新.
复杂的页面是有很多组件构成. 每一个属性要更新的都要调用 更新的方法?
目标, 如果修改了什么属性, 就尽可能只更新这些属性对应的页面 DOM
这样就一定不能将更新的代码写死.
例子: 预售可能一个东西没有现货, 告诉老板, 如果东西到了 就告诉我.
老板就是发布者 订阅什么东西作为中间媒介 我就是订阅者
使用代码的结构来描述:
实际上就是事件模型
实现事件模型, 思考怎么用?
原因:
发布订阅模式 ( 形式不局限于函数, 形式可以是对象等 ) :
Vue 模型
页面中的变更 ( diff ) 是一组件为单位
缺陷:
问题:
实现:
分成两步:
在 Vue 中提供一个构造函数 Watcher Watcher 会有一些方法:
我们的页面渲染是上面那一个方法执行的呢???
我们的 watcher 实例有一个属性 vm, 表示的就是 当前的 vue 实例
该对象提供 依赖收集 ( depend ) 的功能, 和 派发更新 ( notify ) 的功能
在 notify 中去调用 watcher 的 update 方法
之前将 渲染 Watcher 放在全局作用域上, 这样处理是有问题的
我们在访问的时候 就会进行收集, 在修改的时候就会更新, 那么收集什么就更新什么
所谓的依赖收集 实际上就是告诉当前的 watcher 什么属性被访问了, 那么在这个 watcher 计算的时候 或 渲染页面的时候 就会 将这些收集到的属性进行更新.
如何将 属性与 当前 watcher 关联起来??
我们在访问对象属性的时候 ( get ), 我们的渲染 watcher 就在全局中. 将 属性与 watcher 关联, 其实就是将当前渲染的 watcher 存储到属性相关的 dep 中. 同时, 将 dep 也存储到 当前全局的 watcher 中. ( 互相引用的关系 )
我们的 dep 有一个方法, 叫 notify() 内部就是将 dep 中的 subs 取出来, 依次调用其 update 方法.
subs 中存储的是 知道要渲染什么属性的 watcher
假设: 有三个属性 name, age, gender. 页面将三个属性渲染出来
flow 本身 只是一个 静态的 语法 检查工具
优势:
准备使用:
用法:
安装的内容
// note // flow 是一个静态类型的检查工具 // 给 js 增加了 类型 // 在变量的名字后面 跟上 `:类型名` // 在使用的 需要在文件一开始的时候使用注释 // 使用这个注释是告诉 flow 工具 需要检查这个文件, 如果不使用这个注释 flow 工具就会忽略该文件 // @flow /* @flow */
首先需要安装软件
$ npm i flow-bin flow-remove-types
编写代码
检查代码
$ npx flow
注意:
npx
是 node 工具, 是为了使用项目文件夹下 node_modules 中的可执行程序的工具将代码转换为纯 js 的代码
npx flow-remove-types 源文件 -d 生成的文件
一般会将该命令配置 到 package.json 文件中
补充一下: 第一次打开 flow 代码的时候, VS CODE 会下滑红色波浪线
推荐使用 flow language support 这个插件 ( VS Code 编辑器 )
.flowconfig 中
module.name_mapper='^sfc/(.*)$' -> '<PROJECT_ROOT>/src/sfc/\1'
的含义是将 代码中 from 后面导入模块使用的路径
sfc/xxx/aa
映射到项目根目录/src/sfc/xxx/aa
注意:
使用
面试题:
let params = 'a=b&c=d&e=f'; // params.split( '&' ).reduce( (res, v) => { // let kv = v.split( '=' ); // res[ kv[ 0 ] ] = kv[ 1 ]; // return res; // }, {} ); let t = null; params.split( '&' ).reduce( ( res, v ) => ( t = v.split( '=' ), res[ t[ 0 ] ] = t[ 1 ], res ), {} );
observer 文件夹中各个文件的作用
面试题: 对数组去重
let arr = [ 1, 1, 1, 2, 2, 3, 3, 3 ]; // => [ 1, 2, 3 ] // 一般的做法 // let newarr = []; // arr.forEach( v => newarr.indexOf( v ) === -1 && newarr.push( v ) ); // indexOf 原本隐含着循环 // 利用 集合 来简化实现 ( ES6 Set ) let _set = {}; let _newarr = []; arr.forEach( v => _set[ v ] || ( _set[ v ] = true, _newarr.push( v ) ) ) // 减少赋值行为 // Object.kyes( _set ) // 获得去重后的数组 // { 1: true } // { 1: true, 2: true } // 在网络中有一个终极的算法, 就是如何 "判同"
面试题: