vue的第一個commit分析

沐曉發表於2020-06-04

為什麼寫這篇vue的分析文章?

對於天資愚鈍的前端(我)來說,閱讀原始碼是件不容易的事情,畢竟有時候看原始碼分析的文章都看不懂。每次看到大佬們用了1~2年的vue就能掌握原理,甚至精通原始碼,再看看自己用了好幾年都還在基本的使用階段,心中總是羞愧不已。如果一直滿足於基本的業務開發,怕是得在初級水平一直待下去了吧。所以希望在學習原始碼的同時記錄知識點,可以讓自己的理解和記憶更加深刻,也方便將來查閱。

目錄結構

本文以vue的第一次 commit a879ec06 作為分析版本

├── build
│   └── build.js               // `rollup` 打包配置
├── dist                        
│   └── vue.js    
├── package.json
├── src                        // vue原始碼目錄
│   ├── compiler               // 將vue-template轉化為render函式
│   │   ├── codegen.js         // 遞迴ast提取指令,分類attr,style,class,並生成render函式
│   │   ├── html-parser.js     // 通過正則匹配將html字串轉化為ast
│   │   ├── index.js           // compile主入口
│   │   └── text-parser.js     // 編譯{{}}
│   ├── config.js              // 對於vue的全域性配置檔案
│   ├── index.js               // 主入口
│   ├── index.umd.js           // 未知(應該是umd格式的主入口)
│   ├── instance               // vue例項函式
│   │   └── index.js           // 包含了vue例項的初始化,compile,data代理,methods代理,watch資料,執行渲染
│   ├── observer               // 資料訂閱釋出的實現
│   │   ├── array.js           // 實現array變異方法,$set $remove 實現
│   │   ├── batcher.js         // watch執行佇列的收集,執行
│   │   ├── dep.js             // 訂閱中心實現
│   │   ├── index.js           // 資料劫持的實現,收集訂閱者
│   │   └── watcher.js         // watch實現,訂閱者
│   ├── util                   // 工具函式
│   │   ├── component.js
│   │   ├── debug.js
│   │   ├── dom.js
│   │   ├── env.js             // nexttick實現
│   │   ├── index.js
│   │   ├── lang.js
│   │   └── options.js
│   └── vdom
│       ├── dom.js             // dom操作的封裝
│       ├── h.js               // 節點資料分析(元素節點,文字節點)
│       ├── index.js           // vdom主入口
│       ├── modules            // 不同屬性處理函式
│       │   ├── attrs.js       // 普通attr屬性處理
│       │   ├── class.js       // class處理
│       │   ├── events.js      // event處理
│       │   ├── props.js       // props處理
│       │   └── style.js       // style處理
│       ├── patch.js           // node樹的渲染,包括節點的加減更新處理,及對應attr的處理
│       └── vnode.js           // 返回最終的節點資料
└── webpack.config.js          // webpack配置

從template到html的過程分析

我們的程式碼是從new Vue()開始的,Vue的建構函式如下:

constructor (options) {
  // options就是我們對於vue的配置
  this.$options = options
  this._data = options.data
  // 獲取元素html,即template
  const el = this._el = document.querySelector(options.el)
  // 編譯模板 -> render函式
  const render = compile(getOuterHTML(el))
  this._el.innerHTML = ''
  // 例項代理data資料
  Object.keys(options.data).forEach(key => this._proxy(key))
  // 將method的this指向例項
  if (options.methods) {
    Object.keys(options.methods).forEach(key => {
      this[key] = options.methods[key].bind(this)
    })
  }
  // 資料觀察
  this._ob = observe(options.data)
  this._watchers = []
  // watch資料及更新
  this._watcher = new Watcher(this, render, this._update)
  // 渲染函式
  this._update(this._watcher.value)
}

當我們初始化專案的時候,即會執行建構函式,該函式向我們展示了vue初始化的主線:編譯template字串 => 代理data資料/methods的this繫結 => 資料觀察 => 建立watch及更新渲染

1. 編譯template字串

const render = compile(getOuterHTML(el))

其中compile的實現如下:

export function compile (html) {
  html = html.trim()
  // 對編譯結果快取
  const hit = cache[html]
  // parse函式在parse-html中定義,其作用是把我們獲取的html字串通過正則匹配轉化為ast,輸出如下 {tag: 'div', attrs: {}, children: []}
  return hit || (cache[html] = generate(parse(html)))
}

接下來看看generate函式,ast通過genElement的轉化生成了構建節點html的函式,在genElement將對if for 等進行判斷並轉化( 指令的具體處理將在後面做分析,先關注主流程程式碼),最後都會執行genData函式

// 生成節點主函式
export function generate (ast) {
  const code = genElement(ast)
  // 執行code程式碼,並將this作為code的global物件。所以我們在template中的變數將指向為例項的屬性 {{name}} -> this.name 
  return new Function (`with (this) { return ${code}}`)
}

// 解析單個節點 -> genData
function genElement (el, key) {
  let exp
  // 指令的實現,實際就是在模板編譯時實現的
  if (exp = getAttr(el, 'v-for')) {
    return genFor(el, exp)
  } else if (exp = getAttr(el, 'v-if')) {
    return genIf(el, exp)
  } else if (el.tag === 'template') {
    return genChildren(el)
  } else {
    // 分別為 tag 自身屬性 子節點資料
    return `__h__('${ el.tag }', ${ genData(el, key) }, ${ genChildren(el) })`
  }
}

我們可以看看在genData中都做了什麼。上面的parse函式將html字串轉化為ast,而在genData中則將節點的attrs資料進一步處理,例如class -> renderClass style class props attr 分類。在這裡可以看到 bind 指令的實現,即通過正則匹配 : 和 bind,如果匹配則把相應的 value值轉化為 (value) 的形式,而不匹配的則通過JSON.stringify()轉化為字串('value')。最後輸出attrs(key-value),在這裡得到的物件是字串形式的,例如(value)等也僅僅是將變數名,而在generate中通過new Function進一步通過(this.value)得到變數值。

function genData (el, key) {
  // 沒有屬性返回空物件
  if (!el.attrs.length) {
    return '{}'
  }
  // key
  let data = key ? `{key:${ key },` : `{`
  // class處理
  if (el.attrsMap[':class'] || el.attrsMap['class']) {
    data += `class: _renderClass(${ el.attrsMap[':class'] }, "${ el.attrsMap['class'] || '' }"),`
  }
  // attrs
  let attrs = `attrs:{`
  let props = `props:{`
  let hasAttrs = false
  let hasProps = false
  for (let i = 0, l = el.attrs.length; i < l; i++) {
    let attr = el.attrs[i]
    let name = attr.name
    // bind屬性
    if (bindRE.test(name)) {
      name = name.replace(bindRE, '')
      if (name === 'class') {
        continue
      // style處理
      } else if (name === 'style') {
        data += `style: ${ attr.value },`
      // props屬性處理
      } else if (mustUsePropsRE.test(name)) {
        hasProps = true
        props += `"${ name }": (${ attr.value }),` 
      // 其他屬性
      } else {
        hasAttrs = true
        attrs += `"${ name }": (${ attr.value }),`
      }
    // on指令,未實現
    } else if (onRE.test(name)) {
      name = name.replace(onRE, '')
    // 普通屬性
    } else if (name !== 'class') {
      hasAttrs = true
      attrs += `"${ name }": (${ JSON.stringify(attr.value) }),`
    }
  }
  if (hasAttrs) {
    data += attrs.slice(0, -1) + '},'
  }
  if (hasProps) {
    data += props.slice(0, -1) + '},'
  }
  return data.replace(/,$/, '') + '}'
}

而對於genChildren,我們可以猜到就是對ast中的children進行遍歷呼叫genElement,實際上在這裡還包括了對文字節點的處理。

// 遍歷子節點 -> genNode
function genChildren (el) {
  if (!el.children.length) {
    return 'undefined'
  }
  // 對children扁平化處理
  return '__flatten__([' + el.children.map(genNode).join(',') + '])'
}

function genNode (node) {
  if (node.tag) {
    return genElement(node)
  } else {
    return genText(node)
  }
}

// 解析{{}}
function genText (text) {
  if (text === ' ') {
    return '" "'
  } else {
    const exp = parseText(text)
    if (exp) {
      return 'String(' + escapeNewlines(exp) + ')'
    } else {
      return escapeNewlines(JSON.stringify(text))
    }
  }
}

genText處理了text及換行,在parseText函式中利用正則解析{{}},輸出字串(value)形式的字串。

現在我們再看看__h__('${ el.tag }', ${ genData(el, key) }, ${ genChildren(el) })__h__函式

// h 函式利用上面得到的節點資料得到 vNode物件 => 虛擬dom
export default function h (tag, b, c) {
  var data = {}, children, text, i
  if (arguments.length === 3) {
    data = b
    if (isArray(c)) { children = c }
    else if (isPrimitive(c)) { text = c }
  } else if (arguments.length === 2) {
    if (isArray(b)) { children = b }
    else if (isPrimitive(b)) { text = b }
    else { data = b }
  }
  if (isArray(children)) {
    // 子節點遞迴處理
    for (i = 0; i < children.length; ++i) {
      if (isPrimitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i])
    }
  }
  // svg處理
  if (tag === 'svg') {
    addNS(data, children)
  }
  // 子節點為文字節點
  return VNode(tag, data, children, text, undefined)
}

到此為止,我們分析了const render = compile(getOuterHTML(el)),從elhtml字串到render函式都是怎麼處理的。

2. 代理data資料/methods的this繫結

// 例項代理data資料
Object.keys(options.data).forEach(key => this._proxy(key))
// 將method的this指向例項
if (options.methods) {
  Object.keys(options.methods).forEach(key => {
    this[key] = options.methods[key].bind(this)
  })
}

例項代理data資料的實現比較簡單,就是利用了物件的setter和getter,讀取this資料時返回data資料,在設定this資料時同步設定data資料

_proxy (key) {
  if (!isReserved(key)) {
    // need to store ref to self here
    // because these getter/setters might
    // be called by child scopes via
    // prototype inheritance.
    var self = this
    Object.defineProperty(self, key, {
      configurable: true,
      enumerable: true,
      get: function proxyGetter () {
        return self._data[key]
      },
      set: function proxySetter (val) {
        self._data[key] = val
      }
    })
  }
}

3. Obaerve的實現

Observe的實現原理在很多地方都有分析,主要是利用了Object.defineProperty()來建立對資料更改的訂閱,在很多地方也稱之為資料劫持。下面我們來學習從零開始建立這樣一個資料的訂閱釋出體系。

從簡單處開始,我們希望有個函式可以幫我們監聽資料的改變,每當資料改變時執行特定回撥函式

function observe(data, callback) {
  if (!data || typeof data !== 'object') {
    return
  }

  // 遍歷key
  Object.keys(data).forEach((key) => {
    let value = data[key];

    // 遞迴遍歷監聽深度變化
    observe(value, callback);

    // 監聽單個可以的變化
    Object.defineProperty(data, key, {
      configurable: true,
      enumerable: true,
      get() {
        return value;
      },
      set(val) {
        if (val === value) {
          return
        }

        value = val;

        // 監聽新的資料
        observe(value, callback);
        
        // 資料改變的回撥
        callback();
      }
    });
  });
}

// 使用observe函式監聽data
const data = {};
observe(data, () => {
  console.log('data修改');
})

上面我們實現了一個簡單的observe函式,只要我們將編譯函式作為callback傳入,那麼每次資料更改時都會觸發回撥函式。但是我們現在不能為單獨的key設定監聽及回撥函式,只能監聽整個物件的變化執行回撥。下面我們對函式進行改進,達到為某個key設定監聽及回撥。同時建立排程中心,讓整個訂閱釋出模式更加清晰。

// 首先是訂閱中心
class Dep {
  constructor() {
    this.subs = []; // 訂閱者陣列
  }

  addSub(sub) {
    // 新增訂閱者
    this.subs.push(sub);
  }

  notify() {
    // 釋出通知
    this.subs.forEach((sub) => {
      sub.update();
    });
  }
}

// 當前訂閱者,在getter中標記
Dep.target = null;

// 訂閱者
class Watch {
  constructor(express, cb) {
    this.cb = cb;
    if (typeof express === 'function') {
      this.expressFn = express;
    } else {
      this.expressFn = () => {
        return new Function(express)();
      }
    }
    
    this.get();
  }

  get() {
    // 利用Dep.target存當前訂閱者
    Dep.target = this;
    // 執行表示式 -> 觸發getter -> 在getter中新增訂閱者
    this.expressFn();
    // 及時置空
    Dep.taget = null;
  }

  update() {
    // 更新
    this.cb();
  }

  addDep(dep) {
    // 新增訂閱
    dep.addSub(this);
  }
}

// 觀察者 建立觀察
class Observe {
  constructor(data) {
    if (!data || typeof data !== 'object') {
      return
    }
  
    // 遍歷key
    Object.keys(data).forEach((key) => {
      // key => dep 對應
      const dep = new Dep();
      let value = data[key];
  
      // 遞迴遍歷監聽深度變化
      const observe = new Observe(value);
  
      // 監聽單個可以的變化
      Object.defineProperty(data, key, {
        configurable: true,
        enumerable: true,
        get() {
          if (Dep.target) {
            const watch = Dep.target;
            watch.addDep(dep);
          }
          return value;
        },
        set(val) {
          if (val === value) {
            return
          }
  
          value = val;
  
          // 監聽新的資料
          new Observe(value);
          
          // 資料改變的回撥
          dep.notify();
        }
      });
    });
  }
}

// 監聽資料中某個key的更改
const data = {
  name: 'xiaoming',
  age: 26
};

const observe = new Observe(data);

const watch = new Watch('data.age', () => {
  console.log('age update');
});

data.age = 22

現在我們實現了訂閱中心訂閱者觀察者。觀察者監測資料的更新,訂閱者通過訂閱中心訂閱資料的更新,當資料更新時,觀察者會告訴訂閱中心,訂閱中心再逐個通知所有的訂閱者執行更新函式。到現在為止,我們可以大概猜出vue的實現原理:

  1. 建立觀察者觀察data資料的更改 (new Observe)

  2. 在編譯的時候,當某個程式碼片段或節點依賴data資料,為該節點建議訂閱者,訂閱data中某些資料的更新(new Watch)

  3. 當dada資料更新時,通過訂閱中心通知資料更新,執行節點更新函式,新建或更新節點(dep.notify())

上面是我們對vue實現原理訂閱釋出模式的基本實現,及編譯到更新過程的猜想,現在我們接著分析vue原始碼的實現:

在例項的初始化中

// ...
// 為資料建立資料觀察
this._ob = observe(options.data)
this._watchers = []
// 新增訂閱者 執行render 會觸發 getter 訂閱者訂閱更新,資料改變觸發 setter 訂閱中心通知訂閱者執行 update
this._watcher = new Watcher(this, render, this._update)
// ...

vue中資料觀察的實現

// observe函式
export function observe (value, vm) {
  if (!value || typeof value !== 'object') {
    return
  }
  if (
    hasOwn(value, '__ob__') &&
    value.__ob__ instanceof Observer
  ) {
    ob = value.__ob__
  } else if (
    shouldConvert &&
    (isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 為資料建立觀察者
    ob = new Observer(value)
  }
  // 儲存關聯的vm
  if (ob && vm) {
    ob.addVm(vm)
  }
  return ob
}

// => Observe 函式
export function Observer (value) {
  this.value = value
  // 在陣列變異方法中有用
  this.dep = new Dep()
  // observer例項存在__ob__中
  def(value, '__ob__', this)
  if (isArray(value)) {
    var augment = hasProto
      ? protoAugment
      : copyAugment
    // 陣列遍歷,新增變異的陣列方法
    augment(value, arrayMethods, arrayKeys)
    // 對陣列的每個選項呼叫observe函式
    this.observeArray(value)
  } else {
    // walk -> convert -> defineReactive -> setter/getter
    this.walk(value)
  }
}

// => walk
Observer.prototype.walk = function (obj) {
  var keys = Object.keys(obj)
  for (var i = 0, l = keys.length; i < l; i++) {
    this.convert(keys[i], obj[keys[i]])
  }
}

// => convert
Observer.prototype.convert = function (key, val) {
  defineReactive(this.value, key, val)
}

// 重點看看defineReactive
export function defineReactive (obj, key, val) {
  // key對應的的訂閱中心
  var dep = new Dep()

  var property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // 相容原有setter/getter
  // cater for pre-defined getter/setters
  var getter = property && property.get
  var setter = property && property.set

  // 實現遞迴監聽屬性 val = obj[key]
  // 深度優先遍歷 先為子屬性設定 reactive
  var childOb = observe(val)
  // 設定 getter/setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val
      // Dep.target 為當前 watch 例項
      if (Dep.target) {
        // dep 為 obj[key] 對應的排程中心 dep.depend 將當前 wtcher 例項新增到排程中心
        dep.depend()
        if (childOb) {
          // childOb.dep 為 obj[key] 值 val 對應的 observer 例項的 dep
          // 實現array的變異方法和$set方法訂閱
          childOb.dep.depend()
        }

        // TODO: 此處作用未知?
        if (isArray(value)) {
          for (var e, i = 0, l = value.length; i < l; i++) {
            e = value[i]
            e && e.__ob__ && e.__ob__.dep.depend()
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val
      // 通過 getter 獲取 val 判斷是否改變
      if (newVal === value) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 為新值設定 reactive
      childOb = observe(newVal)
      // 通知key對應的訂閱中心更新
      dep.notify()
    }
  })
}

訂閱中心的實現

let uid = 0

export default function Dep () {
  this.id = uid++
  // 訂閱排程中心的watch陣列
  this.subs = []
}

// 當前watch例項
Dep.target = null

// 新增訂閱者
Dep.prototype.addSub = function (sub) {
  this.subs.push(sub)
}

// 移除訂閱者
Dep.prototype.removeSub = function (sub) {
  this.subs.$remove(sub)
}

// 訂閱
Dep.prototype.depend = function () {
  // Dep.target.addDep(this) => this.addSub(Dep.target) => this.subs.push(Dep.target)
  Dep.target.addDep(this)
}

// 通知更新
Dep.prototype.notify = function () {
  // stablize the subscriber list first
  var subs = this.subs.slice()
  for (var i = 0, l = subs.length; i < l; i++) {
    // subs[i].update() => watch.update()
    subs[i].update()
  }
}

訂閱者的實現

export default function Watcher (vm, expOrFn, cb, options) {
  // mix in options
  if (options) {
    extend(this, options)
  }
  var isFn = typeof expOrFn === 'function'
  this.vm = vm
  // vm 的 _watchers 包含了所有 watch
  vm._watchers.push(this)
  this.expression = expOrFn
  this.cb = cb
  this.id = ++uid // uid for batching
  this.active = true
  this.dirty = this.lazy // for lazy watchers
  // deps 一個 watch 例項可以對應多個 dep
  this.deps = []
  this.newDeps = []
  this.depIds = Object.create(null)
  this.newDepIds = null
  this.prevError = null // for async error stacks
  // parse expression for getter/setter
  if (isFn) {
    this.getter = expOrFn
    this.setter = undefined
  } else {
    warn('vue-lite only supports watching functions.')
  }
  this.value = this.lazy
    ? undefined
    : this.get()
  this.queued = this.shallow = false
}

Watcher.prototype.get = function () {
  this.beforeGet()
  var scope = this.scope || this.vm
  var value
  try {
    // 執行 expOrFn,此時會觸發 getter => dep.depend() 將watch例項新增到對應 obj[key] 的 dep
    value = this.getter.call(scope, scope)
  }
  if (this.deep) {
    // 深度watch
    // 觸發每個key的getter watch例項將對應多個dep
    traverse(value)
  }
  // ...
  this.afterGet()
  return value
}

// 觸發getter,實現訂閱
Watcher.prototype.beforeGet = function () {
  Dep.target = this
  this.newDepIds = Object.create(null)
  this.newDeps.length = 0
}

// 新增訂閱
Watcher.prototype.addDep = function (dep) {
  var id = dep.id
  if (!this.newDepIds[id]) {
    // 將新出現的dep新增到newDeps中
    this.newDepIds[id] = true
    this.newDeps.push(dep)
    // 如果已在排程中心,不再重複新增
    if (!this.depIds[id]) {
      // 將watch新增到排程中心的陣列中
      dep.addSub(this)
    }
  }
}

Watcher.prototype.afterGet = function () {
  // 切除key的getter聯絡
  Dep.target = null
  var i = this.deps.length
  while (i--) {
    var dep = this.deps[i]
    if (!this.newDepIds[dep.id]) {
      // 移除不在expOrFn表示式中關聯的dep中watch的訂閱
      dep.removeSub(this)
    }
  }
  this.depIds = this.newDepIds
  var tmp = this.deps
  this.deps = this.newDeps
  // TODO: 既然newDeps最終會被置空,這邊賦值的意義在於?
  this.newDeps = tmp
}

// 訂閱中心通知訊息更新
Watcher.prototype.update = function (shallow) {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync || !config.async) {
    this.run()
  } else {
    // if queued, only overwrite shallow with non-shallow,
    // but not the other way around.
    this.shallow = this.queued
      ? shallow
        ? this.shallow
        : false
      : !!shallow
    this.queued = true
    // record before-push error stack in debug mode
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.debug) {
      this.prevError = new Error('[vue] async stack trace')
    }
    // 新增到待執行池
    pushWatcher(this)
  }
}

// 執行更新回撥
Watcher.prototype.run = function () {
  if (this.active) {
    var value = this.get()
    if (
      ((isObject(value) || this.deep) && !this.shallow)
    ) {
      // set new value
      var oldValue = this.value
      this.value = value
      var prevError = this.prevError
      // ...
      this.cb.call(this.vm, value, oldValue)
    }
    this.queued = this.shallow = false
  }
}

Watcher.prototype.depend = function () {
  var i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}

wtach回撥執行佇列

在上面我們可以發現,watch在收到資訊更新執行update時。如果非同步情況下會執行pushWatcher(this)將例項推入執行池中,那麼在何時會執行回撥函式,如何執行呢?我們一起看看pushWatcher的實現。

// batch.js
var queueIndex
var queue = []
var userQueue = []
var has = {}
var circular = {}
var waiting = false
var internalQueueDepleted = false

// 重置執行池
function resetBatcherState () {
  queue = []
  userQueue = []
  // has 避免重複
  has = {}
  circular = {}
  waiting = internalQueueDepleted = false
}

// 執行執行佇列
function flushBatcherQueue () {
  runBatcherQueue(queue)
  internalQueueDepleted = true
  runBatcherQueue(userQueue)
  resetBatcherState()
}

// 批量執行
function runBatcherQueue (queue) {
  for (queueIndex = 0; queueIndex < queue.length; queueIndex++) {
    var watcher = queue[queueIndex]
    var id = watcher.id
    // 執行後置為null
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > config._maxUpdateCount) {
        warn(
          'You may have an infinite update loop for watcher ' +
          'with expression "' + watcher.expression + '"',
          watcher.vm
        )
        break
      }
    }
  }
}

// 新增到執行池
export function pushWatcher (watcher) {
  var id = watcher.id
  if (has[id] == null) {
    if (internalQueueDepleted && !watcher.user) {
      // an internal watcher triggered by a user watcher...
      // let's run it immediately after current user watcher is done.
      userQueue.splice(queueIndex + 1, 0, watcher)
    } else {
      // push watcher into appropriate queue
      var q = watcher.user
        ? userQueue
        : queue
      has[id] = q.length
      q.push(watcher)
      // queue the flush
      if (!waiting) {
        waiting = true
        // 在nextick中執行
        nextTick(flushBatcherQueue)
      }
    }
  }
}

4. patch實現

上面便是vue中資料驅動的實現原理,下面我們接著回到主流程中,在執行完watch後,便執行this._update(this._watcher.value)開始節點渲染

// _update => createPatchFunction => patch => patchVnode => (dom api)

// vtree是通過compile函式編譯的render函式執行的結果,返回了當前表示當前dom結構的物件(虛擬節點樹)
_update (vtree) {
  if (!this._tree) {
    // 第一次渲染
    patch(this._el, vtree)
  } else {
    patch(this._tree, vtree)
  }
  this._tree = vtree
}

// 在處理節點時,需要針對class,props,style,attrs,events做不同處理
// 在這裡注入針對不同屬性的處理函式
const patch = createPatchFunction([
  _class, // makes it easy to toggle classes
  props,
  style,
  attrs,
  events
])

// => createPatchFunction返回patch函式,patch函式通過對比虛擬節點的差異,對節點進行增刪更新
// 最後呼叫原生的dom api更新html
return function patch (oldVnode, vnode) {
  var i, elm, parent
  var insertedVnodeQueue = []
  // pre hook
  for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()

  if (isUndef(oldVnode.sel)) {
    oldVnode = emptyNodeAt(oldVnode)
  }

  if (sameVnode(oldVnode, vnode)) {
    // someNode can patch
    patchVnode(oldVnode, vnode, insertedVnodeQueue)
  } else {
    // 正常的不復用 remove insert
    elm = oldVnode.elm
    parent = api.parentNode(elm)

    createElm(vnode, insertedVnodeQueue)

    if (parent !== null) {
      api.insertBefore(parent, vnode.elm, api.nextSibling(elm))
      removeVnodes(parent, [oldVnode], 0, 0)
    }
  }

  for (i = 0; i < insertedVnodeQueue.length; ++i) {
    insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i])
  }

  // hook post
  for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
  return vnode
}

結尾

以上分析了vue從template 到節點渲染的大致實現,當然也有某些地方沒有全面分析的地方,其中template解析為ast主要通過正則匹配實現,及節點渲染及更新的patch過程主要通過節點操作對比來實現。但是我們對編譯template字串 => 代理data資料/methods的this繫結 => 資料觀察 => 建立watch及更新渲染的大致流程有了個比較完整的認知。


歡迎到前端學習打卡群一起學習~516913974

相關文章