Repository: ustbhuangyi/vue-analysis Branch: master Commit: 813cfe1a8fe9 Files: 569 Total size: 2.0 MB Directory structure: gitextract_7kf9tf8l/ ├── .flowconfig ├── .gitignore ├── LICENSE ├── README.md ├── build.sh ├── docs/ │ ├── .vuepress/ │ │ ├── config.js │ │ └── public/ │ │ └── manifest.json │ ├── README.md │ ├── v2/ │ │ ├── compile/ │ │ │ ├── codegen.md │ │ │ ├── entrance.md │ │ │ ├── index.md │ │ │ ├── optimize.md │ │ │ └── parse.md │ │ ├── components/ │ │ │ ├── async-component.md │ │ │ ├── component-register.md │ │ │ ├── create-component.md │ │ │ ├── index.md │ │ │ ├── lifecycle.md │ │ │ ├── merge-option.md │ │ │ └── patch.md │ │ ├── data-driven/ │ │ │ ├── create-element.md │ │ │ ├── index.md │ │ │ ├── mounted.md │ │ │ ├── new-vue.md │ │ │ ├── render.md │ │ │ ├── update.md │ │ │ └── virtual-dom.md │ │ ├── extend/ │ │ │ ├── event.md │ │ │ ├── index.md │ │ │ ├── keep-alive.md │ │ │ ├── slot.md │ │ │ ├── tansition-group.md │ │ │ ├── tansition.md │ │ │ └── v-model.md │ │ ├── prepare/ │ │ │ ├── build.md │ │ │ ├── directory.md │ │ │ ├── entrance.md │ │ │ ├── flow.md │ │ │ └── index.md │ │ ├── reactive/ │ │ │ ├── component-update.md │ │ │ ├── computed-watcher.md │ │ │ ├── getters.md │ │ │ ├── index.md │ │ │ ├── next-tick.md │ │ │ ├── props.md │ │ │ ├── questions.md │ │ │ ├── reactive-object.md │ │ │ ├── setters.md │ │ │ └── summary.md │ │ ├── vue-router/ │ │ │ ├── index.md │ │ │ ├── install.md │ │ │ ├── matcher.md │ │ │ ├── router.md │ │ │ └── transition-to.md │ │ └── vuex/ │ │ ├── api.md │ │ ├── index.md │ │ ├── init.md │ │ └── plugin.md │ └── v3/ │ ├── guide/ │ │ └── index.md │ └── new/ │ └── index.md ├── package.json ├── vue/ │ ├── flow/ │ │ ├── compiler.js │ │ ├── component.js │ │ ├── global-api.js │ │ ├── modules.js │ │ ├── options.js │ │ ├── ssr.js │ │ ├── vnode.js │ │ └── weex.js │ ├── package.json │ ├── scripts/ │ │ ├── alias.js │ │ ├── build.js │ │ ├── config.js │ │ ├── gen-release-note.js │ │ ├── get-weex-version.js │ │ ├── git-hooks/ │ │ │ ├── commit-msg │ │ │ └── pre-commit │ │ ├── release-weex.sh │ │ ├── release.sh │ │ └── verify-commit-msg.js │ ├── src/ │ │ ├── compiler/ │ │ │ ├── codegen/ │ │ │ │ ├── events.js │ │ │ │ └── index.js │ │ │ ├── create-compiler.js │ │ │ ├── directives/ │ │ │ │ ├── bind.js │ │ │ │ ├── index.js │ │ │ │ ├── model.js │ │ │ │ └── on.js │ │ │ ├── error-detector.js │ │ │ ├── helpers.js │ │ │ ├── index.js │ │ │ ├── optimizer.js │ │ │ ├── parser/ │ │ │ │ ├── entity-decoder.js │ │ │ │ ├── filter-parser.js │ │ │ │ ├── html-parser.js │ │ │ │ ├── index.js │ │ │ │ └── text-parser.js │ │ │ └── to-function.js │ │ ├── core/ │ │ │ ├── components/ │ │ │ │ ├── index.js │ │ │ │ └── keep-alive.js │ │ │ ├── config.js │ │ │ ├── global-api/ │ │ │ │ ├── assets.js │ │ │ │ ├── extend.js │ │ │ │ ├── index.js │ │ │ │ ├── mixin.js │ │ │ │ └── use.js │ │ │ ├── index.js │ │ │ ├── instance/ │ │ │ │ ├── events.js │ │ │ │ ├── index.js │ │ │ │ ├── init.js │ │ │ │ ├── inject.js │ │ │ │ ├── lifecycle.js │ │ │ │ ├── proxy.js │ │ │ │ ├── render-helpers/ │ │ │ │ │ ├── bind-object-listeners.js │ │ │ │ │ ├── bind-object-props.js │ │ │ │ │ ├── check-keycodes.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── render-list.js │ │ │ │ │ ├── render-slot.js │ │ │ │ │ ├── render-static.js │ │ │ │ │ ├── resolve-filter.js │ │ │ │ │ └── resolve-slots.js │ │ │ │ ├── render.js │ │ │ │ └── state.js │ │ │ ├── observer/ │ │ │ │ ├── array.js │ │ │ │ ├── dep.js │ │ │ │ ├── index.js │ │ │ │ ├── scheduler.js │ │ │ │ ├── traverse.js │ │ │ │ └── watcher.js │ │ │ ├── util/ │ │ │ │ ├── debug.js │ │ │ │ ├── env.js │ │ │ │ ├── error.js │ │ │ │ ├── index.js │ │ │ │ ├── lang.js │ │ │ │ ├── next-tick.js │ │ │ │ ├── options.js │ │ │ │ ├── perf.js │ │ │ │ └── props.js │ │ │ └── vdom/ │ │ │ ├── create-component.js │ │ │ ├── create-element.js │ │ │ ├── create-functional-component.js │ │ │ ├── helpers/ │ │ │ │ ├── extract-props.js │ │ │ │ ├── get-first-component-child.js │ │ │ │ ├── index.js │ │ │ │ ├── is-async-placeholder.js │ │ │ │ ├── merge-hook.js │ │ │ │ ├── normalize-children.js │ │ │ │ ├── resolve-async-component.js │ │ │ │ └── update-listeners.js │ │ │ ├── modules/ │ │ │ │ ├── directives.js │ │ │ │ ├── index.js │ │ │ │ └── ref.js │ │ │ ├── patch.js │ │ │ └── vnode.js │ │ ├── platforms/ │ │ │ ├── web/ │ │ │ │ ├── compiler/ │ │ │ │ │ ├── directives/ │ │ │ │ │ │ ├── html.js │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ ├── model.js │ │ │ │ │ │ └── text.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── modules/ │ │ │ │ │ │ ├── class.js │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ ├── model.js │ │ │ │ │ │ └── style.js │ │ │ │ │ ├── options.js │ │ │ │ │ └── util.js │ │ │ │ ├── entry-compiler.js │ │ │ │ ├── entry-runtime-with-compiler.js │ │ │ │ ├── entry-runtime.js │ │ │ │ ├── entry-server-basic-renderer.js │ │ │ │ ├── entry-server-renderer.js │ │ │ │ ├── runtime/ │ │ │ │ │ ├── class-util.js │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ ├── transition-group.js │ │ │ │ │ │ └── transition.js │ │ │ │ │ ├── directives/ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ ├── model.js │ │ │ │ │ │ └── show.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── modules/ │ │ │ │ │ │ ├── attrs.js │ │ │ │ │ │ ├── class.js │ │ │ │ │ │ ├── dom-props.js │ │ │ │ │ │ ├── events.js │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ ├── style.js │ │ │ │ │ │ └── transition.js │ │ │ │ │ ├── node-ops.js │ │ │ │ │ ├── patch.js │ │ │ │ │ └── transition-util.js │ │ │ │ ├── server/ │ │ │ │ │ ├── compiler.js │ │ │ │ │ ├── directives/ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ ├── model.js │ │ │ │ │ │ └── show.js │ │ │ │ │ ├── modules/ │ │ │ │ │ │ ├── attrs.js │ │ │ │ │ │ ├── class.js │ │ │ │ │ │ ├── dom-props.js │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── style.js │ │ │ │ │ └── util.js │ │ │ │ └── util/ │ │ │ │ ├── attrs.js │ │ │ │ ├── class.js │ │ │ │ ├── compat.js │ │ │ │ ├── element.js │ │ │ │ ├── index.js │ │ │ │ └── style.js │ │ │ └── weex/ │ │ │ ├── compiler/ │ │ │ │ ├── directives/ │ │ │ │ │ ├── index.js │ │ │ │ │ └── model.js │ │ │ │ ├── index.js │ │ │ │ └── modules/ │ │ │ │ ├── append.js │ │ │ │ ├── class.js │ │ │ │ ├── index.js │ │ │ │ ├── props.js │ │ │ │ ├── recycle-list/ │ │ │ │ │ ├── component-root.js │ │ │ │ │ ├── component.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── recycle-list.js │ │ │ │ │ ├── text.js │ │ │ │ │ ├── v-bind.js │ │ │ │ │ ├── v-for.js │ │ │ │ │ ├── v-if.js │ │ │ │ │ ├── v-on.js │ │ │ │ │ └── v-once.js │ │ │ │ └── style.js │ │ │ ├── entry-compiler.js │ │ │ ├── entry-framework.js │ │ │ ├── entry-runtime-factory.js │ │ │ ├── runtime/ │ │ │ │ ├── components/ │ │ │ │ │ ├── index.js │ │ │ │ │ ├── richtext.js │ │ │ │ │ ├── transition-group.js │ │ │ │ │ └── transition.js │ │ │ │ ├── directives/ │ │ │ │ │ └── index.js │ │ │ │ ├── index.js │ │ │ │ ├── modules/ │ │ │ │ │ ├── attrs.js │ │ │ │ │ ├── class.js │ │ │ │ │ ├── events.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── style.js │ │ │ │ │ └── transition.js │ │ │ │ ├── node-ops.js │ │ │ │ ├── patch.js │ │ │ │ ├── recycle-list/ │ │ │ │ │ ├── render-component-template.js │ │ │ │ │ └── virtual-component.js │ │ │ │ └── text-node.js │ │ │ └── util/ │ │ │ ├── element.js │ │ │ ├── index.js │ │ │ └── parser.js │ │ ├── server/ │ │ │ ├── bundle-renderer/ │ │ │ │ ├── create-bundle-renderer.js │ │ │ │ ├── create-bundle-runner.js │ │ │ │ └── source-map-support.js │ │ │ ├── create-basic-renderer.js │ │ │ ├── create-renderer.js │ │ │ ├── optimizing-compiler/ │ │ │ │ ├── codegen.js │ │ │ │ ├── index.js │ │ │ │ ├── modules.js │ │ │ │ ├── optimizer.js │ │ │ │ └── runtime-helpers.js │ │ │ ├── render-context.js │ │ │ ├── render-stream.js │ │ │ ├── render.js │ │ │ ├── template-renderer/ │ │ │ │ ├── create-async-file-mapper.js │ │ │ │ ├── index.js │ │ │ │ ├── parse-template.js │ │ │ │ └── template-stream.js │ │ │ ├── util.js │ │ │ ├── webpack-plugin/ │ │ │ │ ├── client.js │ │ │ │ ├── server.js │ │ │ │ └── util.js │ │ │ └── write.js │ │ ├── sfc/ │ │ │ └── parser.js │ │ └── shared/ │ │ ├── constants.js │ │ └── util.js │ └── test/ │ ├── e2e/ │ │ ├── .eslintrc │ │ ├── nightwatch.config.js │ │ ├── runner.js │ │ └── specs/ │ │ ├── async-edge-cases.html │ │ ├── async-edge-cases.js │ │ ├── basic-ssr.html │ │ ├── basic-ssr.js │ │ ├── commits.js │ │ ├── grid.js │ │ ├── markdown.js │ │ ├── modal.js │ │ ├── select2.js │ │ ├── svg.js │ │ ├── todomvc.js │ │ └── tree.js │ ├── helpers/ │ │ ├── .eslintrc │ │ ├── classlist.js │ │ ├── test-object-option.js │ │ ├── to-equal.js │ │ ├── to-have-been-warned.js │ │ ├── trigger-event.js │ │ ├── vdom.js │ │ └── wait-for-update.js │ ├── ssr/ │ │ ├── .eslintrc │ │ ├── async-loader.js │ │ ├── compile-with-webpack.js │ │ ├── fixtures/ │ │ │ ├── app.js │ │ │ ├── async-bar.js │ │ │ ├── async-foo.js │ │ │ ├── cache.js │ │ │ ├── error.js │ │ │ ├── nested-cache.js │ │ │ ├── promise-rejection.js │ │ │ ├── split.js │ │ │ └── test.css │ │ ├── jasmine.json │ │ ├── ssr-basic-renderer.spec.js │ │ ├── ssr-bundle-render.spec.js │ │ ├── ssr-stream.spec.js │ │ ├── ssr-string.spec.js │ │ └── ssr-template.spec.js │ ├── unit/ │ │ ├── .eslintrc │ │ ├── features/ │ │ │ ├── component/ │ │ │ │ ├── component-async.spec.js │ │ │ │ ├── component-keep-alive.spec.js │ │ │ │ ├── component-scoped-slot.spec.js │ │ │ │ ├── component-slot.spec.js │ │ │ │ └── component.spec.js │ │ │ ├── debug.spec.js │ │ │ ├── directives/ │ │ │ │ ├── bind.spec.js │ │ │ │ ├── class.spec.js │ │ │ │ ├── cloak.spec.js │ │ │ │ ├── for.spec.js │ │ │ │ ├── html.spec.js │ │ │ │ ├── if.spec.js │ │ │ │ ├── model-checkbox.spec.js │ │ │ │ ├── model-component.spec.js │ │ │ │ ├── model-dynamic.spec.js │ │ │ │ ├── model-file.spec.js │ │ │ │ ├── model-parse.spec.js │ │ │ │ ├── model-radio.spec.js │ │ │ │ ├── model-select.spec.js │ │ │ │ ├── model-text.spec.js │ │ │ │ ├── on.spec.js │ │ │ │ ├── once.spec.js │ │ │ │ ├── pre.spec.js │ │ │ │ ├── show.spec.js │ │ │ │ ├── static-style-parser.spec.js │ │ │ │ ├── style.spec.js │ │ │ │ └── text.spec.js │ │ │ ├── error-handling.spec.js │ │ │ ├── filter/ │ │ │ │ └── filter.spec.js │ │ │ ├── global-api/ │ │ │ │ ├── assets.spec.js │ │ │ │ ├── compile.spec.js │ │ │ │ ├── config.spec.js │ │ │ │ ├── extend.spec.js │ │ │ │ ├── mixin.spec.js │ │ │ │ ├── set-delete.spec.js │ │ │ │ └── use.spec.js │ │ │ ├── instance/ │ │ │ │ ├── init.spec.js │ │ │ │ ├── methods-data.spec.js │ │ │ │ ├── methods-events.spec.js │ │ │ │ ├── methods-lifecycle.spec.js │ │ │ │ ├── properties.spec.js │ │ │ │ └── render-proxy.spec.js │ │ │ ├── options/ │ │ │ │ ├── _scopeId.spec.js │ │ │ │ ├── comments.spec.js │ │ │ │ ├── components.spec.js │ │ │ │ ├── computed.spec.js │ │ │ │ ├── data.spec.js │ │ │ │ ├── delimiters.spec.js │ │ │ │ ├── directives.spec.js │ │ │ │ ├── el.spec.js │ │ │ │ ├── errorCaptured.spec.js │ │ │ │ ├── extends.spec.js │ │ │ │ ├── functional.spec.js │ │ │ │ ├── inheritAttrs.spec.js │ │ │ │ ├── inject.spec.js │ │ │ │ ├── lifecycle.spec.js │ │ │ │ ├── methods.spec.js │ │ │ │ ├── mixins.spec.js │ │ │ │ ├── name.spec.js │ │ │ │ ├── parent.spec.js │ │ │ │ ├── props.spec.js │ │ │ │ ├── propsData.spec.js │ │ │ │ ├── render.spec.js │ │ │ │ ├── renderError.spec.js │ │ │ │ ├── template.spec.js │ │ │ │ └── watch.spec.js │ │ │ ├── ref.spec.js │ │ │ └── transition/ │ │ │ ├── inject-styles.js │ │ │ ├── transition-group.spec.js │ │ │ ├── transition-mode.spec.js │ │ │ └── transition.spec.js │ │ ├── index.js │ │ ├── karma.base.config.js │ │ ├── karma.cover.config.js │ │ ├── karma.dev.config.js │ │ ├── karma.sauce.config.js │ │ ├── karma.unit.config.js │ │ └── modules/ │ │ ├── compiler/ │ │ │ ├── codegen.spec.js │ │ │ ├── compiler-options.spec.js │ │ │ ├── optimizer.spec.js │ │ │ └── parser.spec.js │ │ ├── observer/ │ │ │ ├── dep.spec.js │ │ │ ├── observer.spec.js │ │ │ ├── scheduler.spec.js │ │ │ └── watcher.spec.js │ │ ├── server-compiler/ │ │ │ └── optimizer.spec.js │ │ ├── sfc/ │ │ │ └── sfc-parser.spec.js │ │ ├── util/ │ │ │ └── next-tick.spec.js │ │ └── vdom/ │ │ ├── create-component.spec.js │ │ ├── create-element.spec.js │ │ ├── modules/ │ │ │ ├── attrs.spec.js │ │ │ ├── class.spec.js │ │ │ ├── directive.spec.js │ │ │ ├── dom-props.spec.js │ │ │ ├── events.spec.js │ │ │ └── style.spec.js │ │ └── patch/ │ │ ├── children.spec.js │ │ ├── edge-cases.spec.js │ │ ├── element.spec.js │ │ ├── hooks.spec.js │ │ └── hydration.spec.js │ └── weex/ │ ├── .eslintrc │ ├── cases/ │ │ ├── cases.spec.js │ │ ├── event/ │ │ │ ├── click.after.vdom.js │ │ │ ├── click.before.vdom.js │ │ │ └── click.vue │ │ ├── recycle-list/ │ │ │ ├── attrs.vdom.js │ │ │ ├── attrs.vue │ │ │ ├── classname.vdom.js │ │ │ ├── classname.vue │ │ │ ├── components/ │ │ │ │ ├── banner.vue │ │ │ │ ├── counter.vue │ │ │ │ ├── editor.vue │ │ │ │ ├── footer.vue │ │ │ │ ├── lifecycle.vue │ │ │ │ ├── poster.vue │ │ │ │ ├── stateful-lifecycle.vdom.js │ │ │ │ ├── stateful-lifecycle.vue │ │ │ │ ├── stateful-v-model.vdom.js │ │ │ │ ├── stateful-v-model.vue │ │ │ │ ├── stateful.vdom.js │ │ │ │ ├── stateful.vue │ │ │ │ ├── stateless-multi-components.vdom.js │ │ │ │ ├── stateless-multi-components.vue │ │ │ │ ├── stateless-with-props.vdom.js │ │ │ │ ├── stateless-with-props.vue │ │ │ │ ├── stateless.vdom.js │ │ │ │ └── stateless.vue │ │ │ ├── inline-style.vdom.js │ │ │ ├── inline-style.vue │ │ │ ├── text-node.vdom.js │ │ │ ├── text-node.vue │ │ │ ├── v-else-if.vdom.js │ │ │ ├── v-else-if.vue │ │ │ ├── v-else.vdom.js │ │ │ ├── v-else.vue │ │ │ ├── v-for-iterator.vdom.js │ │ │ ├── v-for-iterator.vue │ │ │ ├── v-for.vdom.js │ │ │ ├── v-for.vue │ │ │ ├── v-if.vdom.js │ │ │ ├── v-if.vue │ │ │ ├── v-on-inline.vdom.js │ │ │ ├── v-on-inline.vue │ │ │ ├── v-on.vdom.js │ │ │ ├── v-on.vue │ │ │ ├── v-once.vdom.js │ │ │ └── v-once.vue │ │ └── render/ │ │ ├── sample.vdom.js │ │ └── sample.vue │ ├── compiler/ │ │ ├── append.spec.js │ │ ├── class.spec.js │ │ ├── compile.spec.js │ │ ├── parser.spec.js │ │ ├── props.spec.js │ │ ├── style.spec.js │ │ └── v-model.spec.js │ ├── helpers/ │ │ └── index.js │ ├── jasmine.json │ └── runtime/ │ ├── attrs.spec.js │ ├── class.spec.js │ ├── components/ │ │ └── richtext.spec.js │ ├── events.spec.js │ ├── framework.spec.js │ ├── node.spec.js │ └── style.spec.js ├── vue-router/ │ ├── build/ │ │ ├── build.js │ │ ├── configs.js │ │ ├── release.sh │ │ ├── rollup.dev.config.js │ │ └── update-docs.sh │ ├── flow/ │ │ └── declarations.js │ ├── src/ │ │ ├── components/ │ │ │ ├── link.js │ │ │ └── view.js │ │ ├── create-matcher.js │ │ ├── create-route-map.js │ │ ├── history/ │ │ │ ├── abstract.js │ │ │ ├── base.js │ │ │ ├── hash.js │ │ │ └── html5.js │ │ ├── index.js │ │ ├── install.js │ │ └── util/ │ │ ├── async.js │ │ ├── dom.js │ │ ├── location.js │ │ ├── params.js │ │ ├── path.js │ │ ├── push-state.js │ │ ├── query.js │ │ ├── resolve-components.js │ │ ├── route.js │ │ ├── scroll.js │ │ └── warn.js │ └── test/ │ ├── e2e/ │ │ ├── nightwatch.config.js │ │ ├── runner.js │ │ └── specs/ │ │ ├── active-links.js │ │ ├── auth-flow.js │ │ ├── basic.js │ │ ├── data-fetching.js │ │ ├── hash-mode.js │ │ ├── hash-scroll-behavior.js │ │ ├── lazy-loading.js │ │ ├── named-routes.js │ │ ├── named-views.js │ │ ├── navigation-guards.js │ │ ├── nested-router.js │ │ ├── nested-routes.js │ │ ├── redirect.js │ │ ├── route-alias.js │ │ ├── route-matching.js │ │ ├── route-props.js │ │ ├── scroll-behavior.js │ │ └── transitions.js │ └── unit/ │ ├── jasmine.json │ └── specs/ │ ├── api.spec.js │ ├── async.spec.js │ ├── create-map.spec.js │ ├── create-matcher.spec.js │ ├── custom-query.spec.js │ ├── discrete-components.spec.js │ ├── error-handling.spec.js │ ├── location.spec.js │ ├── node.spec.js │ ├── path.spec.js │ ├── query.spec.js │ └── route.spec.js └── vuex/ ├── build/ │ ├── build.main.js │ ├── configs.js │ ├── release.sh │ ├── rollup.dev.config.js │ └── rollup.logger.config.js ├── src/ │ ├── helpers.js │ ├── index.esm.js │ ├── index.js │ ├── mixin.js │ ├── module/ │ │ ├── module-collection.js │ │ └── module.js │ ├── plugins/ │ │ ├── devtool.js │ │ └── logger.js │ ├── store.js │ └── util.js └── test/ ├── e2e/ │ ├── nightwatch.config.js │ ├── runner.js │ └── specs/ │ ├── cart.js │ ├── chat.js │ ├── counter.js │ └── todomvc.js └── unit/ ├── .eslintrc ├── helpers.spec.js ├── hot-reload.spec.js ├── jasmine.json ├── module/ │ ├── module-collection.spec.js │ └── module.spec.js ├── modules.spec.js ├── setup.js ├── store.spec.js └── util.spec.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .flowconfig ================================================ [ignore] .*/node_modules/.* .*/test/.* .*/build/.* .*/examples/.* .*/benchmarks/.* [include] [libs] flow [options] unsafe.enable_getters_and_setters=true module.name_mapper='^compiler/\(.*\)$' -> '/src/compiler/\1' module.name_mapper='^core/\(.*\)$' -> '/src/core/\1' module.name_mapper='^shared/\(.*\)$' -> '/src/shared/\1' module.name_mapper='^web/\(.*\)$' -> '/src/platforms/web/\1' module.name_mapper='^weex/\(.*\)$' -> '/src/platforms/weex/\1' module.name_mapper='^server/\(.*\)$' -> '/src/server/\1' module.name_mapper='^entries/\(.*\)$' -> '/src/entries/\1' module.name_mapper='^sfc/\(.*\)$' -> '/src/sfc/\1' suppress_comment= \\(.\\|\n\\)*\\$flow-disable-line ================================================ FILE: .gitignore ================================================ .idea/ node_modules dist example/ .DS_Store ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 HuangYi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Vue.js 技术揭秘 [电子书](https://ustbhuangyi.github.io/vue-analysis/) 目前社区有很多 Vue.js 的源码解析文章,但是质量层次不齐,不够系统和全面,这本电子书的目标是全方位细致深度解析 Vue.js 的实现原理,让同学们可以彻底掌握 Vue.js。目前分析的版本是 Vue.js 的最新版本 Vue.js 2.5.17-beta.0,并且之后会随着版本升级而做相应的更新,充分发挥电子书的优势。 这本电子书是作为 [《Vue.js 源码揭秘》](http://coding.imooc.com/class/228.html)视频课程的辅助教材。电子书是开源的,同学们可以免费阅读,视频是收费的,25+小时纯干货课程,如果有需要的同学可以购买来学习,**但请务必支持正版,请尊重作者的劳动成果**。 ## 章节目录 为了把 Vue.js 的源码讲明白,课程设计成由浅入深,分为核心、编译、扩展、生态四个方面去讲,并拆成了八个章节,如下图: **第一章:准备工作** 介绍了 Flow、Vue.js 的源码目录设计、Vue.js 的源码构建方式,以及从入口开始分析了 Vue.js 的初始化过程。 **第二章:数据驱动** 详细讲解了模板数据到 DOM 渲染的过程,从 `new Vue` 开始,分析了 `mount`、`render`、`update`、`patch` 等流程。 **第三章:组件化** 分析了组件化的实现原理,并且分析了组件周边的原理实现,包括合并配置、生命周期、组件注册、异步组件。 **第四章:深入响应式原理** 详细讲解了数据的变化如何驱动视图的变化,分析了响应式对象的创建,依赖收集、派发更新的实现过程,一些特殊情况的处理,并对比了计算属性和侦听属性的实现,最后分析了组件更新的过程。 **第五章:编译** 从编译的入口函数开始,分析了编译的三个核心流程的实现:`parse` -> `optimize` -> `codegen`。 **第六章:扩展** 详细讲解了 `event`、`v-model`、`slot`、`keep-alive`、`transition`、`transition-group` 等常用功能的原理实现,该章节作为一个可扩展章节,未来会分析更多 Vue 提供的特性。 **第七章:Vue-Router** 分析了 Vue-Router 的实现原理,从路由注册开始,分析了路由对象、`matcher`,并深入分析了整个路径切换的实现过程和细节。 **第八章:Vuex** 分析了 Vuex 的实现原理,深入分析了它的初始化过程,常用 API 以及插件部分的实现。 ================================================ FILE: build.sh ================================================ #!/usr/bin/env sh set -e npm run build cd dist git init git add -A git commit -m 'deploy' git push -f git@github.com:ustbhuangyi/vue-analysis.git master:gh-pages cd - ================================================ FILE: docs/.vuepress/config.js ================================================ module.exports = { base: '/vue-analysis/', dest: 'dist', title: 'Vue.js 技术揭秘', description: 'Analysis vue.js deeply', head: [ ['link', { rel: 'icon', href: `/logo.png` }], ['link', { rel: 'manifest', href: '/manifest.json' }], ['meta', { name: 'theme-color', content: '#3eaf7c' }], ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }], ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }], ['link', { rel: 'apple-touch-icon', href: `/icons/apple-touch-icon-152x152.png` }], ['link', { rel: 'mask-icon', href: '/icons/safari-pinned-tab.svg', color: '#3eaf7c' }], ['meta', { name: 'msapplication-TileImage', content: '/icons/msapplication-icon-144x144.png' }], ['meta', { name: 'msapplication-TileColor', content: '#000000' }] ], serviceWorker: false, themeConfig: { repo: 'ustbhuangyi/vue-analysis', editLinks: true, docsDir: 'docs', editLinkText: '在 GitHub 上编辑此页', lastUpdated: '上次更新', nav: [ { text: '2.x 版本', link: '/v2/prepare/' }, { text: '3.x 版本', link: '/v3/new/' }, { text: '2.x 源码配套视频', link: 'https://coding.imooc.com/class/228.html' }, { text: '3.x 源码解析课程', link: 'https://kaiwu.lagou.com/course/courseInfo.htm?courseId=326#/content' }, { text: 'React技术揭秘', link: 'https://react.iamkasong.com/' } ], sidebar: { '/v2/': [ { title: '准备工作', collapsable: false, children: [ ['prepare/', 'Introduction'], 'prepare/flow', 'prepare/directory', 'prepare/build', 'prepare/entrance' ] }, { title: '数据驱动', collapsable: false, children: [ ['data-driven/', 'Introduction'], 'data-driven/new-vue', 'data-driven/mounted', 'data-driven/render', 'data-driven/virtual-dom', 'data-driven/create-element', 'data-driven/update' ] }, { title: '组件化', collapsable: false, children: [ ['components/', 'Introduction'], 'components/create-component', 'components/patch', 'components/merge-option', 'components/lifecycle', 'components/component-register', 'components/async-component' ] }, { title: '深入响应式原理', collapsable: false, children: [ ['reactive/', 'Introduction'], 'reactive/reactive-object', 'reactive/getters', 'reactive/setters', 'reactive/next-tick', 'reactive/questions', 'reactive/computed-watcher', 'reactive/component-update', 'reactive/props', 'reactive/summary' ] }, { title: '编译', collapsable: false, children: [ ['compile/', 'Introduction'], 'compile/entrance', 'compile/parse', 'compile/optimize', 'compile/codegen' ] }, { title: '扩展', collapsable: false, children: [ ['extend/', 'Introduction'], 'extend/event', 'extend/v-model', 'extend/slot', 'extend/keep-alive', 'extend/tansition', 'extend/tansition-group' ] }, { title: 'Vue Router', collapsable: false, children: [ ['vue-router/', 'Introduction'], 'vue-router/install', 'vue-router/router', 'vue-router/matcher', 'vue-router/transition-to' ] }, { title: 'Vuex', collapsable: false, children: [ ['vuex/', 'Introduction'], 'vuex/init', 'vuex/api', 'vuex/plugin' ] } ], '/v3/': [ { title: '先导篇', collapsable: false, children: [ ['guide/', 'Introduction'] ] }, { title: 'Vue.js 3.0 核心源码解析​', collapsable: false, children: [ ['new/', 'Introduction'] ] } ] } } } ================================================ FILE: docs/.vuepress/public/manifest.json ================================================ { "name": "VueAnalysis", "short_name": "VueAnalysis", "icons": [ { "src": "/icons/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icons/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } ], "start_url": "/vue-analysis/", "display": "standalone", "background_color": "#fff", "theme_color": "#3eaf7c" } ================================================ FILE: docs/README.md ================================================ ## 前言 目前社区有很多 Vue.js 的源码解析文章,但是质量层次不齐,不够系统和全面,这本电子书的目标是全方位细致深度解析 Vue.js 的实现原理,让同学们可以彻底掌握 Vue.js。目前分析的版本是 Vue.js 的最新版本 Vue.js 2.5.17-beta.0,并且之后会随着版本升级而做相应的更新,充分发挥电子书的优势。 这本电子书是作为 [《Vue.js 源码揭秘》](http://coding.imooc.com/class/228.html)视频课程的辅助教材。电子书是开源的,同学们可以免费阅读,视频是收费的,25+小时纯干货课程,如果有需要的同学可以购买来学习,**但请务必支持正版,请尊重作者的劳动成果**。 ## 章节目录 为了把 Vue.js 的源码讲明白,课程设计成由浅入深,分为核心、编译、扩展、生态四个方面去讲,并拆成了八个章节,如下图: **第一章:准备工作** 介绍了 Flow、Vue.js 的源码目录设计、Vue.js 的源码构建方式,以及从入口开始分析了 Vue.js 的初始化过程。 **第二章:数据驱动** 详细讲解了模板数据到 DOM 渲染的过程,从 `new Vue` 开始,分析了 `mount`、`render`、`update`、`patch` 等流程。 **第三章:组件化** 分析了组件化的实现原理,并且分析了组件周边的原理实现,包括合并配置、生命周期、组件注册、异步组件。 **第四章:深入响应式原理** 详细讲解了数据的变化如何驱动视图的变化,分析了响应式对象的创建,依赖收集、派发更新的实现过程,一些特殊情况的处理,并对比了计算属性和侦听属性的实现,最后分析了组件更新的过程。 **第五章:编译** 从编译的入口函数开始,分析了编译的三个核心流程的实现:`parse` -> `optimize` -> `codegen`。 **第六章:扩展** 详细讲解了 `event`、`v-model`、`slot`、`keep-alive`、`transition`、`transition-group` 等常用功能的原理实现,该章节作为一个可扩展章节,未来会分析更多 Vue 提供的特性。 **第七章:Vue-Router** 分析了 Vue-Router 的实现原理,从路由注册开始,分析了路由对象、`matcher`,并深入分析了整个路径切换的实现过程和细节。 **第八章:Vuex** 分析了 Vuex 的实现原理,深入分析了它的初始化过程,常用 API 以及插件部分的实现。 ================================================ FILE: docs/v2/compile/codegen.md ================================================ # codegen 编译的最后一步就是把优化后的 AST 树转换成可执行的代码,这部分内容也比较多,我并不打算把所有的细节都讲了,了解整体流程即可。部分细节我们会在之后的章节配合一个具体 case 去详细讲。 为了方便理解,我们还是用之前的例子: ```html
  • {{item}}:{{index}}
``` 它经过编译,执行 `const code = generate(ast, options)`,生成的 `render` 代码串如下: ```js with(this){ return (isShow) ? _c('ul', { staticClass: "list", class: bindCls }, _l((data), function(item, index) { return _c('li', { on: { "click": function($event) { clickItem(index) } } }, [_v(_s(item) + ":" + _s(index))]) }) ) : _e() } ``` 这里的 `_c` 函数定义在 `src/core/instance/render.js` 中。 ```js vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) ``` 而 `_l`、`_v` 定义在 `src/core/instance/render-helpers/index.js` 中: ```js export function installRenderHelpers (target: any) { target._o = markOnce target._n = toNumber target._s = toString target._l = renderList target._t = renderSlot target._q = looseEqual target._i = looseIndexOf target._m = renderStatic target._f = resolveFilter target._k = checkKeyCodes target._b = bindObjectProps target._v = createTextVNode target._e = createEmptyVNode target._u = resolveScopedSlots target._g = bindObjectListeners } ``` 顾名思义,`_c` 就是执行 `createElement` 去创建 VNode,而 `_l` 对应 `renderList` 渲染列表;`_v` 对应 `createTextVNode` 创建文本 VNode;`_e` 对于 `createEmptyVNode`创建空的 VNode。 在 `compileToFunctions` 中,会把这个 `render` 代码串转换成函数,它的定义在 `src/compler/to-function.js` 中: ```js const compiled = compile(template, options) res.render = createFunction(compiled.render, fnGenErrors) function createFunction (code, errors) { try { return new Function(code) } catch (err) { errors.push({ err, code }) return noop } } ``` 实际上就是把 `render` 代码串通过 `new Function` 的方式转换成可执行的函数,赋值给 `vm.options.render`,这样当组件通过 `vm._render` 的时候,就会执行这个 `render` 函数。那么接下来我们就重点关注一下这个 `render` 代码串的生成过程。 ## generate ```js const code = generate(ast, options) ``` `generate` 函数的定义在 `src/compiler/codegen/index.js` 中: ```js export function generate ( ast: ASTElement | void, options: CompilerOptions ): CodegenResult { const state = new CodegenState(options) const code = ast ? genElement(ast, state) : '_c("div")' return { render: `with(this){return ${code}}`, staticRenderFns: state.staticRenderFns } } ``` `generate` 函数首先通过 `genElement(ast, state)` 生成 `code`,再把 `code` 用 `with(this){return ${code}}}` 包裹起来。这里的 `state` 是 `CodegenState` 的一个实例,稍后我们在用到它的时候会介绍它。先来看一下 `genElement`: ```js export function genElement (el: ASTElement, state: CodegenState): string { if (el.staticRoot && !el.staticProcessed) { return genStatic(el, state) } else if (el.once && !el.onceProcessed) { return genOnce(el, state) } else if (el.for && !el.forProcessed) { return genFor(el, state) } else if (el.if && !el.ifProcessed) { return genIf(el, state) } else if (el.tag === 'template' && !el.slotTarget) { return genChildren(el, state) || 'void 0' } else if (el.tag === 'slot') { return genSlot(el, state) } else { // component or element let code if (el.component) { code = genComponent(el.component, el, state) } else { const data = el.plain ? undefined : genData(el, state) const children = el.inlineTemplate ? null : genChildren(el, state, true) code = `_c('${el.tag}'${ data ? `,${data}` : '' // data }${ children ? `,${children}` : '' // children })` } // module transforms for (let i = 0; i < state.transforms.length; i++) { code = state.transforms[i](el, code) } return code } } ``` 基本就是判断当前 AST 元素节点的属性执行不同的代码生成函数,在我们的例子中,我们先了解一下 `genFor` 和 `genIf`。 ## `genIf` ```js export function genIf ( el: any, state: CodegenState, altGen?: Function, altEmpty?: string ): string { el.ifProcessed = true // avoid recursion return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty) } function genIfConditions ( conditions: ASTIfConditions, state: CodegenState, altGen?: Function, altEmpty?: string ): string { if (!conditions.length) { return altEmpty || '_e()' } const condition = conditions.shift() if (condition.exp) { return `(${condition.exp})?${ genTernaryExp(condition.block) }:${ genIfConditions(conditions, state, altGen, altEmpty) }` } else { return `${genTernaryExp(condition.block)}` } // v-if with v-once should generate code like (a)?_m(0):_m(1) function genTernaryExp (el) { return altGen ? altGen(el, state) : el.once ? genOnce(el, state) : genElement(el, state) } } ``` `genIf` 主要是通过执行 `genIfConditions`,它是依次从 `conditions` 获取第一个 `condition`,然后通过对 `condition.exp` 去生成一段三元运算符的代码,`:` 后是递归调用 `genIfConditions`,这样如果有多个 `conditions`,就生成多层三元运算逻辑。这里我们暂时不考虑 `v-once` 的情况,所以 `genTernaryExp` 最终是调用了 `genElement`。 在我们的例子中,只有一个 `condition`,`exp` 为 `isShow`,因此生成如下伪代码: ```js return (isShow) ? genElement(el, state) : _e() ``` ## `genFor` ```js export function genFor ( el: any, state: CodegenState, altGen?: Function, altHelper?: string ): string { const exp = el.for const alias = el.alias const iterator1 = el.iterator1 ? `,${el.iterator1}` : '' const iterator2 = el.iterator2 ? `,${el.iterator2}` : '' if (process.env.NODE_ENV !== 'production' && state.maybeComponent(el) && el.tag !== 'slot' && el.tag !== 'template' && !el.key ) { state.warn( `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` + `v-for should have explicit keys. ` + `See https://vuejs.org/guide/list.html#key for more info.`, true /* tip */ ) } el.forProcessed = true // avoid recursion return `${altHelper || '_l'}((${exp}),` + `function(${alias}${iterator1}${iterator2}){` + `return ${(altGen || genElement)(el, state)}` + '})' } ``` `genFor` 的逻辑很简单,首先 AST 元素节点中获取了和 `for` 相关的一些属性,然后返回了一个代码字符串。 在我们的例子中,`exp` 是 `data`,`alias` 是 `item`,`iterator1` ,因此生成如下伪代码: ```js _l((data), function(item, index) { return genElememt(el, state) }) ``` ## `genData` & `genChildren` 再次回顾我们的例子,它的最外层是 `ul`,首先执行 `genIf`,它最终调用了 `genElement(el, state)` 去生成子节点,注意,这里的 `el` 仍然指向的是 `ul` 对应的 AST 节点,但是此时的 `el.ifProcessed` 为 true,所以命中最后一个 else 逻辑: ```js // component or element let code if (el.component) { code = genComponent(el.component, el, state) } else { const data = el.plain ? undefined : genData(el, state) const children = el.inlineTemplate ? null : genChildren(el, state, true) code = `_c('${el.tag}'${ data ? `,${data}` : '' // data }${ children ? `,${children}` : '' // children })` } // module transforms for (let i = 0; i < state.transforms.length; i++) { code = state.transforms[i](el, code) } return code ``` 这里我们只关注 2 个逻辑,`genData` 和 `genChildren`: - genData ```js export function genData (el: ASTElement, state: CodegenState): string { let data = '{' // directives first. // directives may mutate the el's other properties before they are generated. const dirs = genDirectives(el, state) if (dirs) data += dirs + ',' // key if (el.key) { data += `key:${el.key},` } // ref if (el.ref) { data += `ref:${el.ref},` } if (el.refInFor) { data += `refInFor:true,` } // pre if (el.pre) { data += `pre:true,` } // record original tag name for components using "is" attribute if (el.component) { data += `tag:"${el.tag}",` } // module data generation functions for (let i = 0; i < state.dataGenFns.length; i++) { data += state.dataGenFns[i](el) } // attributes if (el.attrs) { data += `attrs:{${genProps(el.attrs)}},` } // DOM props if (el.props) { data += `domProps:{${genProps(el.props)}},` } // event handlers if (el.events) { data += `${genHandlers(el.events, false, state.warn)},` } if (el.nativeEvents) { data += `${genHandlers(el.nativeEvents, true, state.warn)},` } // slot target // only for non-scoped slots if (el.slotTarget && !el.slotScope) { data += `slot:${el.slotTarget},` } // scoped slots if (el.scopedSlots) { data += `${genScopedSlots(el.scopedSlots, state)},` } // component v-model if (el.model) { data += `model:{value:${ el.model.value },callback:${ el.model.callback },expression:${ el.model.expression }},` } // inline-template if (el.inlineTemplate) { const inlineTemplate = genInlineTemplate(el, state) if (inlineTemplate) { data += `${inlineTemplate},` } } data = data.replace(/,$/, '') + '}' // v-bind data wrap if (el.wrapData) { data = el.wrapData(data) } // v-on data wrap if (el.wrapListeners) { data = el.wrapListeners(data) } return data } ``` `genData` 函数就是根据 AST 元素节点的属性构造出一个 `data` 对象字符串,这个在后面创建 VNode 的时候的时候会作为参数传入。 之前我们提到了 `CodegenState` 的实例 `state`,这里有一段关于 `state` 的逻辑: ```js for (let i = 0; i < state.dataGenFns.length; i++) { data += state.dataGenFns[i](el) } ``` `state.dataGenFns` 的初始化在它的构造器中。 ```js export class CodegenState { constructor (options: CompilerOptions) { // ... this.dataGenFns = pluckModuleFunction(options.modules, 'genData') // ... } } ``` 实际上就是获取所有 `modules` 中的 `genData` 函数,其中,`class module` 和 `style module` 定义了 `genData` 函数。比如定义在 `src/platforms/web/compiler/modules/class.js` 中的 `genData` 方法: ```js function genData (el: ASTElement): string { let data = '' if (el.staticClass) { data += `staticClass:${el.staticClass},` } if (el.classBinding) { data += `class:${el.classBinding},` } return data } ``` 在我们的例子中,`ul` AST 元素节点定义了 `el.staticClass` 和 `el.classBinding`,因此最终生成的 `data` 字符串如下: ```js { staticClass: "list", class: bindCls } ``` - genChildren 接下来我们再来看一下 `genChildren`,它的定义在 `src/compiler/codegen/index.js` 中: ```js export function genChildren ( el: ASTElement, state: CodegenState, checkSkip?: boolean, altGenElement?: Function, altGenNode?: Function ): string | void { const children = el.children if (children.length) { const el: any = children[0] if (children.length === 1 && el.for && el.tag !== 'template' && el.tag !== 'slot' ) { return (altGenElement || genElement)(el, state) } const normalizationType = checkSkip ? getNormalizationType(children, state.maybeComponent) : 0 const gen = altGenNode || genNode return `[${children.map(c => gen(c, state)).join(',')}]${ normalizationType ? `,${normalizationType}` : '' }` } } ``` 在我们的例子中,`li` AST 元素节点是 `ul` AST 元素节点的 `children` 之一,满足 `(children.length === 1 && el.for && el.tag !== 'template' && el.tag !== 'slot')` 条件,因此通过 `genElement(el, state)` 生成 `li` AST元素节点的代码,也就回到了我们之前调用 `genFor` 生成的代码,把它们拼在一起生成的伪代码如下: ```js return (isShow) ? _c('ul', { staticClass: "list", class: bindCls }, _l((data), function(item, index) { return genElememt(el, state) }) ) : _e() ``` 在我们的例子中,在执行 `genElememt(el, state)` 的时候,`el` 还是 `li` AST 元素节点,`el.forProcessed` 已为 true,所以会继续执行 `genData` 和 `genChildren` 的逻辑。由于 `el.events` 不为空,在执行 `genData` 的时候,会执行 如下逻辑: ```js if (el.events) { data += `${genHandlers(el.events, false, state.warn)},` } ``` `genHandlers` 的定义在 `src/compiler/codegen/events.js` 中: ```js export function genHandlers ( events: ASTElementHandlers, isNative: boolean, warn: Function ): string { let res = isNative ? 'nativeOn:{' : 'on:{' for (const name in events) { res += `"${name}":${genHandler(name, events[name])},` } return res.slice(0, -1) + '}' } ``` `genHandler` 的逻辑就不介绍了,很大部分都是对修饰符 `modifier` 的处理,感兴趣同学可以自己看,对于我们的例子,它最终 `genData` 生成的 `data` 字符串如下: ```js { on: { "click": function($event) { clickItem(index) } } } ``` `genChildren` 的时候,会执行到如下逻辑: ```js export function genChildren ( el: ASTElement, state: CodegenState, checkSkip?: boolean, altGenElement?: Function, altGenNode?: Function ): string | void { // ... const normalizationType = checkSkip ? getNormalizationType(children, state.maybeComponent) : 0 const gen = altGenNode || genNode return `[${children.map(c => gen(c, state)).join(',')}]${ normalizationType ? `,${normalizationType}` : '' }` } function genNode (node: ASTNode, state: CodegenState): string { if (node.type === 1) { return genElement(node, state) } if (node.type === 3 && node.isComment) { return genComment(node) } else { return genText(node) } } ``` `genChildren` 的就是遍历 `children`,然后执行 `genNode` 方法,根据不同的 `type` 执行具体的方法。在我们的例子中,`li` AST 元素节点的 `children` 是 type 为 2 的表达式 AST 元素节点,那么会执行到 `genText(node)` 逻辑。 ```js export function genText (text: ASTText | ASTExpression): string { return `_v(${text.type === 2 ? text.expression : transformSpecialNewlines(JSON.stringify(text.text)) })` } ``` 因此在我们的例子中,`genChildren` 生成的代码串如下: ```js [_v(_s(item) + ":" + _s(index))] ``` 和之前拼在一起,最终生成的 `code` 如下: ```js return (isShow) ? _c('ul', { staticClass: "list", class: bindCls }, _l((data), function(item, index) { return _c('li', { on: { "click": function($event) { clickItem(index) } } }, [_v(_s(item) + ":" + _s(index))]) }) ) : _e() ``` ## 总结 这一节通过例子配合解析,我们对从 `ast -> code ` 这一步有了一些了解,编译后生成的代码就是在运行时执行的代码。由于 `genCode` 的内容有很多,所以我对大家的建议是没必要把所有的细节都一次性看完,我们应该根据具体一个 case,走完一条主线即可。 在之后的章节我们会对 `slot` 的实现做解析,我们会重新复习编译的章节,针对具体问题做具体分析,有利于我们排除干扰,对编译过程的学习有更深入的理解。 ================================================ FILE: docs/v2/compile/entrance.md ================================================ # 编译入口 当我们使用 Runtime + Compiler 的 Vue.js,它的入口是 `src/platforms/web/entry-runtime-with-compiler.js`,看一下它对 `$mount` 函数的定义: ```js const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) /* istanbul ignore if */ if (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue to or - mount to normal elements instead.` ) return this } const options = this.$options // resolve template/el and convert to render function if (!options.render) { let template = options.template if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { template = idToTemplate(template) /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !template) { warn( `Template element not found or is empty: ${options.template}`, this ) } } } else if (template.nodeType) { template = template.innerHTML } else { if (process.env.NODE_ENV !== 'production') { warn('invalid template option:' + template, this) } return this } } else if (el) { template = getOuterHTML(el) } if (template) { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile') } const { render, staticRenderFns } = compileToFunctions(template, { shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) options.render = render options.staticRenderFns = staticRenderFns /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile end') measure(`vue ${this._name} compile`, 'compile', 'compile end') } } } return mount.call(this, el, hydrating) } ``` 这段函数逻辑之前分析过,关于编译的入口就是在这里: ```js const { render, staticRenderFns } = compileToFunctions(template, { shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) options.render = render options.staticRenderFns = staticRenderFns ``` `compileToFunctions` 方法就是把模板 `template` 编译生成 `render` 以及 `staticRenderFns`,它的定义在 `src/platforms/web/compiler/index.js` 中: ```js import { baseOptions } from './options' import { createCompiler } from 'compiler/index' const { compile, compileToFunctions } = createCompiler(baseOptions) export { compile, compileToFunctions } ``` 可以看到 `compileToFunctions` 方法实际上是 `createCompiler` 方法的返回值,该方法接收一个编译配置参数,接下来我们来看一下 `createCompiler` 方法的定义,在 `src/compiler/index.js` 中: ```js // `createCompilerCreator` allows creating compilers that use alternative // parser/optimizer/codegen, e.g the SSR optimizing compiler. // Here we just export a default compiler using the default parts. export const createCompiler = createCompilerCreator(function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { const ast = parse(template.trim(), options) if (options.optimize !== false) { optimize(ast, options) } const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns } }) ``` `createCompiler` 方法实际上是通过调用 `createCompilerCreator` 方法返回的,该方法传入的参数是一个函数,真正的编译过程都在这个 `baseCompile` 函数里执行,那么 `createCompilerCreator` 又是什么呢,它的定义在 `src/compiler/create-compiler.js` 中: ```js export function createCompilerCreator (baseCompile: Function): Function { return function createCompiler (baseOptions: CompilerOptions) { function compile ( template: string, options?: CompilerOptions ): CompiledResult { const finalOptions = Object.create(baseOptions) const errors = [] const tips = [] finalOptions.warn = (msg, tip) => { (tip ? tips : errors).push(msg) } if (options) { // merge custom modules if (options.modules) { finalOptions.modules = (baseOptions.modules || []).concat(options.modules) } // merge custom directives if (options.directives) { finalOptions.directives = extend( Object.create(baseOptions.directives || null), options.directives ) } // copy other options for (const key in options) { if (key !== 'modules' && key !== 'directives') { finalOptions[key] = options[key] } } } const compiled = baseCompile(template, finalOptions) if (process.env.NODE_ENV !== 'production') { errors.push.apply(errors, detectErrors(compiled.ast)) } compiled.errors = errors compiled.tips = tips return compiled } return { compile, compileToFunctions: createCompileToFunctionFn(compile) } } } ``` 可以看到该方法返回了一个 `createCompiler` 的函数,它接收一个 `baseOptions` 的参数,返回的是一个对象,包括 `compile` 方法属性和 `compileToFunctions` 属性,这个 `compileToFunctions` 对应的就是 `$mount` 函数调用的 `compileToFunctions` 方法,它是调用 `createCompileToFunctionFn` 方法的返回值,我们接下来看一下 `createCompileToFunctionFn` 方法,它的定义在 `src/compiler/to-function/js` 中: ```js export function createCompileToFunctionFn (compile: Function): Function { const cache = Object.create(null) return function compileToFunctions ( template: string, options?: CompilerOptions, vm?: Component ): CompiledFunctionResult { options = extend({}, options) const warn = options.warn || baseWarn delete options.warn /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production') { // detect possible CSP restriction try { new Function('return 1') } catch (e) { if (e.toString().match(/unsafe-eval|CSP/)) { warn( 'It seems you are using the standalone build of Vue.js in an ' + 'environment with Content Security Policy that prohibits unsafe-eval. ' + 'The template compiler cannot work in this environment. Consider ' + 'relaxing the policy to allow unsafe-eval or pre-compiling your ' + 'templates into render functions.' ) } } } // check cache const key = options.delimiters ? String(options.delimiters) + template : template if (cache[key]) { return cache[key] } // compile const compiled = compile(template, options) // check compilation errors/tips if (process.env.NODE_ENV !== 'production') { if (compiled.errors && compiled.errors.length) { warn( `Error compiling template:\n\n${template}\n\n` + compiled.errors.map(e => `- ${e}`).join('\n') + '\n', vm ) } if (compiled.tips && compiled.tips.length) { compiled.tips.forEach(msg => tip(msg, vm)) } } // turn code into functions const res = {} const fnGenErrors = [] res.render = createFunction(compiled.render, fnGenErrors) res.staticRenderFns = compiled.staticRenderFns.map(code => { return createFunction(code, fnGenErrors) }) // check function generation errors. // this should only happen if there is a bug in the compiler itself. // mostly for codegen development use /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production') { if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) { warn( `Failed to generate render function:\n\n` + fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'), vm ) } } return (cache[key] = res) } } ``` 至此我们总算找到了 `compileToFunctions` 的最终定义,它接收 3 个参数、编译模板 `template`,编译配置 `options` 和 Vue 实例 `vm`。核心的编译过程就一行代码: ```js const compiled = compile(template, options) ``` `compile` 函数在执行 `createCompileToFunctionFn` 的时候作为参数传入,它是 `createCompiler` 函数中定义的 `compile` 函数,如下: ```js function compile ( template: string, options?: CompilerOptions ): CompiledResult { const finalOptions = Object.create(baseOptions) const errors = [] const tips = [] finalOptions.warn = (msg, tip) => { (tip ? tips : errors).push(msg) } if (options) { // merge custom modules if (options.modules) { finalOptions.modules = (baseOptions.modules || []).concat(options.modules) } // merge custom directives if (options.directives) { finalOptions.directives = extend( Object.create(baseOptions.directives || null), options.directives ) } // copy other options for (const key in options) { if (key !== 'modules' && key !== 'directives') { finalOptions[key] = options[key] } } } const compiled = baseCompile(template, finalOptions) if (process.env.NODE_ENV !== 'production') { errors.push.apply(errors, detectErrors(compiled.ast)) } compiled.errors = errors compiled.tips = tips return compiled } ``` `compile` 函数执行的逻辑是先处理配置参数,真正执行编译过程就一行代码: ```js const compiled = baseCompile(template, finalOptions) ``` `baseCompile` 在执行 `createCompilerCreator` 方法时作为参数传入,如下: ````js export const createCompiler = createCompilerCreator(function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { const ast = parse(template.trim(), options) optimize(ast, options) const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns } }) ```` 所以编译的入口我们终于找到了,它主要就是执行了如下几个逻辑: - 解析模板字符串生成 AST ```js const ast = parse(template.trim(), options) ``` - 优化语法树 ```js optimize(ast, options) ``` - 生成代码 ```js const code = generate(ast, options) ``` 那么接下来的章节我会带大家去逐步分析这几个过程。 ## 总结 编译入口逻辑之所以这么绕,是因为 Vue.js 在不同的平台下都会有编译的过程,因此编译过程中的依赖的配置 `baseOptions` 会有所不同。而编译过程会多次执行,但这同一个平台下每一次的编译过程配置又是相同的,为了不让这些配置在每次编译过程都通过参数传入,Vue.js 利用了函数柯里化的技巧很好的实现了 `baseOptions` 的参数保留。同样,Vue.js 也是利用函数柯里化技巧把基础的编译过程函数抽出来,通过 `createCompilerCreator(baseCompile)` 的方式把真正编译的过程和其它逻辑如对编译配置处理、缓存处理等剥离开,这样的设计还是非常巧妙的。 ================================================ FILE: docs/v2/compile/index.md ================================================ # 编译 之前我们分析过模板到真实 DOM 渲染的过程,中间有一个环节是把模板编译成 `render` 函数,这个过程我们把它称作编译。 虽然我们可以直接为组件编写 `render` 函数,但是编写 `template` 模板更加直观,也更符合我们的开发习惯。 Vue.js 提供了 2 个版本,一个是 Runtime + Compiler 的,一个是 Runtime only 的,前者是包含编译代码的,可以把编译过程放在运行时做,后者是不包含编译代码的,需要借助 webpack 的 `vue-loader` 事先把模板编译成 `render `函数。 这一章我们就来分析编译的过程,对编译过程的了解会让我们对 Vue 的指令、内置组件等有更好的理解。不过由于编译的过程是一个相对复杂的过程,我们只要求理解整体的流程、输入和输出即可,对于细节我们不必抠太细。有些细节比如对于 `slot` 的处理我们可以在之后去分析插槽实现的时候再详细分析。 ================================================ FILE: docs/v2/compile/optimize.md ================================================ # optimize 当我们的模板 `template` 经过 `parse` 过程后,会输出生成 AST 树,那么接下来我们需要对这颗树做优化,`optimize` 的逻辑是远简单于 `parse` 的逻辑,所以理解起来会轻松很多。 为什么要有优化过程,因为我们知道 Vue 是数据驱动,是响应式的,但是我们的模板并不是所有数据都是响应式的,也有很多数据是首次渲染后就永远不会变化的,那么这部分数据生成的 DOM 也不会变化,我们可以在 `patch` 的过程跳过对他们的比对。 来看一下 `optimize` 方法的定义,在 `src/compiler/optimizer.js` 中: ```js /** * Goal of the optimizer: walk the generated template AST tree * and detect sub-trees that are purely static, i.e. parts of * the DOM that never needs to change. * * Once we detect these sub-trees, we can: * * 1. Hoist them into constants, so that we no longer need to * create fresh nodes for them on each re-render; * 2. Completely skip them in the patching process. */ export function optimize (root: ?ASTElement, options: CompilerOptions) { if (!root) return isStaticKey = genStaticKeysCached(options.staticKeys || '') isPlatformReservedTag = options.isReservedTag || no // first pass: mark all non-static nodes. markStatic(root) // second pass: mark static roots. markStaticRoots(root, false) } function genStaticKeys (keys: string): Function { return makeMap( 'type,tag,attrsList,attrsMap,plain,parent,children,attrs' + (keys ? ',' + keys : '') ) } ``` 我们在编译阶段可以把一些 AST 节点优化成静态节点,所以整个 `optimize` 的过程实际上就干 2 件事情,`markStatic(root)` 标记静态节点 ,`markStaticRoots(root, false)` 标记静态根。 ## 标记静态节点 ```js function markStatic (node: ASTNode) { node.static = isStatic(node) if (node.type === 1) { // do not make component slot content static. this avoids // 1. components not able to mutate slot nodes // 2. static slot content fails for hot-reloading if ( !isPlatformReservedTag(node.tag) && node.tag !== 'slot' && node.attrsMap['inline-template'] == null ) { return } for (let i = 0, l = node.children.length; i < l; i++) { const child = node.children[i] markStatic(child) if (!child.static) { node.static = false } } if (node.ifConditions) { for (let i = 1, l = node.ifConditions.length; i < l; i++) { const block = node.ifConditions[i].block markStatic(block) if (!block.static) { node.static = false } } } } } function isStatic (node: ASTNode): boolean { if (node.type === 2) { // expression return false } if (node.type === 3) { // text return true } return !!(node.pre || ( !node.hasBindings && // no dynamic bindings !node.if && !node.for && // not v-if or v-for or v-else !isBuiltInTag(node.tag) && // not a built-in isPlatformReservedTag(node.tag) && // not a component !isDirectChildOfTemplateFor(node) && Object.keys(node).every(isStaticKey) )) } ``` 首先执行 `node.static = isStatic(node)` `isStatic` 是对一个 AST 元素节点是否是静态的判断,如果是表达式,就是非静态;如果是纯文本,就是静态;对于一个普通元素,如果有 pre 属性,那么它使用了 `v-pre` 指令,是静态,否则要同时满足以下条件:没有使用 `v-if`、`v-for`,没有使用其它指令(不包括 `v-once`),非内置组件,是平台保留的标签,非带有 `v-for` 的 `template` 标签的直接子节点,节点的所有属性的 `key` 都满足静态 key;这些都满足则这个 AST 节点是一个静态节点。 如果这个节点是一个普通元素,则遍历它的所有 `children`,递归执行 `markStatic`。因为所有的 `elseif` 和 `else` 节点都不在 `children` 中, 如果节点的 `ifConditions` 不为空,则遍历 `ifConditions` 拿到所有条件中的 `block`,也就是它们对应的 AST 节点,递归执行 `markStatic`。在这些递归过程中,一旦子节点有不是 `static` 的情况,则它的父节点的 `static` 均变成 false。 ## 标记静态根 ```js function markStaticRoots (node: ASTNode, isInFor: boolean) { if (node.type === 1) { if (node.static || node.once) { node.staticInFor = isInFor } // For a node to qualify as a static root, it should have children that // are not just static text. Otherwise the cost of hoisting out will // outweigh the benefits and it's better off to just always render it fresh. if (node.static && node.children.length && !( node.children.length === 1 && node.children[0].type === 3 )) { node.staticRoot = true return } else { node.staticRoot = false } if (node.children) { for (let i = 0, l = node.children.length; i < l; i++) { markStaticRoots(node.children[i], isInFor || !!node.for) } } if (node.ifConditions) { for (let i = 1, l = node.ifConditions.length; i < l; i++) { markStaticRoots(node.ifConditions[i].block, isInFor) } } } } ``` `markStaticRoots` 第二个参数是 `isInFor`,对于已经是 `static` 的节点或者是 `v-once` 指令的节点,`node.staticInFor = isInFor`。 接着就是对于 `staticRoot` 的判断逻辑,从注释中我们可以看到,对于有资格成为 `staticRoot` 的节点,除了本身是一个静态节点外,必须满足拥有 `children`,并且 `children` 不能只是一个文本节点,不然的话把它标记成静态根节点的收益就很小了。 接下来和标记静态节点的逻辑一样,遍历 `children` 以及 `ifConditions`,递归执行 `markStaticRoots`。 回归我们之前的例子,经过 `optimize` 后,AST 树变成了如下: ```js ast = { 'type': 1, 'tag': 'ul', 'attrsList': [], 'attrsMap': { ':class': 'bindCls', 'class': 'list', 'v-if': 'isShow' }, 'if': 'isShow', 'ifConditions': [{ 'exp': 'isShow', 'block': // ul ast element }], 'parent': undefined, 'plain': false, 'staticClass': 'list', 'classBinding': 'bindCls', 'static': false, 'staticRoot': false, 'children': [{ 'type': 1, 'tag': 'li', 'attrsList': [{ 'name': '@click', 'value': 'clickItem(index)' }], 'attrsMap': { '@click': 'clickItem(index)', 'v-for': '(item,index) in data' }, 'parent': // ul ast element 'plain': false, 'events': { 'click': { 'value': 'clickItem(index)' } }, 'hasBindings': true, 'for': 'data', 'alias': 'item', 'iterator1': 'index', 'static': false, 'staticRoot': false, 'children': [ 'type': 2, 'expression': '_s(item)+":"+_s(index)' 'text': '{{item}}:{{index}}', 'tokens': [ {'@binding':'item'}, ':', {'@binding':'index'} ], 'static': false ] }] } ``` 我们发现每一个 AST 元素节点都多了 `staic` 属性,并且 `type` 为 1 的普通元素 AST 节点多了 `staticRoot` 属性。 ## 总结 那么至此我们分析完了 `optimize` 的过程,就是深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,如果是静态节点则它们生成 DOM 永远不需要改变,这对运行时对模板的更新起到极大的优化作用。 我们通过 `optimize` 我们把整个 AST 树中的每一个 AST 元素节点标记了 `static` 和 `staticRoot`,它会影响我们接下来执行代码生成的过程。 ================================================ FILE: docs/v2/compile/parse.md ================================================ # parse 编译过程首先就是对模板做解析,生成 AST,它是一种抽象语法树,是对源代码的抽象语法结构的树状表现形式。在很多编译技术中,如 babel 编译 ES6 的代码都会先生成 AST。 这个过程是比较复杂的,它会用到大量正则表达式对字符串解析,如果对正则不是很了解,建议先去补习正则表达式的知识。为了直观地演示 `parse` 的过程,我们先来看一个例子: ```html
  • {{item}}:{{index}}
``` 经过 `parse` 过程后,生成的 AST 如下: ```js ast = { 'type': 1, 'tag': 'ul', 'attrsList': [], 'attrsMap': { ':class': 'bindCls', 'class': 'list', 'v-if': 'isShow' }, 'if': 'isShow', 'ifConditions': [{ 'exp': 'isShow', 'block': // ul ast element }], 'parent': undefined, 'plain': false, 'staticClass': 'list', 'classBinding': 'bindCls', 'children': [{ 'type': 1, 'tag': 'li', 'attrsList': [{ 'name': '@click', 'value': 'clickItem(index)' }], 'attrsMap': { '@click': 'clickItem(index)', 'v-for': '(item,index) in data' }, 'parent': // ul ast element 'plain': false, 'events': { 'click': { 'value': 'clickItem(index)' } }, 'hasBindings': true, 'for': 'data', 'alias': 'item', 'iterator1': 'index', 'children': [ 'type': 2, 'expression': '_s(item)+":"+_s(index)' 'text': '{{item}}:{{index}}', 'tokens': [ {'@binding':'item'}, ':', {'@binding':'index'} ] ] }] } ``` 可以看到,生成的 AST 是一个树状结构,每一个节点都是一个 `ast element`,除了它自身的一些属性,还维护了它的父子关系,如 `parent` 指向它的父节点,`children` 指向它的所有子节点。先对 AST 有一些直观的印象,那么接下来我们来分析一下这个 AST 是如何得到的。 ## 整体流程 首先来看一下 `parse` 的定义,在 `src/compiler/parser/index.js` 中: ```js export function parse ( template: string, options: CompilerOptions ): ASTElement | void { getFnsAndConfigFromOptions(options) parseHTML(template, { // options ... start (tag, attrs, unary) { let element = createASTElement(tag, attrs) processElement(element) treeManagement() }, end () { treeManagement() closeElement() }, chars (text: string) { handleText() createChildrenASTOfText() }, comment (text: string) { createChildrenASTOfComment() } }) return astRootElement } ``` `parse` 函数的代码很长,贴一遍对同学的理解没有好处,我先把它拆成伪代码的形式,方便同学们对整体流程先有一个大致的了解。接下来我们就来分解分析每段伪代码的作用。 ### 从 options 中获取方法和配置 对应伪代码: ```js getFnsAndConfigFromOptions(options) ``` `parse` 函数的输入是 `template` 和 `options`,输出是 AST 的根节点。`template` 就是我们的模板字符串,而 `options` 实际上是和平台相关的一些配置,它的定义在 `src/platforms/web/compiler/options` 中: ```js import { isPreTag, mustUseProp, isReservedTag, getTagNamespace } from '../util/index' import modules from './modules/index' import directives from './directives/index' import { genStaticKeys } from 'shared/util' import { isUnaryTag, canBeLeftOpenTag } from './util' export const baseOptions: CompilerOptions = { expectHTML: true, modules, directives, isPreTag, isUnaryTag, mustUseProp, canBeLeftOpenTag, isReservedTag, getTagNamespace, staticKeys: genStaticKeys(modules) } ``` 这些属性和方法之所以放到 `platforms` 目录下是因为它们在不同的平台(web 和 weex)的实现是不同的。 我们用伪代码 `getFnsAndConfigFromOptions` 表示了这一过程,它的实际代码如下: ```js warn = options.warn || baseWarn platformIsPreTag = options.isPreTag || no platformMustUseProp = options.mustUseProp || no platformGetTagNamespace = options.getTagNamespace || no transforms = pluckModuleFunction(options.modules, 'transformNode') preTransforms = pluckModuleFunction(options.modules, 'preTransformNode') postTransforms = pluckModuleFunction(options.modules, 'postTransformNode') delimiters = options.delimiters ``` 这些方法和配置都是后续解析时候需要的,可以不用去管它们的具体作用,我们先往后看。 ### 解析 HTML 模板 对应伪代码: ```js parseHTML(template, options) ``` 对于 `template` 模板的解析主要是通过 `parseHTML` 函数,它的定义在 `src/compiler/parser/html-parser` 中: ```js export function parseHTML (html, options) { let lastTag while (html) { if (!lastTag || !isPlainTextElement(lastTag)){ let textEnd = html.indexOf('<') if (textEnd === 0) { if(matchComment) { advance(commentLength) continue } if(matchDoctype) { advance(doctypeLength) continue } if(matchEndTag) { advance(endTagLength) parseEndTag() continue } if(matchStartTag) { parseStartTag() handleStartTag() continue } } handleText() advance(textLength) } else { handlePlainTextElement() parseEndTag() } } } ``` 由于 `parseHTML` 的逻辑也非常复杂,因此我也用了伪代码的方式表达,整体来说它的逻辑就是循环解析 `template` ,用正则做各种匹配,对于不同情况分别进行不同的处理,直到整个 template 被解析完毕。 在匹配的过程中会利用 `advance` 函数不断前进整个模板字符串,直到字符串末尾。 ```js function advance (n) { index += n html = html.substring(n) } ``` 为了更加直观地说明 `advance` 的作用,可以通过一副图表示: 调用 `advance` 函数: ```js advance(4) ``` 得到结果: 匹配的过程中主要利用了正则表达式,如下: ```js const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ const ncname = '[a-zA-Z_][\\w\\-\\.]*' const qnameCapture = `((?:${ncname}\\:)?${ncname})` const startTagOpen = new RegExp(`^<${qnameCapture}`) const startTagClose = /^\s*(\/?)>/ const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`) const doctype = /^]+>/i const comment = /^') if (commentEnd >= 0) { if (options.shouldKeepComment) { options.comment(html.substring(4, commentEnd)) } advance(commentEnd + 3) continue } } if (conditionalComment.test(html)) { const conditionalEnd = html.indexOf(']>') if (conditionalEnd >= 0) { advance(conditionalEnd + 2) continue } } const doctypeMatch = html.match(doctype) if (doctypeMatch) { advance(doctypeMatch[0].length) continue } ``` 对于注释和条件注释节点,前进至它们的末尾位置;对于文档类型节点,则前进它自身长度的距离。 - 开始标签 ```js const startTagMatch = parseStartTag() if (startTagMatch) { handleStartTag(startTagMatch) if (shouldIgnoreFirstNewline(lastTag, html)) { advance(1) } continue } ``` 首先通过 `parseStartTag` 解析开始标签: ```js function parseStartTag () { const start = html.match(startTagOpen) if (start) { const match = { tagName: start[1], attrs: [], start: index } advance(start[0].length) let end, attr while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) { advance(attr[0].length) match.attrs.push(attr) } if (end) { match.unarySlash = end[1] advance(end[0].length) match.end = index return match } } } ``` 对于开始标签,除了标签名之外,还有一些标签相关的属性。函数先通过正则表达式 `startTagOpen` 匹配到开始标签,然后定义了 `match` 对象,接着循环去匹配开始标签中的属性并添加到 `match.attrs` 中,直到匹配的开始标签的闭合符结束。如果匹配到闭合符,则获取一元斜线符,前进到闭合符尾,并把当前索引赋值给 `match.end`。 `parseStartTag` 对开始标签解析拿到 `match` 后,紧接着会执行 `handleStartTag` 对 `match` 做处理: ```js function handleStartTag (match) { const tagName = match.tagName const unarySlash = match.unarySlash if (expectHTML) { if (lastTag === 'p' && isNonPhrasingTag(tagName)) { parseEndTag(lastTag) } if (canBeLeftOpenTag(tagName) && lastTag === tagName) { parseEndTag(tagName) } } const unary = isUnaryTag(tagName) || !!unarySlash const l = match.attrs.length const attrs = new Array(l) for (let i = 0; i < l; i++) { const args = match.attrs[i] if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) { if (args[3] === '') { delete args[3] } if (args[4] === '') { delete args[4] } if (args[5] === '') { delete args[5] } } const value = args[3] || args[4] || args[5] || '' const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href' ? options.shouldDecodeNewlinesForHref : options.shouldDecodeNewlines attrs[i] = { name: args[1], value: decodeAttr(value, shouldDecodeNewlines) } } if (!unary) { stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs }) lastTag = tagName } if (options.start) { options.start(tagName, attrs, unary, match.start, match.end) } } ``` `handleStartTag` 的核心逻辑很简单,先判断开始标签是否是一元标签,类似 `
` 这样,接着对 `match.attrs` 遍历并做了一些处理,最后判断如果非一元标签,则往 `stack` 里 push 一个对象,并且把 `tagName` 赋值给 `lastTag`。至于 `stack` 的作用,稍后我会介绍。 最后调用了 `options.start` 回调函数,并传入一些参数,这个回调函数的作用稍后我会详细介绍。 - 闭合标签 ```js const endTagMatch = html.match(endTag) if (endTagMatch) { const curIndex = index advance(endTagMatch[0].length) parseEndTag(endTagMatch[1], curIndex, index) continue } ``` 先通过正则 `endTag` 匹配到闭合标签,然后前进到闭合标签末尾,然后执行 `parseEndTag` 方法对闭合标签做解析。 ```js function parseEndTag (tagName, start, end) { let pos, lowerCasedTagName if (start == null) start = index if (end == null) end = index if (tagName) { lowerCasedTagName = tagName.toLowerCase() } if (tagName) { for (pos = stack.length - 1; pos >= 0; pos--) { if (stack[pos].lowerCasedTag === lowerCasedTagName) { break } } } else { pos = 0 } if (pos >= 0) { for (let i = stack.length - 1; i >= pos; i--) { if (process.env.NODE_ENV !== 'production' && (i > pos || !tagName) && options.warn ) { options.warn( `tag <${stack[i].tag}> has no matching end tag.` ) } if (options.end) { options.end(stack[i].tag, start, end) } } stack.length = pos lastTag = pos && stack[pos - 1].tag } else if (lowerCasedTagName === 'br') { if (options.start) { options.start(tagName, [], true, start, end) } } else if (lowerCasedTagName === 'p') { if (options.start) { options.start(tagName, [], false, start, end) } if (options.end) { options.end(tagName, start, end) } } } ``` `parseEndTag` 的核心逻辑很简单,在介绍之前我们回顾一下在执行 `handleStartTag` 的时候,对于非一元标签(有 endTag)我们都把它构造成一个对象压入到 `stack` 中,如图所示: 那么对于闭合标签的解析,就是倒序 `stack`,找到第一个和当前 `endTag` 匹配的元素。如果是正常的标签匹配,那么 `stack` 的最后一个元素应该和当前的 `endTag` 匹配,但是考虑到如下错误情况: ```html
``` 这个时候当 `endTag` 为 `` 的时候,从 `stack` 尾部找到的标签是 ``,就不能匹配,因此这种情况会报警告。匹配后把栈到 `pos` 位置的都弹出,并从 `stack` 尾部拿到 `lastTag`。 最后调用了 `options.end` 回调函数,并传入一些参数,这个回调函数的作用稍后我会详细介绍。 - 文本 ```js let text, rest, next if (textEnd >= 0) { rest = html.slice(textEnd) while ( !endTag.test(rest) && !startTagOpen.test(rest) && !comment.test(rest) && !conditionalComment.test(rest) ) { next = rest.indexOf('<', 1) if (next < 0) break textEnd += next rest = html.slice(textEnd) } text = html.substring(0, textEnd) advance(textEnd) } if (textEnd < 0) { text = html html = '' } if (options.chars && text) { options.chars(text) } ``` 接下来判断 `textEnd` 是否大于等于 0 的,满足则说明到从当前位置到 `textEnd` 位置都是文本,并且如果 `<` 是纯文本中的字符,就继续找到真正的文本结束的位置,然后前进到结束的位置。 再继续判断 `textEnd` 小于 0 的情况,则说明整个 `template` 解析完毕了,把剩余的 `html` 都赋值给了 `text`。 最后调用了 `options.chars` 回调函数,并传 `text` 参数,这个回调函数的作用稍后我会详细介绍。 因此,在循环解析整个 `template` 的过程中,会根据不同的情况,去执行不同的回调函数,下面我们来看看这些回调函数的作用。 ### 处理开始标签 对应伪代码: ```js start (tag, attrs, unary) { let element = createASTElement(tag, attrs) processElement(element) treeManagement() } ``` 当解析到开始标签的时候,最后会执行 `start` 回调函数,函数主要就做 3 件事情,创建 AST 元素,处理 AST 元素,AST 树管理。下面我们来分别来看这几个过程。 - 创建 AST 元素 ```js // check namespace. // inherit parent ns if there is one const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag) // handle IE svg bug /* istanbul ignore if */ if (isIE && ns === 'svg') { attrs = guardIESVGBug(attrs) } let element: ASTElement = createASTElement(tag, attrs, currentParent) if (ns) { element.ns = ns } export function createASTElement ( tag: string, attrs: Array, parent: ASTElement | void ): ASTElement { return { type: 1, tag, attrsList: attrs, attrsMap: makeAttrsMap(attrs), parent, children: [] } } ``` 通过 `createASTElement` 方法去创建一个 AST 元素,并添加了 namespace。可以看到,每一个 AST 元素就是一个普通的 JavaScript 对象,其中,`type` 表示 AST 元素类型,`tag` 表示标签名,`attrsList` 表示属性列表,`attrsMap` 表示属性映射表,`parent` 表示父的 AST 元素,`children` 表示子 AST 元素集合。 - 处理 AST 元素 ```js if (isForbiddenTag(element) && !isServerRendering()) { element.forbidden = true process.env.NODE_ENV !== 'production' && warn( 'Templates should only be responsible for mapping the state to the ' + 'UI. Avoid placing tags with side-effects in your templates, such as ' + `<${tag}>` + ', as they will not be parsed.' ) } // apply pre-transforms for (let i = 0; i < preTransforms.length; i++) { element = preTransforms[i](element, options) || element } if (!inVPre) { processPre(element) if (element.pre) { inVPre = true } } if (platformIsPreTag(element.tag)) { inPre = true } if (inVPre) { processRawAttrs(element) } else if (!element.processed) { // structural directives processFor(element) processIf(element) processOnce(element) // element-scope stuff processElement(element, options) } ``` 首先是对模块 `preTransforms` 的调用,其实所有模块的 `preTransforms`、 `transforms` 和 `postTransforms` 的定义都在 `src/platforms/web/compiler/modules` 目录中,这部分我们暂时不会介绍,之后会结合具体的例子说。接着判断 `element` 是否包含各种指令通过 `processXXX` 做相应的处理,处理的结果就是扩展 AST 元素的属性。这里我并不会一一介绍所有的指令处理,而是结合我们当前的例子,我们来看一下 `processFor` 和 `processIf`: ```js export function processFor (el: ASTElement) { let exp if ((exp = getAndRemoveAttr(el, 'v-for'))) { const res = parseFor(exp) if (res) { extend(el, res) } else if (process.env.NODE_ENV !== 'production') { warn( `Invalid v-for expression: ${exp}` ) } } } export const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/ export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ const stripParensRE = /^\(|\)$/g export function parseFor (exp: string): ?ForParseResult { const inMatch = exp.match(forAliasRE) if (!inMatch) return const res = {} res.for = inMatch[2].trim() const alias = inMatch[1].trim().replace(stripParensRE, '') const iteratorMatch = alias.match(forIteratorRE) if (iteratorMatch) { res.alias = alias.replace(forIteratorRE, '') res.iterator1 = iteratorMatch[1].trim() if (iteratorMatch[2]) { res.iterator2 = iteratorMatch[2].trim() } } else { res.alias = alias } return res } ``` `processFor` 就是从元素中拿到 `v-for` 指令的内容,然后分别解析出 `for`、`alias`、`iterator1`、`iterator2` 等属性的值添加到 AST 的元素上。就我们的示例 `v-for="(item,index) in data"` 而言,解析出的的 `for` 是 `data`,`alias` 是 `item`,`iterator1` 是 `index`,没有 `iterator2`。 ```js function processIf (el) { const exp = getAndRemoveAttr(el, 'v-if') if (exp) { el.if = exp addIfCondition(el, { exp: exp, block: el }) } else { if (getAndRemoveAttr(el, 'v-else') != null) { el.else = true } const elseif = getAndRemoveAttr(el, 'v-else-if') if (elseif) { el.elseif = elseif } } } export function addIfCondition (el: ASTElement, condition: ASTIfCondition) { if (!el.ifConditions) { el.ifConditions = [] } el.ifConditions.push(condition) } ``` `processIf` 就是从元素中拿 `v-if` 指令的内容,如果拿到则给 AST 元素添加 `if` 属性和 `ifConditions` 属性;否则尝试拿 `v-else` 指令及 `v-else-if` 指令的内容,如果拿到则给 AST 元素分别添加 `else` 和 `elseif` 属性。 - AST 树管理 我们在处理开始标签的时候为每一个标签创建了一个 AST 元素,在不断解析模板创建 AST 元素的时候,我们也要为它们建立父子关系,就像 DOM 元素的父子关系那样。 AST 树管理相关代码如下: ```js function checkRootConstraints (el) { if (process.env.NODE_ENV !== 'production') { if (el.tag === 'slot' || el.tag === 'template') { warnOnce( `Cannot use <${el.tag}> as component root element because it may ` + 'contain multiple nodes.' ) } if (el.attrsMap.hasOwnProperty('v-for')) { warnOnce( 'Cannot use v-for on stateful component root element because ' + 'it renders multiple elements.' ) } } } // tree management if (!root) { root = element checkRootConstraints(root) } else if (!stack.length) { // allow root elements with v-if, v-else-if and v-else if (root.if && (element.elseif || element.else)) { checkRootConstraints(element) addIfCondition(root, { exp: element.elseif, block: element }) } else if (process.env.NODE_ENV !== 'production') { warnOnce( `Component template should contain exactly one root element. ` + `If you are using v-if on multiple elements, ` + `use v-else-if to chain them instead.` ) } } if (currentParent && !element.forbidden) { if (element.elseif || element.else) { processIfConditions(element, currentParent) } else if (element.slotScope) { // scoped slot currentParent.plain = false const name = element.slotTarget || '"default"' ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element } else { currentParent.children.push(element) element.parent = currentParent } } if (!unary) { currentParent = element stack.push(element) } else { closeElement(element) } ``` AST 树管理的目标是构建一颗 AST 树,本质上它要维护 `root` 根节点和当前父节点 `currentParent`。为了保证元素可以正确闭合,这里也利用了 `stack` 栈的数据结构,和我们之前解析模板时用到的 `stack` 类似。 当我们在处理开始标签的时候,判断如果有 `currentParent`,会把当前 AST 元素 push 到 `currentParent.chilldren` 中,同时把 AST 元素的 `parent` 指向 `currentParent`。 接着就是更新 `currentParent` 和 `stack` ,判断当前如果不是一个一元标签,我们要把它生成的 AST 元素 push 到 `stack` 中,并且把当前的 AST 元素赋值给 `currentParent`。 `stack` 和 `currentParent` 除了在处理开始标签的时候会变化,在处理闭合标签的时候也会变化,因此整个 AST 树管理要结合闭合标签的处理逻辑看。 ### 处理闭合标签 对应伪代码: ```js end () { treeManagement() closeElement() } ``` 当解析到闭合标签的时候,最后会执行 `end` 回调函数: ```js // remove trailing whitespace const element = stack[stack.length - 1] const lastNode = element.children[element.children.length - 1] if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) { element.children.pop() } // pop stack stack.length -= 1 currentParent = stack[stack.length - 1] closeElement(element) ``` 首先处理了尾部空格的情况,然后把 `stack` 的元素弹一个出栈,并把 `stack` 最后一个元素赋值给 `currentParent`,这样就保证了当遇到闭合标签的时候,可以正确地更新 `stack` 的长度以及 `currentParent` 的值,这样就维护了整个 AST 树。 最后执行了 `closeElement(element)`: ```js function closeElement (element) { // check pre state if (element.pre) { inVPre = false } if (platformIsPreTag(element.tag)) { inPre = false } // apply post-transforms for (let i = 0; i < postTransforms.length; i++) { postTransforms[i](element, options) } } ``` `closeElement` 逻辑很简单,就是更新一下 `inVPre` 和 `inPre` 的状态,以及执行 `postTransforms` 函数,这些我们暂时都不必了解。 ### 处理文本内容 对应伪代码: ```js chars (text: string) { handleText() createChildrenASTOfText() } ``` 除了处理开始标签和闭合标签,我们还会在解析模板的过程中去处理一些文本内容: ```js const children = currentParent.children text = inPre || text.trim() ? isTextTag(currentParent) ? text : decodeHTMLCached(text) // only preserve whitespace if its not right after a starting tag : preserveWhitespace && children.length ? ' ' : '' if (text) { let res if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) { children.push({ type: 2, expression: res.expression, tokens: res.tokens, text }) } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') { children.push({ type: 3, text }) } } ``` 文本构造的 AST 元素有 2 种类型,一种是有表达式的,`type` 为 2,一种是纯文本,`type` 为 3。在我们的例子中,文本就是 `{{item}}:{{index}}`,是个表达式,通过执行 `parseText(text, delimiters)` 对文本解析,它的定义在 `src/compiler/parser/text-parser.js` 中: ```js const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g const regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g const buildRegex = cached(delimiters => { const open = delimiters[0].replace(regexEscapeRE, '\\$&') const close = delimiters[1].replace(regexEscapeRE, '\\$&') return new RegExp(open + '((?:.|\\n)+?)' + close, 'g') }) export function parseText ( text: string, delimiters?: [string, string] ): TextParseResult | void { const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE if (!tagRE.test(text)) { return } const tokens = [] const rawTokens = [] let lastIndex = tagRE.lastIndex = 0 let match, index, tokenValue while ((match = tagRE.exec(text))) { index = match.index // push text token if (index > lastIndex) { rawTokens.push(tokenValue = text.slice(lastIndex, index)) tokens.push(JSON.stringify(tokenValue)) } // tag token const exp = parseFilters(match[1].trim()) tokens.push(`_s(${exp})`) rawTokens.push({ '@binding': exp }) lastIndex = index + match[0].length } if (lastIndex < text.length) { rawTokens.push(tokenValue = text.slice(lastIndex)) tokens.push(JSON.stringify(tokenValue)) } return { expression: tokens.join('+'), tokens: rawTokens } } ``` `parseText` 首先根据分隔符(默认是 `{{}}`)构造了文本匹配的正则表达式,然后再循环匹配文本,遇到普通文本就 push 到 `rawTokens` 和 `tokens` 中,如果是表达式就转换成 `_s(${exp})` push 到 `tokens` 中,以及转换成 `{@binding:exp}` push 到 `rawTokens` 中。 对于我们的例子 `{{item}}:{{index}}`,`tokens` 就是 `[_s(item),'":"',_s(index)]`;`rawTokens` 就是 `[{'@binding':'item'},':',{'@binding':'index'}]`。那么返回的对象如下: ```js return { expression: '_s(item)+":"+_s(index)', tokens: [{'@binding':'item'},':',{'@binding':'index'}] } ``` ## 流程图 ## 总结 那么至此,`parse` 的过程就分析完了,看似复杂,但我们可以抛开细节理清它的整体流程。`parse` 的目标是把 `template` 模板字符串转换成 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。那么整个 `parse` 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。 AST 元素节点总共有 3 种类型,`type` 为 1 表示是普通元素,为 2 表示是表达式,为 3 表示是纯文本。其实这里我觉得源码写的不够友好,这种是典型的魔术数字,如果转换成用常量表达会更利于源码阅读。 当 AST 树构造完毕,下一步就是 `optimize` 优化这颗树。 ================================================ FILE: docs/v2/components/async-component.md ================================================ # 异步组件 在我们平时的开发工作中,为了减少首屏代码体积,往往会把一些非首屏的组件设计成异步组件,按需加载。Vue 也原生支持了异步组件的能力,如下: ```js Vue.component('async-example', function (resolve, reject) { // 这个特殊的 require 语法告诉 webpack // 自动将编译后的代码分割成不同的块, // 这些块将通过 Ajax 请求自动下载。 require(['./my-async-component'], resolve) }) ``` 示例中可以看到,Vue 注册的组件不再是一个对象,而是一个工厂函数,函数有两个参数 `resolve` 和 `reject`,函数内部用 `setTimout` 模拟了异步,实际使用可能是通过动态请求异步组件的 JS 地址,最终通过执行 `resolve` 方法,它的参数就是我们的异步组件对象。 在了解了异步组件如何注册后,我们从源码的角度来分析一下它的实现。 上一节我们分析了组件的注册逻辑,由于组件的定义并不是一个普通对象,所以不会执行 `Vue.extend` 的逻辑把它变成一个组件的构造函数,但是它仍然可以执行到 `createComponent` 函数,我们再来对这个函数做回顾,它的定义在 `src/core/vdom/create-component/js` 中: ```js export function createComponent ( Ctor: Class | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array, tag?: string ): VNode | Array | void { if (isUndef(Ctor)) { return } const baseCtor = context.$options._base // plain options object: turn it into a constructor if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor) } // ... // async component let asyncFactory if (isUndef(Ctor.cid)) { asyncFactory = Ctor Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context) if (Ctor === undefined) { // return a placeholder node for async component, which is rendered // as a comment node but preserves all the raw information for the node. // the information will be used for async server-rendering and hydration. return createAsyncPlaceholder( asyncFactory, data, context, children, tag ) } } } ``` 我们省略了不必要的逻辑,只保留关键逻辑,由于我们这个时候传入的 `Ctor` 是一个函数,那么它也并不会执行 `Vue.extend` 逻辑,因此它的 `cid` 是 `undefiend`,进入了异步组件创建的逻辑。这里首先执行了 `Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)` 方法,它的定义在 `src/core/vdom/helpers/resolve-async-component.js` 中: ```js export function resolveAsyncComponent ( factory: Function, baseCtor: Class, context: Component ): Class | void { if (isTrue(factory.error) && isDef(factory.errorComp)) { return factory.errorComp } if (isDef(factory.resolved)) { return factory.resolved } if (isTrue(factory.loading) && isDef(factory.loadingComp)) { return factory.loadingComp } if (isDef(factory.contexts)) { // already pending factory.contexts.push(context) } else { const contexts = factory.contexts = [context] let sync = true const forceRender = () => { for (let i = 0, l = contexts.length; i < l; i++) { contexts[i].$forceUpdate() } } const resolve = once((res: Object | Class) => { // cache resolved factory.resolved = ensureCtor(res, baseCtor) // invoke callbacks only if this is not a synchronous resolve // (async resolves are shimmed as synchronous during SSR) if (!sync) { forceRender() } }) const reject = once(reason => { process.env.NODE_ENV !== 'production' && warn( `Failed to resolve async component: ${String(factory)}` + (reason ? `\nReason: ${reason}` : '') ) if (isDef(factory.errorComp)) { factory.error = true forceRender() } }) const res = factory(resolve, reject) if (isObject(res)) { if (typeof res.then === 'function') { // () => Promise if (isUndef(factory.resolved)) { res.then(resolve, reject) } } else if (isDef(res.component) && typeof res.component.then === 'function') { res.component.then(resolve, reject) if (isDef(res.error)) { factory.errorComp = ensureCtor(res.error, baseCtor) } if (isDef(res.loading)) { factory.loadingComp = ensureCtor(res.loading, baseCtor) if (res.delay === 0) { factory.loading = true } else { setTimeout(() => { if (isUndef(factory.resolved) && isUndef(factory.error)) { factory.loading = true forceRender() } }, res.delay || 200) } } if (isDef(res.timeout)) { setTimeout(() => { if (isUndef(factory.resolved)) { reject( process.env.NODE_ENV !== 'production' ? `timeout (${res.timeout}ms)` : null ) } }, res.timeout) } } } sync = false // return in case resolved synchronously return factory.loading ? factory.loadingComp : factory.resolved } } ``` `resolveAsyncComponent` 函数的逻辑略复杂,因为它实际上处理了 3 种异步组件的创建方式,除了刚才示例的组件注册方式,还支持 2 种,一种是支持 `Promise` 创建组件的方式,如下: ```js Vue.component( 'async-webpack-example', // 该 `import` 函数返回一个 `Promise` 对象。 () => import('./my-async-component') ) ``` 另一种是高级异步组件,如下: ```js const AsyncComp = () => ({ // 需要加载的组件。应当是一个 Promise component: import('./MyComp.vue'), // 加载中应当渲染的组件 loading: LoadingComp, // 出错时渲染的组件 error: ErrorComp, // 渲染加载中组件前的等待时间。默认:200ms。 delay: 200, // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity timeout: 3000 }) Vue.component('async-example', AsyncComp) ``` 那么解下来,我们就根据这 3 种异步组件的情况,来分别去分析 `resolveAsyncComponent` 的逻辑。 ## 普通函数异步组件 针对普通函数的情况,前面几个 if 判断可以忽略,它们是为高级组件所用,对于 `factory.contexts` 的判断,是考虑到多个地方同时初始化一个异步组件,那么它的实际加载应该只有一次。接着进入实际加载逻辑,定义了 `forceRender`、`resolve` 和 `reject` 函数,注意 `resolve` 和 `reject` 函数用 `once` 函数做了一层包装,它的定义在 `src/shared/util.js` 中: ````js /** * Ensure a function is called only once. */ export function once (fn: Function): Function { let called = false return function () { if (!called) { called = true fn.apply(this, arguments) } } } ```` `once` 逻辑非常简单,传入一个函数,并返回一个新函数,它非常巧妙地利用闭包和一个标志位保证了它包装的函数只会执行一次,也就是确保 `resolve` 和 `reject` 函数只执行一次。 接下来执行 `const res = factory(resolve, reject)` 逻辑,这块儿就是执行我们组件的工厂函数,同时把 `resolve` 和 `reject` 函数作为参数传入,组件的工厂函数通常会先发送请求去加载我们的异步组件的 JS 文件,拿到组件定义的对象 `res` 后,执行 `resolve(res)` 逻辑,它会先执行 `factory.resolved = ensureCtor(res, baseCtor)`: ```js function ensureCtor (comp: any, base) { if ( comp.__esModule || (hasSymbol && comp[Symbol.toStringTag] === 'Module') ) { comp = comp.default } return isObject(comp) ? base.extend(comp) : comp } ``` 这个函数目的是为了保证能找到异步组件 JS 定义的组件对象,并且如果它是一个普通对象,则调用 `Vue.extend` 把它转换成一个组件的构造函数。 `resolve` 逻辑最后判断了 `sync`,显然我们这个场景下 `sync` 为 false,那么就会执行 `forceRender` 函数,它会遍历 `factory.contexts`,拿到每一个调用异步组件的实例 `vm`, 执行 `vm.$forceUpdate()` 方法,它的定义在 `src/core/instance/lifecycle.js` 中: ```js Vue.prototype.$forceUpdate = function () { const vm: Component = this if (vm._watcher) { vm._watcher.update() } } ``` `$forceUpdate` 的逻辑非常简单,就是调用渲染 `watcher` 的 `update` 方法,让渲染 `watcher` 对应的回调函数执行,也就是触发了组件的重新渲染。之所以这么做是因为 Vue 通常是数据驱动视图重新渲染,但是在整个异步组件加载过程中是没有数据发生变化的,所以通过执行 `$forceUpdate` 可以强制组件重新渲染一次。 ## `Promise` 异步组件 ```js Vue.component( 'async-webpack-example', // 该 `import` 函数返回一个 `Promise` 对象。 () => import('./my-async-component') ) ``` webpack 2+ 支持了异步加载的语法糖:`() => import('./my-async-component')`,当执行完 `res = factory(resolve, reject)`,返回的值就是 ` import('./my-async-component')` 的返回值,它是一个 `Promise` 对象。接着进入 if 条件,又判断了 `typeof res.then === 'function')`,条件满足,执行: ```js if (isUndef(factory.resolved)) { res.then(resolve, reject) } ``` 当组件异步加载成功后,执行 `resolve`,加载失败则执行 `reject`,这样就非常巧妙地实现了配合 webpack 2+ 的异步加载组件的方式(`Promise`)加载异步组件。 ## 高级异步组件 由于异步加载组件需要动态加载 JS,有一定网络延时,而且有加载失败的情况,所以通常我们在开发异步组件相关逻辑的时候需要设计 loading 组件和 error 组件,并在适当的时机渲染它们。Vue.js 2.3+ 支持了一种高级异步组件的方式,它通过一个简单的对象配置,帮你搞定 loading 组件和 error 组件的渲染时机,你完全不用关心细节,非常方便。接下来我们就从源码的角度来分析高级异步组件是怎么实现的。 ```js const AsyncComp = () => ({ // 需要加载的组件。应当是一个 Promise component: import('./MyComp.vue'), // 加载中应当渲染的组件 loading: LoadingComp, // 出错时渲染的组件 error: ErrorComp, // 渲染加载中组件前的等待时间。默认:200ms。 delay: 200, // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity timeout: 3000 }) Vue.component('async-example', AsyncComp) ``` 高级异步组件的初始化逻辑和普通异步组件一样,也是执行 `resolveAsyncComponent`,当执行完 `res = factory(resolve, reject)`,返回值就是定义的组件对象,显然满足 `else if (isDef(res.component) && typeof res.component.then === 'function')` 的逻辑,接着执行 `res.component.then(resolve, reject)`,当异步组件加载成功后,执行 `resolve`,失败执行 `reject`。 因为异步组件加载是一个异步过程,它接着又同步执行了如下逻辑: ```js if (isDef(res.error)) { factory.errorComp = ensureCtor(res.error, baseCtor) } if (isDef(res.loading)) { factory.loadingComp = ensureCtor(res.loading, baseCtor) if (res.delay === 0) { factory.loading = true } else { setTimeout(() => { if (isUndef(factory.resolved) && isUndef(factory.error)) { factory.loading = true forceRender() } }, res.delay || 200) } } if (isDef(res.timeout)) { setTimeout(() => { if (isUndef(factory.resolved)) { reject( process.env.NODE_ENV !== 'production' ? `timeout (${res.timeout}ms)` : null ) } }, res.timeout) } ``` 先判断 `res.error` 是否定义了 error 组件,如果有的话则赋值给 `factory.errorComp`。 接着判断 `res.loading` 是否定义了 loading 组件,如果有的话则赋值给 `factory.loadingComp`,如果设置了 `res.delay` 且为 0,则设置 `factory.loading = true`,否则延时 `delay` 的时间执行: ```js if (isUndef(factory.resolved) && isUndef(factory.error)) { factory.loading = true forceRender() } ``` 最后判断 `res.timeout`,如果配置了该项,则在 `res.timout` 时间后,如果组件没有成功加载,执行 `reject`。 在 `resolveAsyncComponent` 的最后有一段逻辑: ```js sync = false return factory.loading ? factory.loadingComp : factory.resolved ``` 如果 `delay` 配置为 0,则这次直接渲染 loading 组件,否则则延时 `delay` 执行 `forceRender`,那么又会再一次执行到 `resolveAsyncComponent`。 那么这时候我们有几种情况,按逻辑的执行顺序,对不同的情况做判断。 ### 异步组件加载失败 当异步组件加载失败,会执行 `reject` 函数: ```js const reject = once(reason => { process.env.NODE_ENV !== 'production' && warn( `Failed to resolve async component: ${String(factory)}` + (reason ? `\nReason: ${reason}` : '') ) if (isDef(factory.errorComp)) { factory.error = true forceRender() } }) ``` 这个时候会把 `factory.error` 设置为 `true`,同时执行 `forceRender()` 再次执行到 `resolveAsyncComponent`: ```js if (isTrue(factory.error) && isDef(factory.errorComp)) { return factory.errorComp } ``` 那么这个时候就返回 `factory.errorComp`,直接渲染 error 组件。 ### 异步组件加载成功 当异步组件加载成功,会执行 `resolve` 函数: ```js const resolve = once((res: Object | Class) => { factory.resolved = ensureCtor(res, baseCtor) if (!sync) { forceRender() } }) ``` 首先把加载结果缓存到 `factory.resolved` 中,这个时候因为 `sync` 已经为 false,则执行 `forceRender()` 再次执行到 `resolveAsyncComponent`: ```js if (isDef(factory.resolved)) { return factory.resolved } ``` 那么这个时候直接返回 `factory.resolved`,渲染成功加载的组件。 ### 异步组件加载中 如果异步组件加载中并未返回,这时候会走到这个逻辑: ```js if (isTrue(factory.loading) && isDef(factory.loadingComp)) { return factory.loadingComp } ``` 那么则会返回 `factory.loadingComp`,渲染 loading 组件。 ### 异步组件加载超时 如果超时,则走到了 `reject` 逻辑,之后逻辑和加载失败一样,渲染 error 组件。 ## 异步组件 patch 回到 `createComponent` 的逻辑: ```js Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context) if (Ctor === undefined) { return createAsyncPlaceholder( asyncFactory, data, context, children, tag ) } ``` 如果是第一次执行 `resolveAsyncComponent`,除非使用高级异步组件 `0 delay` 去创建了一个 loading 组件,否则返回是 `undefiend`,接着通过 `createAsyncPlaceholder` 创建一个注释节点作为占位符。它的定义在 `src/core/vdom/helpers/resolve-async-components.js` 中: ```js export function createAsyncPlaceholder ( factory: Function, data: ?VNodeData, context: Component, children: ?Array, tag: ?string ): VNode { const node = createEmptyVNode() node.asyncFactory = factory node.asyncMeta = { data, context, children, tag } return node } ``` 实际上就是就是创建了一个占位的注释 VNode,同时把 `asyncFactory` 和 `asyncMeta` 赋值给当前 `vnode`。 当执行 `forceRender` 的时候,会触发组件的重新渲染,那么会再一次执行 `resolveAsyncComponent`,这时候就会根据不同的情况,可能返回 loading、error 或成功加载的异步组件,返回值不为 `undefined`,因此就走正常的组件 `render`、`patch` 过程,与组件第一次渲染流程不一样,这个时候是存在新旧 `vnode` 的,下一章我会分析组件更新的 `patch` 过程。 ## 总结 通过以上代码分析,我们对 Vue 的异步组件的实现有了深入的了解,知道了 3 种异步组件的实现方式,并且看到高级异步组件的实现是非常巧妙的,它实现了 loading、resolve、reject、timeout 4 种状态。异步组件实现的本质是 2 次渲染,除了 0 delay 的高级异步组件第一次直接渲染成 loading 组件外,其它都是第一次渲染生成一个注释节点,当异步获取组件成功后,再通过 `forceRender` 强制重新渲染,这样就能正确渲染出我们异步加载的组件了。 ================================================ FILE: docs/v2/components/component-register.md ================================================ # 组件注册 在 Vue.js 中,除了它内置的组件如 `keep-alive`、`component`、`transition`、`transition-group` 等,其它用户自定义组件在使用前必须注册。很多同学在开发过程中可能会遇到如下报错信息: ``` 'Unknown custom element: - did you register the component correctly? For recursive components, make sure to provide the "name" option.' ``` 一般报这个错的原因都是我们使用了未注册的组件。Vue.js 提供了 2 种组件的注册方式,全局注册和局部注册。接下来我们从源码分析的角度来分析这两种注册方式。 ## 全局注册 要注册一个全局组件,可以使用 `Vue.component(tagName, options)`。例如: ```js Vue.component('my-component', { // 选项 }) ``` 那么,`Vue.component` 函数是在什么时候定义的呢,它的定义过程发生在最开始初始化 Vue 的全局函数的时候,代码在 `src/core/global-api/assets.js` 中: ```js import { ASSET_TYPES } from 'shared/constants' import { isPlainObject, validateComponentName } from '../util/index' export function initAssetRegisters (Vue: GlobalAPI) { /** * Create asset registration methods. */ ASSET_TYPES.forEach(type => { Vue[type] = function ( id: string, definition: Function | Object ): Function | Object | void { if (!definition) { return this.options[type + 's'][id] } else { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && type === 'component') { validateComponentName(id) } if (type === 'component' && isPlainObject(definition)) { definition.name = definition.name || id definition = this.options._base.extend(definition) } if (type === 'directive' && typeof definition === 'function') { definition = { bind: definition, update: definition } } this.options[type + 's'][id] = definition return definition } } }) } ``` 函数首先遍历 `ASSET_TYPES`,得到 `type` 后挂载到 Vue 上 。`ASSET_TYPES` 的定义在 `src/shared/constants.js` 中: ```js export const ASSET_TYPES = [ 'component', 'directive', 'filter' ] ``` 所以实际上 Vue 是初始化了 3 个全局函数,并且如果 `type` 是 `component` 且 `definition` 是一个对象的话,通过 `this.opitons._base.extend`, 相当于 `Vue.extend` 把这个对象转换成一个继承于 Vue 的构造函数,最后通过 `this.options[type + 's'][id] = definition` 把它挂载到 `Vue.options.components` 上。 由于我们每个组件的创建都是通过 `Vue.extend` 继承而来,我们之前分析过在继承的过程中有这么一段逻辑: ```js Sub.options = mergeOptions( Super.options, extendOptions ) ``` 也就是说它会把 `Vue.options` 合并到 `Sub.options`,也就是组件的 `options` 上, 然后在组件的实例化阶段,会执行 `merge options` 逻辑,把 `Sub.options.components` 合并到 `vm.$options.components` 上。 然后在创建 `vnode` 的过程中,会执行 `_createElement` 方法,我们再来回顾一下这部分的逻辑,它的定义在 `src/core/vdom/create-element.js` 中: ```js export function _createElement ( context: Component, tag?: string | Class | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array { // ... let vnode, ns if (typeof tag === 'string') { let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) if (config.isReservedTag(tag)) { // platform built-in elements vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // component vnode = createComponent(Ctor, data, context, children, tag) } else { // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { // direct component options / constructor vnode = createComponent(tag, data, context, children) } // ... } ``` 这里有一个判断逻辑 `isDef(Ctor = resolveAsset(context.$options, 'components', tag))`,先来看一下 `resolveAsset` 的定义,在 `src/core/utils/options.js` 中: ```js /** * Resolve an asset. * This function is used because child instances need access * to assets defined in its ancestor chain. */ export function resolveAsset ( options: Object, type: string, id: string, warnMissing?: boolean ): any { /* istanbul ignore if */ if (typeof id !== 'string') { return } const assets = options[type] // check local registration variations first if (hasOwn(assets, id)) return assets[id] const camelizedId = camelize(id) if (hasOwn(assets, camelizedId)) return assets[camelizedId] const PascalCaseId = capitalize(camelizedId) if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId] // fallback to prototype chain const res = assets[id] || assets[camelizedId] || assets[PascalCaseId] if (process.env.NODE_ENV !== 'production' && warnMissing && !res) { warn( 'Failed to resolve ' + type.slice(0, -1) + ': ' + id, options ) } return res } ``` 这段逻辑很简单,先通过 `const assets = options[type]` 拿到 `assets`,然后再尝试拿 `assets[id]`,这里有个顺序,先直接使用 `id` 拿,如果不存在,则把 `id` 变成驼峰的形式再拿,如果仍然不存在则在驼峰的基础上把首字母再变成大写的形式再拿,如果仍然拿不到则报错。这样说明了我们在使用 `Vue.component(id, definition)` 全局注册组件的时候,id 可以是连字符、驼峰或首字母大写的形式。 那么回到我们的调用 `resolveAsset(context.$options, 'components', tag)`,即拿 `vm.$options.components[tag]`,这样我们就可以在 `resolveAsset` 的时候拿到这个组件的构造函数,并作为 `createComponent` 的钩子的参数。 ## 局部注册 Vue.js 也同样支持局部注册,我们可以在一个组件内部使用 `components` 选项做组件的局部注册,例如: ```js import HelloWorld from './components/HelloWorld' export default { components: { HelloWorld } } ``` 其实理解了全局注册的过程,局部注册是非常简单的。在组件的 Vue 的实例化阶段有一个合并 `option` 的逻辑,之前我们也分析过,所以就把 `components` 合并到 `vm.$options.components` 上,这样我们就可以在 `resolveAsset` 的时候拿到这个组件的构造函数,并作为 `createComponent` 的钩子的参数。 注意,局部注册和全局注册不同的是,只有该类型的组件才可以访问局部注册的子组件,而全局注册是扩展到 `Vue.options` 下,所以在所有组件创建的过程中,都会从全局的 `Vue.options.components` 扩展到当前组件的 `vm.$options.components` 下,这就是全局注册的组件能被任意使用的原因。 ## 总结 通过这一小节的分析,我们对组件的注册过程有了认识,并理解了全局注册和局部注册的差异。其实在平时的工作中,当我们使用到组件库的时候,往往更通用基础组件都是全局注册的,而编写的特例场景的业务组件都是局部注册的。了解了它们的原理,对我们在工作中到底使用全局注册组件还是局部注册组件是有这非常好的指导意义的。 ================================================ FILE: docs/v2/components/create-component.md ================================================ # createComponent 上一章我们在分析 `createElement` 的实现的时候,它最终会调用 `_createElement` 方法,其中有一段逻辑是对参数 `tag` 的判断,如果是一个普通的 html 标签,像上一章的例子那样是一个普通的 div,则会实例化一个普通 VNode 节点,否则通过 `createComponent` 方法创建一个组件 VNode。 ```js if (typeof tag === 'string') { let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) if (config.isReservedTag(tag)) { // platform built-in elements vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // component vnode = createComponent(Ctor, data, context, children, tag) } else { // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { // direct component options / constructor vnode = createComponent(tag, data, context, children) } ``` 在我们这一章传入的是一个 App 对象,它本质上是一个 `Component` 类型,那么它会走到上述代码的 else 逻辑,直接通过 `createComponent` 方法来创建 `vnode`。所以接下来我们来看一下 `createComponent` 方法的实现,它定义在 `src/core/vdom/create-component.js` 文件中: ```js export function createComponent ( Ctor: Class | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array, tag?: string ): VNode | Array | void { if (isUndef(Ctor)) { return } const baseCtor = context.$options._base // plain options object: turn it into a constructor if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor) } // if at this stage it's not a constructor or an async component factory, // reject. if (typeof Ctor !== 'function') { if (process.env.NODE_ENV !== 'production') { warn(`Invalid Component definition: ${String(Ctor)}`, context) } return } // async component let asyncFactory if (isUndef(Ctor.cid)) { asyncFactory = Ctor Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context) if (Ctor === undefined) { // return a placeholder node for async component, which is rendered // as a comment node but preserves all the raw information for the node. // the information will be used for async server-rendering and hydration. return createAsyncPlaceholder( asyncFactory, data, context, children, tag ) } } data = data || {} // resolve constructor options in case global mixins are applied after // component constructor creation resolveConstructorOptions(Ctor) // transform component v-model data into props & events if (isDef(data.model)) { transformModel(Ctor.options, data) } // extract props const propsData = extractPropsFromVNodeData(data, Ctor, tag) // functional component if (isTrue(Ctor.options.functional)) { return createFunctionalComponent(Ctor, propsData, data, context, children) } // extract listeners, since these needs to be treated as // child component listeners instead of DOM listeners const listeners = data.on // replace with listeners with .native modifier // so it gets processed during parent component patch. data.on = data.nativeOn if (isTrue(Ctor.options.abstract)) { // abstract components do not keep anything // other than props & listeners & slot // work around flow const slot = data.slot data = {} if (slot) { data.slot = slot } } // install component management hooks onto the placeholder node installComponentHooks(data) // return a placeholder vnode const name = Ctor.options.name || tag const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, asyncFactory ) // Weex specific: invoke recycle-list optimized @render function for // extracting cell-slot template. // https://github.com/Hanks10100/weex-native-directive/tree/master/component /* istanbul ignore if */ if (__WEEX__ && isRecyclableComponent(vnode)) { return renderRecyclableComponentTemplate(vnode) } return vnode } ``` 可以看到,`createComponent` 的逻辑也会有一些复杂,但是分析源码比较推荐的是只分析核心流程,分支流程可以之后针对性的看,所以这里针对组件渲染这个 case 主要就 3 个关键步骤: 构造子类构造函数,安装组件钩子函数和实例化 `vnode`。 ## 构造子类构造函数 ```js const baseCtor = context.$options._base // plain options object: turn it into a constructor if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor) } ``` 我们在编写一个组件的时候,通常都是创建一个普通对象,还是以我们的 App.vue 为例,代码如下: ```js import HelloWorld from './components/HelloWorld' export default { name: 'app', components: { HelloWorld } } ``` 这里 export 的是一个对象,所以 `createComponent` 里的代码逻辑会执行到 `baseCtor.extend(Ctor)`,在这里 `baseCtor` 实际上就是 Vue,这个的定义是在最开始初始化 Vue 的阶段,在 `src/core/global-api/index.js` 中的 `initGlobalAPI` 函数有这么一段逻辑: ```js // this is used to identify the "base" constructor to extend all plain-object // components with in Weex's multi-instance scenarios. Vue.options._base = Vue ``` 细心的同学会发现,这里定义的是 `Vue.options`,而我们的 `createComponent` 取的是 `context.$options`,实际上在 `src/core/instance/init.js` 里 Vue 原型上的 `_init` 函数中有这么一段逻辑: ```js vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) ``` 这样就把 Vue 上的一些 `option` 扩展到了 vm.$options 上,所以我们也就能通过 `vm.$options._base` 拿到 Vue 这个构造函数了。`mergeOptions` 的实现我们会在后续章节中具体分析,现在只需要理解它的功能是把 Vue 构造函数的 `options` 和用户传入的 `options` 做一层合并,到 `vm.$options ` 上。 在了解了 `baseCtor` 指向了 Vue 之后,我们来看一下 `Vue.extend` 函数的定义,在 `src/core/global-api/extend.js` 中。 ```js /** * Class inheritance */ Vue.extend = function (extendOptions: Object): Function { extendOptions = extendOptions || {} const Super = this const SuperId = Super.cid const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}) if (cachedCtors[SuperId]) { return cachedCtors[SuperId] } const name = extendOptions.name || Super.options.name if (process.env.NODE_ENV !== 'production' && name) { validateComponentName(name) } const Sub = function VueComponent (options) { this._init(options) } Sub.prototype = Object.create(Super.prototype) Sub.prototype.constructor = Sub Sub.cid = cid++ Sub.options = mergeOptions( Super.options, extendOptions ) Sub['super'] = Super // For props and computed properties, we define the proxy getters on // the Vue instances at extension time, on the extended prototype. This // avoids Object.defineProperty calls for each instance created. if (Sub.options.props) { initProps(Sub) } if (Sub.options.computed) { initComputed(Sub) } // allow further extension/mixin/plugin usage Sub.extend = Super.extend Sub.mixin = Super.mixin Sub.use = Super.use // create asset registers, so extended classes // can have their private assets too. ASSET_TYPES.forEach(function (type) { Sub[type] = Super[type] }) // enable recursive self-lookup if (name) { Sub.options.components[name] = Sub } // keep a reference to the super options at extension time. // later at instantiation we can check if Super's options have // been updated. Sub.superOptions = Super.options Sub.extendOptions = extendOptions Sub.sealedOptions = extend({}, Sub.options) // cache constructor cachedCtors[SuperId] = Sub return Sub } ``` `Vue.extend` 的作用就是构造一个 `Vue` 的子类,它使用一种非常经典的原型继承的方式把一个纯对象转换一个继承于 `Vue` 的构造器 `Sub` 并返回,然后对 `Sub` 这个对象本身扩展了一些属性,如扩展 `options`、添加全局 API 等;并且对配置中的 `props` 和 `computed` 做了初始化工作;最后对于这个 `Sub` 构造函数做了缓存,避免多次执行 `Vue.extend` 的时候对同一个子组件重复构造。 这样当我们去实例化 `Sub` 的时候,就会执行 `this._init` 逻辑再次走到了 `Vue` 实例的初始化逻辑,实例化子组件的逻辑在之后的章节会介绍。 ```js const Sub = function VueComponent (options) { this._init(options) } ``` ## 安装组件钩子函数 ```js // install component management hooks onto the placeholder node installComponentHooks(data) ``` 我们之前提到 Vue.js 使用的 Virtual DOM 参考的是开源库 [snabbdom](https://github.com/snabbdom/snabbdom),它的一个特点是在 VNode 的 patch 流程中对外暴露了各种时机的钩子函数,方便我们做一些额外的事情,Vue.js 也是充分利用这一点,在初始化一个 Component 类型的 VNode 的过程中实现了几个钩子函数: ```js const componentVNodeHooks = { init (vnode: VNodeWithData, hydrating: boolean): ?boolean { if ( vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { // kept-alive components, treat as a patch const mountedNode: any = vnode // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode) } else { const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) child.$mount(hydrating ? vnode.elm : undefined, hydrating) } }, prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) { const options = vnode.componentOptions const child = vnode.componentInstance = oldVnode.componentInstance updateChildComponent( child, options.propsData, // updated props options.listeners, // updated listeners vnode, // new parent vnode options.children // new children ) }, insert (vnode: MountedComponentVNode) { const { context, componentInstance } = vnode if (!componentInstance._isMounted) { componentInstance._isMounted = true callHook(componentInstance, 'mounted') } if (vnode.data.keepAlive) { if (context._isMounted) { // vue-router#1212 // During updates, a kept-alive component's child components may // change, so directly walking the tree here may call activated hooks // on incorrect children. Instead we push them into a queue which will // be processed after the whole patch process ended. queueActivatedComponent(componentInstance) } else { activateChildComponent(componentInstance, true /* direct */) } } }, destroy (vnode: MountedComponentVNode) { const { componentInstance } = vnode if (!componentInstance._isDestroyed) { if (!vnode.data.keepAlive) { componentInstance.$destroy() } else { deactivateChildComponent(componentInstance, true /* direct */) } } } } const hooksToMerge = Object.keys(componentVNodeHooks) function installComponentHooks (data: VNodeData) { const hooks = data.hook || (data.hook = {}) for (let i = 0; i < hooksToMerge.length; i++) { const key = hooksToMerge[i] const existing = hooks[key] const toMerge = componentVNodeHooks[key] if (existing !== toMerge && !(existing && existing._merged)) { hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge } } } function mergeHook (f1: any, f2: any): Function { const merged = (a, b) => { // flow complains about extra args which is why we use any f1(a, b) f2(a, b) } merged._merged = true return merged } ``` 整个 `installComponentHooks` 的过程就是把 `componentVNodeHooks` 的钩子函数合并到 `data.hook` 中,在 VNode 执行 `patch` 的过程中执行相关的钩子函数,具体的执行我们稍后在介绍 `patch` 过程中会详细介绍。这里要注意的是合并策略,在合并过程中,如果某个时机的钩子已经存在 `data.hook` 中,那么通过执行 `mergeHook` 函数做合并,这个逻辑很简单,就是在最终执行的时候,依次执行这两个钩子函数即可。 ## 实例化 VNode ```js const name = Ctor.options.name || tag const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, asyncFactory ) return vnode ``` 最后一步非常简单,通过 `new VNode` 实例化一个 `vnode` 并返回。需要注意的是和普通元素节点的 `vnode` 不同,组件的 `vnode` 是没有 `children` 的,这点很关键,在之后的 `patch` 过程中我们会再提。 ## 总结 这一节我们分析了 `createComponent` 的实现,了解到它在渲染一个组件的时候的 3 个关键逻辑:构造子类构造函数,安装组件钩子函数和实例化 `vnode`。`createComponent` 后返回的是组件 `vnode`,它也一样走到 `vm._update` 方法,进而执行了 `patch` 函数,我们在上一章对 `patch` 函数做了简单的分析,那么下一节我们会对它做进一步的分析。 ================================================ FILE: docs/v2/components/index.md ================================================ # 组件化 Vue.js 另一个核心思想是组件化。所谓组件化,就是把页面拆分成多个组件 (component),每个组件依赖的 CSS、JavaScript、模板、图片等资源放在一起开发和维护。组件是资源独立的,组件在系统内部可复用,组件和组件之间可以嵌套。 我们在用 Vue.js 开发实际项目的时候,就是像搭积木一样,编写一堆组件拼装生成页面。在 Vue.js 的官网中,也是花了大篇幅来介绍什么是组件,如何编写组件以及组件拥有的属性和特性。 那么在这一章节,我们将从源码的角度来分析 Vue 的组件内部是如何工作的,只有了解了内部的工作原理,才能让我们使用它的时候更加得心应手。 接下来我们会用 Vue-cli 初始化的代码为例,来分析一下 Vue 组件初始化的一个过程。 ```js import Vue from 'vue' import App from './App.vue' var app = new Vue({ el: '#app', // 这里的 h 是 createElement 方法 render: h => h(App) }) ``` 这段代码相信很多同学都很熟悉,它和我们上一章相同的点也是通过 `render` 函数去渲染的,不同的这次通过 `createElement` 传的参数是一个组件而不是一个原生的标签,那么接下来我们就开始分析这一过程。 ================================================ FILE: docs/v2/components/lifecycle.md ================================================ # 生命周期 每个 Vue 实例在被创建之前都要经过一系列的初始化过程。例如需要设置数据监听、编译模板、挂载实例到 DOM、在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,给予用户机会在一些特定的场景下添加他们自己的代码。 在我们实际项目开发过程中,会非常频繁地和 Vue 组件的生命周期打交道,接下来我们就从源码的角度来看一下这些生命周期的钩子函数是如何被执行的。 源码中最终执行生命周期的函数都是调用 `callHook` 方法,它的定义在 `src/core/instance/lifecycle` 中: ```js export function callHook (vm: Component, hook: string) { // #7573 disable dep collection when invoking lifecycle hooks pushTarget() const handlers = vm.$options[hook] if (handlers) { for (let i = 0, j = handlers.length; i < j; i++) { try { handlers[i].call(vm) } catch (e) { handleError(e, vm, `${hook} hook`) } } } if (vm._hasHookEvent) { vm.$emit('hook:' + hook) } popTarget() } ``` `callHook` 函数的逻辑很简单,根据传入的字符串 `hook`,去拿到 `vm.$options[hook]` 对应的回调函数数组,然后遍历执行,执行的时候把 `vm` 作为函数执行的上下文。 在上一节中,我们详细地介绍了 Vue.js 合并 `options` 的过程,各个阶段的生命周期的函数也被合并到 `vm.$options` 里,并且是一个数组。因此 `callhook` 函数的功能就是调用某个生命周期钩子注册的所有回调函数。 了解了生命周期的执行方式后,接下来我们会具体介绍每一个生命周期函数它的调用时机。 ## beforeCreate & created `beforeCreate` 和 `created` 函数都是在实例化 `Vue` 的阶段,在 `_init` 方法中执行的,它的定义在 `src/core/instance/init.js` 中: ```js 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') // ... } ``` 可以看到 `beforeCreate` 和 `created` 的钩子调用是在 `initState` 的前后,`initState` 的作用是初始化 `props`、`data`、`methods`、`watch`、`computed` 等属性,之后我们会详细分析。那么显然 `beforeCreate` 的钩子函数中就不能获取到 `props`、`data` 中定义的值,也不能调用 `methods` 中定义的函数。 在这俩个钩子函数执行的时候,并没有渲染 DOM,所以我们也不能够访问 DOM,一般来说,如果组件在加载的时候需要和后端有交互,放在这俩个钩子函数执行都可以,如果是需要访问 `props`、`data` 等数据的话,就需要使用 `created` 钩子函数。之后我们会介绍 vue-router 和 vuex 的时候会发现它们都混合了 `beforeCreate` 钩子函数。 ## beforeMount & mounted 顾名思义,`beforeMount` 钩子函数发生在 `mount`,也就是 DOM 挂载之前,它的调用时机是在 `mountComponent` 函数中,定义在 `src/core/instance/lifecycle.js` 中: ```js export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el // ... callHook(vm, 'beforeMount') let updateComponent /* istanbul ignore if */ 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) 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 = () => { 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) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false // manually mounted instance, call mounted on self // mounted is called for render-created child components in its inserted hook if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm } ``` 在执行 `vm._render()` 函数渲染 VNode 之前,执行了 `beforeMount` 钩子函数,在执行完 `vm._update()` 把 VNode patch 到真实 DOM 后,执行 `mounted` 钩子。注意,这里对 `mounted` 钩子函数执行有一个判断逻辑,`vm.$vnode` 如果为 `null`,则表明这不是一次组件的初始化过程,而是我们通过外部 `new Vue` 初始化过程。那么对于组件,它的 `mounted` 时机在哪儿呢? 之前我们提到过,组件的 VNode patch 到 DOM 后,会执行 `invokeInsertHook` 函数,把 `insertedVnodeQueue` 里保存的钩子函数依次执行一遍,它的定义在 `src/core/vdom/patch.js` 中: ```js function invokeInsertHook (vnode, queue, initial) { // delay insert hooks for component root nodes, invoke them after the // element is really inserted if (isTrue(initial) && isDef(vnode.parent)) { vnode.parent.data.pendingInsert = queue } else { for (let i = 0; i < queue.length; ++i) { queue[i].data.hook.insert(queue[i]) } } } ``` 该函数会执行 `insert` 这个钩子函数,对于组件而言,`insert` 钩子函数的定义在 `src/core/vdom/create-component.js` 中的 `componentVNodeHooks` 中: ```js const componentVNodeHooks = { // ... insert (vnode: MountedComponentVNode) { const { context, componentInstance } = vnode if (!componentInstance._isMounted) { componentInstance._isMounted = true callHook(componentInstance, 'mounted') } // ... }, } ``` 我们可以看到,每个子组件都是在这个钩子函数中执行 `mounted` 钩子函数,并且我们之前分析过,`insertedVnodeQueue` 的添加顺序是先子后父,所以对于同步渲染的子组件而言,`mounted` 钩子函数的执行顺序也是先子后父。 ## beforeUpdate & updated 顾名思义,`beforeUpdate` 和 `updated` 的钩子函数执行时机都应该是在数据更新的时候,到目前为止,我们还没有分析 Vue 的数据双向绑定、更新相关,下一章我会详细介绍这个过程。 `beforeUpdate` 的执行时机是在渲染 Watcher 的 `before` 函数中,我们刚才提到过: ```js export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { // ... // 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) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) // ... } ``` 注意这里有个判断,也就是在组件已经 `mounted` 之后,才会去调用这个钩子函数。 `update` 的执行时机是在`flushSchedulerQueue` 函数调用的时候,它的定义在 `src/core/observer/scheduler.js` 中: ```js function flushSchedulerQueue () { // ... // 获取到 updatedQueue callUpdatedHooks(updatedQueue) } function callUpdatedHooks (queue) { let i = queue.length while (i--) { const watcher = queue[i] const vm = watcher.vm if (vm._watcher === watcher && vm._isMounted) { callHook(vm, 'updated') } } } ``` `flushSchedulerQueue` 函数我们之后会详细介绍,可以先大概了解一下,`updatedQueue` 是更新了的 `wathcer` 数组,那么在 `callUpdatedHooks` 函数中,它对这些数组做遍历,只有满足当前 `watcher` 为 `vm._watcher` 以及组件已经 `mounted` 这两个条件,才会执行 `updated` 钩子函数。 我们之前提过,在组件 mount 的过程中,会实例化一个渲染的 `Watcher` 去监听 `vm` 上的数据变化重新渲染,这段逻辑发生在 `mountComponent` 函数执行的时候: ```js export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { // ... // 这里是简写 let updateComponent = () => { vm._update(vm._render(), hydrating) } new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) // ... } ``` 那么在实例化 `Watcher` 的过程中,在它的构造函数里会判断 `isRenderWatcher`,接着把当前 `watcher` 的实例赋值给 `vm._watcher`,定义在 `src/core/observer/watcher.js` 中: ```js export default class Watcher { // ... constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) // ... } } ``` 同时,还把当前 `wathcer` 实例 push 到 `vm._watchers` 中,`vm._watcher` 是专门用来监听 `vm` 上数据变化然后重新渲染的,所以它是一个渲染相关的 `watcher`,因此在 `callUpdatedHooks` 函数中,只有 `vm._watcher` 的回调执行完毕后,才会执行 `updated` 钩子函数。 ## beforeDestroy & destroyed 顾名思义,`beforeDestroy` 和 `destroyed` 钩子函数的执行时机在组件销毁的阶段,组件的销毁过程之后会详细介绍,最终会调用 `$destroy` 方法,它的定义在 `src/core/instance/lifecycle.js` 中: ```js Vue.prototype.$destroy = function () { const vm: Component = this if (vm._isBeingDestroyed) { return } callHook(vm, 'beforeDestroy') vm._isBeingDestroyed = true // remove self from parent const parent = vm.$parent if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) { remove(parent.$children, vm) } // teardown watchers if (vm._watcher) { vm._watcher.teardown() } let i = vm._watchers.length while (i--) { vm._watchers[i].teardown() } // remove reference from data ob // frozen object may not have observer. if (vm._data.__ob__) { vm._data.__ob__.vmCount-- } // call the last hook... vm._isDestroyed = true // invoke destroy hooks on current rendered tree vm.__patch__(vm._vnode, null) // fire destroyed hook callHook(vm, 'destroyed') // turn off all instance listeners. vm.$off() // remove __vue__ reference if (vm.$el) { vm.$el.__vue__ = null } // release circular reference (#6759) if (vm.$vnode) { vm.$vnode.parent = null } } ``` `beforeDestroy` 钩子函数的执行时机是在 `$destroy` 函数执行最开始的地方,接着执行了一系列的销毁动作,包括从 `parent` 的 `$children` 中删掉自身,删除 `watcher`,当前渲染的 VNode 执行销毁钩子函数等,执行完毕后再调用 `destroy` 钩子函数。 在 `$destroy` 的执行过程中,它又会执行 ` vm.__patch__(vm._vnode, null)` 触发它子组件的销毁钩子函数,这样一层层的递归调用,所以 `destroy` 钩子函数执行顺序是先子后父,和 `mounted` 过程一样。 ## activated & deactivated `activated` 和 `deactivated` 钩子函数是专门为 `keep-alive` 组件定制的钩子,我们会在介绍 `keep-alive` 组件的时候详细介绍,这里先留个悬念。 ## 总结 这一节主要介绍了 Vue 生命周期中各个钩子函数的执行时机以及顺序,通过分析,我们知道了如在 `created` 钩子函数中可以访问到数据,在 `mounted` 钩子函数中可以访问到 DOM,在 `destroy` 钩子函数中可以做一些定时器销毁工作,了解它们有利于我们在合适的生命周期去做不同的事情。 ================================================ FILE: docs/v2/components/merge-option.md ================================================ # 合并配置 通过之前章节的源码分析我们知道,`new Vue` 的过程通常有 2 种场景,一种是外部我们的代码主动调用 `new Vue(options)` 的方式实例化一个 Vue 对象;另一种是我们上一节分析的组件过程中内部通过 `new Vue(options)` 实例化子组件。 无论哪种场景,都会执行实例的 `_init(options)` 方法,它首先会执行一个 ` merge options` 的逻辑,相关的代码在 `src/core/instance/init.js` 中: ```js Vue.prototype._init = function (options?: Object) { // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } // ... } ``` 可以看到不同场景对于 `options` 的合并逻辑是不一样的,并且传入的 `options` 值也有非常大的不同,接下来我会分开介绍 2 种场景的 options 合并过程。 为了更直观,我们可以举个简单的示例: ```js import Vue from 'vue' let childComp = { template: '
{{msg}}
', created() { console.log('child created') }, mounted() { console.log('child mounted') }, data() { return { msg: 'Hello Vue' } } } Vue.mixin({ created() { console.log('parent created') } }) let app = new Vue({ el: '#app', render: h => h(childComp) }) ``` ## 外部调用场景 当执行 `new Vue` 的时候,在执行 `this._init(options)` 的时候,就会执行如下逻辑去合并 `options`: ```js vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) ``` 这里通过调用 `mergeOptions` 方法来合并,它实际上就是把 `resolveConstructorOptions(vm.constructor)` 的返回值和 `options` 做合并,`resolveConstructorOptions` 的实现先不考虑,在我们这个场景下,它还是简单返回 `vm.constructor.options`,相当于 `Vue.options`,那么这个值又是什么呢,其实在 `initGlobalAPI(Vue)` 的时候定义了这个值,代码在 `src/core/global-api/index.js` 中: ```js export function initGlobalAPI (Vue: GlobalAPI) { // ... Vue.options = Object.create(null) ASSET_TYPES.forEach(type => { Vue.options[type + 's'] = Object.create(null) }) // this is used to identify the "base" constructor to extend all plain-object // components with in Weex's multi-instance scenarios. Vue.options._base = Vue extend(Vue.options.components, builtInComponents) // ... } ``` 首先通过 `Vue.options = Object.create(null)` 创建一个空对象,然后遍历 `ASSET_TYPES`,`ASSET_TYPES` 的定义在 `src/shared/constants.js` 中: ```js export const ASSET_TYPES = [ 'component', 'directive', 'filter' ] ``` 所以上面遍历 `ASSET_TYPES` 后的代码相当于: ```js Vue.options.components = {} Vue.options.directives = {} Vue.options.filters = {} ``` 接着执行了 `Vue.options._base = Vue`,它的作用在我们上节实例化子组件的时候介绍了。 最后通过 `extend(Vue.options.components, builtInComponents)` 把一些内置组件扩展到 `Vue.options.components` 上,Vue 的内置组件目前有 ``、`` 和 `` 组件,这也就是为什么我们在其它组件中使用 `` 组件不需要注册的原因,这块儿后续我们介绍 `` 组件的时候会详细讲。 那么回到 `mergeOptions` 这个函数,它的定义在 `src/core/util/options.js` 中: ```js /** * Merge two option objects into a new one. * Core utility used in both instantiation and inheritance. */ export function mergeOptions ( parent: Object, child: Object, vm?: Component ): Object { if (process.env.NODE_ENV !== 'production') { checkComponents(child) } if (typeof child === 'function') { child = child.options } normalizeProps(child, vm) normalizeInject(child, vm) normalizeDirectives(child) const extendsFrom = child.extends if (extendsFrom) { parent = mergeOptions(parent, extendsFrom, vm) } if (child.mixins) { for (let i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm) } } const options = {} let key for (key in parent) { mergeField(key) } for (key in child) { if (!hasOwn(parent, key)) { mergeField(key) } } function mergeField (key) { const strat = strats[key] || defaultStrat options[key] = strat(parent[key], child[key], vm, key) } return options } ``` `mergeOptions` 主要功能就是把 `parent` 和 `child` 这两个对象根据一些合并策略,合并成一个新对象并返回。比较核心的几步,先递归把 `extends` 和 `mixins` 合并到 `parent` 上,然后遍历 `parent`,调用 `mergeField`,然后再遍历 `child`,如果 `key` 不在 `parent` 的自身属性上,则调用 `mergeField`。 这里有意思的是 `mergeField` 函数,它对不同的 `key` 有着不同的合并策略。举例来说,对于生命周期函数,它的合并策略是这样的: ```js function mergeHook ( parentVal: ?Array, childVal: ?Function | ?Array ): ?Array { return childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal } LIFECYCLE_HOOKS.forEach(hook => { strats[hook] = mergeHook }) ``` 这其中的 `LIFECYCLE_HOOKS` 的定义在 `src/shared/constants.js` 中: ```js export const LIFECYCLE_HOOKS = [ 'beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'beforeDestroy', 'destroyed', 'activated', 'deactivated', 'errorCaptured' ] ``` 这里定义了 Vue.js 所有的钩子函数名称,所以对于钩子函数,他们的合并策略都是 `mergeHook` 函数。这个函数的实现也非常有意思,用了一个多层 3 元运算符,逻辑就是如果不存在 `childVal` ,就返回 `parentVal`;否则再判断是否存在 `parentVal`,如果存在就把 `childVal` 添加到 `parentVal` 后返回新数组;否则返回 `childVal` 的数组。所以回到 `mergeOptions` 函数,一旦 `parent` 和 `child` 都定义了相同的钩子函数,那么它们会把 2 个钩子函数合并成一个数组。 关于其它属性的合并策略的定义都可以在 `src/core/util/options.js` 文件中看到,这里不一一介绍了,感兴趣的同学可以自己看。 通过执行 `mergeField` 函数,把合并后的结果保存到 `options` 对象中,最终返回它。 因此,在我们当前这个 case 下,执行完如下合并后: ```js vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) ``` `vm.$options` 的值差不多是如下这样: ```js vm.$options = { components: { }, created: [ function created() { console.log('parent created') } ], directives: { }, filters: { }, _base: function Vue(options) { // ... }, el: "#app", render: function (h) { //... } } ``` ## 组件场景 由于组件的构造函数是通过 `Vue.extend` 继承自 `Vue` 的,先回顾一下这个过程,代码定义在 `src/core/global-api/extend.js` 中。 ```js /** * Class inheritance */ Vue.extend = function (extendOptions: Object): Function { // ... Sub.options = mergeOptions( Super.options, extendOptions ) // ... // keep a reference to the super options at extension time. // later at instantiation we can check if Super's options have // been updated. Sub.superOptions = Super.options Sub.extendOptions = extendOptions Sub.sealedOptions = extend({}, Sub.options) // ... return Sub } ``` 我们只保留关键逻辑,这里的 `extendOptions` 对应的就是前面定义的组件对象,它会和 `Vue.options` 合并到 `Sub.opitons` 中。 接下来我们再回忆一下子组件的初始化过程,代码定义在 `src/core/vdom/create-component.js` 中: ```js export function createComponentInstanceForVnode ( vnode: any, // we know it's MountedComponentVNode but flow doesn't parent: any, // activeInstance in lifecycle state ): Component { const options: InternalComponentOptions = { _isComponent: true, _parentVnode: vnode, parent } // ... return new vnode.componentOptions.Ctor(options) } ``` 这里的 `vnode.componentOptions.Ctor` 就是指向 `Vue.extend` 的返回值 `Sub`, 所以 执行 `new vnode.componentOptions.Ctor(options)` 接着执行 `this._init(options)`,因为 `options._isComponent` 为 true,那么合并 `options` 的过程走到了 ` initInternalComponent(vm, options)` 逻辑。先来看一下它的代码实现,在 `src/core/instance/init.js` 中: ```js export function initInternalComponent (vm: Component, options: InternalComponentOptions) { const opts = vm.$options = Object.create(vm.constructor.options) // doing this because it's faster than dynamic enumeration. const parentVnode = options._parentVnode opts.parent = options.parent opts._parentVnode = parentVnode const vnodeComponentOptions = parentVnode.componentOptions opts.propsData = vnodeComponentOptions.propsData opts._parentListeners = vnodeComponentOptions.listeners opts._renderChildren = vnodeComponentOptions.children opts._componentTag = vnodeComponentOptions.tag if (options.render) { opts.render = options.render opts.staticRenderFns = options.staticRenderFns } } ``` `initInternalComponent` 方法首先执行 `const opts = vm.$options = Object.create(vm.constructor.options)`,这里的 `vm.constructor` 就是子组件的构造函数 `Sub`,相当于 `vm.$options = Object.create(Sub.options)`。 接着又把实例化子组件传入的子组件父 VNode 实例 `parentVnode`、子组件的父 Vue 实例 `parent` 保存到 `vm.$options` 中,另外还保留了 `parentVnode` 配置中的如 `propsData` 等其它的属性。 这么看来,`initInternalComponent` 只是做了简单一层对象赋值,并不涉及到递归、合并策略等复杂逻辑。 因此,在我们当前这个 case 下,执行完如下合并后: ```js initInternalComponent(vm, options) ``` `vm.$options` 的值差不多是如下这样: ```js vm.$options = { parent: Vue /*父Vue实例*/, propsData: undefined, _componentTag: undefined, _parentVnode: VNode /*父VNode实例*/, _renderChildren:undefined, __proto__: { components: { }, directives: { }, filters: { }, _base: function Vue(options) { //... }, _Ctor: {}, created: [ function created() { console.log('parent created') }, function created() { console.log('child created') } ], mounted: [ function mounted() { console.log('child mounted') } ], data() { return { msg: 'Hello Vue' } }, template: '
{{msg}}
' } } ``` ## 总结 那么至此,Vue 初始化阶段对于 `options` 的合并过程就介绍完了,我们需要知道对于 `options` 的合并有 2 种方式,子组件初始化过程通过 `initInternalComponent` 方式要比外部初始化 Vue 通过 `mergeOptions` 的过程要快,合并完的结果保留在 `vm.$options` 中。 纵观一些库、框架的设计几乎都是类似的,自身定义了一些默认配置,同时又可以在初始化阶段传入一些定义配置,然后去 merge 默认配置,来达到定制化不同需求的目的。只不过在 Vue 的场景下,会对 merge 的过程做一些精细化控制,虽然我们在开发自己的 JSSDK 的时候并没有 Vue 这么复杂,但这个设计思想是值得我们借鉴的。 ================================================ FILE: docs/v2/components/patch.md ================================================ # patch 通过前一章的分析我们知道,当我们通过 `createComponent` 创建了组件 VNode,接下来会走到 `vm._update`,执行 `vm.__patch__` 去把 VNode 转换成真正的 DOM 节点。这个过程我们在前一章已经分析过了,但是针对一个普通的 VNode 节点,接下来我们来看看组件的 VNode 会有哪些不一样的地方。 patch 的过程会调用 `createElm` 创建元素节点,回顾一下 `createElm` 的实现,它的定义在 `src/core/vdom/patch.js` 中: ```js function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { // ... if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } // ... } ``` ## createComponent 我们删掉多余的代码,只保留关键的逻辑,这里会判断 `createComponent(vnode, insertedVnodeQueue, parentElm, refElm)` 的返回值,如果为 `true` 则直接结束,那么接下来看一下 `createComponent` 方法的实现: ```js function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data if (isDef(i)) { const isReactivated = isDef(vnode.componentInstance) && i.keepAlive if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */) } // after calling the init hook, if the vnode is a child component // it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm. // in that case we can just return the element and be done. if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue) insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } } ``` `createComponent` 函数中,首先对 `vnode.data` 做了一些判断: ```js let i = vnode.data if (isDef(i)) { // ... if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */) // ... } // .. } ``` 如果 `vnode` 是一个组件 VNode,那么条件会满足,并且得到 `i` 就是 `init` 钩子函数,回顾上节我们在创建组件 VNode 的时候合并钩子函数中就包含 `init` 钩子函数,定义在 `src/core/vdom/create-component.js` 中: ```js init (vnode: VNodeWithData, hydrating: boolean): ?boolean { if ( vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { // kept-alive components, treat as a patch const mountedNode: any = vnode // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode) } else { const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) child.$mount(hydrating ? vnode.elm : undefined, hydrating) } }, ``` `init` 钩子函数执行也很简单,我们先不考虑 `keepAlive` 的情况,它是通过 `createComponentInstanceForVnode` 创建一个 Vue 的实例,然后调用 `$mount` 方法挂载子组件, 先来看一下 `createComponentInstanceForVnode` 的实现: ```js export function createComponentInstanceForVnode ( vnode: any, // we know it's MountedComponentVNode but flow doesn't parent: any, // activeInstance in lifecycle state ): Component { const options: InternalComponentOptions = { _isComponent: true, _parentVnode: vnode, parent } // check inline-template render functions const inlineTemplate = vnode.data.inlineTemplate if (isDef(inlineTemplate)) { options.render = inlineTemplate.render options.staticRenderFns = inlineTemplate.staticRenderFns } return new vnode.componentOptions.Ctor(options) } ``` `createComponentInstanceForVnode` 函数构造的一个内部组件的参数,然后执行 `new vnode.componentOptions.Ctor(options)`。这里的 `vnode.componentOptions.Ctor` 对应的就是子组件的构造函数,我们上一节分析了它实际上是继承于 Vue 的一个构造器 `Sub`,相当于 `new Sub(options)` 这里有几个关键参数要注意几个点,`_isComponent` 为 `true` 表示它是一个组件,`parent` 表示当前激活的组件实例(注意,这里比较有意思的是如何拿到组件实例,后面会介绍。 所以子组件的实例化实际上就是在这个时机执行的,并且它会执行实例的 `_init` 方法,这个过程有一些和之前不同的地方需要挑出来说,代码在 `src/core/instance/init.js` 中: ```js Vue.prototype._init = function (options?: Object) { const vm: Component = this // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } // ... if (vm.$options.el) { vm.$mount(vm.$options.el) } } ``` 这里首先是合并 `options` 的过程有变化,`_isComponent` 为 true,所以走到了 `initInternalComponent` 过程,这个函数的实现也简单看一下: ```js export function initInternalComponent (vm: Component, options: InternalComponentOptions) { const opts = vm.$options = Object.create(vm.constructor.options) // doing this because it's faster than dynamic enumeration. const parentVnode = options._parentVnode opts.parent = options.parent opts._parentVnode = parentVnode const vnodeComponentOptions = parentVnode.componentOptions opts.propsData = vnodeComponentOptions.propsData opts._parentListeners = vnodeComponentOptions.listeners opts._renderChildren = vnodeComponentOptions.children opts._componentTag = vnodeComponentOptions.tag if (options.render) { opts.render = options.render opts.staticRenderFns = options.staticRenderFns } } ``` 这个过程我们重点记住以下几个点即可:`opts.parent = options.parent`、`opts._parentVnode = parentVnode`,它们是把之前我们通过 `createComponentInstanceForVnode` 函数传入的几个参数合并到内部的选项 `$options` 里了。 再来看一下 `_init` 函数最后执行的代码: ```js if (vm.$options.el) { vm.$mount(vm.$options.el) } ``` 由于组件初始化的时候是不传 el 的,因此组件是自己接管了 `$mount` 的过程,这个过程的主要流程在上一章介绍过了,回到组件 `init` 的过程,`componentVNodeHooks` 的 `init` 钩子函数,在完成实例化的 `_init` 后,接着会执行 `child.$mount(hydrating ? vnode.elm : undefined, hydrating)` 。这里 `hydrating` 为 true 一般是服务端渲染的情况,我们只考虑客户端渲染,所以这里 `$mount` 相当于执行 `child.$mount(undefined, false)`,它最终会调用 `mountComponent` 方法,进而执行 `vm._render()` 方法: ```js Vue.prototype._render = function (): VNode { const vm: Component = this const { render, _parentVnode } = vm.$options // 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 = render.call(vm._renderProxy, vm.$createElement) } catch (e) { // ... } // set parent vnode.parent = _parentVnode return vnode } ``` 我们只保留关键部分的代码,这里的 `_parentVnode` 就是当前组件的父 VNode,而 `render` 函数生成的 `vnode` 当前组件的渲染 `vnode`,`vnode` 的 `parent` 指向了 `_parentVnode`,也就是 `vm.$vnode`,它们是一种父子的关系。 我们知道在执行完 `vm._render` 生成 VNode 后,接下来就要执行 `vm._update` 去渲染 VNode 了。来看一下组件渲染的过程中有哪些需要注意的,`vm._update` 的定义在 `src/core/instance/lifecycle.js` 中: ```js export let activeInstance: any = null 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 // update __vue__ reference if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } // updated hook is called by the scheduler to ensure that children are // updated in a parent's updated hook. } ``` `_update` 过程中有几个关键的代码,首先 `vm._vnode = vnode` 的逻辑,这个 `vnode` 是通过 `vm._render()` 返回的组件渲染 VNode,`vm._vnode` 和 `vm.$vnode` 的关系就是一种父子关系,用代码表达就是 `vm._vnode.parent === vm.$vnode`。还有一段比较有意思的代码: ```js export let activeInstance: any = null Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { // ... const prevActiveInstance = activeInstance activeInstance = vm if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } activeInstance = prevActiveInstance // ... } ``` 这个 `activeInstance` 作用就是保持当前上下文的 Vue 实例,它是在 `lifecycle` 模块的全局变量,定义是 `export let activeInstance: any = null`,并且在之前我们调用 `createComponentInstanceForVnode` 方法的时候从 `lifecycle` 模块获取,并且作为参数传入的。因为实际上 JavaScript 是一个单线程,Vue 整个初始化是一个深度遍历的过程,在实例化子组件的过程中,它需要知道当前上下文的 Vue 实例是什么,并把它作为子组件的父 Vue 实例。之前我们提到过对子组件的实例化过程先会调用 `initInternalComponent(vm, options)` 合并 `options`,把 `parent` 存储在 `vm.$options` 中,在 `$mount` 之前会调用 `initLifecycle(vm)` 方法: ```js export function initLifecycle (vm: Component) { const options = vm.$options // locate first non-abstract parent let parent = options.parent if (parent && !options.abstract) { while (parent.$options.abstract && parent.$parent) { parent = parent.$parent } parent.$children.push(vm) } vm.$parent = parent // ... } ``` 可以看到 `vm.$parent` 就是用来保留当前 `vm` 的父实例,并且通过 `parent.$children.push(vm)` 来把当前的 `vm` 存储到父实例的 `$children` 中。 在 `vm._update` 的过程中,把当前的 `vm` 赋值给 `activeInstance`,同时通过 `const prevActiveInstance = activeInstance` 用 `prevActiveInstance` 保留上一次的 `activeInstance`。实际上,`prevActiveInstance` 和当前的 `vm` 是一个父子关系,当一个 `vm` 实例完成它的所有子树的 patch 或者 update 过程后,`activeInstance` 会回到它的父实例,这样就完美地保证了 `createComponentInstanceForVnode` 整个深度遍历过程中,我们在实例化子组件的时候能传入当前子组件的父 Vue 实例,并在 `_init` 的过程中,通过 `vm.$parent` 把这个父子关系保留。 那么回到 `_update`,最后就是调用 `__patch__` 渲染 VNode 了。 ```js vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) function patch (oldVnode, vnode, hydrating, removeOnly) { // ... let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { // empty mount (likely as component), create new root element isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { // ... } // ... } ``` 这里又回到了本节开始的过程,之前分析过负责渲染成 DOM 的函数是 `createElm`,注意这里我们只传了 2 个参数,所以对应的 `parentElm` 是 `undefined`。我们再来看看它的定义: ```js function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { // ... if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } const data = vnode.data const children = vnode.children const tag = vnode.tag if (isDef(tag)) { // ... vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) setScope(vnode) /* istanbul ignore if */ if (__WEEX__) { // ... } else { createChildren(vnode, children, insertedVnodeQueue) if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } insert(parentElm, vnode.elm, refElm) } // ... } else if (isTrue(vnode.isComment)) { vnode.elm = nodeOps.createComment(vnode.text) insert(parentElm, vnode.elm, refElm) } else { vnode.elm = nodeOps.createTextNode(vnode.text) insert(parentElm, vnode.elm, refElm) } } ``` 注意,这里我们传入的 `vnode` 是组件渲染的 `vnode`,也就是我们之前说的 `vm._vnode`,如果组件的根节点是个普通元素,那么 `vm._vnode` 也是普通的 `vnode`,这里 `createComponent(vnode, insertedVnodeQueue, parentElm, refElm)` 的返回值是 false。接下来的过程就和我们上一章一样了,先创建一个父节点占位符,然后再遍历所有子 VNode 递归调用 `createElm`,在遍历的过程中,如果遇到子 VNode 是一个组件的 VNode,则重复本节开始的过程,这样通过一个递归的方式就可以完整地构建了整个组件树。 由于我们这个时候传入的 `parentElm` 是空,所以对组件的插入,在 `createComponent` 有这么一段逻辑: ```js function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data if (isDef(i)) { // .... if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */) } // ... if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue) insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } } ``` 在完成组件的整个 `patch` 过程后,最后执行 `insert(parentElm, vnode.elm, refElm)` 完成组件的 DOM 插入,如果组件 `patch` 过程中又创建了子组件,那么DOM 的插入顺序是先子后父。 ## 总结 那么到此,一个组件的 VNode 是如何创建、初始化、渲染的过程也就介绍完毕了。在对组件化的实现有一个大概了解后,接下来我们来介绍一下这其中的一些细节。我们知道编写一个组件实际上是编写一个 JavaScript 对象,对象的描述就是各种配置,之前我们提到在 `_init` 的最初阶段执行的就是 `merge options` 的逻辑,那么下一节我们从源码角度来分析合并配置的过程。 ================================================ FILE: docs/v2/data-driven/create-element.md ================================================ # createElement Vue.js 利用 createElement 方法创建 VNode,它定义在 `src/core/vdom/create-element.js` 中: ```js // wrapper function for providing a more flexible interface // without getting yelled at by flow export function createElement ( context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean ): VNode | Array { if (Array.isArray(data) || isPrimitive(data)) { normalizationType = children children = data data = undefined } if (isTrue(alwaysNormalize)) { normalizationType = ALWAYS_NORMALIZE } return _createElement(context, tag, data, children, normalizationType) } ``` `createElement` 方法实际上是对 `_createElement` 方法的封装,它允许传入的参数更加灵活,在处理这些参数后,调用真正创建 VNode 的函数 `_createElement`: ```js export function _createElement ( context: Component, tag?: string | Class | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array { if (isDef(data) && isDef((data: any).__ob__)) { 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 ) return createEmptyVNode() } // object syntax in v-bind if (isDef(data) && isDef(data.is)) { tag = data.is } if (!tag) { // in case of component :is set to falsy value return createEmptyVNode() } // warn against non-primitive key if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.key) && !isPrimitive(data.key) ) { if (!__WEEX__ || !('@binding' in data.key)) { warn( 'Avoid using non-primitive value as key, ' + 'use string/number value instead.', context ) } } // support single function children as default scoped slot if (Array.isArray(children) && typeof children[0] === 'function' ) { data = data || {} data.scopedSlots = { default: children[0] } children.length = 0 } if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } let vnode, ns if (typeof tag === 'string') { let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) if (config.isReservedTag(tag)) { // platform built-in elements vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // component vnode = createComponent(Ctor, data, context, children, tag) } else { // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { // direct component options / constructor vnode = createComponent(tag, data, context, children) } 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() } } ``` `_createElement` 方法有 5 个参数,`context` 表示 VNode 的上下文环境,它是 `Component` 类型;`tag` 表示标签,它可以是一个字符串,也可以是一个 `Component`;`data` 表示 VNode 的数据,它是一个 `VNodeData` 类型,可以在 `flow/vnode.js` 中找到它的定义,这里先不展开说;`children` 表示当前 VNode 的子节点,它是任意类型的,它接下来需要被规范为标准的 VNode 数组;`normalizationType` 表示子节点规范的类型,类型不同规范的方法也就不一样,它主要是参考 `render` 函数是编译生成的还是用户手写的。 `createElement` 函数的流程略微有点多,我们接下来主要分析 2 个重点的流程 —— `children` 的规范化以及 VNode 的创建。 ## children 的规范化 由于 Virtual DOM 实际上是一个树状结构,每一个 VNode 可能会有若干个子节点,这些子节点应该也是 VNode 的类型。`_createElement` 接收的第 4 个参数 children 是任意类型的,因此我们需要把它们规范成 VNode 类型。 这里根据 `normalizationType` 的不同,调用了 `normalizeChildren(children)` 和 `simpleNormalizeChildren(children)` 方法,它们的定义都在 `src/core/vdom/helpers/normalzie-children.js` 中: ```js // The template compiler attempts to minimize the need for normalization by // statically analyzing the template at compile time. // // For plain HTML markup, normalization can be completely skipped because the // generated render function is guaranteed to return Array. There are // two cases where extra normalization is needed: // 1. When the children contains components - because a functional component // may return an Array instead of a single root. In this case, just a simple // normalization is needed - if any child is an Array, we flatten the whole // thing with Array.prototype.concat. It is guaranteed to be only 1-level deep // because functional components already normalize their own children. export function simpleNormalizeChildren (children: any) { for (let i = 0; i < children.length; i++) { if (Array.isArray(children[i])) { return Array.prototype.concat.apply([], children) } } return children } // 2. When the children contains constructs that always generated nested Arrays, // e.g.