美文网首页让前端飞Web前端之路
【面试题解析✨】Vue 的数据是如何渲染到页面的?

【面试题解析✨】Vue 的数据是如何渲染到页面的?

作者: 前端develop | 来源:发表于2020-03-24 01:36 被阅读0次

    面试的时候,面试官经常会问 Vue 双向绑定的原理是什么?我猜大部分人会跟我一样,不假思索的回答利用 Object.defineProperty 实现的。

    其实这个回答很笼统,而且也没回答完整?Vue 中 Object.defineProperty 只是对数据做了劫持,具体的如何渲染到页面上,并没有考虑到。接下来从初始化开始,看看 Vue 都做了什么事情。

    前提知识

    在读源码前,需要了解 Object.defineProperty 的使用,以及 Vue Dep 的用法。这里就简单带过,各位大佬可以直接跳过,进行源码分析。

    Object.defineProperty

    当使用 Object.defineProperty 对对象的属性进行拦截时,调用该对象的属性,则会调用 get 函数,属性值则是 get 函数的返回值。当修改属性值时,则会调用 set 函数。

    当然也可以通过 Object.defineProperty 给对象添加属性值,Vue 中就是通过这个方法将 datacomputed 等属性添加到 vm 上。

    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        const value = getter ? getter.call(obj) : val
        // 用于依赖收集,Dep 中讲到
        if (Dep.target) {
          dep.depend()
          if (childOb) {
            childOb.dep.depend()
            if (Array.isArray(value)) {
              dependArray(value)
            }
          }
        }
        return value
      },
      set: function reactiveSetter (newVal) {
        val = newVal
        // val 发生变化时,发出通知,Dep 中讲到
        dep.notify()
      }
    })
    

    Dep

    这里不讲什么设计模式了,直接看代码。

    let uid = 0
    
    export default class Dep {
      static target: ?Watcher;
      id: number;
      subs: Array<Watcher>;
    
      constructor () {
        this.id = uid++
        this.subs = []
      }
    
      addSub (sub: Watcher) {
        // 添加 Watcher
        this.subs.push(sub)
      }
    
      removeSub (sub: Watcher) {
        // 从列表中移除某个 Watcher
        remove(this.subs, sub)
      }
    
      depend () {
        // 当 target 存在时,也就是目标 Watcher 存在的时候,
        // 就可以为这个目标 Watcher 收集依赖
        // Watcher 的 addDep 方法在下文中
        if (Dep.target) {
          Dep.target.addDep(this)
        }
      }
    
      notify () {
        // 对 Watcher 进行排序
        const subs = this.subs.slice()
        if (process.env.NODE_ENV !== 'production' && !config.async) {
          subs.sort((a, b) => a.id - b.id)
        }
        // 当该依赖发生变化时, 调用添加到列表中的 Watcher 的 update 方法进行更新
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
      }
    }
    
    // target 为某个 Watcher 实例,一次只能为一个 Watcher 收集依赖
    Dep.target = null
    // 通过堆栈存放 Watcher 实例,
    // 当某个 Watcher 的实例未收集完,又有新的 Watcher 实例需要收集依赖,
    // 那么旧的 Watcher 就先存放到 targetStack,
    // 等待新的 Watcher 收集完后再为旧的 Watcher 收集
    // 配合下面的 pushTarget 和 popTarget 实现
    const targetStack = []
    
    export function pushTarget (target: ?Watcher) {
      targetStack.push(target)
      Dep.target = target
    }
    
    export function popTarget () {
      targetStack.pop()
      Dep.target = targetStack[targetStack.length - 1]
    }
    

    当某个 Watcher 需要依赖某个 dep 时,那么调用 dep.addSub(Watcher) 即可,当 dep 发生变化时,调用 dep.notify() 就可以触发 Watcher 的 update 方法。接下来看看 Vue 中 Watcher 的实现。

    class Watcher {
      // 很多属性,这里省略
      ...
      // 构造函数
      constructor (
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
      ) { ... }
      
      get () {
        // 当执行 Watcher 的 get 函数时,会将当前的 Watcher 作为 Dep 的 target
        pushTarget(this)
        let value
        const vm = this.vm
        try {
          // 在执行 getter 时,当遇到响应式数据,会触发上面讲到的 Object.defineProperty 中的 get 函数
          // Vue 就是在 Object.defineProperty 的 get 中调用 dep.depend() 进行依赖收集。
          value = this.getter.call(vm, vm)
        } catch (e) {
          ...
        } finally {
          ...
          // 当前 Watcher 的依赖收集完后,调用 popTarget 更换 Watcher
          popTarget()
          this.cleanupDeps()
        }
        return value
      }
      
      // dep.depend() 收集依赖时,会经过 Watcher 的 addDep 方法
      // addDep 做了判断,避免重复收集,然后调用 dep.addSub 将该 Watcher 添加到 dep 的 subs 中
      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.addSub(this)
          }
        }
      }
    }
    

    通过 Object.defineProperty 中的 getDepdepend 以及 WatcheraddDep 这三个函数的配合,完成了依赖的收集,就是将 Watcher 添加到 depsubs 列表中。

    当依赖发生变化时,就会调用 Object.defineProperty 中的 set,在 set 中调用 depnotify,使得 subs 中的每个 Watcher 都执行 update 函数。

    Watcher 中的 update 最终会重新调用 get 函数,重新求值并重新收集依赖。

    源码分析

    先看看 new Vue 都做了什么?

    // vue/src/core/instance/index.js
    function Vue (options) {
      if (process.env.NODE_ENV !== 'production' &&
        !(this instanceof Vue)
      ) {
        // 只能使用 new Vue 调用该方法,否则输入警告
        warn('Vue is a constructor and should be called with the `new` keyword')
      }
      // 开始初始化
      this._init(options)
    }
    

    _init 方法通过原型挂载在 Vue 上

    // vue/src/core/instance/init.js
    export function initMixin (Vue: Class<Component>) {
      Vue.prototype._init = function (options?: Object) {
        const vm: Component = this
        // a uid
        vm._uid = uid++
    
        let startTag, endTag
        // 初始化前打点,用于记录 Vue 实例初始化所消耗的时间
        if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
          startTag = `vue-perf-start:${vm._uid}`
          endTag = `vue-perf-end:${vm._uid}`
          mark(startTag)
        }
    
        // a flag to avoid this being observed
        vm._isVue = true
        // 合并参数到 $options
        if (options && options._isComponent) {
          initInternalComponent(vm, options)
        } else {
          vm.$options = mergeOptions(
            resolveConstructorOptions(vm.constructor),
            options || {},
            vm
          )
        }
    
        if (process.env.NODE_ENV !== 'production') {
          // 非生产环境以及支持 Proxy 的浏览器中,对 vm 的属性进行劫持,并将代理后的 vm 赋值给 _renderProxy
          // 当调用 vm 不存在的属性时,进行错误提示。
          // 在不支持 Proxy 的浏览器中,_renderProxy = vm; 为了简单理解,就看成等同于 vm
    
          // 代码在 src/core/instance/proxy.js
          initProxy(vm)
        } else {
          vm._renderProxy = vm
        }
        // expose real self
        vm._self = vm
        // 初始化声明周期函数
        initLifecycle(vm)
        // 初始化事件
        initEvents(vm)
        // 初始化 render 函数
        initRender(vm)
        // 触发 beforeCreate 钩子
        callHook(vm, 'beforeCreate')
        // 初始化 inject
        initInjections(vm) // resolve injections before data/props
        // 初始化 data/props 等
        // 通过 Object.defineProperty 对数据进行劫持
        initState(vm)
        // 初始化 provide
        initProvide(vm) // resolve provide after data/props
        // 数据处理完后,触发 created 钩子
        callHook(vm, 'created')
    
        // 从 new Vue 到 created 所消耗的时间
        if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
          vm._name = formatComponentName(vm, false)
          mark(endTag)
          measure(`vue ${vm._name} init`, startTag, endTag)
        }
    
        // 如果 options 有 el 参数则进行 mount
        if (vm.$options.el) {
          vm.$mount(vm.$options.el)
        }
      }
    }
    

    接下来进入 $mount,因为用的是完整版的 Vue,直接看 vue/src/platforms/web/entry-runtime-with-compiler.js 这个文件。

    // vue/src/platforms/web/entry-runtime-with-compiler.js
    // 首先将 runtime 中的 $mount 方法赋值给 mount 进行保存
    const mount = Vue.prototype.$mount
    // 重写 $mount,对 template 编译为 render 函数后再调用 runtime 的 $mount
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      el = el && query(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
      if (!options.render) {
        let template = options.template
        // render 函数不存在时,将 template 转化为 render 函数
        // 具体就不展开了
        ...
        if (template) {
            ...
        } else if (el) {
          // template 不存在,则将 el 转成 template
          // 从这里可以看出 Vue 支持 render、template、el 进行渲染
          template = getOuterHTML(el)
        }
        if (template) {
          const { render, staticRenderFns } = compileToFunctions(template, {
            outputSourceRange: process.env.NODE_ENV !== 'production',
            shouldDecodeNewlines,
            shouldDecodeNewlinesForHref,
            delimiters: options.delimiters,
            comments: options.comments
          }, this)
          options.render = render
          options.staticRenderFns = staticRenderFns
        }
      }
      // 调用 runtime 中 $mount
      return mount.call(this, el, hydrating)
    }
    

    查看 runtime 中的 $mount

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

    mountComponent 定义在 vue/src/core/instance/lifecycle.js

    // vue/src/core/instance/lifecycle.js
    export function mountComponent (
      vm: Component,
      el: ?Element,
      hydrating?: boolean
    ): Component {
      vm.$el = el
      if (!vm.$options.render) {
        // 未定义 render 函数时,将 render 赋值为 createEmptyVNode 函数
        vm.$options.render = createEmptyVNode
        if (process.env.NODE_ENV !== 'production') {
          /* istanbul ignore if */
          if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
            vm.$options.el || el) {
            // 用了 Vue 的 runtime 版本,而没有 render 函数时,报错处理
            warn(
              'You are using the runtime-only build of Vue where the template ' +
              'compiler is not available. Either pre-compile the templates into ' +
              'render functions, or use the compiler-included build.',
              vm
            )
          } else {
            // template 和 render 都未定义时,报错处理
            warn(
              'Failed to mount component: template or render function not defined.',
              vm
            )
          }
        }
      }
      // 调用 beforeMount 钩子
      callHook(vm, 'beforeMount')
      // 定义 updateComponent 函数
      let updateComponent
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        // 需要做监控性能时,在 updateComponent 内加入打点的操作
        updateComponent = () => {
          const name = vm._name
          const id = vm._uid
          const startTag = `vue-perf-start:${id}`
          const endTag = `vue-perf-end:${id}`
    
          mark(startTag)
          const vnode = vm._render()
          mark(endTag)
          measure(`vue ${name} render`, startTag, endTag)
    
          mark(startTag)
          vm._update(vnode, hydrating)
          mark(endTag)
          measure(`vue ${name} patch`, startTag, endTag)
        }
      } else {
        updateComponent = () => {
          // updateComponent 主要调用 _update 进行浏览器渲染
          // _render 返回 VNode
          // 先继续往下看,等会再回来看这两个函数
          vm._update(vm._render(), hydrating)
        }
      }
    
      // new 一个渲染 Watcher
      new Watcher(vm, updateComponent, noop, {
        before () {
          if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate')
          }
        }
      }, true /* isRenderWatcher */)
      hydrating = false
    
      // 挂载完成,触发 mounted
      if (vm.$vnode == null) {
        vm._isMounted = true
        callHook(vm, 'mounted')
      }
      return vm
    }
    

    先继续往下看,看看 new Watcher 做了什么,再回过头看 updateComponent 中的 _update_render

    Watcher 的构造函数如下

    // vue/src/core/observer/watcher.js
    constructor (
      vm: Component,
      expOrFn: string | Function,
      cb: Function,
      options?: ?Object,
      isRenderWatcher?: boolean
    ) {
      this.vm = vm
      if (isRenderWatcher) {
        vm._watcher = this
      }
      vm._watchers.push(this)
      // options
      if (options) {
        this.deep = !!options.deep
        this.user = !!options.user
        this.lazy = !!options.lazy
        this.sync = !!options.sync
        this.before = options.before
      } else {
        this.deep = this.user = this.lazy = this.sync = false
      }
      this.cb = cb
      this.id = ++uid // uid for batching
      this.active = true
      this.dirty = this.lazy // for lazy watchers
      ...
      // expOrFn 为上文的 updateComponent 函数,赋值给 getter
      if (typeof expOrFn === 'function') {
        this.getter = expOrFn
      } else {
        this.getter = parsePath(expOrFn)
        if (!this.getter) {
          this.getter = noop
          ...
        }
      }
      // lazy 为 false,调用 get 方法
      this.value = this.lazy
        ? undefined
        : this.get()
    }
    
    // 执行 getter 函数,getter 函数为 updateComponent,并收集依赖
    get () {
      pushTarget(this)
      let value
      const vm = this.vm
      try {
        value = this.getter.call(vm, vm)
      } catch (e) {
        ...
      } finally {
        if (this.deep) {
          traverse(value)
        }
        popTarget()
        this.cleanupDeps()
      }
      return value
    }
    

    new Watcher 后会调用 updateComponent 函数,上文中 updateComponent 内执行了 vm._update_update 执行前会通过 _render 获得 vnode,接下里看看 _update 做了什么。_update 定义在 vue/src/core/instance/lifecycle.js

    // vue/src/core/instance/lifecycle.js
    Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
      const vm: Component = this
      const prevVnode = vm._vnode
      vm._vnode = vnode
      ...
      
      if (!prevVnode) {
        // 初始渲染
        vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
      } else {
        // 更新 vnode
        vm.$el = vm.__patch__(prevVnode, vnode)
      }
      ...
    }
    

    接下来到了 __patch__ 函数进行页面渲染。

    // vue/src/platforms/web/runtime/index.js
    import { patch } from './patch'
    Vue.prototype.__patch__ = inBrowser ? patch : noop
    
    // vue/src/platforms/web/runtime/patch.js
    import { createPatchFunction } from 'core/vdom/patch'
    export const patch: Function = createPatchFunction({ nodeOps, modules })
    

    createPatchFunction 提供了很多操作 virtual dom 的方法,最终会返回一个 path 函数。

    export function createPatchFunction (backend) {
      ...
      // oldVnode 代表旧的节点,vnode 代表新的节点
      return function patch (oldVnode, vnode, hydrating, removeOnly) {
        // vnode 为 undefined, oldVnode 不为 undefined 则需要执行 destroy
        if (isUndef(vnode)) {
          if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
          return
        }
    
        let isInitialPatch = false
        const insertedVnodeQueue = []
    
        if (isUndef(oldVnode)) {
          // oldVnode 不存在,表示初始渲染,则根据 vnode 创建元素
          isInitialPatch = true
          createElm(vnode, insertedVnodeQueue)
        } else {
          
          const isRealElement = isDef(oldVnode.nodeType)
          if (!isRealElement && sameVnode(oldVnode, vnode)) {
            // oldVnode 与 vnode 为相同节点,调用 patchVnode 更新子节点
            patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
          } else {
            if (isRealElement) {
              // 服务端渲染的处理
              ...
            }
            // 其他操作
            ...
          }
        }
    
        invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
        // 最终渲染到页面上
        return vnode.elm
      }
    }
    

    当渲染 Watcher 的依赖的数据发生变化时,会触发 Object.defineProperty 中的 set 函数。

    从而调用 dep.notify() 通知该 Watcher 进行 update 操作。最终达到数据改变时,自动更新页面。 Watcherupdate 函数就不再展开了,有兴趣的小伙伴可以自行查看。

    最后再回过头看看前面遗留的 _render 函数。

    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
    

    之前说了 _render 函数会返回 vnode,看看具体做了什么吧。

    // vue/src/core/instance/render.js
    Vue.prototype._render = function (): VNode {
      const vm: Component = this
      // 从 $options 取出 render 函数以及 _parentVnode
      // 这里的 render 函数可以是 template 或者 el 编译的
      const { render, _parentVnode } = vm.$options
    
      if (_parentVnode) {
        vm.$scopedSlots = normalizeScopedSlots(
          _parentVnode.data.scopedSlots,
          vm.$slots,
          vm.$scopedSlots
        )
      }
    
      vm.$vnode = _parentVnode
      let vnode
      try {
        currentRenderingInstance = vm
        // 最终会执行 $options 中的 render 函数
        // _renderProxy 可以看做 vm
        // 将 vm.$createElement 函数传递给 render,也就是经常看到的 h 函数
        // 最终生成 vnode
        vnode = render.call(vm._renderProxy, vm.$createElement)
      } catch (e) {
        // 异常处理
        ...
      } finally {
        currentRenderingInstance = null
      }
    
      // 如果返回的数组只包含一个节点,则取第一个值
      if (Array.isArray(vnode) && vnode.length === 1) {
        vnode = vnode[0]
      }
      
      // vnode 如果不是 VNode 实例,报错并返回空的 vnode
      if (!(vnode instanceof VNode)) {
        if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
          warn(
            'Multiple root nodes returned from render function. Render function ' +
            'should return a single root node.',
            vm
          )
        }
        vnode = createEmptyVNode()
      }
      // 设置父节点
      vnode.parent = _parentVnode
      // 最终返回 vnode
      return vnode
    }
    

    接下来就是看 vm.$createElement 也就是 render 函数中的 h

    // vue/src/core/instance/render.js
    import { createElement } from '../vdom/create-element'
    export function initRender (vm: Component) {
      ...
      vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
      vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
      ...
    }
    
    // vue/src/core/vdom/create-element.js
    export function createElement (
      context: Component,
      tag: any,
      data: any,
      children: any,
      normalizationType: any,
      alwaysNormalize: boolean
    ): VNode | Array<VNode> {
      // data 是数组或简单数据类型,代表 data 没传,将参数值赋值给正确的变量
      if (Array.isArray(data) || isPrimitive(data)) {
        normalizationType = children
        children = data
        data = undefined
      }
      if (isTrue(alwaysNormalize)) {
        normalizationType = ALWAYS_NORMALIZE
      }
      // 将正确的参数传递给 _createElement
      return _createElement(context, tag, data, children, normalizationType)
    }
    
    export function _createElement (
      context: Component,
      tag?: string | Class<Component> | Function | Object,
      data?: VNodeData,
      children?: any,
      normalizationType?: number
    ): VNode | Array<VNode> {
      if (isDef(data) && isDef((data: any).__ob__)) {
        // render 函数中的 data 不能为响应式数据
        process.env.NODE_ENV !== 'production' && warn(
          `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
          'Always create fresh vnode data objects in each render!',
          context
        )
        // 返回空的 vnode 节点
        return createEmptyVNode()
      }
      // 用 is 指定标签
      if (isDef(data) && isDef(data.is)) {
        tag = data.is
      }
      if (!tag) {
        // in case of component :is set to falsy value
        return createEmptyVNode()
      }
      // key 值不是简单数据类型时,警告提示
      if (process.env.NODE_ENV !== 'production' &&
        isDef(data) && isDef(data.key) && !isPrimitive(data.key)
      ) { ... }
    
      if (Array.isArray(children) &&
        typeof children[0] === 'function'
      ) {
        data = data || {}
        data.scopedSlots = { default: children[0] }
        children.length = 0
      }
      // 处理子节点
      if (normalizationType === ALWAYS_NORMALIZE) {
        // VNode 数组
        children = normalizeChildren(children)
      } else if (normalizationType === SIMPLE_NORMALIZE) {
        children = simpleNormalizeChildren(children)
      }
      
      // 生成 vnode
      let vnode, ns
      if (typeof tag === 'string') {
        let Ctor
        ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
        if (config.isReservedTag(tag)) {
          ...
          vnode = new VNode(
            config.parsePlatformTagName(tag), data, children,
            undefined, undefined, context
          )
        } else if ((!data || !data.pre) && 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)
      }
      
      // 返回 vnode
      if (Array.isArray(vnode)) {
        return vnode
      } else if (isDef(vnode)) {
        if (isDef(ns)) applyNS(vnode, ns)
        if (isDef(data)) registerDeepBindings(data)
        return vnode
      } else {
        return createEmptyVNode()
      }
    }
    

    总结

    代码看起来很多,其实主要流程可以分为以下 4 点:

    1、 new Vue 初始化数据等

    2、$mount 将 render、template 或 el 转为 render 函数

    3、生成一个渲染 Watcher 收集依赖,并将执行 render 函数生成 vnode 传递给 patch 函数执行,渲染页面。

    4、当渲染 Watcher 依赖发生变化时,执行 Watcher 的 getter 函数,重新依赖收集。并且重新执行 render 函数生成 vnode 传递给 patch 函数进行页面的更新。


    以上内容均是个人理解,如果有讲的不对的地方,还请各位大佬指点。

    如果觉得内容还不错的话,希望小伙伴可以帮忙点赞转发,给更多的小伙伴看到,感谢感谢!

    如果你喜欢我的文章,还可以关注我的公众号【前端develop】

    前端develop

    相关文章

      网友评论

        本文标题:【面试题解析✨】Vue 的数据是如何渲染到页面的?

        本文链接:https://www.haomeiwen.com/subject/sqjnyhtx.html