vue2首次渲染和更新

83 阅读5分钟

Vue版本2.7.4

// 在git下载vue仓库,修改package.json
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:full-dev"

运行yarn dev,dist文件会多一个vue.js.map

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
    <script src="../../dist/vue.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script>
      new Vue({
        el: document.getElementById('root'),
        template: `<div class="container"><span>n:{{name}},</span><span>a:{{age}}</span><button @click="fnAdd">+</button></div>`,
        data() {
          return { name: 'tt', age: 0 }
        },
        methods: {
          fnAdd() {
            this.age++
          }
        }
      })
    </script>
  </body>
</html>

beforeCreate

主要任务是处理数据,initInjections、initState、initProvide都是在这个阶段执行的

initState

  1. initProps:把props的属性都代理到_props
  2. initMethods:把方法都挂到vm实例上,并bind
  3. initData:执行data函数,结果被挂到了_data上,将data的各个属性值代理到_data
  4. 初始化computed和watch
data = vm._data = isFunction(data) ? getData(data, vm) : data || {}

Observer data每个属性,每层对象都添加一个ob实例

export class Observer {
  dep: Dep
  vmCount: number // number of vms that have this object as root $data

  constructor(public value: any, public shallow = false, public mock = false) {
    // this.value = value
    this.dep = mock ? mockDep : new Dep()
    this.vmCount = 0
    // 对象的隐式属性__ob__指向Observer实例
    def(value, '__ob__', this)
    if (isArray(value)) {
      ...
      if (!shallow) {
        this.observeArray(value)
      }
    } else {
      const keys = Object.keys(value)
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i]
        // defineReactive对象的每个属性
        defineReactive(value, key, NO_INIITIAL_VALUE, undefined, shallow, mock)
      }
    }
  }
  // defineReactive数组的每个值
  observeArray (value: any[]) {
    for (let i = 0, l = value.length; i < l; i++) {
      observe(value[i], false, this.mock)
    }
  }
}

defineReactive作用就是用Object.defineProperty对属性的get/set方法做拦截。由于get和set内部会用到dep和obj,所以他们会被一直存在内存里

created

执行实例的$mount: 这个mount是临时定义的,功能是编译template

  • 首先检查是否定义render,如果有不再执行编译过程
  • 如果没有会把定义的template作为参数执行compileToFunctions

vue-template-compiler

初始化一个项目,引入vue-template-compiler。就可以查看template中定义的html片段的最终产物

// 插值
const template = `<p>{{message}}</p>`
// with (this) { return _c('p', [_v(_s(message))]) }

// 表达式
const template = `<p>{{flag ? message : 'no message found'}}</p>`
// with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}

// 属性和动态属性
const template = `
    <div id="div1" class="container">
        <img :src="imgUrl"/>
    </div>
`
// with(this){return _c('div',{staticClass:"container",attrs:{"id":"div1"}},[_c('img',{attrs:{"src":imgUrl}})])}

// 条件判断
const template = `
    <div>
        <p v-if="flag === 'a'">A</p>
        <p v-else>B</p>
    </div>
`
// with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}

// 循环
const template = `
    <ul>
        <li v-for="item in list" :key="item.id">{{item.title}}</li>
    </ul>
`
// with(this){return _c('ul',_l((list),function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}),0)}

// 事件
const template = `
    <button @click="clickHandler">submit</button>
`
// with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}

// v-model
const template = `<input type="text" v-model="name">`
// with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})}

编译过程

// template
<div class="container"><span>n:{{name}},</span><span>a:{{age}}</span><button @click="fnAdd">+</button></div>
  1. 将template解析成类似抽象语法树的ast对象
  2. 优化ast树,检不需要更新的节点,比如button的文本节点,会被定义static: true
  3. 把ast对象转成一段字符串,经过new Function(code),就是直接执行的函数语句。这个函数就是render,会被挂到vm.$options
export const createCompiler = createCompilerCreator(function baseCompile(
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
// 最终产物
with(this){return _c('div',{staticClass:"container"},[_c('span',[_v("n:"+_s(name)+",")]),_c('span',[_v("a:"+_s(age))]),_c('button',{on:{"click":fnAdd}},[_v("+")])])}

with代码块中访问变量,访问的就是this的,this就是vue实例。_c就是createElement

// 下划线小写方法就是对函数的映射
function installRenderHelpers(target) {
    target._o = markOnce;
    target._n = toNumber;
    target._s = toString;
    target._l = renderList;
    target._t = renderSlot;
    target._q = looseEqual;
    target._i = looseIndexOf;
    target._m = renderStatic;
    target._f = resolveFilter;
    target._k = checkKeyCodes;
    target._b = bindObjectProps;
    target._v = createTextVNode;
    target._e = createEmptyVNode;
    target._u = resolveScopedSlots;
    target._g = bindObjectListeners;
    target._d = bindDynamicKeys;
    target._p = prependModifier;
}

有了render,接着调用mount,这个是原型上的,和一开始那个mount,这个是原型上的,和一开始那个mount不是同一个

// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

beforeMount

// 定义实例的更新函数
updateComponent = () => {
    vm._update(vm._render(), hydrating)
}

// watcher会把更新函数存起来,初始化和更新时会被调用
new Watcher(
    vm,
    updateComponent,
    noop,
    watcherOptions,
    true /* isRenderWatcher */
)

依赖收集

Watcher实例化后,render函数会被立即执行

(function anonymous () {
  with (this) {
    return _c('div', { staticClass: "container" }, [_c('span', [_v("n:" + _s(name) + ",")]), _c('span', [_v("a:" + _s(age))]), _c('button', { on: { "click": fnAdd } }, [_v("+")])])
  }
})

render执行过程会访问到变量,触发属性的get方法

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    // Dep.target指向当前watcher实例
    if (Dep.target) {
      if (__DEV__) {
        // defineReactive时产生的dep,对象每个属性都有一个dep
        dep.depend({
          target: obj, // 属性所在的对象,和dep一样,都是defineReactive过程产生的
          type: TrackOpTypes.GET,
          key
        })
      } else {
        dep.depend()
      }
      ...
  }
})

depend(info ?: DebuggerEventExtraInfo) {
  if (Dep.target) {
    // 当前属性的dep会被添加到当前的watcher实例中
    Dep.target.addDep(this)
    ...
  }
}

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)
    }
  }
}

render的执行过程,就是让watcher和被访问到的属性的dep互相建立联系,产物就是vnode。接着调用_update里的vm.__patch__方法,将vnode转为dom

首次渲染

// 真实节点被转成没有属性和子节点的vnode,elm指向真实节点
oldVnode = emptyNodeAt(oldVnode)

// replacing existing element
const oldElm = oldVnode.elm
// 找到根节点的父节点
const parentElm = nodeOps.parentNode(oldElm)

// create new node
createElm(
  vnode, // 新的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) // 根节点端的下一个兄弟节点
)
// vnode -> dom的过程,有children会递归调用,深度遍历
function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm?: any,
  refElm?: any,
  nested?: any,
  ownerArray?: any,
  index?: any
) {
  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  // 有tag,创建dom
  if (isDef(tag)) {
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)
    // 遍历children,根据子的vode创建真实dom,依次插入父dom
    createChildren(vnode, children, insertedVnodeQueue)
    if (isDef(data)) {
      invokeCreateHooks(vnode, insertedVnodeQueue)
    }
    // 当前dom,插入父dom
    insert(parentElm, vnode.elm, refElm)

    if (__DEV__ && data && data.pre) {
      creatingElmInVPre--
    }
  } else if (isTrue(vnode.isComment)) { // 注释
    vnode.elm = nodeOps.createComment(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  } else {
    // 文本节点
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}
// 执行创建时期的钩子函数,主要功能是把data中的属性,追加到dom上
function invokeCreateHooks (vnode, insertedVnodeQueue) {
  // updateAttrs、updateClass、updateDOMListeners、updateDOMProps、updateStyle、_enter、create、updateDirectives
  for (let i = 0; i < cbs.create.length; ++i) {
    cbs.create[i](emptyNode, vnode)
  }
  i = vnode.data.hook // Reuse variable
  if (isDef(i)) {
    if (isDef(i.create)) i.create(emptyNode, vnode)
    if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
  }
}

最终root节点是被替换了,说明root的作用就是找到父节点和下一个兄弟节点,首次渲染过程结束,触发mounted

beforeUpdate

只要修改被页面用到的data的属性值,会触发set

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter() {
    ...
  },
  set: function reactiveSetter(newVal) {
    const value = getter ? getter.call(obj) : val
    // 比对新旧值是否相等
    if (!hasChanged(value, newVal)) {
      return
    }
    if (__DEV__) {
      // dep触发通知方法
      dep.notify({
        type: TriggerOpTypes.SET,
        target: obj,
        key,
        newValue: newVal,
        oldValue: value
      })
    } else {
      // 就是调用dep.subs里的所有watcher的update方法
      dep.notify()
    }
  }
})

watcher queue

watcher的update并不会被立即执行,而是被queueWatcher(this),queueWatcher函数的功能:

  1. 去重,已经在queue里的watcher,不在进入队列
  2. 当前正在处理watcher,也不在进入队列。Dep有个static属性target,指向当前watcher
  3. flushing为false,表示没有正在处理watcher,就把当前wathcer push到队列
  4. 将调度方法,放到nextTick里。nextTick(flushSchedulerQueue),等待(宏/微)任务队列
// 执行被nextTick的回调函数
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
// 任何使用nextTick的函数,都是先被push到全局的callbacks
export function nextTick (cb?: (...args: any[]) => any, ctx?: object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e: any) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  ...
}

// 截取一小部分nextTick代码
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
}

flushSchedulerQueue函数就是遍历watcher queue

  • 遍历到哪个watcher就将当前watcher,从队列中删掉,这个情况就对应queueWatcher的第2中情况
  • 执行watcher.run(),即vm._update(vm._render(), hydrating),最终产物是新的vnode 更新过程,vm上有旧的vnode,接下来还是执行patch方法

更新渲染

sameVnode
// 对比新旧vnode,主要还是对比key、tag
function sameVnode(a, b) {
  return (
    a.key === b.key &&
    a.asyncFactory === b.asyncFactory &&
    ((a.tag === b.tag &&
      a.isComment === b.isComment &&
      isDef(a.data) === isDef(b.data) &&
      sameInputType(a, b)) ||
      (isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error)))
  )
}

新旧节点相同,就要patchVnode,主要功能:

  1. 静态节点,直接返回,比如在编译的优化阶段,被打上标记的静态节点
  2. vnode的text和children,不是共存的。是text,不相同就修改内容。都有children执行updateChildren,有旧vnode就删除旧dom,否则就创建新dom
更新节点的属性
if (isDef(data) && isPatchable(vnode)) {
  for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
  if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
updateChildren
function updateChildren (
  parentElm,
  oldCh,
  newCh,
  insertedVnodeQueue,
  removeOnly
) {
  let oldStartIdx = 0
  let newStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newEndIdx = newCh.length - 1
  let newStartVnode = newCh[0]
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm

  // removeOnly is a special flag used only by <transition-group>
  // to ensure removed elements stay in correct relative positions
  // during leaving transitions
  const canMove = !removeOnly

  if (__DEV__) {
    checkDuplicateKeys(newCh)
  }

  // 新旧节点的头、尾,共四个
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      // 旧的没有开始节点,头指针右移
      oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
      // 旧的没有尾节点,尾指针左移
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      // 新旧头节点相同,就去更新属性、比对子节点
      patchVnode(
        oldStartVnode,
        newStartVnode,
        insertedVnodeQueue,
        newCh,
        newStartIdx
      )
      oldStartVnode = oldCh[++oldStartIdx] // 两个头指针都右移
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      // 新旧尾节点相同
      patchVnode(
        oldEndVnode,
        newEndVnode,
        insertedVnodeQueue,
        newCh,
        newEndIdx
      )
      oldEndVnode = oldCh[--oldEndIdx] // 两个尾指针都左移
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      // 旧的头结点、新尾节点相同,把新的尾节点属性更新到旧的头结点
      patchVnode(
        oldStartVnode,
        newEndVnode,
        insertedVnodeQueue,
        newCh,
        newEndIdx
      )
      // 把旧的头结点移动到尾节点的下一个节点,就是头结点移动到末尾
      canMove &&
        nodeOps.insertBefore(
          parentElm,
          oldStartVnode.elm,
          nodeOps.nextSibling(oldEndVnode.elm)
        )
      // 上一步只是把真实dom做了移动,vnode顺序还是不变,所以旧的头结点右移
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx] // 新的尾节点左移
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      // 过程同上,先更新属性,再移动尾dom到头部
      patchVnode(
        oldEndVnode,
        newStartVnode,
        insertedVnodeQueue,
        newCh,
        newStartIdx
      )
      canMove &&
        nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx] // 旧尾指针左移
      newStartVnode = newCh[++newStartIdx] // 新头指针右移
    } else {
      // 没有找到新旧相同的节点
      if (isUndef(oldKeyToIdx))
      // 把所有有key的旧节点,都收集到一个map里,以key为键,索引是值
      oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      // 如果新的开始节点有key,去旧节点map按key匹配
      // 否则去旧的节点里去匹配(这是新旧节点都没有key的情况,匹配tag等)
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
      if (isUndef(idxInOld)) {
        // New element,没找到就直接创建新dom,查到旧dom前
        createElm(
          newStartVnode,
          insertedVnodeQueue,
          parentElm,
          oldStartVnode.elm,
          false,
          newCh,
          newStartIdx
        )
      } else {
        vnodeToMove = oldCh[idxInOld]
        // 找到新旧相同节点,同样是更新熟悉,移动到旧dom前
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(
            vnodeToMove,
            newStartVnode,
            insertedVnodeQueue,
            newCh,
            newStartIdx
          )
          oldCh[idxInOld] = undefined
          canMove &&
            nodeOps.insertBefore(
              parentElm,
              vnodeToMove.elm,
              oldStartVnode.elm
            )
        } else {
          // 对应oldKeyToIdx的map集合,key相同但是其他属性不同
          // same key but different element. treat as new element
          createElm(
            newStartVnode,
            insertedVnodeQueue,
            parentElm,
            oldStartVnode.elm,
            false,
            newCh,
            newStartIdx
          )
        }
      }
      newStartVnode = newCh[++newStartIdx] // 这个过程的新节点都已创建,所以新头指针右移
    }
  }
  if (oldStartIdx > oldEndIdx) {
    // 旧节点遍历完了,剩余的新节点都插入最后一个新节点前面
    // newEndIdx如果左移过,它就有真实dom,比如旧头结点和新尾节点相同情况
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
    addVnodes(
      parentElm,
      refElm,
      newCh,
      newStartIdx,
      newEndIdx,
      insertedVnodeQueue
    )
  } else if (newStartIdx > newEndIdx) {
    // 新节点遍历完,删掉剩余的旧节点
    removeVnodes(oldCh, oldStartIdx, oldEndIdx)
  }
}

updated结束