Vue原始碼解讀一

Qin菇涼發表於2019-01-08

前言

作為一個vue愛好學習者,也加入了原始碼解讀的學習陣營,對一個vue和react框架都用過的前端妹子來說,還是更喜歡寫vue的語法,現在也很主流,一直想研究一下vue框架背後的實現機制,對api掌握、資料驅動、資料更新、以及元件等有個更全面的認識、而不僅僅侷限於會用它,現在就當做記錄一下自己的理解,會持續更新~


1:Vue的本質:

其實就是一個用Function實現的Class,通過它的原型prototype以及它本身擴充套件的一系列的方法和屬性,所以一般我們會在main.js中會先new Vue一個例項物件出來,否則會報錯warn('Vue is a constructor and should be called with the new keyword')

在src/core/instance/index.js程式碼如下

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

2:核心思想:資料驅動

所謂的資料驅動,是指檢視是由資料驅動生成的,對檢視的修改,不再直接操作DOM,而是通過修改資料。我們所關心的只是資料的修改,DOM變成了資料的對映。

3:初始化主要乾的幾件事情

合併配置,初始化生命週期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher

4:new Vue的時候做的事情

做了一層initState()的方法,給設定了data屬性,會執行getData()方法,這裡會先對data進行判斷,是不是一個function,程式碼如下

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
function initData (vm: Component) {
  let data = vm.$options.data
  // 這裡判斷data是不是一個function
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    // 會報錯給我們我們data未初始換成一個物件的錯誤
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }

注意:在迴圈遍歷物件屬性時,會對props和data進行一層判斷,二者不能重名,因為最後都會掛載到vm物件上,然後對vm物件進行一層proxy代理,下面的程式碼很重要

 // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      //會報props和data重名一樣的警告
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      // 進行資料代理操作
      proxy(vm, `_data`, key)
    }
  }
// 將vm物件用_data進行代理,收集和觸發更新依賴
proxy(vm, `_data`, key)
export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

這裡的proxy,通過Object.defineProtery可以做到給原型去做代理,get()方法收集依賴、set()方法去觸發更新,所以比如在mounted()時,例如列印一個console.log(this.messags)和console.log(this._data.message)是一樣的結果,實際上訪問的就是vm._data.message

接著el設定了之後,進行mount函式處理,即mount鉤子函式

if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }

5:Vue.prototype.$mount函式中,說明的幾個點

Vue不能掛載到body或html這樣的根節點上,一般都用div巢狀包括起來,會被覆蓋,Vue2.0版本中,所有的vue元件渲染最終都需要rendr方法,不論寫的是el或者template屬性,最終都會轉換陳render方法,即"線上編譯的過程"

// 原型上新增$mount方法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  // 若el掛載到body或者html上會報如下警告
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }
 const options = this.$options
  // resolve template/el and convert to render function
   // 如果是已經render()的話,不必再compile()
  if (!options.render) {
    let template = options.template
    if (template) {
        .....
    }
  }
 // 如果是template模板,需要進行compile解析
 if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
  }
// 最後會建立DOM元素,在這裡內容進行覆蓋,這也是為什麼外層一般要有一個父級div包裹它,而不是寫在body或html上,實際上template會走一個compileToFunctions的過程
function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

Vue.compile = compileToFunctions

_render():Vue例項的一個私有方法,它用來把例項渲染成一個虛擬Node,用一個原生的JS物件去描述一個DOM節點,會比建立一個DOM的代價要小很多,這裡和react的思想是一樣的

onstructor (
    tag?: string, // vNode的標籤,例如div、p等標籤
    data?: VNodeData,         // vNode上的的data值,包括其所有的class、attribute屬性、style屬性已經繫結的時間
    children?: ?Array<VNode>, // vNode上的子節點
    text?: string,        // 文字
    elm?: Node,           // vNode上對應的真實dom元素
    context?: Component,  //vdom的上下文
    componentOptions?: VNodeComponentOptions
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.functionalContext = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

上面是VNode的初始化,然後Vue它是通過createElement方法建立的VNode

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
// 注意:這裡會先進行一層判斷,進行屬性值前移,該方法可以借鑑在實際專案中
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  // _createElement()是它的私有方法,建立成一個VNode,每個 VNode 有 children,children 每個元素也是一個 VNode,這樣就形成了一個 VNode Tree
  return _createElement(context, tag, data, children, normalizationType)
}

6:Vue.prototype._update(重要重要)

目的是為了把vNode轉換為真實的DOM,_update會再首次渲染和資料更新的時候去呼叫,核心方法其實是其中的_patch()方法

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const prevActiveInstance = activeInstance
  activeInstance = vm
  // 建立一個新的vNode
  vm._vnode = vnode
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates
    // 和之前的vNode,進行diff,將需要更新的dom操作和已經patch的vNode大道需要更新的vNode,完成真實的dom操作
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  activeInstance = prevActiveInstance
  // update __vue__ reference
  if (prevEl) {
    prevEl.__vue__ = null
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
  // if parent is an HOC, update its $el as well
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el
  }
  // updated hook is called by the scheduler to ensure that children are
  // updated in a parent's updated hook.
}

看一下_patch裡面做了什麼

// 定義了生命週期,這些鉤子函式
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

export function createPatchFunction (backend) {
  let i, j
  const cbs = {}

  const { modules, nodeOps } = backend

  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }

  // ...

  // oldVnode:舊的VNode節點or DOM物件
  // vnode: 執行了_render()之後範湖的VNode的節點
  // hydrating:是否是服務端渲染,因為patch是和平臺相關的,在Web和Weex環境下,把VNode對映到平臺DOM的方法也是不同(有它自己的nodeOps和modules)
  // removeOnly: 給transition-group用的
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      // 建立新的節點
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        // oldVNode和vnode進行diff,並對oldVnode打patch
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
      } else {
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // create new node
        // createElm的作用:通過傳入的VNode去建立真是的DOM元素,並插圖到它的父節點中,
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
         ...
        }

        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes(parentElm, [oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }
    //執行所有created的鉤子並把vnodepush到insertedVnodeQueue 中
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}

其中對oldVNode和vnode型別判斷中有一個sameVnode方法,這個方法很重要,是oldVNode和vnode需要進行diff和patch的前提

function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

注意:insert()方法把DOM插入到父節點中,進行了遞迴呼叫,子元素會優先呼叫 insert,所以整個 vnode 樹節點的插入順序是先子後父

insert(parentElm, vnode.elm, refElm)

function insert (parent, elm, ref) {
  if (isDef(parent)) {
    if (isDef(ref)) {
      if (ref.parentNode === parent) {
        nodeOps.insertBefore(parent, elm, ref)
      }
    } else {
      nodeOps.appendChild(parent, elm)
    }
  }
}
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  parentNode.insertBefore(newNode, referenceNode)
}

export function appendChild (node: Node, child: Node) {
  node.appendChild(child)
}

所以在patch的過程中,會有這個問題丟擲來

 if (isDef(tag)) {
      if (process.env.NODE_ENV !== 'production') {
        if (data && data.pre) {
          creatingElmInVPre++
        }
        // 忘記註冊元件的時候,會經常遇到如下報錯,這個剛開始的時候遇到的情況很多
        if (isUnknownElement(vnode, creatingElmInVPre)) {
          warn(
            'Unknown custom element: <' + tag + '> - did you ' +
            'register the component correctly? For recursive components, ' +
            'make sure to provide the "name" option.',
            vnode.context
          )
        }
      }
    .....
}

可以看到最終返回的是一個patch()方法,賦值給vm.__patch__()方法
在createElm過程中,可以看到如果vnode節點不包含tag的話,它有可能是一個註釋或者純文字節點,可以直接插入到父元素中,遞迴建立一個完整的DOM並插入到body中。

總結

對資料渲染的過程有了更深的一層理解,從new Vue()開始,建立了一個vue是物件,會先進行init初始化——>$mount()——>compile(若已經是render則該過程不需要)——>render——>建立VNode——>patch過程——>生成真實的DOM

相關文章