10 Vue 特性要点

Vue2 特性要点


Vue2 源码理解

Vue 双向数据绑定

先从单向绑定切入单向绑定非常简单,就是把Mode1绑定到view,当我们用Javascript代码更新Model时, view就会自动更新
双向绑定就很容易联想到了,在单向绑定的基础上,用户更新了View, Mode1的数据也自动被更新了

因为 Vue 是数据双向绑定的框架,而整个框架的由三个部分组成:

  • 数据层(Model):应用的数据及业务逻辑,为开发者编写的业务代码
  • 视图层(View):应用的展示效果,各类UI组件,由 template 和 css 组成的代码
  • 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来
理解 ViewModel

它的主要职责就是

  • 数据变化后更新视图
  • 视图变化后更新数据

当然,它还有两个主要部分组成

  • 监听器(Observer):对所有数据的属性进行监听
  • 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
Proxy (Vue3)
  • Proxy可以直接监听对象而非属性
  • Proxy 可以直接监听数组的变化
  • Proxy有多达13种拦截方法,不限于 apply, ownKeys, deleteProperty, has 等 Object.defineProperty 不具备的
  • Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改
  • Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利
Object.defineProperty (Vue2,Vue3 ref())
  • 监听对象属性变化,通过 getter/setter 作出反应
  • 无法监听数组属性的变化,Vue 通过重写数组方法从而实现
  • 兼容性好,支持IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平
Vue 实现双向数据绑定
class Vue {
  constructor(options) {
    // * 数据备份
    this.$data = this._data = options.data

    // * 数据劫持
    Observe(this.$data)

    // * 数据代理
    Object.keys(this.$data).forEach(key => {
      Object.defineProperty(this, key, {
        enumerable: true, // 允许迭代
        configurable: true, // 允许修改
        get() {
          return this.$data[key]
        },
        set(newValue) {
          this.$data[key] = newValue
        }
      })
    })

    // * 模板编译
    Compile(options.el, this)
  }
}

// * 数据劫持
function Observe(obj) {
  // 递归终止条件
  if (!obj || typeof obj !== 'object') return
  const dep = new Dep()
  // 通过Object.keys(obj)获取到当前odj上的每个属性
  Object.keys(obj).forEach(key => {
    //获取属性值
    let value = obj[key]
    // 把value子节点进行递归
    Observe(value)
    Object.defineProperty(obj, key, {
      enumerable: true, // 允许迭代
      configurable: true, // 允许修改
      get() {
        Dep.target && dep.addSubs(Dep.target)
        return value
      },
      set(newValue) {
        value = newValue
        Observe(value)
        // 通知每一个订阅者更新自己的文本
        dep.notify()
      }
    })
  })
}

// * 模板编译
function Compile(el, vm) {
  // 获取el对应的DOM元素
  vm.$el = document.querySelector(el)
  // 创建文档碎片,提高DOM操作的性能
  const fragment = document.createDocumentFragment()
  while ((childNode = vm.$el.firstChild)) {
    fragment.appendChild(childNode)
  }
  // 模板编译
  replaceNode(fragment)
  // 重新渲染页面
  vm.$el.appendChild(fragment)

  // 负责对DOM模板进行编译的方法
  function replaceNode(node) {
    // 定义匹配差值表达式的正则
    const regMustache = /\{\{\s*(\S+)\s*\}\}/
    // 证明当前的node节点是一个文本子节点,需要进行正则替换
    if (node.nodeType === 3) {
      // 注意:文本子节点,也是一个DOM对象,如果要获取文本子节点的字符串内容,需要调用textContent属性获取
      // console.log(node.textContent);
      const text = node.textContent
      // 进行字符串的正则匹配与提取
      const execResult = regMustache.exec(text)
      // console.log(execResult);
      if (execResult) {
        const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm)
        node.textContent = text.replace(regMustache, value)
        // 在这里,创建watcher的实例
        new Watcher(vm, execResult[1], newValue => {
          node.textContent = text.replace(regMustache, newValue)
        })
      }
      // 终止递归条件
      return
    }
    // 如果是DMO节点 并且是输入框
    if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
      // 获取节点的所有属性
      const attrs = Array.from(node.attributes)
      // 判断属性中是否存在 v-model
      const findResult = attrs.find(attr => attr.name === 'v-model')
      if (findResult) {
        //获取到当前v-model的属性值 name info.a
        const expStr = findResult.value
        const value = expStr.split('.').reduce((newObj, k) => newObj[k], vm)
        node.value = value
        // 在这里,创建watcher的实例
        new Watcher(vm, expStr, newValue => {
          node.value = newValue
        })

        // 监听文本框的input输入事件,拿到文本框最新的值,把最新的值,更新到vm上即可
        node.addEventListener('input', e => {
          const keyArr = expStr.split('.')
          const obj = keyArr.slice(0, keyArr.length - 1).reduce((newObj, k) => newObj[k], vm)
          console.log(obj)
          obj[keyArr[keyArr.length - 1]] = e.target.value
        })
      }
    }
    // 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
    node.childNodes.forEach(child => replaceNode(child))
  }
}

class Dep {
  constructor() {
    // 设置存放订阅者信息的数组
    this.subs = []
  }

  // 向subs数组中添加订阅者的信息
  addSubs(watcher) {
    this.subs.push(watcher)
  }

  // 发布通知的方法
  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
}

// 订阅者的类
class Watcher {
  // cb回调函数中,记录着当前Watcher如何更新自己的文本内容
  // 但是,只知道如何更新自己还不行,还必须拿到最新的数据,
  // 因此,还需要在new Watcher 期间,把Vm也传递进来(因为Vm中保存着最新的数据)
  // 除此之外,还需要知道,在Vm身上众多的数据中,哪个数据,才是当前自己所需要的数据,
  // 因此,必须在new Watcher期间,指定watcher 对应的数据的名字key

  constructor(vm, key, cb) {
    this.vm = vm
    this.key = key
    // 实例的回调函数
    this.cb = cb
    // 下面三行负责把创建的Watcher 实例存到Dep实例的subs数组中
    Dep.target = this
    key.split('.').reduce((newObj, k) => newObj[k], vm)
    Dep.target = null
  }

  // 触发回调函数的方法 发布者通知我们更新
  update() {
    const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)
    this.cb(value)
  }
}

Vue 数据劫持

通过 vm 对象来代理 data 对象中属性的操作(读/写);能够更加方便的操作data中的数据

JS 实现: Object.defineProperty()
Vue2 实现: 通过 Object.defineProperty() 把data对象中所有属性添加到 vm 上,为每一个添加到 vm 上的属性,都指定一个 getter/setter ,在 getter/setter 内部去操作(读/写) data 中对应的属性
Vue3 实现: 通过 Proxy,Reflect 实现

Vue 数据监测(发布/订阅者模式)

Vue 实现数据监听主要使用 监视者设计模式 设置一个监视者,将数据设置为响应式数据后,添加到 Vue._data 中,Vue 再将 Vue._data 属性 代理到 vm 实例上,方便使用这就导致,如果在实例化过程中,没有传入数据,后续想要添加数据时,Vue 就不会对数据进行响应式处理,另外: 数组内元素在实例化时不做响应式处理
Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新

// 模拟 Vue 中属性监测
function Observer(obj){
    // 汇总对象中所有的属性形成一个数组
    const keys = Object.keys(obj)
    // 遍历
    keys.forEach((k)=>{
        Object.defineProperty(this,k,{
            get(){
                return obj[k]
            },
            set(val){
                console.log(`${k}被改了,我要去解析模板,生成虚拟DOM.....我要开始忙了`)
                obj[k] = val
            }
        })
    })
}

Vue.set() and vm.$set()

使用 Vue 所提供的方法动态添加属性并做响应式处理

Vue.set(object, propertyName, value)
// 还可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名
this.$set(this.someObject,'b',2)

Vue 数组包裹方法实现响应式

push()
pop()
shift()
unshift()
splice()
sort()
reverse()

实现 mustache 模板引擎

模板引擎是将数据转变为视图最优雅的方案
实现功能: 解析模板字符串,将模板字符串转为 JS的数组表示(tokens),然后转换为 domStr 并上树

数据变视图方法
  1. 纯 DOM 法: 通过createElement标签,在追加节点(上树)
  2. 数组 join 法: var str=[‘A’,‘B’,‘C’,‘D’].join(‘’) 输出结果:ABCD (可以实现换行)
  3. ES6 模板字符串: 反引号、变量
  4. 模板引擎: mustache: {{}}
mustache 模板引擎结构
  • mian.js: 程序主入口,调用各项功能
  • parseTemplateToTokens.js: 将模板字符串转换为 tokens
    • Scanner.js: 扫描器类,扫描模板字符串,返回匹配的字符串
    • nestTokens.js: 字符串转换为 tokens
  • renderTemplate.js: 判断 tokens 类型分别作出处理
    • lookup.js: 判断读取数据是否存在 ( . ),存在循环后返回数据,不存在直接调用返回数据
    • parseArray.js: 递归调用 renderTemplate 实现数组循环

mustache 模板引擎结构.jpeg

main.js
import parseTemplateToTokens from './parseTemplateToTokens'
import renderTemplate from './renderTemplate'

/**
 * ! 程序主入口
 * * 1. 在 window 全局对象挂载 mustache 对象,并添加 render 方法
 * * 2. 调用 parseTemplateToTokens 方法将模板字符串转换为 tokens
 * * 3. 调用 renderTemplate 方法将 tokens 转换为 domStr
 * * 4. 将 domStr 上树
 */
window.mustache = {
  render(templateStr, data) {
    let tokens = parseTemplateToTokens(templateStr) // 模板字符串转换 tokens
    let domStr = renderTemplate(tokens, data) // tokens 转换为 domStr
    document.body.innerHTML = domStr // domStr 上树
  }
}

parseTemplateToTokens.js
import Scanner from './Scanner'
import nestTokens from './nestTokens'

/**
 * ! 模板字符串 转换 tokens 函数
 * * 1. 创建 Scanner 扫描器对象,传入数据进行扫描返回对应数据
 * * 2. 对数据进行分类处理
 * * 3. 将数据传入 nestTokens 方法,将需要折叠的 token 进行折叠处理
 *
 * @export
 * @param {String} templateStr
 * @return {Array}
 */
export default function parseTemplateToTokens(templateStr) {
  const scanner = new Scanner(templateStr) // 创建扫描器对象
  const tokens = [] // 创建数组接收扫描的数据
  let words = '' // 临时存储字符串变量

  // 判断指针是否扫描到尾部
  while (scanner.eos()) {
    words = scanner.scanUtil('{{') // scanUtil 方法最后一次会返回空字符串
    if (words !== '') {
      tokens.push(['text', words])
    }
    scanner.scan('{{')

    words = scanner.scanUtil('}}')
    if (words !== '') {
      if (words[0] === '#') {
        tokens.push(['#', words.substring(1)])
      } else if (words[0] === '/') {
        tokens.push(['/', words.substring(1)])
      } else {
        tokens.push(['name', words])
      }
    }
    scanner.scan('}}')
  }
  return nestTokens(tokens)
}

Scanner.js
/**
 * ! 扫描器 (对特定数据扫描并返回)
 * * 1. scan() 传入特定字符,跳过此字符串长度,改变尾部字符串
 * * 2. scanUtil() 传入特定字符,直至到达指定字符处,返回指针指过的字符,改变尾部字符串
 * * 3. eos() 判断指针是否已到尾部,返回布尔值
 *
 * @export
 * @class Scanner
 */
export default class Scanner {
  constructor(templateStr) {
    this.templateStr = templateStr
    this.pos = 0 // 指针
    this.tail = templateStr // 尾巴字符串,初始化为字符串原文
  }

  // * 跳过模板语法特定字符
  scan(tag) {
    if (this.tail.indexOf(tag) === 0) {
      this.pos += tag.length
      this.tail = this.templateStr.substring(this.pos)
    }
  }

  // * 指针进行扫描,遇到特定内容停止,并且返回扫描过得字符
  scanUtil(stopTag) {
    // 保存调用此函数时的指针值,以便返回扫描过得字符
    let pos_start = this.pos
    while (this.eos() && this.tail.indexOf(stopTag) !== 0) {
      this.pos++ // 指针自增
      this.tail = this.templateStr.substring(this.pos) // 根据指针改变尾巴字符串(如果对this.tail进行截取操作, this.tail 一直被缩短,每次截取后的基准位置是不同的)
    }
    return this.templateStr.substring(pos_start, this.pos)
  }

  // * 判断指针指向是否已到尾部
  eos() {
    return this.pos !== this.templateStr.length
  }
}

nestTokens.js
/**
 * !  JS 字符串多维数组 token 化,折叠 tokens, 将 # 和 / 之间的 tokens 能够整合起来
 * * 1. 利用栈结构先进后出的特点,遇到 # 对栈顶进行压入操作,遇到 / 对栈顶进行出栈操作
 * * 2. 自我实现: 主要利用出栈时判断栈数组长度,为0 则直接压入返回数据容器中,不为 0 证明栈中还有数组存在,将此次出栈数据压入当前栈顶数据中(sections[sections.length - 1][2].push(section))
 * * 3. 改进实现: 利用一个收集器,默认指向返回数据容器,一旦出现特定字符,数据会被压入栈结构栈顶,而收集器指向此栈结构栈顶,出栈时改变指向,栈顶还有数据指向栈顶,没有则指向返回数据容器
 * * 4. 收集器本质上是存储了栈顶数据引用的变量,简化代码
 *
 * @export
 * @param {String} tokens
 * @return {Array}
 */
// ? 1. 自我实现
/* export default function nestTokens(tokens) {
  const nestedTokens = [] // 创建返回数据容器
  let sections = [] // 利用栈结构,创建一个栈顶数组确定现在所操作的数据属于哪个结构

  tokens.forEach((token, index) => {
    switch (token[0]) {
      case '#':
        token[2] = [] // 创建 token [2] 数组以便接收后续数据
        sections.push(token) // 压栈(入栈)
        break
      case '/':
        let section = sections.pop() // 弹栈
        // 出栈时判断长度,长度不为 0,证明栈顶还有数据,则将此数据压入倒指定栈顶数组中(可行),否则压入返回数据容器
        sections.length > 0 ? sections[sections.length - 1][2].push(section) : nestedTokens.push(section)
        break
      default:
        // 长度不为 0,证明栈顶还有数据,则将此数据压入倒指定栈顶数组中(可行),否则压入返回数据容器
        sections.length > 0 ? sections[sections.length - 1][2].push(token) : nestedTokens.push(token)
    }
  })

  return nestedTokens // 返回数据容器
} */

// ? 2. 收集器实现
export default function nestTokens(tokens) {
  const nestedTokens = [] // 创建返回数据数组
  let sections = [] // 利用栈结构,创建一个栈顶数组确定现在所操作的数据属于哪个结构
  let collector = nestedTokens // 创建一个收集器,默认指向返回数据容器(引用数据类型)

  tokens.forEach(token => {
    switch (token[0]) {
      case '#': // 特定字符处理
        collector.push(token) // 将数据放入收集器(默认返回数据容器,如改变指向则为栈顶元素)
        sections.push(token) // 压栈(入栈)
        
        collector = token[2] = [] // 创建 token [2] 数组以便接收后续数据,同时也改变收集器指向,以便后续数据压入
        break
      case '/': // 特定字符处理
        sections.pop() // 弹栈
        collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens // 重新指定收集器执行
        break
      default:
        collector.push(token) // 向收集器中 push 数据
    }
  })

  return nestedTokens // 返回数据容器
}

renderTemplate.js
import lookup from './lookup'
import parseArray from './parseArray'

/**
 * ! 判断 token[0] 项类型,针对类型作出相应操作
 * * 1. 对 text 类型直接添加
 * * 2. 对 name 类型进行数据查找返回
 * * 3. 对 # 类型进行 parseArray 方法调用,递归解析类型
 *
 * @export
 * @param {String} tokens
 * @param {*} data
 * @return {String}
 */
export default function renderTemplate(tokens, data) {
  let resultStr = '' // 创建字符串容器
  console.log(tokens)
  // 遍历 tokens 判断类型作出相应处理
  for (let i = 0; i < tokens.length; i++) {
    const token = tokens[i]
    if (token[0] === 'text') {
      resultStr += token[1]
    } else if (token[0] === 'name') {
      resultStr += lookup(data, token[1])
    } else if (token[0] === '#') {
      resultStr += parseArray(token, data)
    }
  }

  return resultStr
}

lookup.js
/**
 * ! 为传入字符串匹配对应数据
 * * 1. 对字符串中带 . 且自身不是 . 的字符串进行循环查找数据
 * * 2. 其他字符串直接数据引用返回
 *
 * @export
 * @param {*} dataObj
 * @param {String} keyName
 * @return {*}
 */
export default function lookup(dataObj, keyName) {
  // 判断是否有点符号且自身不能为点符号(防止 . 匹配被舍弃),如有则处理后返回,没有点符号直接访问对象属性返回
  if (keyName.indexOf('.') !== -1 && keyName !== '.') {
    let keyArr = keyName.split('.') // 将字符串转换为数组
    // 引用类型,需一层一层向下寻找
    let resultValue = dataObj
    keyArr.forEach(item => {
      resultValue = resultValue[item] // 当前层数数据保存
    })
    return resultValue // 返回访问的数据
  }
  return dataObj[keyName] // 返回访问的数据
}

parseArray.js
import renderTemplate from './renderTemplate'
import lookup from './lookup'

/**
 * ! 为传入的字符串进行循环操作(递归调用)
 * * 1. 获取 token[1] 的对应数据
 * * 2. 遍历数据,获奖递归调用完成后的 domStr
 *
 * @export
 * @param {String} token
 * @param {*} data
 * @return {String}
 */
export default function parseArray(token, data) {
  let v = lookup(data, token[1]) // 找到 token[1] 在 data 中的数据
  let resultStr = '' // 创建返回字符串容器

  // 遍历数据(次数取决于对象数组长度)
  v.forEach((item, index) => {
    resultStr += renderTemplate(token[2], {
      ...v[index], // 展开数据中的值;每次递归调用时要把对应数组的数据传输过去
      '.': v[index] // 匹配 .
    })
  })

  return resultStr
}

VNode 是什么? 什么是虚拟 DOM

1. VNode是什么

VNode是JavaScript对象, VNode表示Virtual DOM,实际上它只是一层对真实DOM的抽象,以Javascript对象(VNode节点)作为基础的树,用对象的属性来描述节点,最终可以通过一系列操作使这棵树映射到真实环境上,通过render将template 模版描述成VNode,然后进行一系列操作之后形成真实的DOM进行挂载。

(name:Hello Kitty', age: 1,
children: null]

2. 为什么需要 VNode

因为真实 DOM 对象是非常大的,而使用原生 API 去操作 DOM 时,浏览器会从构建 DOM 树从头到尾执行一遍流程,而且更新多个DOM对象并渲染时,无法精确的控制渲染的时机,导致多次修改,多次渲染,大量频繁的DOM操作会使页面速度变慢

3. VNode的优点
  1. 兼容性强,不受执行环境的影响。VNode因为是JS对象,不管Node还是浏览器,都可以统一操作,从而获得了服务端渲染、原生渲染、手写渲染函数等能力
  2. Vue 通过在内存中实现文档结构的虚拟表示来解决此问题,其中虚拟节点(VNode)表示DOM树中的节点。当需要操纵时,可以在虚拟DOM的内存中执行计算和操作,而不是在真实DOM上进行操纵。这自然会更快,并且允许虚拟DOM算法计算出最优化的方式来更新实际DOM结构,一旦计算出,就将其应用于实际的DOM树,这就提高了性能,这就是为什么基于虚拟DOM的框架(例如Vue和React)如此突出的原因

Vue 中如何实现一个虚拟 DOM

  1. 首先要构建一个VNode的类, DOM元素上的所有属性在VNode类实例化出来的对象上都存在对应的属性。例如tag表示一个元素节点的名称,text表示个文本节点的文本, chlidren表示子节点等。将VNode类实例化出来的对象进行分类,例如注释节点、文本节点、元素节点、组件节点、函数式节点、克隆节点
  2. 然后通过编译将模板转成宣染函数render,执行宣染函数render,在其中创建不同类型的VNode类,最后整合就可以得到一个虚拟DOM(vnode)
  3. 最后通过patch将vnode和oldVnode进行比较后,生成真实DOM

Vue 的 diff 算法

Diff算法实现的是最小量更新虚拟DOM。这句话虽然简短,但是涉及到了两个核心要素:虚拟DOM、最小量更新

  • 首先比较一下新旧节点是不是同一个节点(可通过比较sel(选择器)和key(唯一标识)值是不是相同),不是同一个节点则进行暴力删除(注:先以旧节点为基准插入新节点,然后再删除旧节点)
  • 若是同一个节点则需要进一步比较
    1. 完全相同,不做处理
    2. 新节点内容为文本,直接替换完事
    3. 新节点有子节点,这个时候就要仔细考虑一下了:若老节点没有子元素,则直接清空老节点,将新节点的子元素插入即可;若老节点有子元素则就需要按照上述的更新策略进行执行

Vue2 面试题

Vue 页面第一次加载了那几个生命周期钩子

当页面第一次页面加载时会触发 beforeCreate, created, beforeMount,mounted这几个钩子函数

Vue 命名规则

  • 一种是使用链式命名 my-component
  • 一种是使用大驼峰命名 MyComponent
  • 在字符串模板中 和都可以使用
  • 在非字符串模板中最好使用,因为要遵循W3C规范中的自定义组件名(字母全小写且必须包含一个连字符),避免和当前以及未来的HTML元素相冲突(Vue 推荐)

v-if 和 v-show 的区别,使用场景

  • 两种指令可以在判断为 true 时占据页面位置,false 时不占据页面位置
  • 控制手段: v-show 本质上是通过 css-display: none 来控制元素的显示与隐藏,而 v-if 是直接控制删除/添加 DOM 元素的方式来控制显示与隐藏
  • 编译过程: v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件; v-show只是简单的基于css切换
  • 编译条件: v-if 是真正的条件宣染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建(触发生命周期函数)。只有渲染条件为 false 时,并不做操作,直到为 true 才渲染
  • 使用场景: v-if与v-show都能控制dom元素在页面的显示; v-if 相比v-show开销更大的(直接操作dom节点增加与删除); 如果需要非常频繁地切换,则使用v-show较好; 如果在运行时条件很少改变,则使用v-if较好

v-if 为什么不能和 v-for 一起使用

Vue2 处理指令时, v-for 比 v-if 具有更高的优先级; Vue3 处理指令时, v-if 比 v-for 的优先级更高

  1. 我们的使用意图是通过 v-if 来控制循环列表的显示与隐藏,但实际情况会更预想的有很大不同
  2. v-if和v-for同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断)
  3. 如果避免出现这种情况,则在外层嵌套template (页面渲染不生成dom节点) ,在这一层进行v-if判断,然后在内部进行v-for循环

为什么 data 属性必须返回一个函数

组件实例对象data必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染。采用函数的形式, initData 时会将其作为工厂函数都会返回全新 data 对象

动态给vue的data添加一个新的属性时会发生什么?怎样解决?

Vue2 响应式数据是通过 object.defineProperty 将我们 data 中的数据配置了 getter setter而来,我们动态添加的数据并没有添加 getter 和 setter ,此时数据当然不是响应式

解决方案

  1. Vue.set/$set: 通过这个方法可以为制定对象添加响应式数据(注意: 无法向根数据/data 和 组件实例上追加响应式数据)
  2. Object.assign: 直接使用是没有响应式的,这个的做法是合并对象属性,将响应式对象和我们追加的属性进行合并,此时就会触发更新追加数据成功
  3. $forceUpdate: 如果你发现你自己需要在vue中做一次强制更新,99.9%的情况,是你在某个地方做错了事,直接强制重新刷新数据

说说你对vue的mixin的理解,有什么应用场景

本质其实就是一个js对象,它可以包含我们组件中任意功能选项,如data、components、methods, created, computed 等等

全局混入
将对应数据混入到所有组件中,使用全局混入需要特别注意,因为它会影响到每一个组件实例(包括第三方组件) PS:全局混入常用于插件的编写
局部混入
将数据混入制定的组件
使用场景
日常开发中对一些相同或者相似的代码,多次出现,此时可以考虑使用将代码进行抽离,在需要使用此数据的组件进行混入,达到代码复用的目的
合并策略

  • 替换型: 替换型策略有props、methods, inject、computed,就是将新的同名参数替代旧的参数
  • 合并型: 合并型策略是data,通过set方法进行合并和重新赋值,重名数据为纯对象时递归合并数据,否则最终留存组件数据,舍弃混入数据
  • 队列型: 全部生命周期和watch,原理是将函数存入一个数组,然后正序遍历依次执行
  • 叠加型: 叠加型有component, directives, filters,通过原型链进行层层的叠加

说说你对keep-alive的理解是什么

keep-alive是vue中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM
keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们

keep-alive 可以设置以下props属性

  • include -字符串或正则表达式。只有名称匹配的组件会被缓存
  • exclude -字符串或正则表达式。任何名称匹配的组件都不会被缓存
  • max -数字。最多可以缓存多少组件实例

备注: 匹配首先检查组件自身的name选项,如果name选项不可用,则匹配它的局部注册名称(父组件components选项的键值),匿名组件不能被匹配
设置了keep-alive缓存的组件,会多出两个生命周期钩子(activated 组件激活 与 deactivated 组件失活)
缓存后如何获取数据

  1. beforeRouteEnter: 路由前置守卫,每次组件渲染的时候,都会执行beforeRouteEnter
  2. actived: 在keep-alive缓存的组件被激活的时候,都会执行actived 钩子

watch, methods, computed的执行顺序

  1. computed 是在HTML DOM加载后马上执行的,如赋值 (属性将被混入到Vue实例),创建一个新的变量进行使用
  2. methods 则必须要有一定的触发条件才能执行,如点击事件
  3. watch 在 Vue 实例初始化时绑定到 Vue 实例上,必须要有一定的触发条件才能执行,用于观察Vue实例上的数据变动,监听现有数据
  • 默认加载时: 先 computed 再 watch, 不执行 methods
  • 触发某一事件时: 先 computed 再 methods 再到 watch
  • computed vs methods: 计算属性可以缓存,以便复用,而方法不行

总结: 计算属性computed只有在它的相关依赖发生改变时才会重新求值,当有一个性能开销比较大的的计算属性A,它需要遍历一个极大的数组和做大量的计算,然后我们可能有其他的计算属性依赖于A,这时候,我们就需要缓存,每次确实需要重新加载;而不需要缓存时用methods

computed 中的属性名和 data 中的属性名可以相同吗

不能同名,因为不管是 computed 属性名还是 data 数据名还是 props 数据名都会被挂载在 vm 实例 上,因此这三个都不能同名

Vue 中 template 编译的原理

简而言之,就是先转化成AST树,再得到的render函数返回VNode (Vue的虚拟DOM节点)

  • 首先,通过compile编译器把template编译成AST 语法树(abstract syntaxtree 即源代码的抽象语法结构的树状表现形式), compile是createCompiler的返回值, createCompiler是用以创建编译器的。另外compile还负责合并option
  • 然后, AST 会经过generate(将AST 语法树转化成render funtion字符串的过程)得到render函数, render的返回值是VNode, VNode是Vue的虚拟DOM节点,里面有(标签名、子节点、文本等等)

Vue2 兼容什么版本的浏览器

不支持ie8及以下,部分兼容ie9 ,完全兼容10以上,因为Vue的响应式原理是基于 ES5 的 Object. defineProperty() 而这个方法不支持ie8及以下

Vue 解决 SPA 首页卡顿情况

卡顿原因

  • 网络延时问题
  • 资源文件体积过大
  • 资源重复发送请求加载
  • 加载脚本,渲染内容堵塞

解决方案

  1. 减小入口文件体积: 常用的手段是路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加,开启路由懒加载,像Vue这种单页面应用,如果没有应用懒加载,运用webpack打包后的文件将会异常的大,造成进入首页时,需要加载的内容过多,时间过长,会出现长时间的白屏,即使做了loading也是不利于用户体验,而运用懒加载则可以将页面进行划分,需要的时候加载页面,可以有效的分担首页所承担的加载压力,减少首页加载用时
  2. 静态资源本地存储: 前端多使用善用本地存储,对一些长期资源可以使用 localStorage 进行存储
  3. UI 框架按需加载: 根据 UI 框架文档使用按需加载
  4. 组件重复打包: 对多次引用的文件可以考虑抽取成公共文件,避免重复引用
  5. 图片资源的压缩: 图片资源可以考虑使用雪碧图,小图片转换 base64 的方式减少网络请求
  6. 开启 Gzip 压缩: 网络传输数据时进行压缩处理
  7. 使用 SSR: 服务器端渲染技术

<style> 中添加 scoped 属性的原理

1. scoped 是什么

在Vue组件中,为了使样式私有化(模块化),不对全局造成污染,可以在style标签上添加scoped属性以表示它的只属于当下的模块,局部有效。如果一个项目中的所有Vue组件style标签全部加上了scoped,相当于实现了样式的私有化。如果引用了第三方组件,需要在当前组件中局部修改第三方组件的样式,而又不想去除scoped属性造成组件之间的样式污染。此时只能通过穿透scoped的方式来解决

2. scoped 实现原理

Vue 中的 scoped 属性的效果主要通过 PostCSS 转译实现

即; PostCSS 给所有 dom 添加了一个唯一不重复的动态属性,然后,给CSS选择器额外添加一个对应的属性选择器来选择该组件中 dom ,这种做法使得样式私有化

对 SPA 单页面的理解

单页Web应用(single-page application简称为SPA)是一种特殊的Web应用。它将所有的活动局限于一个Web页面中,仅在该Web页面初始化时加载相应的HTML、JavaScript和CSS。一旦页面加载完成了, SPA不会因为用户的操作而进行页面的重新加载或跳转。取而代之的是利用JavaScript动态的变换HTML的内容,从而实现UI与用户的交互。由于避免了页面的重新加载,SPA可以提供较为流畅的用户体验。得益于ajax,我们可以实现无跳转刷新,又多亏了浏览器的histroy机制,我们用hash的变化从而可以实现推动界面变化。从而模拟客户端的单页面切换效果

SPA 的优点

1、无刷新界面,给用户体验原生的应用感觉
2、节省原生(android和ios)app开发成本(小程序)
3、提高发布效率,程序开发无需每次安装更新包
4、容易借助其他知名平台更有利于营销和推广
5、符合web2.0的趋势

SPA 的缺点

1、效果和性能确实和原生的有较大差距
2、各个浏览器的版本兼容性不一样
3、业务随着代码量增加而增加,不利于首屏优化
4、某些平台对hash有偏见,有些甚至不支持pushstate
5、不利于搜索引擎抓取

实现核心

通过监听 url 中 hash 值得变化来进行相应的跳转动作

SPA SEO
  1. SSR 服务器渲染

将组件或页面通过服务器生成 html,再返回给浏览器,如 index.js

  1. 静态化
  1. 一种是通过程序将动态页面抓取并保存为静态页面,这样的页面的实际存在于服务器的硬盘中
  2. 另外一种是通过WEB服务器的URL Rewrite的方式,它的原理是通过web服务器内部模块按一定规则将外部的URL请求转化为内部的文件地址,一句话来说就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。这两种方法都达到了实现URL静态化的效果
  1. 使用Phantomjs针对爬虫处理

原理是通过Nginx配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server,再通过 PhantomJs 来解析完整的HTML,返回给爬虫

Vue 中如何重置 data

要初始化data中的数据,可以使用Object.assign ()方法,实现重置data中的数据

1. Object.assign() 方法基本定义

Object. assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
用法: Object.assign (target, … sources),第一个参数是目标对象,第二个参数是源对象,就是将源对象属性复制到目标对象,返回目标对象

2)具体使用方式
vm.$data // 获取当前状态下的 data
vm.$options.data(this) // 获取组件初始化状态下的 data
Object.assign(this.$data, this.$options.data(this)) // 将初始化状态的 data 数组赋值给现有 data 数组,实现覆盖

Vue 插件

插件通常用来为 Vue 添加全局功能

插件的功能范围没有严格的限制——一般有下面几种

  1. 添加全局方法或者 property;如: vue-custom-element
  2. 添加全局资源: 指令/过滤器/过渡等;如 vue-touch
  3. 通过全局混入来添加一些组件选项;如 vue-router
  4. 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现;
  5. 一个库,提供自己的 API,同时提供上面提到的一个或多个功能;如 vue-router
使用插件

通过全局方法 Vue.use() 使用插件;它需要在你调用 new Vue() 启动应用之前完成

// 调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)

new Vue({
  // ...组件选项
})

也可以传入一个可选的选项对象

Vue.use(MyPlugin, { someOption: true })

注意: Vue.use 会自动阻止多次注册相同插件,届时即使多次调用也只会注册一次该插件

开发插件

Vue.js 的插件应该暴露一个 install 方法;这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象

MyPlugin.install = function (Vue, options) {
  // 1. 添加全局方法或 property
  Vue.myGlobalMethod = function () {
    // 逻辑...
  }

  // 2. 添加全局资源
  Vue.directive('my-directive', {
    bind (el, binding, vnode, oldVnode) {
      // 逻辑...
    }
    ...
  })

  // 3. 注入组件选项
  Vue.mixin({
    created: function () {
      // 逻辑...
    }
    ...
  })

  // 4. 添加实例方法
  Vue.prototype.$myMethod = function (methodOptions) {
    // 逻辑...
  }
}

Vue 过度与动画

在插入、更新或移除 DOM 元素时,在合适的时候给元素添加样式类名

元素进入的样式类

  1. v-enter: 进入的起点
  2. v-enter-active: 进入过程中
  3. v-enter-to: 进入的终点

元素离开的样式类

  1. v-leave: 离开的起点
  2. v-leave-active: 离开过程中
  3. v-leave-to: 离开的终点

使用方法

// 使用<transition>包裹要过度的元素,并配置name属性
<transition name="hello">
    <h1 v-show="isShow">你好啊!</h1>
</transition>

注意: 若配置了 name 属性,则样式指定类名时需将 v- 更换成 name
备注: 若有多个元素需要过度,则需要使用: <transition-group>,且每个元素都要指定key

Vue 组件化编程

解决问题

组件可以提升整个项目的开发效率。能够把页面抽象成多个相对独立的模块,解决了我们传统项目开发:效率低、难维护、复用性等问题。

需求实现
  1. 拆分静态组件: 组件要按照功能点拆分,命名不要与html元素冲突
  2. 实现动态组件: 考虑数据存放位置,是一个组件使用,还是众多组件使用
    • 一个组件: 放置在组件自身
    • 众多组件: 放置到它们共同的父组件中
  3. 实现交互: 针对功能进行方法封装实现
具体使用
  1. 使用 import 导入组件(导入)
  2. 在组件内使用 component 注册局部组件或者使用 Vue.component 注册全局组件(注册)
  3. 在组件内 template 以标签的形式使用(使用)
  4. 数据传递可以使用自定义属性,自定义方法,全局事件总线,Vuex;需要传递模板可以使用 solt 插槽

Vue-loader 的作用

Vue-loader 会解析文件,提取出每个语言块,如果有必要会通过其他 loader 处理,最后将他们组装成一个 commonjs 模块; module.exports 出一个 Vue.js 组件对象

<temlate> 模板
  1. 默认语言: html
  2. 每个 .Vue 文件最多包含一个 块
  3. 内容将被提取为字符串,将编译用作 Vue组件 的 template 选项
  4. 这个标签可以使用 comments 为 True 选项保留模板中的 HTML 注释
<script> 脚本
  1. 默认语言: JS(在监测到babel-loader或者buble-loader配置时,自动支持ES2015)
  2. 每个 .Vue 文件最多包含一个 块
  3. 该脚本在类 CommonJS 环境中执行(就像通过webpack打包的正常JS模块)。所以你可以require()其他依赖。在ES2015支持下,也可以使用import跟export语法
  4. 脚本必须导出 Vue.js 组件对象,也可以导出由 Vue. extend() 创建的扩展,对象;但是普通对象是更好的选择
<style> 样式

默认语言: css
1、一个.Vue文件可以包含多个
2、这个标签可以有 scoped 或者 module 属性来帮助你讲样式封装到当前组件;具有不同封装模式的多个
3、默认情况下,可以使用 style-loader 提取内容,并且通过

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

taciturn丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值