mount挂载流程

95 阅读1分钟

和前面的watcher关联起来,数据变化了,怎么就watcher变化,然后patch,然后就虚拟dom了

1.  细节知识点

1.  $mount什么时候挂到vue原型上

mountComponent函数定义在src\core\instance\lifecycle.js

$mount挂载是在src\platforms\web\runtime\index.js文件中,运行时文件初次挂载,编译版本文件再次覆盖挂载

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

2.  mount挂载在什么时候执行

初始化数据执行完,执行完created之后,立刻执行挂载

3.  dep怎么和响应式的数据一一对应的

答:每一此把数据变成响应式(执行defineReactive)时候,new 一个Dep,这个dep被getter方法使用到,形成闭包被保存下来(也可以理解为getter方法是唯一的,每一个响应式数据生成一个唯一的getter函数,dep是写在getter里面的代码)

4.  watcher怎么和dep对应的

1)  watcher的初始化在挂载阶段,此时数据响应式初始化已经执行完毕,所有数据一一对应的dep已经创建,但是dep里面保存的watcher还是空的

2)  watcher初始化的时候,执行一次渲染,触发数据响应式的get方法,Dep.target有值,触发dep收集,整个watcher实例被收集到dep里面去(依赖收集过程)。最终在派发更新执行的时候,是执行watcher里面update方法,update方法最终又执行渲染render,此时会再次访问到响应式的get方法,触发dep.depend(依赖收集),但是收集过程中检测到了当前watcher已经被当前dep收集了,于是不再收集

  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)) { // 第二次访问值开始,此处就为false,不触发dep.addSub方法
        dep.addSub(this)
      }
    }
  }

5.  组件怎么和watcher一一对应的,代码体现在

组件初次渲染的时候会生成一个watcher,这个watch传了参数有组件的渲染函数,dep通知这个watcher更新的时候,watcher只会执行这个组件的渲染函数(只传了一个渲染函数)

6.  watcher是用来做什么的

答:就是用来做渲染的(里面藏了render函数),以及和dep做对应的(里面有和dep关联的方法addDep)

2. 代码步骤汇总

new Vue =》 

_init() =>》

各种初始化,执行created =》

vm.mount(vm.mount(vm.options.el) (编译版本重写了多了个编译步骤) =》

mountComponent =》

new Watcher =》

vm._update(vm._render(), hydrating) =》

vm._render() =》

vm.update =》

patch =》

patch =》

createPatchFunction(这个函数七百多行,diff算法,渲染在此执行) =>

return function patch(node, vnode, inVPre) { ... } (详情看diff算法部分)

3. 详细代码步骤

mountComponent

1)  执行beforecreated。

2)  定义updateComponent函数

3)  new Watcher,在new Watcher的初始化时候,会执行传入的updateComponent函数,执行渲染render

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
    }
  }
  // 执行beforeMount钩子函数
  callHook(vm, 'beforeMount')
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    // TODO
  } else {
    updateComponent = () => {
      // 这个render函数都干了什么:拿到了vnode,最终生成dom
      // _render最后返回的是vnode
      // 最终渲染的地方是在_update里面的patch方法里面做的
      // _render最终会调用$createElement函数生成vnode
      vm._update(vm._render(), hydrating)
    }
  }
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false
  // 如果是根节点,就在此执行mounted钩子函数
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

new Watcher

new Watcher时候,传入的expOrFn参数就是渲染函数updateComponent,这个函数会在new的时候执行一次,

    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // 写那种以字符串命名的函数解析方式
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()

vm._render()

1.  _render(在src\core\instance\render.js文件中的renderMixin函数中被挂载到prototype上,renderMixin在new Vue之前就执行了。

2.  _render()的目的是返回vnode,vnode详细资料到vnode文档中去看

  Vue.prototype._render = function (): VNode { // 最终返回的是vnode
    const vm: Component = this
    const { render, _parentVnode } = vm.$options

    if (_parentVnode) {
      vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
    }

    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      // vnode在此创建
      // vm._renderProxy是当前上下文,在生产环境就是this
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      // TODO..
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }

vnode = render.call(vm._renderProxy, vm.$createElement)

render函数编译产生的,在src\platforms\web\entry-runtime-with-compiler.js文件中$mount函数内

      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

根据不同的模板产生不同的render函数,例如长的如下的样子

模板

<div id="demo" >
  {{currentBranch}}
</div>

render函数

(function anonymous() {
  with(this) {
    return _c('div',{attrs:{"id":"demo"}},[_v("\n  "+_s(currentBranch)+"\n")])
  }
})

_update

_update传两个参数,一个是vnode即_render,另一个是是否混合开发.

_render是拿到vnode,_update是真的把vnode渲染程真是的dom,diff算法在此内部执行

有旧的vnode,则走diff算法的patch,没有旧的vnode,则走直接渲染的vnode

  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
    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
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    // TODO..
  }

patch

patch

createPatchFunction

createPatchFunction,这个函数七百多行,diff算法,渲染在此执行。详情查看diff算法部分