A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

前言vue官方对响应式原理的解释:深入响应式原理


上一节讲了VUE中依赖收集和依赖触发的原理,然鹅对响应式的整体流程我们还是有很多疑问:
  • VUE是何时进行依赖收集的?
  • 依赖触发了以后又是怎么进行页面响应式变化的?
  • watcher对象到底起到了什么作用?
为了回答以上的几个问题,我们不得不梳理一波VUE响应式的整体流程

从实例初始化阶段开始说起vue源码的 instance/init.js 中是初始化的入口,其中初始化中除了初始化的几个步骤以外,在最后有这样一段 代码:
if (vm.$options.el) {        vm.$mount(vm.$options.el)}复制代码在初始化结束后,调用options.el中。
关于$mount的定义在两处可以看到:platforms/web/runtime/index.js、platforms/web/entry-runtime-with-compiler.js
其中runtime/index.js的代码如下:
Vue.prototype.$mount = function (  el?: string | Element,  hydrating?: boolean): Component {  el = el && inBrowser ? query(el) : undefined  // 划重点!!!  return mountComponent(this, el, hydrating)}复制代码runtime/index.js是运行时vue的入口,其中定义的mount功能,其中主要调用了mountComponent()函数完成挂载。entry-runtime-with-compiler.js是完整的vue的入口,在运行时vue的$mount基础上加入了编译模版的能力。
编译模版,为挂载提供渲染函数entry-runtime-with-compiler.js中定义了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) {    ...//构建渲染函数  }    //调用运行时vue的$mount()函数,  return mount.call(this, el, hydrating)}复制代码entry-runtime-with-compiler.js中的$mount()函数主要做了三件事:
  • 判断挂载点是不是元素或者元素,因为挂载点会被自身模版替代掉,因此挂载点不能为元素或者元素;
  • 判断渲染函数是否存在,如果渲染函数不存在,则构建渲染函数;
  • 调用运行时vue的mount();
创建渲染函数上述第二步,若渲染函数不存在时,构建渲染函数,代码如下:
let template = options.template         //如果template存在,则通过template获取真正的【模版】    if (template) {      //template是字符串      if (typeof template === 'string') {        //template第一个字符是#,则将该字符串作为id选择器获取对应元素作为【模版】        if (template.charAt(0) === '#') {          template = idToTemplate(template)          ... //省略        }        //如果template是元素节点,则将template的innerHTML作为【模版】      } else if (template.nodeType) {        template = template.innerHTML        //若template无效,则显示提示      } else {        if (process.env.NODE_ENV !== 'production') {          warn('invalid template option:' + template, this)        }        return this      }      //若template不存在,则将el元素的outerHTML作为【模版】    } else if (el) {      template = getOuterHTML(el)    }    //此时template中是最终的【模版】,下面根据【模版】生成rander函数    if (template) {      ... //省略      // 划重点!!!      // 使用compileToFunctions函数将【模版】template,编译成为渲染函数。      const { render, staticRenderFns } = compileToFunctions(template, {        shouldDecodeNewlines,        shouldDecodeNewlinesForHref,        delimiters: options.delimiters,        comments: options.comments      }, this)      options.render = render      options.staticRenderFns = staticRenderFns      ... //省略    }                        复制代码创建渲染函数阶段主要做了两件事:
  • 得到【模版】字符串:
    • 如果template存在,且template是字符串以#开头,则将该字符串作为id选择器获取对应元素作为【模版】
    • 如果template是元素节点,则将template的innerHTML作为【模版】
    • 如果tempalte是无效字符串,则显示warning
    • 若template不存在,则将el元素的outerHTML作为【模版】
  • 根据【模版】字符串生成渲染函数render()
    • 生成的options.render,在挂载组件的mountComponent函数中用到
实现挂载的mountComponent()函数上一步确保渲染函数render()存在后,就进入到了这正的挂载阶段。前面讲到挂载函数主要在mountComponent()中完成。
mountComponent()函数的定义在src/core/instance/lifecycle.js文件中。代码如下:
export function mountComponent (  vm: Component,  el: ?Element,  hydrating?: boolean): Component {  vm.$el = el  //如果render不存在  if (!vm.$options.render) {    //为render赋初始值,并打印warning提示信息    vm.$options.render = createEmptyVNode    ... //省略    }  }  //触发beforeMount钩子  callHook(vm, 'beforeMount')  // 开始挂载  let updateComponent  /* istanbul ignore if */  // 定义并初始化updateComponent函数  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {    updateComponent = () => {      const name = vm._name      const id = vm._uid      const startTag = `vue-perf-start:${id}`      const endTag = `vue-perf-end:${id}`      mark(startTag)      // 调用_render函数生成vnode虚拟节点      const vnode = vm._render()      mark(endTag)      measure(`vue ${name} render`, startTag, endTag)      mark(startTag)      // 以虚拟节点vnode作为参数调用_update函数,生成真正的DOM      vm._update(vnode, hydrating)      mark(endTag)      measure(`vue ${name} patch`, startTag, endTag)    }  } else {    updateComponent = () => {      //调用_render函数生成vnode虚拟节点;以虚拟节点vnode作为参数调用_update函数,生成真正的DOM      vm._update(vm._render(), hydrating)    }  }复制代码mountComponent主要做了三件事:
  • 如果render不存在,为render赋初始值,并打印warning信息
  • 触发beforeMount
  • 定义并初始化updateComponent函数:
  • 调用_render函数生成vnode虚拟节点
  • 虚拟节点vnode作为参数调用_update函数,生成真正的DOM
Watcher类watcher类的定义在core/observer/watcher.js中,代码如下:
export default class Watcher {  ... //  // 构造函数  constructor (    vm: Component,    expOrFn: string | Function,    cb: Function,    options?: ?Object,    isRenderWatcher?: boolean  ) {    this.vm = vm    if (isRenderWatcher) {      // 将渲染函数的观察者存入_watcher      vm._watcher = this    }    //将所有观察者push到_watchers列表    vm._watchers.push(this)    // options    if (options) {      // 是否深度观测      this.deep = !!options.deep      // 是否为开发者定义的watcher(渲染函数观察者、计算属性观察者属于内部定义的watcher)      this.user = !!options.user      // 是否为计算属性的观察者      this.computed = !!options.computed      this.sync = !!options.sync      //在数据变化之后、触发更新之前调用      this.before = options.before    } else {      this.deep = this.user = this.computed = this.sync = false    }    // 定义一系列实例属性    this.cb = cb    this.id = ++uid // uid for batching    this.active = true    this.dirty = this.computed // for computed watchers    this.deps = []    this.newDeps = []    // depIds 和 newDepIds 用书避免重复收集依赖    this.depIds = new Set()    this.newDepIds = new Set()    this.expression = process.env.NODE_ENV !== 'production'      ? expOrFn.toString()      : ''    // parse expression for getter    // 兼容被观测数据,当被观测数据是function时,直接将其作为getter    // 当被观测数据不是function时通过parsePath解析其真正的返回值    if (typeof expOrFn === 'function') {      this.getter = expOrFn    } else {      this.getter = parsePath(expOrFn)      if (!this.getter) {        this.getter = function () {}        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        )      }    }    if (this.computed) {      this.value = undefined      this.dep = new Dep()    } else {      // 除计算属性的观察者以外的所有观察者调用this.get()方法      this.value = this.get()    }  }  // get方法  get () {    ...  }  // 添加依赖  addDep (dep: Dep) {    ...  }  // 移除废弃观察者;清空newDepIds 属性和 newDeps 属性的值  cleanupDeps () {    ...  }  // 当依赖变化时,触发更新  update () {    ...  }  // 数据变化函数的入口  run () {    ...  }  // 真正进行数据变化的函数  getAndInvoke (cb: Function) {    ...  }  //  evaluate () {    ...  }  //  depend () {    ...  }  //  teardown () {    ...  }}复制代码watcher构造函数由以上代码可见,在watcher构造函数中做了如下几件事:
  • 将组件的渲染函数的观察者存入_watcher,将所有的观察者存入_watchers中
  • 保存before函数,在数据变化之后、触发更新之前调用
  • 定义一系列实例属性
  • 兼容被观测数据,当被观测数据是function时,直接将其作为getter; 当被观测数据不是function时通过parsePath解析其真正的返回值,被观测数据是 'obj.name'时,通过parsePath拿到真正的obj.name的返回值
  • 除计算属性的观察者以外的所有观察者调用this.get()方法
get()中收集依赖get中的代码如下:
get () {    // 将观察者对象保存至Dep.target中(Dep.target在上一章提到过)    pushTarget(this)    let value    const vm = this.vm    try {      //调用getter方法,获得被观察目标的值      value = this.getter.call(vm, vm)    } catch (e) {      ...    } finally {      ...    }    return value  }复制代码get()函数中主要做了如下几件事:
  • 调用pushTarget()方法,将观察者对象保存至Dep.target中,其中Dep.target在上一章提到过
  • 调用defineReactive中的get实现依赖收集、返回正确值
  • 上一章讲过,defineReactive中调用dep.depend(),dep.depend()中调用Dep.target.addDep()进行依赖收集
addDep添加依赖  // 添加依赖  addDep (dep: Dep) {    const id = dep.id    // newDepIds避免本次get中重复收集依赖    if (!this.newDepIds.has(id)) {      this.newDepIds.add(id)      this.newDeps.push(dep)      // 避免多次求值中重复收集依赖,每次求值之后newDepIds会被清空,因此需要depIds来判断。newDepIds中清空      if (!this.depIds.has(id)) {        dep.addSub(this)      }    }  }复制代码
  • 在addDep中添加依赖,并避免对一个数据多次求值时,其观察者被重复收集。
  • newDepIds避免一次求值的过程中重复收集依赖
  • depIds 属性避免多次求值中重复收集依赖
响应式的整体流程根据上一章和本章的讲解,总结一下响应式的整体流程:假设有模版:
<div id="test">  {{str}}</div>复制代码
  • 调用$mount()函数进入到挂载阶段
  • 检查是否有render()函数,根据上述模版创建render()函数
  • 调用了mountComponent()函数完成挂载,并在mountComponen()中定义并初始化updateComponent()
  • 为渲染函数添加观察者,在观察者中对渲染函数求值
  • 在求值的过程中触发数据对象str的get,在str的get中收集str的观察者到数据的dep中
  • 修改str的值时,触发str的set,在set中调用数据的dep的notify触发响应
  • notify中对每一个观察者调用update方法
  • 在run中调用getAndInvoke函数,进行数据变化。 在getAndInvoke函数中调用回调函数
  • 对于渲染函数的观察者来说getAndInvoke就相当于执行updateComponent函数
  • 在updateComponent函数中调用_render函数生成vnode虚拟节点,以虚拟节点vnode作为参数调用_update函数,生成真正的DOM
至此响应式过程完成。
参考文章:揭开数据响应系统的面纱


作者:叫我女王大人
链接:https://juejin.im/post/5b94e0e95188255c5b5c19f7



1 个回复

倒序浏览
奈斯
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马