1

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就是解决这个痛点的。


JohnsonGH
32 声望1 粉丝