1 последовательность
почему это отличаетсяVue2Принцип отзывчивости?
Дело было в том, что два дня назад я дал коллегеreviewкод, нашел выход за пределы собственного пониманияVue2Отзывчивое явление, очень интересное явление, вы можете обратиться к нему за примерами.точка кипения
Vue2Отзывчивость объекта, общее восприятие таково,来源于官网:
-
для объекта
Проще говоря: отзывчивость объекта можно использовать только для существующих свойств,
obj.newProа такжеdelete obj.oldProВремяVue2Отзывчивость не может быть перехвачена и должна быть использованаthis.$setилиVue.setметод может -
для массивов
Проще говоря: ответ массива обычно осуществляется через 7 методов работы с массивом (по
Object.definePropertyвоспитал семь бахчевых), чтобы добиться отзывчивости, какarr[0] = 1,arr.length = 0Невозможно добиться отзывчивости
но,不一样的Vue2响应式原理Это заставит вас заново понять и обнаружить, что приведенное выше утверждение не совсем верно.
Далее мы будем использовать простой пример для源码Уровень, чтобы найти ответ, чтобы проанализироватьVueЧто сделали два процесса от инициализации до обновления?Конечно, эта статья предназначена в основном для иллюстрации проблемы.Исходный код был частично упрощен, и слишком много слов нужно убрать.
2 Каково явление и внутренний принцип (процесс выполнения) следующего примера?
Вы можете сначала посмотреть код, а потом посмотреть ответ.Конечно, вы также можете написать пример и попробовать его самостоятельно.Проверьте следующее
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id = "app">
<p>{{ form }}</p>
<ul>
<li v-for = "item in arr" :key = "item">{{ item }}</li>
</ul>
</div>
<script src = "../../dist/vue.js"></script>
<script>
const ins = new Vue({
data () {
return {
form: {
name: 'lyn'
},
arr: [1]
}
},
mounted () {
setTimeout(() => {
this.form.name = 'test'
this.arr[0] = 11
}, 2000)
}
})
ins.$mount('#app')
</script>
</body>
</html>
2.1 Сначала сделайте вывод
Я полагаю, что ответ многих студентов заключается в том, что страница изначально заполнена, а отображаемый контент:
Затем позже выполняются два типа временных функций, и страница обновляется до следующего:
Если ваш ответ такой, то эту статью стоит прочитать
Конечно, теоретическая основа вашего ответа верна, но почему ответ неверен? Причина очень проста.Есть какие-то недочеты в понимании и познании всего процесса реализации отзывчивости.По крайней мере, это то, что я увидел, когда увидел это явление два дня назад.
2.1.1 Феномен
Первоначальный результат рендеринга:
Через 2 секунды выполняется функция синхронизации, и страница обновляется до:
2.1.2 Внутренний принцип
После загрузки примера кода в браузер,VueЗапустите инициализацию, выполните различныеinitоперации, наиболее важной из которых является создание экземпляров компонентовWatcher, инициализировать данные, собрать зависимости (dep), связатьdepа такжеwatcher, естественно вперемежку с выполнением методов жизненного цикла, таких какbeforeCreate,created,beforeMount,mounted, если есть дочерние компоненты,beforemountПосле завершения выполнения дочерний компонент будет инициализирован до тех пор, пока не будет запущен собственный компонент.mountedвыполнение завершено, затем вернитесь к выполнениюmounted, увы, далековато, но не влияет, проблема не в инициализации, а в последующем обновлении.
Выполнить функцию синхронизации после рендеринга страницы в течение 2 с.
сначала выполнитьthis.form.name = 'test', который делится на два шага
-
первый
this.formвызыватьgetterполучатьvalue = { name: 'lyn' } -
затем выполнить
value.name = 'test'вызыватьsetter, обновите данные, сейчасthis.form.nameценностьtest
существуетsetterсредний триггерdep.notify(),уведомлятьwatcherВыполняйте свои собственныеupdateметод,updateметод будетwatcherВставьте себя в очередь (массив очередей), а затем вызовитеnextTickМетод регистрирует функцию, которая обновляет очередь (фактически выполняет метод run каждого наблюдателя в массиве очереди),nextTickметод оборачивает функцию, очищающую очередь, стрелочной функцией и сохраняет ее вcallbacksв массиве следующийnextTickбудет выполнятьtimerFuncфункция
timerFuncФункция использует асинхронный механизм браузера и будет обновлятьcallbacksФункция массива зарегистрирована как асинхронная задача.Когда все синхронные задачи будут выполнены, только что зарегистрированная очередь будет обновлена.Поскольку синхронная задача еще не выполнена, осталась еще одна.this.arr[0] = 11, поэтому асинхронная задача сначала временно приостанавливается
выполнить следующийthis.arr[0] = 11, который также выполняется в два этапа
-
первый
this.arrвызоветgetterполучатьvalue = [1] -
Потом его нет, потому что
this.arr[0] = 11написано так,Vue2отзывчивое ядроObject.definePropertyне может быть перехвачен, но
Но важный момент,this.arr[0] = 11Этот код действительно выполняется, а это значит, чтоthis.arrТекущее значение действительно[11]Да, это очень важно, если у вас есть какие-то сомнения, продолжайте смотреть вниз с сомнениями.
В этот момент все синхронные задачи завершаются, и только что зарегистрированные асинхронные задачи начинают выполняться.
Упомянутая выше асинхронная задача — это всего лишь набор callback-функций, что в итоге она делает очень просто (чисто), то есть выполняетwatcher.runметод,watcher.runвыполнение методаwatcher.getметод,getметод отвечает за выполнениеupdateComponentметод, этот метод создается при инициализации компонентаwatcherкогда перешел кwatcherда, выполнитьupdateComponentметод будет выполнен первымvm._renderфункция для создания новыхvdom, обратите внимание, что создание новыхvdomнужно читатьvueэкземплярthisЧитаются, конечно, только те атрибуты, которые используются в шаблоне, в нашем примере этоthis.formа такжеthis.arr, Вы немного поняли после прочтения этого?
несмотря на то чтоthis.arr[0] = 11нет способа вызватьVue2, но может изменитьсяthisЗначение атрибута , поэтому я увидел на странице сцену, которую раньше не понимал
генерировать новыеvdom,updateComponentвоплощать в жизньvm._updateметод, вызовpatchМетод сравнения старого и новогоvdomВыявление измененийdomузел, затем обновить
Увидев это, вы уже немного понимаете или у вас другое представление? Например:
я хочу пройтиthis.arr[idx] = xxxОбновите элементы массива, не хотите использоватьthis.spliceи т. д., вам нужно только принести еще один, чтобы вызватьsetterЭффективная работа黑魔法Ну а ведь если вы это напишете, то не очень хорошо будет навлекать на окружающих негативный опыт.
Я не знаю, понимаю ли я это прямо, когда вижу это здесь? Я все еще немного запутался, поэтому я могу продолжать смотреть вниз и найти ответ из исходного кода.Код упрощен и имеет подробные комментарии.Прочитав его, я сам вспомню процесс, а затем вернусь и посмотрю на этом выводе, и я определенно выиграю много. .
2.2 Найдите ответ в исходном коде
В этом разделе будет проанализирован процесс выполнения всего примера кода. Когда контент загружается в браузер,
VueПроцесс выполнения исходного кода выглядит следующим образом:
-
src/core/instance/index.js
/** * Vue 构造函数,执行初始化操作 */ function Vue (options) { this._init(options) } -
src/core/instance/init.js
/** * 执行各种初始化操作,比如: * 最重要的给数据设置响应式(这部分内容就不展开了,否则太多了)然后执行实例的 $mount 方法 */ Vue.prototype._init = function (options?: Object) { initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') if (vm.$options.el) { vm.$mount(vm.$options.el) } } -
src/platforms/web/runtime/index.js
/** * $mount, 负责执行 mountComponent 方法 */ Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) } -
src/platforms/web/entry-runtime-with-compiler.js
/** * 不用管这个,和问题无关,这里其实重写了 $mount 执行了编译模版的动作, * 最后生成 render 函数,这部分内容被我删了 */ const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { return mount.call(this, el, hydrating) } -
src/core/instance/lifecycle.js
/** * mountComponent 方法 * 很重要的几点 * 1、定义组件的 updateComponent 方法 * 2、实例化组件 watcher,并将 updateComponent 方法传递给 watcher * * watcher 后面会在自己的 run 方法中调用 get 方法,get 方法会负责执行这个 updateComponent 方法,重新生成新的 vdom,watcher 相关看下面的 watcher 部分 */ export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { callHook(vm, 'beforeMount') let updateComponent = () => { // vm._render 执行后会生成新的 vdom,vm._update 方法会调用 patch 方法,对比新旧 dom,更新视图 vm._update(vm._render(), hydrating) } // we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined 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 } /** *负责执行各种各样的生命周期方法,比如 mounted */ export function callHook (vm: Component, hook: string) { // handlers = vm.$options.mounted const handlers = vm.$options[hook] const info = `${hook} hook` if (handlers) { for (let i = 0, j = handlers.length; i < j; i++) { invokeWithErrorHandling(handlers[i], vm, null, vm, info) } } } /** * 负责执行 patch 方法,分为首次渲染和再次更新 */ Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode const restoreActiveInstance = setActiveInstance(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) } } -
src/core/util/error.js
// 执行 声明周期方法 export function invokeWithErrorHandling ( handler: Function, context: any, args: null | any[], vm: any, info: string ) { // 真正执行声明周期方法的地方 return args ? handler.apply(context, args) : handler.call(context) }
К этому моменту смонтированный метод был выполнен, и страница была отрисована.Следующим шагом является выполнение функции синхронизации через 2 секунды, обновление данных и запуск обновления представления посредством перехвата ответа.
```javascript
/**
* mounted 方法中的定时函数
*/
setTimeout(() => {
this.form.name = 'test'
this.arr[0] = 11
}, 2000)
```
Далее проанализируем, что представляет собой процесс выполнения callback-функции, зарегистрированный таймером
-
src/core/observer/index.js
Когда функция синхронизации выполняется, будет запущено следующее
getterа такжеsetter,Например:this.form.name = 'test'будет выполняться первымthis.formвызыватьgetterполучатьvalue = { name: 'lyn' }, а затем выполнитьvalue.name = 'test'вызыватьsetterвозобновитьnameсвойства, затем выполнитеdep.notify()/** * 这个其实就是数据响应式的核心了,拦截了示例对象上的各个属性,数据读取时执行 get,设置数据时执行 set */ export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } // #7981: for accessor properties without setter if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) // 通知 watcher 去执行 update 方法 dep.notify() } }) } -
src/core/observer/dep.js
/** * A dep is an observable that can have multiple * directives subscribing to it. * * dep 负责收集依赖,通知 watcher 更新 * 这里只保留了 notify(通知watcher更新) 和 构造函数 */ export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } // 通知通知执行 update 方法 notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { // 这个其实就是 watcher 的 update 方法 subs[i].update() } } } -
src/core/observer/watcher.js
/** * A watcher parses an expression, collects dependencies, * and fires callback when the expression value changes. * This is used for both the $watch() api and directives. * * 一个组件对应一个 watcher 实例(渲染watcher),实例化过程是在 mountComponent 方法中做的,也就是执行 vm.$mount 之后 * 在同步执行过程中最重要的就是将当前 watcher 实例 push 到一个 watcher 执行队列中, * 待将来执行,通过一个 Promise.resolve().then() 来执行 run 方法,从而执行 updateComponent 方法 */ export default class Watcher { constructor ( vm: Component, // updateComponent expOrFn: string | Function, // noop cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm // 很重要,就是租价更新方法,updateComponent this.getter = expOrFn } /** * Evaluate the getter, and re-collect dependencies. * * 由 this.run 执行,执行 updateComponent 方法,生成新的 vdom,然后执行 patch,更新视图 */ get () { // Dep.target = watcher实例,这里让 dep 和 watcher 关联 pushTarget(this) let value // 组件实例 const vm = this.vm try { // 这里其实执行的是这个 updateComponent 方法: // let updateComponent = () => { vm._update(vm._render(), hydrating) } value = this.getter.call(vm, vm) } catch (e) { } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } popTarget() } return value } /** * 将 watcher 实例加入 watcher 队列 */ update () { queueWatcher(this) } /** * Scheduler job interface. * Will be called by the scheduler. * * * 这里其实通过 timerFunc 来调用,借用了浏览器的异步机制(Promise) * 执行 this.get 方法,让 get 执行 updateComponent */ run () { const value = this.get() } } -
src/core/observer/scheduler.js
/** * 将 watcher push 进 queue 数组,然后注册一个回到函数,在将来[Promise.resolve().then()]来执行这些 watcher 的 run 方法 */ export function queueWatcher (watcher: Watcher) { queue.push(watcher) // 这里其实就是注册回调函数 flushSchedulerQueue nextTick(flushSchedulerQueue) } /** * 负责让队列中所有的 watcher 执行自己的 run 方法. */ function flushSchedulerQueue () { for (index = 0; index < queue.length; index++) { watcher = queue[index] watcher.run() } } -
src/core/util/next-tick.js
Если вы не понимаете макрозадачи и микрозадачи, вы можете прочитатьэта статья
/** * 很重要的几个点 * * 定义 nextTick 方法,将回调函数全部放到一个 callbacks 数组,然后执行 timerFunc * 定义 timerFunc,其实就是就是利用了浏览器的异步任务机制,这里选了 Promise 微任务,Vue首选就是Promise * Promise.resolve().then() 注册的回调函数就是刷新刚才存储的 queue 队列(数组), * 执行 watcher.run(),触发 updateComponent,这里很关键的一点是理解宏任务、微任务, * 当宏任务都执行结束后,比如示例中的整个setTimeout 回调,就会执行这里注册的微任务,Promise.resolve().then() */ const callbacks =[] let pending = false // nextTick 就是用一个箭头函数将 flushSchedulerQueue 函数包裹然后放到 callbacks 数组 export function nextTick (cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { cb.call(ctx) }) if (!pending) { pending = true // 就是执行一个 异步 方法,首选 Promise timerFunc() } } // 执行一个立即就绪 Promise,Promise 回调负责执行 flushCallbacks 函数 let timerFunc = () => { Promise.resolve().then(flushCallbacks) } /** * 执行 callbacks 数组中的 () => flushSchedulerQueue.call(ctx),而最终会放 watcher 去执行自己的 run 方法, * run 方法执行 get 方法,get 方法中最终会调用组件的 updateComponent 方法,然后执行 render 重新生成 vnode,然后执行 * patch 过程,最终更新 dom */ function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } -
Что генерируется после выполнения функции vm._render?
/** * 可以看到,生成 vdom 的时候,会去组件实例对象上读取响应的属性值,比如我们这里的,this.form,this.arr * 理解这里很重要,为什么我们的视图会被有效更新?是因为 vm.arr 确实被更新成了 [11] */ function anonymous() { with(this) { return _c( 'div', {attrs:{"id":"app"}}, [ // this.form _c('p',[_v(_s(form))]),_v(" "), _c( 'ul', _l( // this.arr (arr), function(item) { return _c('li',{key:item},[_v(_s(item))]) } ), 0 ) ] ) } }
3 сублимация?
Прочитав это, я разбираюсь в своих мыслях и оглядываюсь на предыдущие выводы.Чувствую ли я просветление? ?