记录Vue的一些原理

290 阅读5分钟

init

if (options && options._isComponent) {
  // 如果是组件,不需要合并选项
  // 由于合并操作很慢,并且需要特殊处理
  initInternalComponent(vm, options)
} else {
  // 序列化 props,Inject,Directives
  // 合并父类和mixins的选项
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
}

//初始化生命周期相关的标记
initLifecycle(vm)
//初始化事件相关的标记
//如果设置了 listeners, 需要更新父级传下的事件
initEvents(vm)
//初始化渲染相关的标记,如 _vnode _staticTrees slots createElement
//代理 attrs listeners
initRender(vm)
callHook(vm, 'beforeCreate')
//代理 Inject
initInjections(vm) // resolve injections before data/props
//主要是初始化 props data computed methods, 检查key是否重复
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
if (vm.$options.el) {
    vm.$mount(vm.$options.el)
}

template check

if (template) {
  if (typeof template === 'string') {
    // 如果是id
    if (template.charAt(0) === '#') {
      template = idToTemplate(template)
    }
  } else if (template.nodeType) {
    // 如果是原始引用
    template = template.innerHTML
  } 
} else if (el) {
  // 如果没有 `template`
  template = getOuterHTML(el)
}

compiler

compiler主要分成3步

const ast = parse(template.trim(), options)
if (options.optimize !== false) {
  optimize(ast, options)
}
const code = generate(ast, options)

parser

​ 分词

​ 使用正则匹配 标签 属性 文本

wihle(html){
  comment.test(html) //匹配注释
  html.match(doctype)//匹配文档类型
  html.match(endTag) //匹配结束标签
  parseEndTag(endTagMatch[1], curIndex, index)
  parseStartTag()		 //匹配开始标签
  handleStartTag(startTagMatch) //处理标签
}

​ 转换

​ AST 元素节点总共有 3 种类型,

  • 为 1 表示是普通元素
  • 为 2 表示是表达式
  • 为 3 表示是纯文本,注释节点或者是文本节点
//原始的ast starthandler
let element = createASTElement(tag, attrs, currentParent)
preTransforms() //内置转换 
if("v-pre"){
  processRawAttrs() //原始attr
}else {
  processFor(element)
  processIf(element)
  processOnce(element)
  // 处理元素 比如 ref slot is attr
  processElement(element, options)  {
    processKey(element)
    element.plain = !element.key && !element.attrsList.length
    processRef(element)
    processSlot(element)
    processComponent(element)  //is
    transformNode()
    processAttrs(element)
  }
}

transforms 外部扩展预留钩子

ast里有3个transforms钩子,用户外部扩展使用,本质上等同于processXXX

  • preTransformNode
  • transformNode
  • postTransformNode( 这个在endhandler里 )

web平台的版本提供了3个模块

  • class
  • style
  • v-model

optimize

主要是标记静态根 ,为后面的渲染做一些优化。

// first pass: mark all non-static nodes.
markStatic(root)
// second pass: mark static roots.
markStaticRoots(root, false)

静态节点是指没事表达式或者没有v-for、v-if或者有v-once等等

静态根除了本身是一个静态节点外,必须满足拥有 children,并且 children 不能只是一个文本节点

静态根有2种

  • 普通的静态根

    普通的静态根会生成一个独立的渲染函数,保存在 options.staticRenderFns 里。

    需要在render函数里使用 renderStatic ( Vue.prototype._m)缓存静态根,实例缓存在 vm._staticTrees

  • 在 v-for 列表渲染内的静态根

    这种静态根的渲染函数不会保存在 options.staticRenderFns 里,实例也不会缓存在 vm._staticTrees

    在对比新旧节点的时候的时候,vnode.isonce 为 true 的节点,不会生成新的实例

    可以在render函数里使用 markOnce ( Vue.prototype._o) 标记下节点。

 if (isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    vnode.componentInstance = oldVnode.componentInstance
    return
  }

generate code

generate code 就不添加主流程代码,基本就是把所有的东西都处理一遍,下面是v-if的生成方式。

function genIfConditions(){
  const condition = conditions.shift()
  if (condition.exp) {
    return `(${condition.exp})?${
      genTernaryExp(condition.block)
      }:${
      genIfConditions(conditions, state, altGen, altEmpty)
      }`
  } else {
    return `${genTernaryExp(condition.block)}`
  }
}

mount component

​ 渲染组件主要有三个步骤

  1. 生成 vnode
  2. 更新 实例
  3. 收集 依赖
function mountComponent{
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  new Watcher(vm, updateComponent, noop, {
    before() {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
}  

create element

​ 使用 createElement 可以创建 vnode,createElement会当初render的一个参数传入, 或者使用 Vue.prototype.$createElement

​ createElement 首先会先优化参数和规范化子树,再根据tag创建元素vnode或者组件vnode。

function createElement (){
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  
    if (typeof tag === 'string') {
    if (config.isReservedTag(tag)) {
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  }else{
    vnode = createComponent(tag, data, context, children)
  }
}

create component

createComponent 会创建一个组件vnode。组件vnode.children 为空, 但会有个componentInstance,本质上是个Vue实例。

组件默认内置了4类的生命周期函数供diff期间调用。

  • init
  • prepatch
  • insert
  • destroy

diff期间还有其他的生命周期函数

  • create
  • update
  • postpatch
  • remove
  • activate

vue-diff周期生命周期图
)

由于在template中,所以的未知属性都是被当成元素的属性,所以是无法注册额外的生命周期函数,但是render函数中是可以注册组件生命周期函数的。

render(h){
  return h('Component',{
    hook: {
      init(){
        console.log("init")
      }
    }
  })
}

一般情况没有必要注册组件生命周期函数,非要在父级注册组件的生命周期函数,可以这样注册实例的生命周期函数。

render(h){
  return h('Component',{
    hook: {
     on: {
        "hook:created"() {
          console.log("hook:created")
        }
      }
    }
  })
}


组件有4种特殊类型

  • 异步组件 async component
  • 函数组件 functional component
  • 抽象组件 abstract component
  • 递归组件 recyclable component
function createComponent(){
  
  //异步组件 async component
 if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
    if (Ctor === undefined) {
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }
  
  //函数组件 functional component
    if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }
  
  //抽象组件 abstract component
  if (isTrue(Ctor.options.abstract)) {
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }
  // 注册组件生命周期函数
  installComponentHooks(data)
  
  //生成组件的vnode
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  
  //递归组件 recyclable component
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }
  
  return vnode
}

create functional component

函数式组件更像一个闭包函数,有自己的上下文,返回的vnode和普通元素生成的vnode一样,不像组件的vnode那样子代都保存在componentInstance里,并且没有要求只有1个根节点,甚至不会随便编译template。

function createFunctionalComponent(){
  const renderContext = new FunctionalRenderContext(
    data,
    props,
    children,
    contextVm,
    Ctor
  )

  const vnode = options.render.call(null, renderContext._c, renderContext)
  // 克隆,避免复用时使用同一个vnode
  return cloneAndMarkFunctionalResult(vnode)
}

resolve async component

异步组件本质上就是在不同阶阶段返回不同的组件,在状态改变的时候调用Vue.prototype.$forceUpdate更新视图。

function resolveAsyncComponent(){
  //根据不同的状态,返回不同的组件
    if (isTrue(factory.error) && isDef(factory.errorComp)) {
    return factory.errorComp
  }

  if (isDef(factory.resolved)) {
    return factory.resolved
  }

  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
    return factory.loadingComp
  }

  if (isDef(factory.contexts)) {
    factory.contexts.push(context)
  } else {
    const contexts = factory.contexts = [context]
    let sync = true

    const forceRender = () => {
      for (let i = 0, l = contexts.length; i < l; i++) {
        contexts[i].$forceUpdate()
      }
    }
		
    // 可以看出 resolve 和 reject 内部都会强制更新视图
    const resolve = once((res) => {
      factory.resolved = ensureCtor(res, baseCtor)
      if (!sync) {
        forceRender()
      }
    })

    const reject = once(reason => {
      if (isDef(factory.errorComp)) {
        factory.error = true
        forceRender()
      }
    })
		
    const res = factory(resolve, reject)
}

built-in components

keep-alive

keep-alive 本质上就是看是否命中缓存,命中就是取出,没命中就新增

{
  name: 'keep-alive',
  render(){
    const slot = this.$slots.default
    const vnode = getFirstComponentChild(slot)
    const componentOptions = vnode && vnode.componentOptions
		// 可以看出只对组件有效,普通节点没有状态,没必要缓存
    if (componentOptions){
      const { cache, keys } = this
      const key =  vnode.key
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        remove(keys, key)
        keys.push(key)
      } else {
        cache[key] = vnode
        keys.push(key)
        // 缓存上限检测
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }
    }
  }
  
    return vnode || (slot && slot[0])
}

transition

transition的组件本身只是用来接收一些参数,实现过度效果是依靠在diff期间执行create,activate,remove 这3个生命周期函数。

function patch (){
  //...
  if (isDef(vnode.parent)) {
    let ancestor = vnode.parent
    const patchable = isPatchable(vnode)
    while (ancestor) {
      ancestor.elm = vnode.elm
      if (patchable) {
        for (let i = 0; i < cbs.create.length; ++i) {
          // 这里会回调 transition 钩子
          cbs.create[i](emptyNode, ancestor)
        }
      } else {
        registerRef(ancestor)
      }
      ancestor = ancestor.parent
    }
  }
  //...
}

截一段添加css class的代码,给元素添加上class,就可以实现过度效果了。

function create(){
  // 确保是个 Transition 组件
  const data = resolveTransition(vnode.data.transition)
  if (isUndef(data)) {
    return
  }
  
  if (expectsCSS) {
    addTransitionClass(el, startClass)
    addTransitionClass(el, activeClass)
    nextFrame(() => {
      removeTransitionClass(el, startClass)
      if (!cb.cancelled) {
        addTransitionClass(el, toClass)
        if (!userWantsControl) {
          if (isValidDuration(explicitEnterDuration)) {
            setTimeout(cb, explicitEnterDuration)
          } else {
            whenTransitionEnds(el, type, cb)
          }
        }
      }
    })
  }
}  

transition-group

首先区分当前节点中,哪些是原有,哪些的移除,顺便标记下位置和添加过度属性。

render(){
  const prevChildren = this.prevChildren = this.children
  const rawChildren = this.$slots.default || []
  
  for (let i = 0; i < rawChildren.length; i++) {
      const c = rawChildren[i]
      if (c.tag) {
        if (c.key != null && String(c.key).indexOf('__vlist') !== 0) {
          children.push(c)
          map[c.key] = c; 
          (c.data || (c.data = {})).transition = transitionData
        }
      }
    }
  if (prevChildren) {
    const kept = []
    const removed = []
    for (let i = 0; i < prevChildren.length; i++) {
      const c = prevChildren[i]
      c.data.transition = transitionData
      c.data.pos = c.elm.getBoundingClientRect()
      if (map[c.key]) {
        kept.push(c)
      } else {
        removed.push(c)
      }
    }
    this.kept = h(tag, null, kept)
    this.removed = removed
  }
  return h(tag, null, children)
}

再使用修改后的update更新视图,先移除节点,在添加和移动节点。其目的是为了保证diff的稳定性。

beforeMount() {
    const update = this._update
    this._update = (vnode, hydrating) => {
      // 先移除节点
      this.__patch__(
        this._vnode,
        this.kept,
        false, // hydrating
        true // removeOnly (!important, avoids unnecessary moves)
      )
      this._vnode = this.kept
      // 再新增节点和移动节点
      update.call(this, vnode, hydrating)
    }
  },

到这步,由于新增和移除的节点都带有过度属性,在diff算法中会执行transition的生命周期函数实现过度效果,但是移动节点还没有实现过度,并且位置已经被更新。

在 transtion-group 在 updated生命函数处理移动节点,实现了过度效果。

updated() {
    if (!children.length || !this.hasMove(children[0].elm, moveClass)) {
      return
    }
  	
    //执行先前的回调
    children.forEach(callPendingCbs)
    //记录位置
    children.forEach(recordPosition)
    //由于节点已经处在更新后的位置 
    //所以对比前后位置,使用 translate 回归到之前的位置
    children.forEach(applyTranslation)
    //强制触发浏览器重绘
    this._reflow = document.body.offsetHeight

    children.forEach((c) => {
      if (c.data.moved) {
        addTransitionClass(el, moveClass)
       	// 执行回调
    })
  }

hasMove是用来检测是否有移动节点的动画,其原理就是

  1. 克隆一个子节点
  2. 添加上class
  3. 插入文档中
  4. 检测getComputedStyle中有没有过度或者动画属性
  5. 移除文档
function(){hasMove(el, moveClass) {
  const clone = el.cloneNode()
  addClass(clone, moveClass)
  clone.style.display = 'none'
  this.$el.appendChild(clone)
  const info = getTransitionInfo(clone)
  this.$el.removeChild(clone)
  return info.hasTransform
}