一、响应式原理揭秘
(1)页面部分html
<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...')。vue
/** * 一、将数据丁意思成响应式 * 二、用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) } }
二、diff&patch过程及算法node
三、keep-alive原理算法
巴拉巴拉
应用场景:npm
四、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/ 来替换。缓存
五、nextTick原理:
使用Vue.js的global API的$nextTick方法,便可在回调中获取已经更新好的DOM实例了。sass
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时触发)的目的。app
如今有这样的一种状况,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就是解决这个痛点的。