数据驱动
前提:
- 你一定得用过 vue
- 如果没有使用过的 可以去 官网 去看一看 使用教程
Vue 与模板
使用步骤:
- 编写 页面 模板
- 直接在 HTML 标签中写 标签
- 使用 template
- 使用 单文件 ( )
- 创建 Vue 的实例
- 在 Vue 的构造函数中提供: data, methods, computed, watcher, props,
- 将 Vue 挂载到 页面中 ( mount )
数据驱动模型
Vue 的执行流程
- 获得模板: 模板中有 "坑"
- 利用 Vue 构造函数中所提供的数据来 "填坑", 得到可以在页面中显示的 "标签了"
- 将标签替换页面中原来有坑的标签
Vue 利用 我们提供的数据 和 页面中 模板 生成了 一个新的 HTML 标签 ( node 元素 ), 替换到了 页面中 放置模板的位置.
我们该怎么实现???
简单的模板渲染
虚拟 DOM
目标:
- 怎么将真正的 DOM 转换为 虚拟 DOM
- 怎么将虚拟 DOM 转换为 真正的 DOM
思路与深拷贝类似
函数科里化
参考资料:
概念:
- 科里化: 一个函数原本有多个参数, 之传入一个参数, 生成一个新函数, 由新函数接收剩下的参数来运行得到结构.
- 偏函数: 一个函数原本有多个参数, 之传入一部分参数, 生成一个新函数, 由新函数接收剩下的参数来运行得到结构.
- 高阶函数: 一个函数参数是一个函数, 该函数对参数这个函数进行加工, 得到一个函数, 这个加工用的函数就是高阶函数.
为什么要使用科里化? 为了提升性能. 使用科里化可以缓存一部分能力.
使用两个案例来说明:
-
判断元素
-
虚拟 DOM 的 render 方法
-
判断元素:
Vue 本质上是使用 HTML 的字符串作为模板的, 将字符串的 模板 转换为 AST, 再转换为 VNode.
- 模板 -> AST
- AST -> VNode
- VNode -> DOM
那一个阶段最消耗性能?
最消耗性能是字符串解析 ( 模板 -> 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 次循环
- 虚拟 DOM 的 render 方法
思考: vue 项目 模板 转换为 抽象语法树 需要执行几次???
- 页面一开始加载需要渲染
- 每一个属性 ( 响应式 ) 数据在发生变化的时候 要渲染
- watch, computed 等等
我们昨天写的代码 每次需要渲染的时候, 模板就会被解析一次 ( 注意, 这里我们简化了解析方法 )
render 的作用是将 虚拟 DOM 转换为 真正的 DOM 加到页面中
- 虚拟 DOM 可以降级理解为 AST
- 一个项目运行的时候 模板是不会变 的, 就表示 AST 是不会变的
我们可以将代码进行优化, 将 虚拟 DOM 缓存起来, 生成一个函数, 函数只需要传入数据 就可以得到 真正的 DOM
makeMap( [ 'div', 'p' ] ) 需要遍历这个数据 生成 键值对
let set = {
div: true
p: true
}
set[ 'div' ] // ture
set[ 'Navigator' ] // undefined -> false
但是如果是使用的函数, 每次都需要循环遍历判断是不是数组中的
响应式原理
- 我们在使用 Vue 时候, 赋值属性获得属性都是直接使用的 Vue 实例
- 我们在设计属性值的时候, 页面的数据更新
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
- shift
- unshift
- reverse
- sort
- splice
要做什么事情呢?
- 在改变数组的数据的时候, 要发出通知
- Vue 2 中的缺陷, 数组发生变化, 设置 length 没法通知 ( Vue 3 中使用 Proxy 语法 ES6 的语法解决了这个问题 )
- 加入的元素应该变成响应式的
技巧: 如果一个函数已经定义了, 但是我们需要扩展其功能, 我们一般的处理办法:
- 使用一个临时的函数名存储函数
- 重新定义原来的函数
- 定义扩展的功能
- 调用临时的那个函数
扩展数组的 push 和 pop 怎么处理呢???
- 直接修改 prototype 不行
- 修改要进行响应式化的数组的原型 ( proto )
已经将对象改成响应式的了. 但是如果直接给对象赋值, 赋值另一个对象, 那么就不是响应式的了
// 继承关系: arr -> Array.prototype -> Object.prototype -> ... // 继承关系: arr -> 改写的方法 -> Array.prototype -> Object.prototype -> ...