04-手写Vue源码

981 阅读5分钟

Vue工作流程

1.Vue.js是什么

Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。

所谓渐进式,可以一步一步,有阶段性的使用vue,不是必在一开始把所有的东西都用上。
1.声明式的渲染(Declarative Rendering)
2.组件系统(component System)
3.客户端路由器(vue-router)
4.大规模的状态管理(vuex)
5.构建工具(vue-cli)

2.Vue工作机制

vue挂载简图
vue工作机制

  • 初始化init

    new Vue()之后,Vue会调用进行初始化,会初始化生命周期、事件、props、methods、data、computed与watch等。其中最重要的是通过object.defineProperty设置setter与getter,用来实现【响应式】以及【依赖收集】。

  • $mount挂载

    初始化之后,$mount挂载

  • 编译compile

    编译模块分三个阶段:

    • parse:使用正则解析template中的vue的指令(v-xxx)变量等等 形成抽象语法AST
    • optimize:标记一些静态节点,用作后面的性能优化,在diff的时候直接略过
    • generate:把第一部分生成的AST转化为渲染函数render function
  • Render生成虚拟dom

    Virtual DOMreact首创,vue2开始支持,就是用JavaScript对象来描述dom结构,数据修改的时候,我们先修改虚拟dom中的数据,然后数组做diff,最后再汇总所有的diff,力求做最少的dom操作,毕竟js里对比很快,而真实dom操作太慢

    // dom
    <div name="轩轩" style="color:red" @click="xx"> 
        <a> click me</a>
    </div>
    // vdom
    {
        tag: 'div',
        props:{ 
            name:'轩轩',
            style:{color:red},
            onClick:xx 
        }
        children: [
            {
                tag: 'a',
                text: 'click me'
            }
        ]
    }
    
  • 更新视图

    Vue_update 是实例的私有方法,它只在首次渲染和数据更新两种情况下被调用,_update 方法把 VNode 渲染成真实的 DOM(数据修改时监听器会执行更新,通过对比新旧vdom,得到最小修改,就是 patch

理解了上面的整个挂载过程后,这时候再看Vue的生命周期图,非常清晰!

vue生命周期

Vue响应式原理

Object.defineProperty:数据劫持(vue核心)

无论是对象还是数组,需要实现双向绑定的话最终都会执行这个函数,该函数可以监听到 set 和 get 的事件。

vue响应式

-作用
    直接在一个对象上定义一个新属性,或者修改一个对象的现有属性
    
-语法
    Object.defineProperty(obj,prop,descriptor)
    
-参数
    obj要在其上定义属性的对象
    prop要定义或修改的属性名称
    descriptor将被定义或修改的属性描述符
    数据描述:(新添加的都默认为false)
        configurable:是否可以删除目标属性,默认为false
        enumerable:此属性是否可以被枚举,默认为false
        value:改属性对应的值,默认为undefined
        writable:属性的值是否可以被重写,默认为false
    访问器描述:
        getter:是一种忽的属性值的方法
        setter:是一种设置属性值的方法
        可以写configurable、enumerable
        不能写value、writable
    删除对象属性:
        delete data.title
    把属性转成访问器的方式:
        getter:获取属性触发的
        setter:设置属性触发的

实现自己的Vue

vue工作机制

1.mVue.js

需注意Dep与Watcher之间的对应关系,Dep是怎么通知Watcher更新的

// 1.数据响应化
class mVue {
  constructor(options) {
    this.$options = options
    this.$data = options.data
    // 对data数据进行劫持
    this.observe(this.$data)
    // 主动编译
    new mCompile(options.el, this)
    // 执行created
    if (options.created) {
      this.$options.created.call(this)
    }
  }
  observe(obj) {
    // 对传入的data进行判断
    if (!obj || typeof obj !== "object") {
      return
    }
    // 遍历传入的data
    Object.keys(obj).forEach(key => {
      this.defineReactive(obj, key, obj[key])
      // 对data中的数据进行代理,在vue中访问data中的数据时this.name这种方式,而不是this.$data.name
      this.proxyData(key)
    })
  }
  defineReactive(obj, key, val) {
    // 递归:深层嵌套就需要递归
    this.observe(val)

    // data中的每个key对应一个dep
    const dep = new Dep()
    Object.defineProperty(obj, key, {
      get() {
        // 每使用一次数据,就存放一个watcher
        Dep.target && dep.addDep(Dep.target) // 将watcher存放到dep数组中
        return val
      },
      set(newVal) {
        if (val === newVal) return
        val=newVal
        dep.notify()
      }
    })
  }
  proxyData(key) {
    Object.defineProperty(this, key, {
      get() {
        return this.$data[key]
      },
      set(newVal) {
        this.$data[key] = newVal
      }
    })
  }
}

// 2.依赖收集与追踪
// 收集器:data中的每个key对应一个Dep
class Dep {
  constructor() {
    this.deps = []
  }
  addDep(dep) {
    this.deps.push(dep)
  }
  notify() {
    this.deps.forEach(item => item.update())
  }
}

// 监听器:负责更新视图
class Watcher {
  constructor(vm, key, cb) {
    this.key = key
    this.vm = vm
    this.cb = cb
    Dep.target = this
    this.vm[this.key] // 读取一下触发get
    Dep.target = null
  }
  update() {
    this.cb.call(this.vm, this.vm[this.key])
  }
}

2.compile.js

需要注意Watcher是怎么收集依赖的

class mCompile {
  constructor(el, vm) {
    this.$el = document.querySelector(el)
    this.$vm = vm
    // 提取宿主中模板内容到Fragment标签,这样会提高效率
    this.$fragment = this.node2Fragment(this.$el)
    // 编译
    this.compile(this.$fragment)
    // 将编译好的内容插入宿主标签中
    this.$el.appendChild(this.$fragment)
  }
  node2Fragment(el) {
    const fragment = document.createDocumentFragment()
    //将el下的每个节点都赋给child,然后移到新建的fragment中去
    let child
    while ((child = el.firstChild)) {
      fragment.appendChild(child)
    }
    return fragment
  }

  compile(el) {
    //文档里几乎每一样东西都是一个节点,甚至连空格和换行符都会被解释成节点。而且都包含在childNodes属性所返回的数组中。
    // chidNoeds返回的事node的集合,每个node都包含有nodeType属性。
    // nodeType取值:
    // 元素节点:1
    // 属性节点:2
    // 文本节点:3
    // 注释节点:8

    // 所有子节点集合
    const childNodes = el.childNodes

    // 遍历所有子节点
    Array.from(childNodes).forEach(node => {
      // 判断节点类型
      if (node.nodeType === 1) {
        // 元素节点——编译元素节点
        this.compileElement(node)
      } else if (this.isInterpolation(node)) {
        // 插值表达式——编译插值表达式
        this.compileText(node)
      }

      // 递归子节点——如果子节点下面还有子节点,则继续编译
      if (node.childNodes && node.childNodes.length) {
        this.compile(node)
      }
    })
  }
  // 判断是否是插值表达式
  isInterpolation(node) {
    // 文本节点3 并且符合 {{}} 正则表达式
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent) // node.textContent是文本内容{{name}}
  }
  // 编译元素节点
  compileElement(node) {
    // 获取元素节点上的所有属性
    const nodeAttrs = node.attributes
    Array.from(nodeAttrs).forEach(attr => {
      const attrname = attr.name
      const value = attr.value
      // 判断是否是指令
      if (this.isDirective(attrname)) {
        const dir = attrname.substring(2) // 取v-后面的值
        this[dir] && this[dir](node, this.$vm, value, dir)
      }

      // 判断是否是事件
      if (this.isEvent(attrname)) {
        const dir = attrname.substring(1) // 取@后面的值
        this.eventhandler(node, this.$vm, value, dir)
      }
    })
  }
  isDirective(attrname) {
    return attrname.indexOf("v-") === 0
  }

  isEvent(attrname) {
    return attrname.indexOf("@") === 0
  }
  eventhandler(node, vm, eventName, event) {
    // 获取methods中函数
    const fn = vm.$options.methods[eventName]
    if (event && fn) {
      node.addEventListener(event, fn.bind(vm))
    }
  }
  // v-text
  text(node, vm, key, dir) {
    this.update(node, vm, key, dir)
  }
  // v-html
  html(node, vm, key, dir) {
    node.innerHTML = vm[key]
  }
  // v-model
  model(node, vm, key, dir) {
    // data——>view
    this.update(node, vm, key, dir)
    //view——>data
    node.addEventListener("input", e => {
      vm[key] = e.target.value
    })
  }
  // 编译文本节点
  compileText(node) {
    console.log(node)

    this.update(node, this.$vm, RegExp.$1, "text")
  }
  update(node, vm, key, dir) {
    console.log(dir)

    let updateFn = this[dir + "Updater"]
    updateFn && updateFn(node, vm[key])
    // 做依赖收集
    new Watcher(vm, key, function(value) {
      updateFn(node, value)
    })
  }
  textUpdater(node, val) {
    node.textContent = val
  }
  // 修改input中的值
  modelUpdater(node, val) {
    node.value = val
  }
}

3.数组如何实现响应式

Object.defineProperty 的局限性: 如果通过下标方式修改数组数据或者给对象新增属性并不会触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作,更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。(数组会发生变化的7种操作:pop、push、reverse、shift、sort、splice、unshift)

Vue 提供了一个 API 解决:

export function set (target: Array<any> | Object, key: any, val: any): any {
// 判断是否为数组且下标是否有效
  if (Array.isArray(target) && isValidArrayIndex(key)) {
  // 调用 splice 函数触发派发更新
  // 该函数已被重写
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  // 判断 key 是否已经存在
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  // 如果对象不是响应式对象,就赋值返回
  if (!ob) {
    target[key] = val
    return val
  }
  // 进行双向绑定
  defineReactive(ob.value, key, val)
  // 手动派发更新
  ob.dep.notify()
  return val
}

// 获得数组原型
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// 重写以下函数
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  // 缓存原生函数
  const original = arrayProto[method]
  // 重写函数
  def(arrayMethods, method, function mutator (...args) {
  // 先调用原生函数获得结果
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    // 调用以下几个函数时,监听新数据
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // 手动派发更新
    ob.dep.notify()
    return result
  })
})

总结

  • 自定义组件
  • 虚拟dom

以上的代码中,并没有出现虚拟dom,这就是vue1.0,vue1.0最大的问题是一个界面中watcher太多了,界面中的每个动态值都会相对应一个watcher,这个消耗太大了,之后出现了vue2.0,最大的变化就是引入了虚拟dom,为什么要引入虚拟dom呢,因为watcher的粒度变小了,小到了组件级别,每个组件对应一个watcher。

参考

vue源码

响应式原理