从源码解析vue的响应式原理-响应式的整体流程
栏目: JavaScript · 发布时间: 6年前
内容简介:vue官方对响应式原理的解释:深入响应式原理上一节讲了VUE中为了回答以上的几个问题,我们不得不梳理一波
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
至此响应式过程完成。
参考文章: 揭开数据响应系统的面纱
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
互联网思维的企业
[美] Dave Gray Thomas Vander Wal / 张 玳 / 人民邮电出版社 / 2014-4-25 / 59.00元
本书指导企业跳出仅更新自家产品和服务的怪圈,在管理方式、组织结构和公司文化方面进行变革,建立具有互联网思维的企业。书中通过大量图示和示例阐述了互联式公司必需的基础元素(透明的互动和交流平台,推崇自治和应变的组织结构,实验和学习的企业文化),以及一套鼓励员工创新的新式管理和奖励体系。最后,讨论板可方便你在工作时间和同事探讨如何增加公司的互联程度。一起来看看 《互联网思维的企业》 这本书的介绍吧!