Vue 与模板
使用步骤
- 编写页面模板
- 直接在HTML标签中写标签
- 使用template
- 使用单文件(
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
- AST -> VNode
- VNode -> DOM(相对不那么消耗性能)
哪一个阶段最消耗性能?
最消耗性能的是字符串解析(模板 -> AST)
例子: let s = 1 + 2 * (3 + 4 * (5 + 6));
写一个程序,解析这个表达式,得到结果(一般化)
我们一般会将这个表达式转化为"波兰式"表达式,然后使用栈结构来运算
在Vue中每一个标签可以是真正的HTML标签,也可以是自定义组件,问怎么区分???
在Vue源码中其实将所有可用的HTML标签已经存起来了
假设这里只考虑几个标签:
let tag = '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
问题
问题:
- 没明白科里化怎么就只要循环一次
- 缓存一部分行为
响应式原理
- 我们在使用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)
已经将对象改成响应式的了,但是如果直接给对象赋值,赋值另一个对象,那么就不是响应式的了,怎么办?
发布订阅模式
- 代理方法(app.name, app._data.name)
- 事件模型(node: event 模块)
- vue 中 Observer 与 Watcher 和 Dep
代理方法,就是要将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方法中
解释 proxy
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进行比较。
- 如果代码中含有自定义组件,那么在比较算法的时候,就会判断更新的是哪一些组件中的属性,指回判断更新数据的组件,其他组件不会更新。
复杂的页面是有很多组件构成。每一个属性要更新的时候都要调用更新的方法
目标,如果修改了什么属性,就尽可能只更新这些属性对应的页面DOM
这样就一定不能将更新的代码写死
例子: 预售可能一个东西没有现货,告诉老板,如果东西到了就告诉我
老板就是发布者
订阅什么东西作为中间媒介
我就是订阅者
使用代码的结构来描述:
- 老板提供一个 账簿(数组)
- 我可以根据需求订阅我的商品(老板要记录下谁定了什么东西,在数组中存储某些东西)
- 等待,可以做其他的事情
- 当货品来到的时候,老板就查看账簿,挨个的打电话(遍历数组,取出数组的元素来使用)
实际上就是事件模型
- 有一个 event 对象
- on, off, emit 方法
实现事件模型,思考怎么用?
- event 是一个全局对象
- event.on('事件名', 处理函数),订阅事件
- 事件可以连续订阅
- 可以移除
- 移除所有
- 移除某一个类型的事件
- 移除某一个类型的某一个处理函数
- 写别的代码
- evnet.emit('事件名', 参数),先前注册的事件处理函数就会依次调用
原因:
- 描述发布订阅模式
- 后面会实用到事件
发布订阅模式(形式不局限于函数,形式可以是对象等):
- 中间的全局的容器,用来存储可以被触发的东西(函数,对象)
- 需要一个方法,可以往容器中传入东西(函数,对象)
- 需要一个方法,可以将容器中的东西取出来使用(函数调用,对象的方法调用)
Vue 模型
页面中的变更 (diff) 是以组件为单位
- 如果页面中只有一个组件(Vue实例), 不会有性能损失
- 但是如果页面中有多个组件(多watcher的一种情况),第一次会有多个组件的watcher存入到全局watcher中。
- 如果修改了局部的数据(例如其中一个组件的数据)
- 表示只会对该组件进行diff算法,也就是说只会重新生成该组件的抽象语法树
- 只会访问该组建的 watcher
- 也就表示再次往全局存储的只有该组件的watcher
- 页面更新的时候也就只需要更新一部分
改写 observe 函数
缺陷:
- 无法处理数组
- 响应式无法在中间集成 Watcher 处理
- 我们实现的 rectify 需要和实例紧紧的绑定在一起, 分离(解耦)
问题
- observe 还没有对单独的数组元素做处理吧
引入watcher
问题:
- 模型(图)
- 关于this的问题
实现:
分成两步:
- 只考虑修改后刷新(响应式)
- 再考虑依赖收集(优化)
在Vue中提供一个构造函数Watcher
Watcher会有以下方法:
- get()用来进行计算或执行处理函数
- updata() 公共的外部方法,该方法会触发内部的run方法
- run() 运行,用来判断内部是使用异步运行还是同步运行等,这个方法最终会调用内部的get方法
- celanupDep() 简答理解为清除队列
我们的页面渲染是上面的哪一个方法执行的呢???get方法
我们的watcher实例有一个属性vm,表示的就是当前的vue实例
引入Dep对象
该对象提供 依赖(depend)的功能,和派发更新(notify)的功能
在notify中去调用watcher的update方法
Watcher 与 Dep
之前将渲染 Watcher放在全局作用域上,这样处理是有问题的
- vue项目中包含很多的组件,各个组件是自治
- 那么watcher就可能会有多个
- 每一个watcher用于描述一个渲染行为或计算行为
- 子组件发生数据的更新,页面需要重新渲染(真正的vue中是局部渲染)
- 例如vue中推荐是使用计算属性代替复杂的插值表达式
- 计算属性是会伴随其使用的属性的变化而变化的
name: () => this.first + this.lastName- 计算属性依赖于属性firstName和属性lastName
- 只要被以来的属性发生变化,那么就会促使计算属性重新计算(Watcher)
- 依赖收集与派发更新是怎么运行起来的
我们在访问的时候就会进行收集,在修改的时候就会更新,那么收集到什么就更新什么
所谓的依赖收集 实际上就是告诉当前的watcher什么属性被访问了
那么在这个watcher计算的时候或渲染页面的时候,就会将这些收集到的属性进行更新
如何将属性与当前watcher关联起来???
- 在全局准备一个tagetStack(watcher 栈,简单的理解为watcher"数组",把一个操作中需要使用的watcher都存储起来)
- 在Watcher调用get方法的时候,将当前Watcher放到全局,在get执行结果的时候(之后),将这个全局watcher移除。提供:pushTarget,popTarget
- 在每一个属性中都有一个Dep对象
Dep的subs中存储的是知道要渲染什么属性的watcher
Dep与watcher有互相关联关系
梳理Watcher与Dep与属性的关系
假设: 有三个属性name,age,gender,页面将三个属性渲染出来
Observer 对象
vue源码解读
- 各个文件夹的作用
- Vue的初始化流程
各个文件夹的作用
- compiler 编译用的
- vue 使用字符串作为模板
- 在编译文件夹中存放对模板字符串的解析的算法,抽象语法树,优化等
- core核心,vue构造函数,以及生命周期等方法的部分
- platforms平台
- 针对运行的环境(设备),有不同的实现
- 也是vue的入口
- server服务端,主要是将vue用在服务端的处理代码(略)
- sfc,单文件组件(略)
- shared公用工具,方法
主要内容
- vue源码
- Observer
- watch 和 computed
- 简单的说明一下patch
observer 文件夹中各个文件的作用
- array.js 创建含有重写 数组方法的数组,让所有的响应式数据数组继承自该数组
- dep.js Dep类
- index.js Observer类,observe 的工厂函数
- scheduler.js vue 当中的任务调度的工具, watcher执行的核心
- traverse.js 递归遍历响应式数据,目的是触发依赖收集。
- watcher.js Watcher类
面试题:对数组去重
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.keys( _set ) // 获得去重后的数组
// { 1: true }
// { 1: true, 2: true }
// 在网络中有一个终极的算法, 就是如何 "判同"