vue知识总结

MVVM

model和view层通过中间的vm连接和驱动。model层数据变化会改变视图,view改变通过事件来修改数据。vue参考了MVVM实现了双向绑定,react是MVC,但是vue仍然可以通过parent等操作dom所以不全是mvvm

vue模板解析

1、先将代码转换为AST树

根据正则匹配,从第一个字符开始,筛选过的就删掉继续index++向后匹配。
如果匹配开始标签就放入一个stack中,此时如果匹配到结束标签则出栈对比是否一致,不一致报错

2、优化AST树

找出静态节点并标记,之后就不需要diff了
递归遍历ast树中的节点,如果没有表达式、v-if、v-for等,就标记static为true

3、生成render函数、在使用new Function(with() {})包裹

转换成render函数。编译结束。一定要包裹new Function和with来更改上下文环境

<div id="app"><p>hello {{name}}</p> hello</div>   ==>
new Function(with(this) {_c("div",{id:app},_c("p",undefined,_v('hello' + _s(name) )),_v('hello'))})

4、render函数执行后得到的是虚拟dom

ast是需要吧代码使用正则匹配生成的,然后转换成render,而虚拟dom则是通过render函数直接生成一个对象

所以Vue中的AST和VNode关系如下:
template > ast > render function > 执行 render function > VNode
ast是转换语法(js、html语法转换为ast)两者很相像

初始化data中的proxy

将所有的数据全部代理到this上

for (let key in data) {
    proxy(vm, '_data', key);
}
function proxy(vm,source,key){
    Object.defineProperty(vm,key,{
        get(){
            return vm[source][key]
        },
        set(newValue){
            vm[source][key] = newValue; 
        }
    })
}
vue的双向数据绑定、响应式原理

监听器 Observer ,用来劫持并监听所有属性(转变成setter/getter形式),如果属性发生变化,就通知订阅者
订阅器 Dep,用来收集订阅者,对监听器 Observer和订阅者 Watcher进行统一管理,每一个属性数据都有一个dep记录保存订阅他的watcher。
订阅者 Watcher,可以收到属性的变化通知并执行相应的方法,从而更新视图,每个watcher上都会保存对应的dep
解析器 Compile,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化

vue的双向数据绑定、响应式原理
数据劫持

利用observe方法递归的去劫持,对外也可以使用这个api。使用defineReactive来劫持数据

class Observe{
 constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    // 判断是否为数组,如果是数组则修改__proto__原型。会再原来原型和实例中间增加一层。
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
    //遍历数组,继续调用observe方法。因为数组中有可能有二维数组或者对象
      this.observeArray(value)
    } else {
    // 如果是对象则直接绑定响应式
      this.walk(value)
    }
  }
}

对象的劫持: 不断的递归,劫持到每一个属性。在defineReactive中会继续递归执行let childOb = !shallow && observe(val)方法递归绑定,因为对象中有可能还有对象

 walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
    //直接遍历对象去递归拦截get、set
      defineReactive(obj, keys[i])
    }
  }

数组的劫持:不劫持下标,value.proto = arrayMethods,增加一层原型链重写数组的push、splice等方法来劫持新增的数据。在数组方法中进行派发更新ob.dep.notify()

// 继续遍历数组,再次执行observe来递归绑定值。
 observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }

响应式原理数据劫持,首先执行observe方法new 一个Observe类,其中会判断是数组还是对象

1、如果数据是[1,[2,3],{a:1}],不会去劫持下标。会修改数组的proto修改原型的方法。但是其中的[2,3],{a:1}并没有被监控,所以继续调用observeArray递归调用,其中又递归调用了let childOb = !shallow && observe(val)继续监控

2、如果数据是{a:{b:2},c:3}, 会执行walk去遍历对象执行defineReactive拦截key的get、set。其中会去递归调用observe方法继续递归劫持

依赖收集

渲染watcher的收集:首次渲染:执行完劫持之后,会走挂载流程会new一个渲染watcher,watcher中会立即执行回调render方法,方法中会去创建Vnode需要去数据中取值,就会进入到属性的get方法。会去收集依赖

 Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // 如果有current watcher,会去收集依赖。Dep.target全局只有一个。一个时刻只能更新一个watcher。每次在执行watcher时会先pushStack,等执行完后会去popstack
      if (Dep.target) {
        // 收集属性的依赖,每个属性获取后都会有个dep。这个dep挂在每个属性上。
        // 例如直接this.a = xxx修改属性就可以找到这个属性上的dep更新watcher
        dep.depend()
        // 如果儿子也是对象或者数组会去递归让儿子也收集
        if (childOb) { 
          // 这个dep挂在对象或者数组上。为了给$set或者数组派发更新使用。在 {b:1} 、[1,2,3]上挂dep
          // 例如新增属性,this.$set(obj, b, xxx)或this.arr.push(2)。
          // a:{b:[1,2,3]}、a:{b:{c:1}},先收集a的依赖挂在属性dep上,因为childOb又为object,需要继续收集依赖挂在该对象上
          // 此时如果更新a,则直接找到属性上的dep更新。但是a上如果想新增一个c属性,则需要使用$set。或者数组上push一个。
          // 此时是找不到属性上的dep的,因为该属性是新增的,数组增加一项需要更新watcher。所以需要在对象或者数组的Ob类上挂一个dep方便更新
          childOb.dep.depend()
          // 如果仍然是数组需要持续递归收集
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },

dep中收集watcher

// dep.js
depend () {
    // Dep.target 是此刻唯一准备被收集的watcher
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

// watcher.js
addDep (dep: Dep) {
    const id = dep.id
    // 去重,如果添加过就不需要添加了
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        // dep中保存此watcher
        dep.addSub(this)
      }
    }
  }
派发更新

在set中派发更新,数组是在劫持的方法中派发更新。会执行当前所有dep中的watcher的notify方法更新视图或者数据

set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // this.a如果直接改了引用,需要重新递归劫持属性,例如a:{b:1}  this.a = {c:2}
      childOb = !shallow && observe(newVal)
      // 执行派发更新操作
      dep.notify()
    }
  })
dep中派发更新
notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      // 遍历执行所有watcher的update方法
      subs[i].update()
    }
  }

update () {
    /* istanbul ignore else */
    if (this.lazy) {
      // 如果是computed, 会去执行重新取值操作
      this.dirty = true
    } else if (this.sync) {
      // 如果是同步watcher直接run()会去执行watcher的get()
      this.run()
    } else {
      // 默认watcher都是放入队列中异步执行的
      queueWatcher(this)
    }
  }

export function queueWatcher (watcher: Watcher) {
  // .......调用全局的nextTick方法来异步执行队列

  nextTick(flushSchedulerQueue)
}

watcher都是会异步更新,调用nexttick去更新,为了整合多次操作为一次。提高效率

watch

watch内部会调用$watch创建一个user watcher(设置user为true),等依赖变化执行update方法

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    // $watcher最终也会去new 一个watcher,传入vm实例和检测的key(expOrFn)和watcher的回调(cb)和watcher的设置(deep等设置)
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
    // 如果设置是immediate,则需要立即执行一次cb
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

// watcher.js中会去判断expOrFn 如果是function说明是渲染watcher传入的回调,
if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // 如果传入的是字符串,则将取值函数赋给getter(调用一次就是取值一次),例如watcher中监控的是'a.b.c'则需要从this中一直取到c
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }

依赖收集: 会去执行watcher的getter方法,其实就是去取值,此时获取的值保存起来。执行取值函数会走属性的get进行依赖收集。

watch初始化时会去取值,为了保存下一次变化时的oldvalue

this.value = this.lazy
      ? undefined
      : this.get()

get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      //如果是user watcher的话,执行的就是取值函数其实就是依赖收集过程
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // 如果设置了deep则需要遍历获取子属性进行全部的依赖收集(把子属性都取值一遍)
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

派发更新:更新时会执行watcher的update方法,其中如果设置同步则直接run,如果没有默认放入队列异步更新

update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

run () {
    if (this.active) {
      // run就是重新调用get执行getter,去重新取值,取出来的就是新值
      const value = this.get()
      // 如果是新老值不相同才需要调用user watcher的回调
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        // 取老的值并设置新值
        const oldValue = this.value
        this.value = value
        // 调用user watcher的回调
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
computed

computed会设置lazy为true。并且会执行脏检查,只有当这些依赖变化时才会去重新计算computed的值,获取完之后再设置dirty为false

// 设置lazy为true表示是computed
const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null)
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    // 如果用户传入对象表示自己定义了get函数则使用用户的,没有则直接设置getter
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // 创建一个computed watcher, 初始化时其中不会执行get函数获取值。
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

      if (!(key in vm)) {
        // 定义computed,需要去劫持计算属性的值进行依赖收集。
        defineComputed(vm, key, userDef)
      } else if (process.env.NODE_ENV !== 'production') {
        if (key in vm.$data) {
          warn(`The computed property "${key}" is already defined in data.`, vm)
        } else if (vm.$options.props && key in vm.$options.props) {
          warn(`The computed property "${key}" is already defined as a prop.`, vm)
        }
      }
  }
}

// 定义computed
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
 //  其实就是重新劫持computed的值, sharedPropertyDefinition中有定义的get函数
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

收集依赖: 创建computed时,需要对computed的变量也进行劫持,如果页面中使用到了这个计算属性,则会走下面的createComputedGetter 创建的get方法。之后会去收集依赖。

// 创建劫持computed的get函数
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 如果dirty为true才取值,创建时默认第一次是true,会去执行get方法
      if (watcher.dirty) {
        watcher.evaluate()
      }
      // 如果有target则去收集依赖。firstName和lastName收集渲染依赖, 计算属性上不需要收集渲染watcher,因为如果页面中使用到了这个计算属性,计算属性是根据函数中依赖变化计算的,所以其中任何一个依赖都需要收集一下渲染watcher,因为任何一个变化都有可能导致重新渲染
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

// 取值
evaluate () {
  // 执行get方法会去取值,例如:return this.firstName + this.lastName,此时也是对依赖firstName和lastName的取值收集依赖的过程,那么他们也会将当前的computed watcher添加到dep的sub队列中。取值完置换成false
  this.value = this.get()
  this.dirty = false
}

所以如果计算属性中写了data中其他的值也会使他进行收集依赖,浪费性能

let vm = new Vue({
    el:'#app',
    data: {
            firstName: 'super',
            lastName: 'kimi',
            kimi: 888
    },
    computed: {
       fullName() {
          // 最后返回没有kimi但是打印进行取值了,他就会收集computed和渲染watcher
           console.log(this.kimi)
           return `${this.firstName}-${this.lastName}`
       }
    }
 })
// 如果更新了kimi也会让视图重新渲染
vm.kimi = 999

派发更新:如果此时改变了firstName的值,因为firstName之前收集依赖中有依赖他的computed watcher和渲染watcher,会去执行两个watcher上的update方法

update () {
    // 如果是计算属性则设置dirty为true即可,之后再去执行渲染watcher的update会重新渲染,那就会重新取计算属性的值,到时候就可以取到最新的值了
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
provide、inject

provide是定义在当前实例上,inject会去遍历$parent找到谁定义了,然后再转成响应式挂在当前实例,只是单向

nextTick

优雅降级,先使用promise,如果不支持会使用MutationObserver,不兼容再使用setImmediate,最后降级成setTimeout

Vue.$set

1、对象会重新递归添加响应式,数组则会调用splice方法,方法已经被劫持
2、执行ob.dep.notify(),让视图更新

diff算法

1、首先比对标签 <div>...</div> --> <ul></ul>

在diff过程中会先比较标签是否一致,如果标签不一致用新的标签替换掉老的标签

 // 如果标签不一致说明是两个不同元素
 if(oldVnode.tag !== vnode.tag){
    oldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el)
 }

如果标签一致,有可能都是文本节点,那就比较文本的内容即可

// 如果标签一致但是不存在则是文本节点
if(!oldVnode.tag){
    if(oldVnode.text !== vnode.text){
        oldVnode.el.textContent = vnode.text;
    }
}

2、对比属性<div>...</div> --> <div className=‘aaa’>...</div>
当标签相同时,我们可以复用老的标签元素,并且进行属性的比对。只需要把新的属性赋值到老的标签上即可

3、对比子元素<div><p>a</p></div> -> <div><p>b</p></div>

[1]新老都有孩子需要updateChildren比对
[2]新有老没有则需要遍历插入
[3]新没有老有则需要删除即可

// 比较孩子节点
let oldChildren = oldVnode.children || [];
let newChildren = vnode.children || [];
// 新老都有需要比对儿子
if(oldChildren.length > 0 && newChildren.length > 0){
    updateChildren(el, oldChildren, newChildren)
    // 老的有儿子新的没有清空即可
}else if(oldChildren.length > 0 ){
    el.innerHTML = '';
// 新的有儿子
}else if(newChildren.length > 0){
    for(let i = 0 ; i < newChildren.length ;i++){
        let child = newChildren[i];
        el.appendChild(createElm(child));
    }
}

4、updateChildren 核心

设置四个index:oldS、oldE、newS、newE

<1>先比对oldS和newS,通过判断sameNode()方法比对key和tag等。如果匹配相等则oldS和newS都++,节点复用即可 例如:ABCD -> ABCE

// 优化向后追加逻辑
if(isSameVnode(oldStartVnode,newStartVnode)){
    patch(oldStartVnode,newStartVnode); // 递归比较儿子
    oldStartVnode = oldChildren[++oldStartIndex];
    newStartVnode = newChildren[++newStartIndex];    
}
对比过程

<2>oldS和newS如果不相等再比对oldE和newE,通过判断sameNode()方法比对key和tag等。如果匹配相等则oldE和newE都--,节点复用即可 例如:ABCD -> EBCD

// 优化向前追加逻辑
else if(isSameVnode(oldEndVnode,newEndVnode)){ 
    patch(oldEndVnode,newEndVnode); // 递归比较孩子 
    oldEndVnode = oldChildren[--oldEndIndex];
    newEndVnode = newChildren[--newEndIndex];
}
对比2

<3>oldE和newE如果不相等再比对oldS和newE,通过判断sameNode()方法比对key和tag等。如果匹配相等则oldS++和newE--,将old节点插入到最后 例如:ABCD -> BCDA

// 头移动到尾部 
else if(isSameVnode(oldStartVnode,newEndVnode)){
    patch(oldStartVnode,newEndVnode); // 递归处理儿子
    parent.insertBefore(oldStartVnode.el,oldEndVnode.el.nextSibling);
    oldStartVnode = oldChildren[++oldStartIndex];
    newEndVnode = newChildren[--newEndIndex]
}
对比3

<4>oldS和newE如果不相等再比对oldE和newS,通过判断sameNode()方法比对key和tag等。如果匹配相等则oldE--和newS++,将old节点插入到最前 例如:ABCD -> DABC

// 尾部移动到头部
else if(isSameVnode(oldEndVnode,newStartVnode)){
    patch(oldEndVnode,newStartVnode);
    parent.insertBefore(oldEndVnode.el,oldStartVnode.el);
    oldEndVnode = oldChildren[--oldEndIndex];
    newStartVnode = newChildren[++newStartIndex]
}
对比4

<5>如果使用index都判断节点不相同,则需要建立vnode的key-index map表,然后匹配map表,如果能匹配上挪到当前oldS前面,如果匹配不上则创建新节点往当前oldS前面插入,newS++ 例如:ABCD -> CDME

// 建立key-index的map表
function makeIndexByKey(children) {
    let map = {};
    children.forEach((item, index) => {
        map[item.key] = index
    });
    return map; 
}
let map = makeIndexByKey(oldChildren);

// 在map表中寻找有没有key匹配的vnode
let moveIndex = map[newStartVnode.key];
if (moveIndex == undefined) { // 老的中没有将新元素插入
    parent.insertBefore(createElm(newStartVnode), oldStartVnode.el);
} else { // 有的话做移动操作
    let moveVnode = oldChildren[moveIndex]; 
    oldChildren[moveIndex] = undefined;
    parent.insertBefore(moveVnode.el, oldStartVnode.el);
    patch(moveVnode, newStartVnode);
}
newStartVnode = newChildren[++newStartIndex]

图中第一步比对index都不同,则开始比对key发现有C相同则把C挪到最前面,newS++;下来发现D有相同的把D挪到oldS前面,newS++;接着M找不到则插入oldS前面,newS++;最后E找不到则插入前面,newS++;

对比5

<6>全部比对完后需要对当前index进行检查,因为有可能有多或者少节点的情况

if (oldStartIdx > oldEndIdx) {
    // oldNode先扫完说明new有多余,需要添加进去
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else if (newStartIdx > newEndIdx) {
    // newNode先扫完说明old有多余,需要删除掉
    removeVnodes(oldCh, oldStartIdx, oldEndIdx);
}

diff对开头、结尾插入删除节点&头节点移到尾部&尾节点移到头部有很大的优化

key:为了高效复用
图例

上图原有A B C D E 现在插入F 变为A B F C D E

如果没有key:首先比对头和头,A、B都复用,比到C和F时,tag一样key相同(都为undefined)则会复用,会成下图情况

image.png

如果有key:比对到C和F时,C和F的key不相同所以跳过,此时就该比oldE和newE,EDC都相同,多下来的F直接插入

image.png

如果key使用index,遇到表单元素比如带checkbox的列表,如果状态勾选后,会复用勾选状态产生bug

keep-alive组件

会将组件缓存到this.cache中,放入内存中缓存起来

mixins缺点

不可知,不易维护。因为你可以在mixins里几乎可以加任何代码,props、data、methods、各种东西,就导致如果不了解mixins封装的代码的话,是很难维护的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容

  • keep-alive 组件有什么作用? keep-alive 是 vue 的内置组件,一般情况下,组件进行切换的时...
    喜喜喜喜喜阅读 729评论 0 1
  • MVVM model和view层通过中间的vm连接和驱动。model层数据变化会改变视图,view改变通过事件来修...
    Super曲江龙Kimi阅读 699评论 0 1
  • Vue是一款高度封装的、开箱即用的、一栈式的前端框架,既可以结合webpack进行编译式前端开发,也适用基于gul...
    Hebborn_hb阅读 1,088评论 0 31
  • 前端常见的一些问题 1.前端性能优化手段? 1. 尽可能使用雪碧图2. 使用字体图标代替图片3. 对HTML,cs...
    十八人言阅读 1,119评论 0 1
  • 生命周期函数面试题 1.什么是Vue生命周期?vue生命周期是指vue是对象从创建到销毁的过程。 2.Vue生命周...
    Angel_6c4e阅读 16,676评论 2 51