1、响应式原理揭秘
(1)页面部分
<body>
<div id="app">
<p>{{name}}</p>
<p k-text="name"></p>
<p>{{age}}</p>
<p>
{{doubleAge}}
</p>
<input type="text" k-model="name">
<button @click="changeName">呵呵</button>
<div k-html="html"></div>
</div>
<script src='./watcher.js'></script>
<script src='./compile.js'></script>
<script src='./kvue.js'></script>
<script src='./dep.js'></script>
<script>
let kaikeba = new KVue({
el:'#app',
data: {
name: "I am test.",
age:12,
html:'<button>这是一个按钮</button>'
},
created(){
console.log('开始啦')
setTimeout(()=>{
this.name='我是蜗牛'
}, 15000)
},
methods:{
changeName(){
this.name = '哈喽,开课吧'
this.age=1
this.id = 'xx'
console.log(1,this)
}
}
})
</script>
</body>
(2)响应式实现代码:
响应式原理口诀:
Vue定义每个响应式数据时,都创建一个Dep依赖收集容器。
Compile编译每个绑定数据的节点时,都创建一个Watcher监听数据变化。
数据-->视图 绑定依靠Watcher。
视图-->数据 绑定依靠addEventListener('input/change...')。
/**
* 1、将数据丁意思成响应式
* 2、用this.key 代理 this.$data.key的访问
*/
class KVue {
constructor(options) {
this.$data = options.data
this.$options = options
this.observer(this.$data)
if(options.created){
options.created.call(this)
}
this.$compile = new Compile(options.el, this)
}
observer(value) {
if (!value || (typeof value !== 'object')) {
return
}
Object.keys(value).forEach((key) => {
this.proxyData(key) // 用this.key 代理 this.$data.key的访问
this.defineReactive(value, key, value[key]) // 将数据丁意思成响应式
})
}
defineReactive(obj, key, val) {
debugger
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 将Dep.target(即当前的Watcher对象存入Dep的deps中)
Dep.target && dep.addDep(Dep.target)
console.log(dep);
return val
},
set(newVal) {
debugger
if (newVal === val) return
val = newVal
// 在set的时候触发dep的notify来通知所有的Watcher对象更新视图
dep.notify()
}
})
}
proxyData(key) {
Object.defineProperty(this, key, {
configurable: false,
enumerable: true,
get() {
return this.$data[key]
},
set(newVal) {
this.$data[key] = newVal
}
})
}
}
/**
* 每个数据定义成响应式时,都创建一个依赖收集容器
* 在Compiler编译器编译文档时,读取数据(触发数据get方法)时
*/
class Dep {
constructor() {
// 存数所有的依赖
this.deps = []
}
// 在deps中添加一个监听器对象
addDep(dep) {
this.deps.push(dep)
}
depend() { // 貌似该方法没用过
Dep.target.addDep(this)
}
// 通知所有监听器去更新视图
notify() {
this.deps.forEach((dep) => {
dep.update()
})
}
}
/**
* 编译文档,深度优先遍历所有HTML节点,完成vm命名空间下 “节点初始赋值” 和 “节点数据双向监听绑定”
* 数据 --> 视图节点绑定: 为监听数据的节点创建Watcher(vm, key, cb),cb(node, value)更新视图
* 视图节点 --> 数据绑定:为输入控件例如<input> 添加 node.addEventListener('input', cb), cb(vm, key)更新数据
*/
class Compile {
constructor(el,vm) {
this.$vm = vm
this.$el = document.querySelector(el)
if (this.$el) {
this.$fragment = this.node2Fragment(this.$el)
this.compileElement(this.$fragment)
this.$el.appendChild(this.$fragment)
}
}
node2Fragment(el) {
// 创建文档片段 DocumentFragment
let fragment = document.createDocumentFragment()
let child
// 将原生节点拷贝到文档片段
while (child = el.firstChild) {
// appendChild() 方法向节点添加最后一个子节点
// 当子节点来源于原生节点时,appendChild() 方法从一个元素向另一个元素中移动元素
fragment.appendChild(child)
}
return fragment
}
compileElement(el) {
let childNodes = el.childNodes
Array.from(childNodes).forEach((node) => {
let text = node.textContent
// 表达式文本
// 就是识别{{}}中的数据
let reg = /\{\{(.*)\}\}/
// 按元素节点方式编译
if (this.isElementNode(node)) {
this.compile(node)
} else if (this.isTextNode(node) && reg.test(text)) {
// 文本 并且有{{}}
this.compileText(node, RegExp.$1)
}
// 遍历编译子节点
if (node.childNodes && node.childNodes.length) {
this.compileElement(node)
}
})
}
compile(node) {
let nodeAttrs = node.attributes
Array.from(nodeAttrs).forEach( (attr)=>{
// 规定:指令以 v-xxx 命名
// 如 <span v-text="content"></span> 中指令为 v-text
let attrName = attr.name // v-text
let exp = attr.value // content
if (this.isDirective(attrName)) {
let dir = attrName.substring(2) // text
// 普通指令
this[dir] && this[dir](node, this.$vm, exp)
}
if(this.isEventDirective(attrName)){
let dir = attrName.substring(1) // text
this.eventHandler(node, this.$vm, exp, dir)
}
})
}
compileText(node, exp) {
this.text(node, this.$vm, exp)
}
isDirective(attr) {
return attr.indexOf('k-') == 0
}
isEventDirective(dir) {
return dir.indexOf('@') === 0
}
isElementNode(node) {
return node.nodeType == 1
}
isTextNode(node) {
return node.nodeType == 3
}
text(node, vm, exp) {
this.update(node, vm, exp, 'text')
}
html(node, vm, exp) {
this.update(node, vm, exp, 'html')
}
model(node, vm, exp) {
this.update(node, vm, exp, 'model')
node.addEventListener('input', (e)=>{
let newValue = e.target.value
vm[exp] = newValue
}) }
// 编译发现节点有数据绑定时,执行对应updater(node, value)赋值
// 并在vm命名空间下创建Watcher(vm, key, cb)监听vm.key改动,再次执行更新函数
update(node, vm, exp, dir) {
let updaterFn = this[dir + 'Updater']
updaterFn && updaterFn(node, vm[exp])
new Watcher(vm, exp, function(value) {
updaterFn && updaterFn(node, value)
})
}
// 编译发现节点有事件绑定时,在命名空间vm中找对应方法,绑定到节点事件上
eventHandler(node, vm, exp, dir) {
let fn = vm.$options.methods && vm.$options.methods[exp]
if (dir && fn) {
node.addEventListener(dir, fn.bind(vm), false)
}
}
textUpdater(node, value) {
node.textContent = value
}
htmlUpdater(node, value) {
node.innerHTML = value
}
modelUpdater(node, value) {
node.value = value
}
}
/**
* 编译文档时,每个绑定数据的节点,都创建一个Watcher监听数据变化
* Watcher初始化时访问所监听响应式数据,并把自身赋值给Dep.target,这时响应式数据执行get方法,给自己的Dep收集到Watcher后,清空Dep.target.
*/
class Watcher {
/**
*
* @param {Vue} vm Vue实例,作用空间
* @param {String} key 监听属性
* @param {Function} cb 更新视图的方法
*/
constructor(vm, key, cb) {
// 在new一个监听器对象时将该对象赋值给Dep.target,在get中会用到
// 将 Dep.target 指向自己
// 然后触发属性的 getter 添加监听
// 最后将 Dep.target 置空
this.cb = cb
this.vm = vm
this.key = key
this.value = this.get()
}
get() {
Dep.target = this
let value = this.vm[this.key] // 监听属性的值
return value
}
// 更新视图的方法
update() {
this.value = this.get() // 有严重缺陷,反复调用响应式数据的get方法会重复创建Watcher并添加到数据的Dep之中,执行几次后浏览器便会卡死。
this.cb.call(this.vm, this.value)
}
}
2、diff&patch过程及算法
3、keep-alive原理
巴拉巴拉
应用场景:
- 从分类页面进入详情页再返回到列表页时,我们不希望刷新,希望列表页能够缓存已加载的数据和自动保存用户上次浏览的位置。
4、CSS的scoped私有作用域和深度选择器原理
编译前:
<div class="example">测试文本</div>
<style scoped>
.example {
color: red;
}
编译后:
<div data-v-4fb7e322 class="example">测试文本</div>
<style scoped>
.example[data-v-4fb7e322] {
color: red;
}
原理:给Vue组件的所有标签添加一个相同的属性,私有样式选择器都加上该属性。但是第三方组件(如UI库)内部的标签并没有编译为附带[data-v-xxx]这个属性,所以局部样式对第三方组件不生效。
注意:尽量别用标签选择器,特别是与特性选择器组合使用时会慢很多。取而代之可以给标签起个class名或者id。
如果希望scoped样式中的一个选择器能够作用的更深,例如影响子组件,可以使用 >>> 操作符。而对于less或者sass等预编译,是不支持 >>> 操作符的,可以使用 /deep/ 来替换。
5、nextTick原理:
使用Vue.js的global API的$nextTick方法,即可在回调中获取已经更新好的DOM实例了。
nextTick的实现比较简单,执行的目的是在microtask或者task中推入一个function,在当前栈执行完毕(也许还会有一些排在前面的需要执行的任务)以后执行nextTick传入的function,看一下源码:
/**
* Defer a task to execute it asynchronously.
*/
/*
延迟一个任务使其异步执行,在下一个tick时执行,一个立即执行函数,返回一个function
这个函数的作用是在task或者microtask中推入一个timerFunc,在当前调用栈执行完以后以此执行直到执行到timerFunc
目的是延迟到当前调用栈执行完以后执行
*/
export const nextTick = (function () {
/*存放异步执行的回调*/
const callbacks = []
/*一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
let pending = false
/*一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/
let timerFunc
/*下一个tick时的回调*/
function nextTickHandler () {
/*一个标记位,标记等待状态(即函数已经被推入任务队列或者主线程,已经在等待当前栈执行完毕去执行),这样就不需要在push多个回调到callbacks时将timerFunc多次推入任务队列或者主线程*/
pending = false
/*执行所有callback*/
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
......
}
看了源码发现timerFunc会检测当前环境而不同实现,其实就是按照Promise,MutationObserver,setTimeout优先级,哪个存在使用哪个,最不济的环境下使用setTimeout。
这里解释一下,一共有Promise、MutationObserver以及setTimeout三种尝试得到timerFunc的方法。
优先使用Promise,在Promise不存在的情况下使用MutationObserver,这两个方法的回调函数都会在microtask中执行,它们会比setTimeout更早执行,所以优先使用。
(1)首先是Promise,Promise.resolve().then()可以在microtask中加入它的回调,
(2)MutationObserver新建一个textNode的DOM对象,用MutationObserver绑定该DOM并指定回调函数,在DOM变化的时候则会触发回调,该回调会进入microtask,即textNode.data = String(counter)时便会加入该回调。
(3)setTimeout是最后的一种备选方案,它会将回调函数加入task中,等到执行。
综上,nextTick的目的就是产生一个回调函数加入task或者microtask中,当前栈执行完以后(可能中间还有别的排在前面的函数)调用该回调函数,起到了异步触发(即下一个tick时触发)的目的。
现在有这样的一种情况,mounted的时候test的值会被++循环执行1000次。每次++时,都会根据响应式触发setter->Dep->Watcher->update->patch。
所以Vue.js实现了一个queue队列,在下一个tick的时候会统一执行queue中Watcher的run。同时,拥有相同id的Watcher不会被重复加入到该queue中去,所以不会执行1000次Watcher的run。最终更新视图只会直接将test对应的DOM的0变成1000。保证更新视图操作DOM的动作是在当前栈执行完以后下一个tick的时候调用,大大优化了性能。
100、Hiper:一款令人愉悦的性能分析工具
安装:
npm install hiper -g
使用:
#请求页面,报告DNS查询耗时、TCP连接耗时、TTFB...
hiper baidu.com?a=1&b=2 //省略协议头时,默认添加 https://
#加载指定页面100次
hiper -n 100 "baidu.com?a=1&b=2" --no-cache
#使用指定useragent加载网页100次
hiper -n 100 "baidu.com?a=1&b=2" -u "Mozilla/5.0(Macintosh;Intel Mac OS X 10_13_14) AppleWebkit/537.36(KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36"
平时我们查看性能的方式,是在performance和network中查看数据,记录下几个关键的性能指标,然后刷新几次再看这些性能指标。有时候我们发现,由于采样太少,受当前【网络】、【CPU】、【内存】的繁忙成都的影响很重,有时优化后的项目反而比优化前更慢。
如果有一个工具,一次性的请求N次网页,然后把各个性能指标取出来求平均值,我们就能非常准确的知道这个优化是【正优化】还是【负优化】。
hiper就是解决这个痛点的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。